From 78bacd16e4a597606cd765091f4ea25f7976a15d Mon Sep 17 00:00:00 2001 From: Parman Date: Thu, 28 Aug 2025 12:49:46 +0330 Subject: [PATCH] feat: Complete WhatsApp API implementation --- examples/whatsapp_example.py | 498 +++++++- .../models/whatsapp.py | 320 ++++- .../resources/whatsapp.py | 408 +++++- tests/test_whatsapp.py | 1099 +++++++++++++++++ 4 files changed, 2263 insertions(+), 62 deletions(-) create mode 100644 tests/test_whatsapp.py diff --git a/examples/whatsapp_example.py b/examples/whatsapp_example.py index a6b1e84..565ca2f 100644 --- a/examples/whatsapp_example.py +++ b/examples/whatsapp_example.py @@ -1,6 +1,6 @@ import os -from devo_global_comms_python import DevoException +from devo_global_comms_python import DevoCommsClient, DevoException def main(): @@ -9,69 +9,473 @@ def main(): print("❌ Please set DEVO_API_KEY environment variable") return + client = DevoCommsClient(api_key=api_key) print("βœ… Devo WhatsApp Client initialized successfully") print("=" * 60) try: - # Example 1: Send a text message - print("πŸ’¬ WHATSAPP TEXT MESSAGE EXAMPLE") + # Example 1: Get WhatsApp accounts + print("πŸ“± WHATSAPP ACCOUNTS EXAMPLE") print("-" * 30) - print("πŸ“€ Sending WhatsApp text message...") - print("⚠️ This is a placeholder implementation.") - print(" Update this example when WhatsApp API is implemented.") - - # Placeholder WhatsApp send - update when implementing WhatsApp resource - print(" ```python") - print(" whatsapp_response = client.whatsapp.send_text(") - print(" to='+1234567890',") - print(" text='Hello from Devo SDK via WhatsApp!'") - print(" )") - print(" print(f'WhatsApp message sent! ID: {whatsapp_response.id}')") - print(" ```") - - # Example 2: Send media message - print("\nπŸ“· WHATSAPP MEDIA MESSAGE EXAMPLE") + print("πŸ“€ Getting WhatsApp accounts...") + accounts_response = client.whatsapp.get_accounts(page=1, limit=10, is_approved=True) + + print("βœ… WhatsApp accounts retrieved successfully!") + print(f" πŸ“Š Total accounts: {accounts_response.total}") + print(f" πŸ“„ Page: {accounts_response.page}") + print(f" πŸ“ Limit: {accounts_response.limit}") + print(f" ➑️ Has next: {accounts_response.has_next}") + + for i, account in enumerate(accounts_response.accounts, 1): + print(f" Account {i}:") + print(f" πŸ†” ID: {account.id}") + print(f" πŸ“› Name: {account.name}") + print(f" πŸ“§ Email: {account.email}") + print(f" πŸ“ž Phone: {account.phone}") + print(f" βœ… Approved: {account.is_approved}") + + # Example 2: Get a template + print("\nπŸ“‹ WHATSAPP TEMPLATE EXAMPLE") print("-" * 30) - print("πŸ“€ Sending WhatsApp media message...") - print(" ```python") - print(" media_response = client.whatsapp.send_media(") - print(" to='+1234567890',") - print(" media_url='https://example.com/image.jpg',") - print(" media_type='image',") - print(" caption='Check out this image!'") - print(" )") - print(" print(f'WhatsApp media sent! ID: {media_response.id}')") - print(" ```") - - # Example 3: Send template message - print("\nπŸ“‹ WHATSAPP TEMPLATE MESSAGE EXAMPLE") + print("πŸ“€ Getting WhatsApp template...") + template = client.whatsapp.get_template("welcome_message") + + print("βœ… Template retrieved successfully!") + print(f" πŸ“› Name: {template.name}") + print(f" 🌍 Language: {template.language}") + print(f" πŸ“Š Status: {template.status}") + print(f" πŸ“‚ Category: {template.category}") + print(f" πŸ”§ Components: {len(template.components)}") + + # Example 3: Upload a file + print("\nπŸ“Ž WHATSAPP FILE UPLOAD EXAMPLE") print("-" * 30) - print("πŸ“€ Sending WhatsApp template message...") - print(" ```python") - print(" template_response = client.whatsapp.send_template(") - print(" to='+1234567890',") - print(" template_name='welcome_message',") - print(" template_variables={'name': 'John', 'company': 'Acme Corp'}") - print(" )") - print(" print(f'WhatsApp template sent! ID: {template_response.id}')") - print(" ```") + print("πŸ“€ Uploading file to WhatsApp...") + + # Create a sample file content for demonstration + sample_content = b"This is a sample file content for WhatsApp upload demonstration." + + upload_response = client.whatsapp.upload_file( + file_content=sample_content, filename="sample_document.txt", content_type="text/plain" + ) + + print("βœ… File uploaded successfully!") + print(f" πŸ†” File ID: {upload_response.file_id}") + print(f" πŸ“„ Filename: {upload_response.filename}") + print(f" πŸ“ File size: {upload_response.file_size} bytes") + print(f" 🎭 MIME type: {upload_response.mime_type}") + print(f" πŸ”— URL: {upload_response.url}") + if upload_response.expires_at: + print(f" ⏰ Expires at: {upload_response.expires_at}") + + # Example 4: Search accounts + print("\nπŸ” WHATSAPP ACCOUNTS SEARCH EXAMPLE") + print("-" * 35) + + print("πŸ” Searching WhatsApp accounts...") + search_response = client.whatsapp.get_accounts(search="test") + + print("βœ… Search completed!") + print(f" πŸ“Š Found {search_response.total} accounts matching 'test'") + + # Example 5: Send a normal message + print("\nπŸ’¬ WHATSAPP SEND MESSAGE EXAMPLE") + print("-" * 32) + + print("πŸ“€ Sending WhatsApp message...") + message_response = client.whatsapp.send_normal_message( + to="+1234567890", + message="Hello from the Devo WhatsApp SDK! πŸ‘‹ This is a test message.", + account_id="acc_123", # Optional - uses default if not provided + ) + + print("βœ… Message sent successfully!") + print(f" πŸ†” Message ID: {message_response.message_id}") + print(f" πŸ“Š Status: {message_response.status}") + print(f" πŸ“ž To: {message_response.to}") + print(f" 🏒 Account ID: {message_response.account_id}") + print(f" πŸ• Timestamp: {message_response.timestamp}") + print(f" βœ… Success: {message_response.success}") + + # Example 6: Send message with emojis and Unicode + print("\n🌍 WHATSAPP UNICODE MESSAGE EXAMPLE") + print("-" * 35) + + unicode_message = "Β‘Hola! πŸŽ‰ Welcome to Devo! 欒迎 Ω…Ψ±Ψ­Ψ¨Ψ§ πŸš€" + unicode_response = client.whatsapp.send_normal_message(to="+1234567890", message=unicode_message) + + print("βœ… Unicode message sent!") + print(f" πŸ†” Message ID: {unicode_response.message_id}") + print(f" πŸ“Š Status: {unicode_response.status}") + + # Example 7: Create a WhatsApp template + print("\nπŸ“‹ WHATSAPP CREATE TEMPLATE EXAMPLE") + print("-" * 35) + + from devo_global_comms_python.models.whatsapp import ( + BodyComponent, + ButtonsComponent, + FooterComponent, + QuickReplyButton, + WhatsAppTemplateRequest, + ) + + print("πŸ“€ Creating WhatsApp template...") + + # Create a utility template for notifications + template_request = WhatsAppTemplateRequest( + name="order_confirmation_notification", + language="en_US", + category="UTILITY", + components=[ + BodyComponent( + type="BODY", + text="Hi {{1}}! Your order #{{2}} has been confirmed and " + "will be delivered to {{3}}. Thank you for your purchase!", + ), + FooterComponent(type="FOOTER", text="Reply STOP to unsubscribe from notifications"), + ButtonsComponent( + type="BUTTONS", + buttons=[ + QuickReplyButton(type="QUICK_REPLY", text="Track Order"), + QuickReplyButton(type="QUICK_REPLY", text="Contact Support"), + ], + ), + ], + ) + + template_response = client.whatsapp.create_template( + account_id="acc_123", template=template_request # Replace with actual account ID + ) + + print("βœ… Template created successfully!") + print(f" πŸ†” Template ID: {template_response.id}") + print(f" πŸ“› Template Name: {template_response.name}") + print(f" πŸ“Š Status: {template_response.status}") + print(f" πŸ“‚ Category: {template_response.category}") + print(f" 🌍 Language: {template_response.language}") + + # Example 8: Get WhatsApp templates + print("\nπŸ“‹ WHATSAPP GET TEMPLATES EXAMPLE") + print("-" * 33) + + print("πŸ“€ Getting WhatsApp templates...") + templates_response = client.whatsapp.get_templates( + account_id="acc_123", category="UTILITY", page=1, limit=5 # Replace with actual account ID + ) + + print("βœ… Templates retrieved successfully!") + print(f" πŸ“Š Total templates: {templates_response.total}") + print(f" πŸ“„ Page: {templates_response.page}") + print(f" πŸ“ Limit: {templates_response.limit}") + print(f" ➑️ Has next: {templates_response.has_next}") + + for i, template in enumerate(templates_response.templates, 1): + print(f" Template {i}:") + print(f" πŸ“› Name: {template.name}") + print(f" πŸ“Š Status: {template.status}") + print(f" πŸ“‚ Category: {template.category}") + print(f" 🌍 Language: {template.language}") + print(f" πŸ”§ Components: {len(template.components)}") + + # Example 9: Create an authentication template + print("\nπŸ” WHATSAPP AUTHENTICATION TEMPLATE EXAMPLE") + print("-" * 42) + + from devo_global_comms_python.models.whatsapp import OTPButton + + print("πŸ“€ Creating authentication template...") + + auth_template_request = WhatsAppTemplateRequest( + name="verification_code_template", + language="en_US", + category="AUTHENTICATION", + components=[ + BodyComponent(type="BODY", add_security_recommendation=True), + FooterComponent(type="FOOTER", code_expiration_minutes=10), + ButtonsComponent( + type="BUTTONS", buttons=[OTPButton(type="OTP", otp_type="COPY_CODE", text="Copy Code")] + ), + ], + ) + + auth_template_response = client.whatsapp.create_template(account_id="acc_123", template=auth_template_request) + + print("βœ… Authentication template created!") + print(f" πŸ†” Template ID: {auth_template_response.id}") + print(f" πŸ” Category: {auth_template_response.category}") + print(f" πŸ“Š Status: {auth_template_response.status}") + + # Example 10: Create a marketing template with buttons + print("\n🎯 WHATSAPP MARKETING TEMPLATE EXAMPLE") + print("-" * 37) + + from devo_global_comms_python.models.whatsapp import ( + HeaderComponent, + PhoneNumberButton, + TemplateExample, + URLButton, + ) + + print("πŸ“€ Creating marketing template...") + + marketing_template_request = WhatsAppTemplateRequest( + name="summer_sale_promotion", + language="en_US", + category="MARKETING", + components=[ + HeaderComponent( + type="HEADER", + format="TEXT", + text="🌞 Summer Sale Alert! {{1}}", + example=TemplateExample(header_text=["50% OFF"]), + ), + BodyComponent( + type="BODY", + text="Don't miss our biggest sale of the year! Get {{1}} off on all " + "summer collections. Use code {{2}} at checkout. Sale ends {{3}}!", + example=TemplateExample(body_text=[["50%", "SUMMER50", "July 31st"]]), + ), + FooterComponent(type="FOOTER", text="Terms and conditions apply. Limited time offer."), + ButtonsComponent( + type="BUTTONS", + buttons=[ + URLButton( + type="URL", + text="Shop Now", + url="https://example.com/summer-sale?promo={{1}}", + example=["SUMMER50"], + ), + PhoneNumberButton(type="PHONE_NUMBER", text="Call Us", phone_number="+1234567890"), + ], + ), + ], + ) + + marketing_template_response = client.whatsapp.create_template( + account_id="acc_123", template=marketing_template_request + ) + + print("βœ… Marketing template created!") + print(f" πŸ†” Template ID: {marketing_template_response.id}") + print(f" 🎯 Category: {marketing_template_response.category}") + print(f" πŸ“Š Status: {marketing_template_response.status}") + + # Example 11: Send template message with text parameters + print("\nπŸ“€ WHATSAPP SEND TEMPLATE MESSAGE EXAMPLE") + print("-" * 40) + + from devo_global_comms_python.models.whatsapp import ( + TemplateMessageComponent, + TemplateMessageLanguage, + TemplateMessageParameter, + TemplateMessageTemplate, + WhatsAppTemplateMessageRequest, + ) + + print("πŸ“€ Sending template message...") + + # Send a simple text template message + template_message_request = WhatsAppTemplateMessageRequest( + to="+1234567890", + template=TemplateMessageTemplate( + name="order_confirmation_notification", + language=TemplateMessageLanguage(code="en_US"), + components=[ + TemplateMessageComponent( + type="body", + parameters=[ + TemplateMessageParameter(type="text", text="John Doe"), + TemplateMessageParameter(type="text", text="ORD-12345"), + TemplateMessageParameter(type="text", text="123 Main St, City"), + ], + ) + ], + ), + ) + + template_message_response = client.whatsapp.send_template_message( + account_id="acc_123", template_message=template_message_request + ) + + print("βœ… Template message sent successfully!") + print(f" πŸ†” Message ID: {template_message_response.message_id}") + print(f" πŸ“Š Status: {template_message_response.status}") + print(f" πŸ“ž To: {template_message_response.to}") + print(f" 🏒 Account ID: {template_message_response.account_id}") + print(f" βœ… Success: {template_message_response.success}") + + # Example 12: Send template message with image header + print("\nπŸ–ΌοΈ WHATSAPP TEMPLATE MESSAGE WITH IMAGE EXAMPLE") + print("-" * 45) + + from devo_global_comms_python.models.whatsapp import ImageParameter + + print("πŸ“€ Sending template message with image header...") + + image_template_request = WhatsAppTemplateMessageRequest( + to="+1234567890", + template=TemplateMessageTemplate( + name="limited_time_offer_tuscan_getaway_2023", + language=TemplateMessageLanguage(code="en_US"), + components=[ + TemplateMessageComponent( + type="header", + parameters=[ + TemplateMessageParameter( + type="image", image=ImageParameter(link="https://example.com/summer-sale.jpg") + ) + ], + ), + TemplateMessageComponent( + type="body", + parameters=[ + TemplateMessageParameter(type="text", text="John"), + TemplateMessageParameter(type="text", text="Summer Vacation Package"), + TemplateMessageParameter(type="text", text="$799"), + ], + ), + TemplateMessageComponent( + type="button", + sub_type="url", + index="0", + parameters=[TemplateMessageParameter(type="text", text="SUMMER2024")], + ), + ], + ), + ) + + image_template_response = client.whatsapp.send_template_message( + account_id="acc_123", template_message=image_template_request + ) + + print("βœ… Image template message sent!") + print(f" πŸ†” Message ID: {image_template_response.message_id}") + print(f" πŸ“Š Status: {image_template_response.status}") + + # Example 13: Send template message with location + print("\nπŸ“ WHATSAPP TEMPLATE MESSAGE WITH LOCATION EXAMPLE") + print("-" * 49) + + from devo_global_comms_python.models.whatsapp import LocationParameter + + print("πŸ“€ Sending template message with location...") + + location_template_request = WhatsAppTemplateMessageRequest( + to="+1234567890", + template=TemplateMessageTemplate( + name="order_delivery_update", + language=TemplateMessageLanguage(code="en_US"), + components=[ + TemplateMessageComponent( + type="header", + parameters=[ + TemplateMessageParameter( + type="location", + location=LocationParameter( + latitude="40.7128", + longitude="-74.0060", + name="Delivery Location", + address="New York, NY 10001", + ), + ) + ], + ), + TemplateMessageComponent( + type="body", + parameters=[ + TemplateMessageParameter(type="text", text="John"), + TemplateMessageParameter(type="text", text="DEL-67890"), + ], + ), + TemplateMessageComponent( + type="button", + sub_type="quick_reply", + index="0", + parameters=[TemplateMessageParameter(type="payload", payload="STOP_DELIVERY_UPDATES")], + ), + ], + ), + ) + + location_template_response = client.whatsapp.send_template_message( + account_id="acc_123", template_message=location_template_request + ) + + print("βœ… Location template message sent!") + print(f" πŸ†” Message ID: {location_template_response.message_id}") + print(f" πŸ“Š Status: {location_template_response.status}") + + # Example 14: Send authentication template with OTP + print("\nπŸ” WHATSAPP AUTHENTICATION TEMPLATE MESSAGE EXAMPLE") + print("-" * 50) + + print("πŸ“€ Sending authentication template message...") + + auth_template_request = WhatsAppTemplateMessageRequest( + to="+1234567890", + template=TemplateMessageTemplate( + name="devotel_otp", + language=TemplateMessageLanguage(code="en_US"), + components=[ + TemplateMessageComponent( + type="body", parameters=[TemplateMessageParameter(type="text", text="123456")] + ), + TemplateMessageComponent( + type="button", + sub_type="url", + index="0", + parameters=[TemplateMessageParameter(type="text", text="123456")], + ), + ], + ), + ) + + auth_template_response = client.whatsapp.send_template_message( + account_id="acc_123", template_message=auth_template_request + ) + + print("βœ… Authentication template message sent!") + print(f" πŸ†” Message ID: {auth_template_response.message_id}") + print(" πŸ” OTP Code: 123456") + print(f" πŸ“Š Status: {auth_template_response.status}") except DevoException as e: print(f"❌ WhatsApp operation failed: {e}") + except Exception as e: + print(f"❌ Unexpected error: {e}") print("\n" + "=" * 60) print("πŸ“Š WHATSAPP EXAMPLE SUMMARY") print("-" * 30) - print("⚠️ This is a placeholder example for WhatsApp functionality.") - print("πŸ’‘ To implement:") - print(" 1. Define WhatsApp API endpoints and specifications") - print(" 2. Create WhatsApp Pydantic models") - print(" 3. Implement WhatsAppResource class") - print(" 4. Update this example with real functionality") - print(" 5. Add support for text, media, and template messages") + print("βœ… WhatsApp API implementation complete!") + print("πŸ“€ Successfully demonstrated:") + print(" β€’ Getting WhatsApp accounts with pagination") + print(" β€’ Retrieving templates by name") + print(" β€’ Uploading files with proper metadata") + print(" β€’ Searching accounts with filters") + print(" β€’ Sending normal messages") + print(" β€’ Unicode and emoji support") + print(" β€’ Creating WhatsApp templates (utility, authentication, marketing)") + print(" β€’ Getting templates with filtering and pagination") + print(" β€’ Complex template components (headers, buttons, examples)") + print(" β€’ Sending template messages with parameters") + print(" β€’ Template messages with images, locations, documents") + print(" β€’ Authentication templates with OTP codes") + print(" β€’ Marketing templates with dynamic content") + print(" β€’ Template validation and error handling") + print(" β€’ Response parsing and error handling") + print("\nπŸ’‘ Available endpoints:") + print(" β€’ GET /user-api/whatsapp/accounts - Get shared accounts") + print(" β€’ GET /user-api/whatsapp/templates/{name} - Get template by name") + print(" β€’ POST /user-api/whatsapp/upload - Upload files") + print(" β€’ POST /user-api/whatsapp/send-normal-message - Send messages") + print(" β€’ POST /user-api/whatsapp/templates - Create templates") + print(" β€’ GET /user-api/whatsapp/templates - Get all templates") + print(" β€’ POST /user-api/whatsapp/send-message-by-template - Send template messages") if __name__ == "__main__": diff --git a/src/devo_global_comms_python/models/whatsapp.py b/src/devo_global_comms_python/models/whatsapp.py index 50b4639..2564a23 100644 --- a/src/devo_global_comms_python/models/whatsapp.py +++ b/src/devo_global_comms_python/models/whatsapp.py @@ -1,9 +1,311 @@ from datetime import datetime -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Literal, Optional, Union from pydantic import BaseModel, Field +# Template Component Models +class TemplateExample(BaseModel): + """Example values for template variables""" + + header_text: Optional[List[str]] = None + body_text: Optional[List[List[str]]] = None + header_handle: Optional[List[str]] = None + + +class OTPButton(BaseModel): + """OTP button for authentication templates""" + + type: Literal["OTP"] = "OTP" + otp_type: Literal["COPY_CODE", "ONE_TAP"] + text: str + autofill_text: Optional[str] = None + package_name: Optional[str] = None + signature_hash: Optional[str] = None + + +class CatalogButton(BaseModel): + """Catalog button for marketing templates""" + + type: Literal["CATALOG"] = "CATALOG" + text: str + + +class MPMButton(BaseModel): + """Multi-Product Message button""" + + type: Literal["MPM"] = "MPM" + text: str + + +class QuickReplyButton(BaseModel): + """Quick reply button""" + + type: Literal["QUICK_REPLY"] = "QUICK_REPLY" + text: str + + +class PhoneNumberButton(BaseModel): + """Phone number button""" + + type: Literal["PHONE_NUMBER"] = "PHONE_NUMBER" + text: str + phone_number: str + + +class URLButton(BaseModel): + """URL button with dynamic parameters""" + + type: Literal["URL"] = "URL" + text: str + url: str + example: Optional[List[str]] = None + + +class HeaderComponent(BaseModel): + """Header component with different formats""" + + type: Literal["HEADER"] = "HEADER" + format: Literal["TEXT", "IMAGE", "DOCUMENT", "LOCATION"] + text: Optional[str] = None + example: Optional[TemplateExample] = None + + +class BodyComponent(BaseModel): + """Body component with text and examples""" + + type: Literal["BODY"] = "BODY" + text: Optional[str] = None + add_security_recommendation: Optional[bool] = None + example: Optional[TemplateExample] = None + + +class FooterComponent(BaseModel): + """Footer component""" + + type: Literal["FOOTER"] = "FOOTER" + text: Optional[str] = None + code_expiration_minutes: Optional[int] = None + + +class ButtonsComponent(BaseModel): + """Buttons component containing various button types""" + + type: Literal["BUTTONS"] = "BUTTONS" + buttons: List[Union[OTPButton, CatalogButton, MPMButton, QuickReplyButton, PhoneNumberButton, URLButton]] + + +class WhatsAppTemplateRequest(BaseModel): + """WhatsApp template creation request""" + + name: str = Field(description="Template name") + language: str = Field(description="Template language code (e.g., 'en_US', 'en')") + category: Literal["AUTHENTICATION", "MARKETING", "UTILITY"] = Field(description="Template category") + components: List[Union[HeaderComponent, BodyComponent, FooterComponent, ButtonsComponent]] = Field( + description="Template components" + ) + + +class WhatsAppTemplateResponse(BaseModel): + """WhatsApp template creation response""" + + id: str = Field(description="Template ID") + name: str = Field(description="Template name") + language: str = Field(description="Template language") + category: str = Field(description="Template category") + status: str = Field(description="Template status") + components: List[Any] = Field(description="Template components") + created_at: Optional[datetime] = Field(None, description="Creation timestamp") + updated_at: Optional[datetime] = Field(None, description="Update timestamp") + + +class GetWhatsAppTemplatesResponse(BaseModel): + """Response for getting WhatsApp templates with pagination""" + + templates: List["WhatsAppTemplate"] = Field(description="List of templates") + total: int = Field(description="Total number of templates") + page: int = Field(description="Current page number") + limit: int = Field(description="Page limit") + has_next: bool = Field(description="Whether there are more pages") + + +# Template Message Sending Models +class ImageParameter(BaseModel): + """Image parameter for template messages""" + + link: str = Field(description="Image URL") + + +class DocumentParameter(BaseModel): + """Document parameter for template messages""" + + link: str = Field(description="Document URL") + filename: Optional[str] = Field(None, description="Document filename") + + +class LocationParameter(BaseModel): + """Location parameter for template messages""" + + latitude: str = Field(description="Latitude coordinate") + longitude: str = Field(description="Longitude coordinate") + name: Optional[str] = Field(None, description="Location name") + address: Optional[str] = Field(None, description="Location address") + + +class TemplateMessageParameter(BaseModel): + """Parameter for template message components""" + + type: Literal["text", "image", "location", "document", "payload"] + text: Optional[str] = Field(None, description="Text parameter value") + image: Optional[ImageParameter] = Field(None, description="Image parameter") + location: Optional[LocationParameter] = Field(None, description="Location parameter") + document: Optional[DocumentParameter] = Field(None, description="Document parameter") + payload: Optional[str] = Field(None, description="Payload parameter value") + + +class TemplateMessageComponent(BaseModel): + """Component for template message""" + + type: Literal["header", "body", "button"] + sub_type: Optional[Literal["url", "quick_reply", "phone_number"]] = Field(None, description="Button sub-type") + index: Optional[str] = Field(None, description="Button index") + parameters: List[TemplateMessageParameter] = Field(description="Component parameters") + + +class TemplateMessageLanguage(BaseModel): + """Language specification for template message""" + + code: str = Field(description="Language code (e.g., 'en_US', 'en')") + + +class TemplateMessageTemplate(BaseModel): + """Template specification for template message""" + + name: str = Field(description="Template name") + language: TemplateMessageLanguage = Field(description="Template language") + components: Optional[List[TemplateMessageComponent]] = Field( + None, description="Template components with parameters" + ) + + +class WhatsAppTemplateMessageRequest(BaseModel): + """Request for sending WhatsApp template message""" + + messaging_product: Literal["whatsapp"] = "whatsapp" + to: str = Field(description="Recipient phone number") + type: Literal["template"] = "template" + template: TemplateMessageTemplate = Field(description="Template configuration") + + +class WhatsAppTemplateMessageResponse(BaseModel): + """Response for WhatsApp template message send""" + + message_id: str = Field(description="Unique message identifier") + status: str = Field(description="Message status") + to: str = Field(description="Recipient phone number") + account_id: str = Field(description="WhatsApp account ID used") + timestamp: datetime = Field(description="Message send timestamp") + success: bool = Field(description="Whether the message was sent successfully") + + +# Request Models +class WhatsAppNormalMessageRequest(BaseModel): + """ + Request model for WhatsApp normal message send. + + Used for POST /api/v1/user-api/whatsapp/send-normal-message + """ + + to: str = Field(..., description="Recipient phone number in E.164 format") + message: str = Field(..., description="Message content") + account_id: Optional[str] = Field(None, description="WhatsApp account ID to send from") + + +class FileUploadRequest(BaseModel): + """ + Request model for WhatsApp file upload. + + Used for POST /api/v1/user-api/whatsapp/upload + """ + + file: bytes = Field(..., description="File content to upload") + filename: str = Field(..., description="Name of the file") + content_type: str = Field(..., description="MIME type of the file") + + +# Response Models +class WhatsAppAccount(BaseModel): + """WhatsApp account information model.""" + + id: str = Field(..., description="Account ID") + name: str = Field(..., description="Account name") + email: Optional[str] = Field(None, description="Account email") + phone: Optional[str] = Field(None, description="Account phone number") + is_approved: bool = Field(..., description="Whether the account is approved") + created_at: Optional[datetime] = Field(None, description="Account creation timestamp") + updated_at: Optional[datetime] = Field(None, description="Account last updated timestamp") + + +class GetWhatsAppAccountsResponse(BaseModel): + """ + Response model for getting WhatsApp accounts. + + Returned from GET /api/v1/user-api/whatsapp/accounts + """ + + accounts: List[WhatsAppAccount] = Field(..., description="List of WhatsApp accounts") + total: int = Field(..., description="Total number of accounts") + page: int = Field(..., description="Current page number") + limit: int = Field(..., description="Page size limit") + has_next: bool = Field(..., description="Whether there are more pages") + + +class WhatsAppTemplate(BaseModel): + """ + WhatsApp template model. + + Returned from GET /api/v1/user-api/whatsapp/templates/{name} + """ + + name: str = Field(..., description="Template name") + language: str = Field(..., description="Template language code") + status: str = Field(..., description="Template status") + category: str = Field(..., description="Template category") + components: List[Dict[str, Any]] = Field(..., description="Template components") + created_at: Optional[datetime] = Field(None, description="Template creation timestamp") + updated_at: Optional[datetime] = Field(None, description="Template last updated timestamp") + + +class WhatsAppUploadFileResponse(BaseModel): + """ + Response model for WhatsApp file upload. + + Returned from POST /api/v1/user-api/whatsapp/upload + """ + + file_id: str = Field(..., description="Unique file identifier") + filename: str = Field(..., description="Original filename") + file_size: int = Field(..., description="File size in bytes") + mime_type: str = Field(..., description="MIME type of the file") + url: str = Field(..., description="File URL") + expires_at: Optional[datetime] = Field(None, description="File expiration timestamp") + + +class WhatsAppSendMessageResponse(BaseModel): + """ + Response model for WhatsApp message send. + + Returned from POST /api/v1/user-api/whatsapp/send-normal-message + """ + + message_id: str = Field(..., description="Unique message identifier") + status: str = Field(..., description="Message status") + to: str = Field(..., description="Recipient phone number") + account_id: str = Field(..., description="WhatsApp account ID used") + timestamp: datetime = Field(..., description="Message send timestamp") + success: bool = Field(..., description="Whether the message was sent successfully") + + class WhatsAppMessage(BaseModel): """ WhatsApp message model. @@ -21,26 +323,18 @@ class WhatsAppMessage(BaseModel): # Content fields text: Optional[Dict[str, str]] = Field(None, description="Text message content") - template: Optional[Dict[str, Any]] = Field( - None, description="Template message content" - ) + template: Optional[Dict[str, Any]] = Field(None, description="Template message content") media: Optional[Dict[str, Any]] = Field(None, description="Media message content") # Metadata pricing: Optional[Dict[str, Any]] = Field(None, description="Pricing information") error_code: Optional[str] = Field(None, description="Error code if failed") error_message: Optional[str] = Field(None, description="Error message if failed") - date_created: Optional[datetime] = Field( - None, description="Message creation timestamp" - ) + date_created: Optional[datetime] = Field(None, description="Message creation timestamp") date_sent: Optional[datetime] = Field(None, description="Message sent timestamp") - date_delivered: Optional[datetime] = Field( - None, description="Message delivered timestamp" - ) + date_delivered: Optional[datetime] = Field(None, description="Message delivered timestamp") date_read: Optional[datetime] = Field(None, description="Message read timestamp") - date_updated: Optional[datetime] = Field( - None, description="Message last updated timestamp" - ) + date_updated: Optional[datetime] = Field(None, description="Message last updated timestamp") metadata: Optional[Dict[str, Any]] = Field(None, description="Custom metadata") class Config: diff --git a/src/devo_global_comms_python/resources/whatsapp.py b/src/devo_global_comms_python/resources/whatsapp.py index 777334d..b4fb0cc 100644 --- a/src/devo_global_comms_python/resources/whatsapp.py +++ b/src/devo_global_comms_python/resources/whatsapp.py @@ -4,11 +4,415 @@ from .base import BaseResource if TYPE_CHECKING: - from ..models.whatsapp import WhatsAppMessage + from ..models.whatsapp import ( + GetWhatsAppAccountsResponse, + GetWhatsAppTemplatesResponse, + WhatsAppMessage, + WhatsAppSendMessageResponse, + WhatsAppTemplate, + WhatsAppTemplateMessageRequest, + WhatsAppTemplateMessageResponse, + WhatsAppTemplateRequest, + WhatsAppTemplateResponse, + WhatsAppUploadFileResponse, + ) class WhatsAppResource(BaseResource): - """WhatsApp resource for sending and managing WhatsApp messages.""" + """ + WhatsApp resource for sending and managing WhatsApp messages. + + This resource provides access to WhatsApp functionality including: + - Managing WhatsApp accounts + - Getting templates + - Uploading files + - Sending messages + + Examples: + Get accounts: + >>> accounts = client.whatsapp.get_accounts() + >>> for account in accounts.accounts: + ... print(f"Account: {account.name} (Approved: {account.is_approved})") + + Get template: + >>> template = client.whatsapp.get_template("welcome_message") + >>> print(f"Template: {template.name} ({template.status})") + + Upload file: + >>> with open("image.jpg", "rb") as f: + ... upload = client.whatsapp.upload_file(f.read(), "image.jpg", "image/jpeg") + >>> print(f"File uploaded: {upload.file_id}") + """ + + def get_accounts( + self, + page: Optional[int] = None, + limit: Optional[int] = None, + is_approved: Optional[bool] = None, + search: Optional[str] = None, + ) -> "GetWhatsAppAccountsResponse": + """ + Get all shared WhatsApp accounts. + + Args: + page: The page number (optional) + limit: The page limit (optional) + is_approved: Filter by approval status (optional) + search: Search by name, email, phone (optional) + + Returns: + GetWhatsAppAccountsResponse: List of WhatsApp accounts with pagination info + + Raises: + DevoAPIException: If the API returns an error + + Example: + >>> accounts = client.whatsapp.get_accounts( + ... page=1, + ... limit=10, + ... is_approved=True + ... ) + >>> print(f"Found {accounts.total} accounts") + """ + params = {} + + if page is not None: + params["page"] = page + if limit is not None: + params["limit"] = limit + if is_approved is not None: + params["isApproved"] = is_approved + if search is not None: + params["search"] = search + + # Send request to the exact API endpoint + response = self.client.get("user-api/whatsapp/accounts", params=params) + + from ..models.whatsapp import GetWhatsAppAccountsResponse + + return GetWhatsAppAccountsResponse.model_validate(response.json()) + + def get_template(self, name: str) -> "WhatsAppTemplate": + """ + Get a WhatsApp template by name. + + Args: + name: Template name + + Returns: + WhatsAppTemplate: The template details + + Raises: + DevoValidationException: If the name is invalid + DevoAPIException: If the API returns an error (404 if not found) + + Example: + >>> template = client.whatsapp.get_template("welcome_message") + >>> print(f"Template: {template.name}") + >>> print(f"Status: {template.status}") + >>> print(f"Components: {len(template.components)}") + """ + name = validate_required_string(name, "name") + + # Send request to the exact API endpoint + response = self.client.get(f"user-api/whatsapp/templates/{name}") + + from ..models.whatsapp import WhatsAppTemplate + + return WhatsAppTemplate.model_validate(response.json()) + + def upload_file( + self, + file_content: bytes, + filename: str, + content_type: str, + ) -> "WhatsAppUploadFileResponse": + """ + Upload a file for WhatsApp messaging. + + Args: + file_content: The file content as bytes + filename: Name of the file + content_type: MIME type of the file + + Returns: + WhatsAppUploadFileResponse: File upload details including file_id and URL + + Raises: + DevoValidationException: If required fields are invalid + DevoAPIException: If the API returns an error (400 for unsupported file type) + + Example: + >>> with open("document.pdf", "rb") as f: + ... upload = client.whatsapp.upload_file( + ... f.read(), + ... "document.pdf", + ... "application/pdf" + ... ) + >>> print(f"File ID: {upload.file_id}") + >>> print(f"URL: {upload.url}") + """ + filename = validate_required_string(filename, "filename") + content_type = validate_required_string(content_type, "content_type") + + if not file_content: + from ..exceptions import DevoValidationException + + raise DevoValidationException("File content cannot be empty") + + # Prepare multipart form data + files = { + "file": (filename, file_content, content_type), + } + + # Send request to the exact API endpoint + response = self.client.post("user-api/whatsapp/upload", files=files) + + from ..models.whatsapp import WhatsAppUploadFileResponse + + return WhatsAppUploadFileResponse.model_validate(response.json()) + + def send_normal_message( + self, + to: str, + message: str, + account_id: Optional[str] = None, + ) -> "WhatsAppSendMessageResponse": + """ + Send a normal WhatsApp message. + + Args: + to: Recipient phone number in E.164 format + message: Message content + account_id: WhatsApp account ID to send from (optional) + + Returns: + WhatsAppSendMessageResponse: The message send response + + Raises: + DevoValidationException: If required fields are invalid + DevoAPIException: If the API returns an error + + Example: + >>> response = client.whatsapp.send_normal_message( + ... to="+1234567890", + ... message="Hello from WhatsApp!", + ... account_id="acc_123" + ... ) + >>> print(f"Message ID: {response.message_id}") + >>> print(f"Status: {response.status}") + """ + # Validate inputs + to = validate_phone_number(to) + message = validate_required_string(message, "message") + + # Prepare request data according to API spec + from ..models.whatsapp import WhatsAppNormalMessageRequest + + request_data = WhatsAppNormalMessageRequest( + to=to, + message=message, + account_id=account_id, + ) + + # Send request to the exact API endpoint + response = self.client.post( + "user-api/whatsapp/send-normal-message", json=request_data.model_dump(exclude_none=True) + ) + + from ..models.whatsapp import WhatsAppSendMessageResponse + + return WhatsAppSendMessageResponse.model_validate(response.json()) + + def create_template( + self, + account_id: str, + template: "WhatsAppTemplateRequest", + ) -> "WhatsAppTemplateResponse": + """ + Create a WhatsApp template. + + Args: + account_id: WhatsApp account ID + template: Template creation request data + + Returns: + WhatsAppTemplateResponse: The created template + + Raises: + DevoValidationException: If required fields are invalid + DevoAPIException: If the API returns an error + + Example: + >>> from ..models.whatsapp import ( + ... WhatsAppTemplateRequest, + ... BodyComponent, + ... FooterComponent + ... ) + >>> template_request = WhatsAppTemplateRequest( + ... name="welcome_message", + ... language="en_US", + ... category="UTILITY", + ... components=[ + ... BodyComponent(type="BODY", text="Welcome to our service!"), + ... FooterComponent(type="FOOTER", text="Thank you for choosing us") + ... ] + ... ) + >>> response = client.whatsapp.create_template( + ... account_id="acc_123", + ... template=template_request + ... ) + >>> print(f"Template ID: {response.id}") + """ + # Validate inputs + account_id = validate_required_string(account_id, "account_id") + + # Prepare query parameters + params = {"account_id": account_id} + + # Send request to the exact API endpoint + response = self.client.post( + "user-api/whatsapp/templates", params=params, json=template.model_dump(exclude_none=True) + ) + + from ..models.whatsapp import WhatsAppTemplateResponse + + return WhatsAppTemplateResponse.model_validate(response.json()) + + def get_templates( + self, + account_id: str, + page: Optional[int] = None, + limit: Optional[int] = None, + category: Optional[str] = None, + search: Optional[str] = None, + ) -> "GetWhatsAppTemplatesResponse": + """ + Get WhatsApp templates for an account. + + Args: + account_id: WhatsApp account ID (required) + page: Page number for pagination (optional) + limit: Number of templates per page (optional) + category: Filter by template category (AUTHENTICATION, MARKETING, UTILITY) + search: Search templates by name (optional) + + Returns: + GetWhatsAppTemplatesResponse: Paginated list of templates + + Raises: + DevoValidationException: If required fields are invalid + DevoAPIException: If the API returns an error + + Example: + >>> response = client.whatsapp.get_templates( + ... account_id="acc_123", + ... category="MARKETING", + ... page=1, + ... limit=10 + ... ) + >>> print(f"Found {response.total} templates") + >>> for template in response.templates: + ... print(f"Template: {template.name}") + """ + # Validate inputs + account_id = validate_required_string(account_id, "account_id") + + # Prepare query parameters + params = {"id": account_id} # Note: API uses 'id' parameter name for account_id + + if page is not None: + params["page"] = page + if limit is not None: + params["limit"] = limit + if category is not None: + # Validate category enum + valid_categories = ["AUTHENTICATION", "MARKETING", "UTILITY"] + if category not in valid_categories: + from ..exceptions import DevoValidationException + + raise DevoValidationException( + f"Invalid category '{category}'. Must be one of: {', '.join(valid_categories)}" + ) + params["category"] = category + if search is not None: + params["search"] = search + + # Send request to the exact API endpoint + response = self.client.get("user-api/whatsapp/templates", params=params) + + from ..models.whatsapp import GetWhatsAppTemplatesResponse + + return GetWhatsAppTemplatesResponse.model_validate(response.json()) + + def send_template_message( + self, + account_id: str, + template_message: "WhatsAppTemplateMessageRequest", + ) -> "WhatsAppTemplateMessageResponse": + """ + Send a WhatsApp template message. + + Args: + account_id: WhatsApp account ID + template_message: Template message request data + + Returns: + WhatsAppTemplateMessageResponse: The message send response + + Raises: + DevoValidationException: If required fields are invalid + DevoAPIException: If the API returns an error + + Example: + >>> from ..models.whatsapp import ( + ... WhatsAppTemplateMessageRequest, + ... TemplateMessageTemplate, + ... TemplateMessageLanguage, + ... TemplateMessageComponent, + ... TemplateMessageParameter + ... ) + >>> # Simple text template message + >>> template_request = WhatsAppTemplateMessageRequest( + ... to="+1234567890", + ... template=TemplateMessageTemplate( + ... name="welcome_message", + ... language=TemplateMessageLanguage(code="en_US"), + ... components=[ + ... TemplateMessageComponent( + ... type="body", + ... parameters=[ + ... TemplateMessageParameter( + ... type="text", + ... text="John Doe" + ... ) + ... ] + ... ) + ... ] + ... ) + ... ) + >>> response = client.whatsapp.send_template_message( + ... account_id="acc_123", + ... template_message=template_request + ... ) + >>> print(f"Message ID: {response.message_id}") + """ + # Validate inputs + account_id = validate_required_string(account_id, "account_id") + + # Prepare query parameters + params = {"account_id": account_id} + + # Send request to the exact API endpoint + response = self.client.post( + "user-api/whatsapp/send-message-by-template", + params=params, + json=template_message.model_dump(exclude_none=True), + ) + + from ..models.whatsapp import WhatsAppTemplateMessageResponse + + return WhatsAppTemplateMessageResponse.model_validate(response.json()) def send_text( self, diff --git a/tests/test_whatsapp.py b/tests/test_whatsapp.py new file mode 100644 index 0000000..b71b52b --- /dev/null +++ b/tests/test_whatsapp.py @@ -0,0 +1,1099 @@ +from unittest.mock import Mock + +import pytest + +from devo_global_comms_python.exceptions import DevoValidationException +from devo_global_comms_python.resources.whatsapp import WhatsAppResource + + +class TestWhatsAppResource: + """Test cases for the WhatsApp resource with new API implementation.""" + + @pytest.fixture + def whatsapp_resource(self, mock_client): + """Create a WhatsApp resource instance.""" + return WhatsAppResource(mock_client) + + def test_get_accounts_success(self, whatsapp_resource): + """Test getting WhatsApp accounts successfully.""" + # Setup mock response matching the API spec + mock_response = Mock() + mock_response.json.return_value = { + "accounts": [ + { + "id": "acc_123", + "name": "Test Account", + "email": "test@example.com", + "phone": "+1234567890", + "is_approved": True, + "created_at": "2024-01-01T12:00:00Z", + "updated_at": "2024-01-01T12:00:00Z", + } + ], + "total": 1, + "page": 1, + "limit": 10, + "has_next": False, + } + whatsapp_resource.client.get.return_value = mock_response + + # Test the get_accounts method + result = whatsapp_resource.get_accounts(page=1, limit=10, is_approved=True) + + # Verify the request was made correctly + whatsapp_resource.client.get.assert_called_once_with( + "user-api/whatsapp/accounts", + params={"page": 1, "limit": 10, "isApproved": True}, + ) + + # Verify the response is correctly parsed + assert len(result.accounts) == 1 + assert result.accounts[0].id == "acc_123" + assert result.accounts[0].name == "Test Account" + assert result.accounts[0].is_approved is True + assert result.total == 1 + assert result.page == 1 + assert result.limit == 10 + assert result.has_next is False + + def test_get_accounts_with_search(self, whatsapp_resource): + """Test getting WhatsApp accounts with search parameter.""" + mock_response = Mock() + mock_response.json.return_value = { + "accounts": [], + "total": 0, + "page": 1, + "limit": 10, + "has_next": False, + } + whatsapp_resource.client.get.return_value = mock_response + + result = whatsapp_resource.get_accounts(search="test@example.com") + + whatsapp_resource.client.get.assert_called_once_with( + "user-api/whatsapp/accounts", + params={"search": "test@example.com"}, + ) + + assert result.total == 0 + + def test_get_accounts_no_params(self, whatsapp_resource): + """Test getting WhatsApp accounts without any parameters.""" + mock_response = Mock() + mock_response.json.return_value = { + "accounts": [], + "total": 0, + "page": 1, + "limit": 10, + "has_next": False, + } + whatsapp_resource.client.get.return_value = mock_response + + result = whatsapp_resource.get_accounts() + + whatsapp_resource.client.get.assert_called_once_with( + "user-api/whatsapp/accounts", + params={}, + ) + + assert result.total == 0 + + def test_get_template_success(self, whatsapp_resource): + """Test getting a WhatsApp template successfully.""" + mock_response = Mock() + mock_response.json.return_value = { + "name": "welcome_message", + "language": "en", + "status": "APPROVED", + "category": "MARKETING", + "components": [ + { + "type": "BODY", + "text": "Welcome to our service, {{1}}!", + "parameters": [{"type": "TEXT"}], + } + ], + "created_at": "2024-01-01T12:00:00Z", + "updated_at": "2024-01-01T12:00:00Z", + } + whatsapp_resource.client.get.return_value = mock_response + + result = whatsapp_resource.get_template("welcome_message") + + whatsapp_resource.client.get.assert_called_once_with("user-api/whatsapp/templates/welcome_message") + + assert result.name == "welcome_message" + assert result.language == "en" + assert result.status == "APPROVED" + assert result.category == "MARKETING" + assert len(result.components) == 1 + + def test_get_template_missing_name(self, whatsapp_resource): + """Test that missing template name raises validation error.""" + with pytest.raises(DevoValidationException) as exc_info: + whatsapp_resource.get_template("") + assert "name" in str(exc_info.value) + + def test_upload_file_success(self, whatsapp_resource): + """Test uploading a file successfully.""" + mock_response = Mock() + mock_response.json.return_value = { + "file_id": "file_123456", + "filename": "document.pdf", + "file_size": 1024, + "mime_type": "application/pdf", + "url": "https://example.com/files/file_123456", + "expires_at": "2024-01-01T12:00:00Z", + } + whatsapp_resource.client.post.return_value = mock_response + + file_content = b"fake file content" + result = whatsapp_resource.upload_file(file_content, "document.pdf", "application/pdf") + + whatsapp_resource.client.post.assert_called_once_with( + "user-api/whatsapp/upload", + files={"file": ("document.pdf", file_content, "application/pdf")}, + ) + + assert result.file_id == "file_123456" + assert result.filename == "document.pdf" + assert result.file_size == 1024 + assert result.mime_type == "application/pdf" + assert result.url == "https://example.com/files/file_123456" + + def test_upload_file_missing_filename(self, whatsapp_resource): + """Test that missing filename raises validation error.""" + with pytest.raises(DevoValidationException) as exc_info: + whatsapp_resource.upload_file(b"content", "", "application/pdf") + assert "filename" in str(exc_info.value) + + def test_upload_file_missing_content_type(self, whatsapp_resource): + """Test that missing content type raises validation error.""" + with pytest.raises(DevoValidationException) as exc_info: + whatsapp_resource.upload_file(b"content", "file.pdf", "") + assert "content_type" in str(exc_info.value) + + def test_upload_file_empty_content(self, whatsapp_resource): + """Test that empty file content raises validation error.""" + with pytest.raises(DevoValidationException) as exc_info: + whatsapp_resource.upload_file(b"", "file.pdf", "application/pdf") + assert "File content cannot be empty" in str(exc_info.value) + + def test_upload_file_with_image(self, whatsapp_resource): + """Test uploading an image file.""" + mock_response = Mock() + mock_response.json.return_value = { + "file_id": "img_789012", + "filename": "image.jpg", + "file_size": 2048, + "mime_type": "image/jpeg", + "url": "https://example.com/files/img_789012", + "expires_at": "2024-01-01T12:00:00Z", + } + whatsapp_resource.client.post.return_value = mock_response + + fake_image = b"\xff\xd8\xff\xe0\x00\x10JFIF" # Fake JPEG header + result = whatsapp_resource.upload_file(fake_image, "image.jpg", "image/jpeg") + + whatsapp_resource.client.post.assert_called_once_with( + "user-api/whatsapp/upload", + files={"file": ("image.jpg", fake_image, "image/jpeg")}, + ) + + assert result.file_id == "img_789012" + assert result.mime_type == "image/jpeg" + + def test_get_accounts_with_all_params(self, whatsapp_resource): + """Test getting WhatsApp accounts with all parameters.""" + mock_response = Mock() + mock_response.json.return_value = { + "accounts": [ + { + "id": "acc_456", + "name": "Another Account", + "email": "another@example.com", + "phone": "+0987654321", + "is_approved": False, + "created_at": "2024-01-02T12:00:00Z", + "updated_at": "2024-01-02T12:00:00Z", + } + ], + "total": 5, + "page": 2, + "limit": 5, + "has_next": True, + } + whatsapp_resource.client.get.return_value = mock_response + + result = whatsapp_resource.get_accounts(page=2, limit=5, is_approved=False, search="another") + + whatsapp_resource.client.get.assert_called_once_with( + "user-api/whatsapp/accounts", + params={ + "page": 2, + "limit": 5, + "isApproved": False, + "search": "another", + }, + ) + + assert len(result.accounts) == 1 + assert result.accounts[0].is_approved is False + assert result.page == 2 + assert result.has_next is True + + def test_get_template_special_characters(self, whatsapp_resource): + """Test getting a template with special characters in name.""" + mock_response = Mock() + mock_response.json.return_value = { + "name": "special_template_123", + "language": "es", + "status": "APPROVED", + "category": "UTILITY", + "components": [], + "created_at": "2024-01-01T12:00:00Z", + "updated_at": "2024-01-01T12:00:00Z", + } + whatsapp_resource.client.get.return_value = mock_response + + result = whatsapp_resource.get_template("special_template_123") + + whatsapp_resource.client.get.assert_called_once_with("user-api/whatsapp/templates/special_template_123") + + assert result.name == "special_template_123" + assert result.language == "es" + + def test_upload_file_various_types(self, whatsapp_resource): + """Test uploading different file types.""" + test_cases = [ + ("document.pdf", "application/pdf"), + ("video.mp4", "video/mp4"), + ("audio.mp3", "audio/mpeg"), + ("image.png", "image/png"), + ] + + for filename, content_type in test_cases: + mock_response = Mock() + mock_response.json.return_value = { + "file_id": f"file_{filename.split('.')[0]}", + "filename": filename, + "file_size": 1024, + "mime_type": content_type, + "url": f"https://example.com/files/{filename}", + "expires_at": "2024-01-01T12:00:00Z", + } + whatsapp_resource.client.post.return_value = mock_response + + result = whatsapp_resource.upload_file(b"fake content", filename, content_type) + + assert result.filename == filename + assert result.mime_type == content_type + + def test_send_normal_message_success(self, whatsapp_resource): + """Test sending a normal WhatsApp message successfully.""" + mock_response = Mock() + mock_response.json.return_value = { + "message_id": "msg_123456", + "status": "sent", + "to": "+1234567890", + "account_id": "acc_123", + "timestamp": "2024-01-01T12:00:00Z", + "success": True, + } + whatsapp_resource.client.post.return_value = mock_response + + result = whatsapp_resource.send_normal_message( + to="+1234567890", message="Hello from WhatsApp!", account_id="acc_123" + ) + + whatsapp_resource.client.post.assert_called_once_with( + "user-api/whatsapp/send-normal-message", + json={ + "to": "+1234567890", + "message": "Hello from WhatsApp!", + "account_id": "acc_123", + }, + ) + + assert result.message_id == "msg_123456" + assert result.status == "sent" + assert result.to == "+1234567890" + assert result.account_id == "acc_123" + assert result.success is True + + def test_send_normal_message_without_account_id(self, whatsapp_resource): + """Test sending a normal WhatsApp message without account_id.""" + mock_response = Mock() + mock_response.json.return_value = { + "message_id": "msg_789012", + "status": "sent", + "to": "+0987654321", + "account_id": "default_acc", + "timestamp": "2024-01-01T12:00:00Z", + "success": True, + } + whatsapp_resource.client.post.return_value = mock_response + + result = whatsapp_resource.send_normal_message(to="+0987654321", message="Hello without account ID!") + + whatsapp_resource.client.post.assert_called_once_with( + "user-api/whatsapp/send-normal-message", + json={ + "to": "+0987654321", + "message": "Hello without account ID!", + }, + ) + + assert result.message_id == "msg_789012" + assert result.account_id == "default_acc" + + def test_send_normal_message_invalid_phone(self, whatsapp_resource): + """Test that invalid phone number raises validation error.""" + with pytest.raises(DevoValidationException) as exc_info: + whatsapp_resource.send_normal_message(to="invalid-phone", message="Hello!") + assert "phone" in str(exc_info.value) or "to" in str(exc_info.value) + + def test_send_normal_message_empty_message(self, whatsapp_resource): + """Test that empty message raises validation error.""" + with pytest.raises(DevoValidationException) as exc_info: + whatsapp_resource.send_normal_message(to="+1234567890", message="") + assert "message" in str(exc_info.value) + + def test_send_normal_message_long_content(self, whatsapp_resource): + """Test sending a long WhatsApp message.""" + mock_response = Mock() + mock_response.json.return_value = { + "message_id": "msg_long_123", + "status": "sent", + "to": "+1234567890", + "account_id": "acc_123", + "timestamp": "2024-01-01T12:00:00Z", + "success": True, + } + whatsapp_resource.client.post.return_value = mock_response + + long_message = "A" * 1000 # Long message + result = whatsapp_resource.send_normal_message(to="+1234567890", message=long_message, account_id="acc_123") + + whatsapp_resource.client.post.assert_called_once_with( + "user-api/whatsapp/send-normal-message", + json={ + "to": "+1234567890", + "message": long_message, + "account_id": "acc_123", + }, + ) + + assert result.success is True + + def test_send_normal_message_unicode_content(self, whatsapp_resource): + """Test sending a WhatsApp message with Unicode content.""" + mock_response = Mock() + mock_response.json.return_value = { + "message_id": "msg_unicode_123", + "status": "sent", + "to": "+1234567890", + "account_id": "acc_123", + "timestamp": "2024-01-01T12:00:00Z", + "success": True, + } + whatsapp_resource.client.post.return_value = mock_response + + unicode_message = "Hello 🌍! Ω…Ψ±Ψ­Ψ¨Ψ§ Ψ¨Ψ§Ω„ΨΉΨ§Ω„Ω… δ½ ε₯½δΈ–η•Œ" + result = whatsapp_resource.send_normal_message(to="+1234567890", message=unicode_message, account_id="acc_123") + + whatsapp_resource.client.post.assert_called_once_with( + "user-api/whatsapp/send-normal-message", + json={ + "to": "+1234567890", + "message": unicode_message, + "account_id": "acc_123", + }, + ) + + assert result.success is True + + def test_send_normal_message_api_error(self, whatsapp_resource): + """Test handling API errors when sending message.""" + mock_response = Mock() + mock_response.json.return_value = { + "message_id": "", + "status": "failed", + "to": "+1234567890", + "account_id": "acc_123", + "timestamp": "2024-01-01T12:00:00Z", + "success": False, + } + whatsapp_resource.client.post.return_value = mock_response + + result = whatsapp_resource.send_normal_message(to="+1234567890", message="Hello!", account_id="acc_123") + + assert result.success is False + assert result.status == "failed" + + # Template Tests + def test_create_template_authentication_success(self, whatsapp_resource): + """Test creating an authentication template successfully.""" + from devo_global_comms_python.models.whatsapp import ( + BodyComponent, + ButtonsComponent, + FooterComponent, + OTPButton, + WhatsAppTemplateRequest, + ) + + # Create authentication template request + template_request = WhatsAppTemplateRequest( + name="authentication_code_copy_code_button", + language="en_US", + category="AUTHENTICATION", + components=[ + BodyComponent(type="BODY", add_security_recommendation=True), + FooterComponent(type="FOOTER", code_expiration_minutes=10), + ButtonsComponent( + type="BUTTONS", buttons=[OTPButton(type="OTP", otp_type="COPY_CODE", text="Copy Code")] + ), + ], + ) + + # Setup mock response + mock_response = Mock() + mock_response.json.return_value = { + "id": "template_123", + "name": "authentication_code_copy_code_button", + "language": "en_US", + "category": "AUTHENTICATION", + "status": "PENDING", + "components": [], + "created_at": "2024-01-01T12:00:00Z", + "updated_at": "2024-01-01T12:00:00Z", + } + whatsapp_resource.client.post.return_value = mock_response + + result = whatsapp_resource.create_template(account_id="acc_123", template=template_request) + + # Verify API call + whatsapp_resource.client.post.assert_called_once_with( + "user-api/whatsapp/templates", + params={"account_id": "acc_123"}, + json={ + "name": "authentication_code_copy_code_button", + "language": "en_US", + "category": "AUTHENTICATION", + "components": [ + {"type": "BODY", "add_security_recommendation": True}, + {"type": "FOOTER", "code_expiration_minutes": 10}, + {"type": "BUTTONS", "buttons": [{"type": "OTP", "otp_type": "COPY_CODE", "text": "Copy Code"}]}, + ], + }, + ) + + assert result.id == "template_123" + assert result.name == "authentication_code_copy_code_button" + assert result.category == "AUTHENTICATION" + + def test_create_template_marketing_with_buttons(self, whatsapp_resource): + """Test creating a marketing template with various button types.""" + from devo_global_comms_python.models.whatsapp import ( + BodyComponent, + ButtonsComponent, + FooterComponent, + HeaderComponent, + PhoneNumberButton, + TemplateExample, + URLButton, + WhatsAppTemplateRequest, + ) + + # Create marketing template with complex structure + template_request = WhatsAppTemplateRequest( + name="limited_time_offer", + language="en_US", + category="MARKETING", + components=[ + HeaderComponent(type="HEADER", format="IMAGE", example=TemplateExample(header_handle=["4::aW..."])), + BodyComponent( + type="BODY", + text="Hi {{1}}! For a limited time only you can get our {{2}} for as low as {{3}}.", + example=TemplateExample(body_text=[["Mark", "Tuscan Getaway package", "800"]]), + ), + FooterComponent(type="FOOTER", text="Offer valid until May 31, 2023"), + ButtonsComponent( + type="BUTTONS", + buttons=[ + PhoneNumberButton(type="PHONE_NUMBER", text="Call", phone_number="15550051310"), + URLButton( + type="URL", + text="Shop Now", + url="https://www.examplesite.com/shop?promo={{1}}", + example=["summer2023"], + ), + ], + ), + ], + ) + + # Setup mock response + mock_response = Mock() + mock_response.json.return_value = { + "id": "template_456", + "name": "limited_time_offer", + "language": "en_US", + "category": "MARKETING", + "status": "APPROVED", + "components": [], + "created_at": "2024-01-01T12:00:00Z", + } + whatsapp_resource.client.post.return_value = mock_response + + result = whatsapp_resource.create_template(account_id="acc_456", template=template_request) + + assert result.id == "template_456" + assert result.status == "APPROVED" + + def test_create_template_missing_account_id(self, whatsapp_resource): + """Test template creation with missing account_id.""" + from devo_global_comms_python.models.whatsapp import BodyComponent, WhatsAppTemplateRequest + + template_request = WhatsAppTemplateRequest( + name="test_template", + language="en_US", + category="UTILITY", + components=[BodyComponent(type="BODY", text="Test message")], + ) + + with pytest.raises(DevoValidationException, match="account_id is required"): + whatsapp_resource.create_template(account_id="", template=template_request) + + def test_get_templates_success(self, whatsapp_resource): + """Test getting templates successfully.""" + # Setup mock response + mock_response = Mock() + mock_response.json.return_value = { + "templates": [ + { + "name": "welcome_message", + "language": "en_US", + "status": "APPROVED", + "category": "UTILITY", + "components": [], + "created_at": "2024-01-01T12:00:00Z", + }, + { + "name": "promotional_offer", + "language": "en_US", + "status": "PENDING", + "category": "MARKETING", + "components": [], + "created_at": "2024-01-01T11:00:00Z", + }, + ], + "total": 2, + "page": 1, + "limit": 10, + "has_next": False, + } + whatsapp_resource.client.get.return_value = mock_response + + result = whatsapp_resource.get_templates(account_id="acc_123", page=1, limit=10, category="MARKETING") + + # Verify API call + whatsapp_resource.client.get.assert_called_once_with( + "user-api/whatsapp/templates", params={"id": "acc_123", "page": 1, "limit": 10, "category": "MARKETING"} + ) + + assert len(result.templates) == 2 + assert result.total == 2 + assert result.page == 1 + assert result.has_next is False + + def test_get_templates_with_search(self, whatsapp_resource): + """Test getting templates with search parameter.""" + mock_response = Mock() + mock_response.json.return_value = { + "templates": [ + { + "name": "welcome_message", + "language": "en_US", + "status": "APPROVED", + "category": "UTILITY", + "components": [], + } + ], + "total": 1, + "page": 1, + "limit": 10, + "has_next": False, + } + whatsapp_resource.client.get.return_value = mock_response + + result = whatsapp_resource.get_templates(account_id="acc_123", search="welcome") + + whatsapp_resource.client.get.assert_called_once_with( + "user-api/whatsapp/templates", params={"id": "acc_123", "search": "welcome"} + ) + + assert len(result.templates) == 1 + assert result.templates[0].name == "welcome_message" + + def test_get_templates_invalid_category(self, whatsapp_resource): + """Test templates retrieval with invalid category.""" + with pytest.raises(DevoValidationException, match="Invalid category"): + whatsapp_resource.get_templates(account_id="acc_123", category="INVALID_CATEGORY") + + def test_get_templates_missing_account_id(self, whatsapp_resource): + """Test templates retrieval with missing account_id.""" + with pytest.raises(DevoValidationException, match="account_id is required"): + whatsapp_resource.get_templates(account_id="") + + def test_create_template_utility_with_location(self, whatsapp_resource): + """Test creating a utility template with location header.""" + from devo_global_comms_python.models.whatsapp import ( + BodyComponent, + ButtonsComponent, + FooterComponent, + HeaderComponent, + QuickReplyButton, + TemplateExample, + WhatsAppTemplateRequest, + ) + + template_request = WhatsAppTemplateRequest( + name="order_delivery_update", + language="en_US", + category="UTILITY", + components=[ + HeaderComponent(type="HEADER", format="LOCATION"), + BodyComponent( + type="BODY", + text="Good news {{1}}! Your order #{{2}} is on its way to the location above.", + example=TemplateExample(body_text=[["Mark", "566701"]]), + ), + FooterComponent(type="FOOTER", text="To stop receiving delivery updates, tap the button below."), + ButtonsComponent( + type="BUTTONS", buttons=[QuickReplyButton(type="QUICK_REPLY", text="Stop Delivery Updates")] + ), + ], + ) + + mock_response = Mock() + mock_response.json.return_value = { + "id": "template_789", + "name": "order_delivery_update", + "language": "en_US", + "category": "UTILITY", + "status": "APPROVED", + "components": [], + } + whatsapp_resource.client.post.return_value = mock_response + + result = whatsapp_resource.create_template(account_id="acc_789", template=template_request) + + assert result.id == "template_789" + assert result.category == "UTILITY" + + def test_create_template_with_catalog_button(self, whatsapp_resource): + """Test creating template with catalog button.""" + from devo_global_comms_python.models.whatsapp import ( + BodyComponent, + ButtonsComponent, + CatalogButton, + FooterComponent, + TemplateExample, + WhatsAppTemplateRequest, + ) + + template_request = WhatsAppTemplateRequest( + name="intro_catalog_offer", + language="en_US", + category="MARKETING", + components=[ + BodyComponent( + type="BODY", + text="Now shop for your favourite products right here on WhatsApp! " + "Get Rs {{1}} off on all orders above {{2}}Rs!", + example=TemplateExample(body_text=[["100", "400", "3"]]), + ), + FooterComponent(type="FOOTER", text="Best grocery deals on WhatsApp!"), + ButtonsComponent(type="BUTTONS", buttons=[CatalogButton(type="CATALOG", text="View catalog")]), + ], + ) + + mock_response = Mock() + mock_response.json.return_value = { + "id": "template_catalog", + "name": "intro_catalog_offer", + "language": "en_US", + "category": "MARKETING", + "status": "PENDING", + "components": [], + } + whatsapp_resource.client.post.return_value = mock_response + + result = whatsapp_resource.create_template(account_id="acc_catalog", template=template_request) + + assert result.id == "template_catalog" + assert result.name == "intro_catalog_offer" + + # Template Message Sending Tests + def test_send_template_message_authentication_success(self, whatsapp_resource): + """Test sending an authentication template message.""" + from devo_global_comms_python.models.whatsapp import ( + TemplateMessageComponent, + TemplateMessageLanguage, + TemplateMessageParameter, + TemplateMessageTemplate, + WhatsAppTemplateMessageRequest, + ) + + # Create authentication template message + template_request = WhatsAppTemplateMessageRequest( + to="905447423184", + template=TemplateMessageTemplate( + name="devotel_otp", + language=TemplateMessageLanguage(code="en_US"), + components=[ + TemplateMessageComponent( + type="body", parameters=[TemplateMessageParameter(type="text", text="123456")] + ), + TemplateMessageComponent( + type="button", + sub_type="url", + index="0", + parameters=[TemplateMessageParameter(type="text", text="123456")], + ), + ], + ), + ) + + # Setup mock response + mock_response = Mock() + mock_response.json.return_value = { + "message_id": "msg_auth_123", + "status": "sent", + "to": "905447423184", + "account_id": "acc_123", + "timestamp": "2024-01-01T12:00:00Z", + "success": True, + } + whatsapp_resource.client.post.return_value = mock_response + + result = whatsapp_resource.send_template_message(account_id="acc_123", template_message=template_request) + + # Verify API call + whatsapp_resource.client.post.assert_called_once_with( + "user-api/whatsapp/send-message-by-template", + params={"account_id": "acc_123"}, + json={ + "messaging_product": "whatsapp", + "to": "905447423184", + "type": "template", + "template": { + "name": "devotel_otp", + "language": {"code": "en_US"}, + "components": [ + {"type": "body", "parameters": [{"type": "text", "text": "123456"}]}, + { + "type": "button", + "sub_type": "url", + "index": "0", + "parameters": [{"type": "text", "text": "123456"}], + }, + ], + }, + }, + ) + + assert result.message_id == "msg_auth_123" + assert result.success is True + + def test_send_template_message_with_image_header(self, whatsapp_resource): + """Test sending template message with image header.""" + from devo_global_comms_python.models.whatsapp import ( + ImageParameter, + TemplateMessageComponent, + TemplateMessageLanguage, + TemplateMessageParameter, + TemplateMessageTemplate, + WhatsAppTemplateMessageRequest, + ) + + template_request = WhatsAppTemplateMessageRequest( + to="+1234567890", + template=TemplateMessageTemplate( + name="limited_time_offer_tuscan_getaway_2023", + language=TemplateMessageLanguage(code="en_US"), + components=[ + TemplateMessageComponent( + type="header", + parameters=[ + TemplateMessageParameter( + type="image", image=ImageParameter(link="https://example.com/image.jpg") + ) + ], + ), + TemplateMessageComponent( + type="body", + parameters=[ + TemplateMessageParameter(type="text", text="Mark"), + TemplateMessageParameter(type="text", text="Tuscan Getaway package"), + TemplateMessageParameter(type="text", text="800"), + ], + ), + ], + ), + ) + + mock_response = Mock() + mock_response.json.return_value = { + "message_id": "msg_image_456", + "status": "sent", + "to": "+1234567890", + "account_id": "acc_456", + "timestamp": "2024-01-01T12:00:00Z", + "success": True, + } + whatsapp_resource.client.post.return_value = mock_response + + result = whatsapp_resource.send_template_message(account_id="acc_456", template_message=template_request) + + assert result.message_id == "msg_image_456" + assert result.success is True + + def test_send_template_message_with_location(self, whatsapp_resource): + """Test sending template message with location parameter.""" + from devo_global_comms_python.models.whatsapp import ( + LocationParameter, + TemplateMessageComponent, + TemplateMessageLanguage, + TemplateMessageParameter, + TemplateMessageTemplate, + WhatsAppTemplateMessageRequest, + ) + + template_request = WhatsAppTemplateMessageRequest( + to="+1234567890", + template=TemplateMessageTemplate( + name="order_delivery_update", + language=TemplateMessageLanguage(code="en_US"), + components=[ + TemplateMessageComponent( + type="header", + parameters=[ + TemplateMessageParameter( + type="location", + location=LocationParameter( + latitude="37.7749", + longitude="-122.4194", + name="Delivery Location", + address="San Francisco, CA", + ), + ) + ], + ), + TemplateMessageComponent( + type="body", + parameters=[ + TemplateMessageParameter(type="text", text="Mark"), + TemplateMessageParameter(type="text", text="566701"), + ], + ), + ], + ), + ) + + mock_response = Mock() + mock_response.json.return_value = { + "message_id": "msg_location_789", + "status": "sent", + "to": "+1234567890", + "account_id": "acc_789", + "timestamp": "2024-01-01T12:00:00Z", + "success": True, + } + whatsapp_resource.client.post.return_value = mock_response + + result = whatsapp_resource.send_template_message(account_id="acc_789", template_message=template_request) + + assert result.message_id == "msg_location_789" + + def test_send_template_message_with_document(self, whatsapp_resource): + """Test sending template message with document parameter.""" + from devo_global_comms_python.models.whatsapp import ( + DocumentParameter, + TemplateMessageComponent, + TemplateMessageLanguage, + TemplateMessageParameter, + TemplateMessageTemplate, + WhatsAppTemplateMessageRequest, + ) + + template_request = WhatsAppTemplateMessageRequest( + to="+1234567890", + template=TemplateMessageTemplate( + name="order_confirmation", + language=TemplateMessageLanguage(code="en_US"), + components=[ + TemplateMessageComponent( + type="header", + parameters=[ + TemplateMessageParameter( + type="document", + document=DocumentParameter( + link="https://example.com/receipt.pdf", filename="OrderReceipt.pdf" + ), + ) + ], + ), + TemplateMessageComponent( + type="body", + parameters=[ + TemplateMessageParameter(type="text", text="Mark"), + TemplateMessageParameter(type="text", text="860198-230332"), + ], + ), + ], + ), + ) + + mock_response = Mock() + mock_response.json.return_value = { + "message_id": "msg_doc_101112", + "status": "sent", + "to": "+1234567890", + "account_id": "acc_101112", + "timestamp": "2024-01-01T12:00:00Z", + "success": True, + } + whatsapp_resource.client.post.return_value = mock_response + + result = whatsapp_resource.send_template_message(account_id="acc_101112", template_message=template_request) + + assert result.message_id == "msg_doc_101112" + + def test_send_template_message_with_buttons_payload(self, whatsapp_resource): + """Test sending template message with button payload parameters.""" + from devo_global_comms_python.models.whatsapp import ( + TemplateMessageComponent, + TemplateMessageLanguage, + TemplateMessageParameter, + TemplateMessageTemplate, + WhatsAppTemplateMessageRequest, + ) + + template_request = WhatsAppTemplateMessageRequest( + to="+1234567890", + template=TemplateMessageTemplate( + name="seasonal_promotion_text_only", + language=TemplateMessageLanguage(code="en"), + components=[ + TemplateMessageComponent( + type="header", parameters=[TemplateMessageParameter(type="text", text="Summer Sale")] + ), + TemplateMessageComponent( + type="body", + parameters=[ + TemplateMessageParameter(type="text", text="the end of August"), + TemplateMessageParameter(type="text", text="25OFF"), + TemplateMessageParameter(type="text", text="25%"), + ], + ), + TemplateMessageComponent( + type="button", + sub_type="quick_reply", + index="0", + parameters=[TemplateMessageParameter(type="payload", payload="UNSUBSCRIBE_PROMOS")], + ), + TemplateMessageComponent( + type="button", + sub_type="quick_reply", + index="1", + parameters=[TemplateMessageParameter(type="payload", payload="UNSUBSCRIBE_ALL")], + ), + ], + ), + ) + + mock_response = Mock() + mock_response.json.return_value = { + "message_id": "msg_buttons_131415", + "status": "sent", + "to": "+1234567890", + "account_id": "acc_131415", + "timestamp": "2024-01-01T12:00:00Z", + "success": True, + } + whatsapp_resource.client.post.return_value = mock_response + + result = whatsapp_resource.send_template_message(account_id="acc_131415", template_message=template_request) + + assert result.message_id == "msg_buttons_131415" + + def test_send_template_message_missing_account_id(self, whatsapp_resource): + """Test template message sending with missing account_id.""" + from devo_global_comms_python.models.whatsapp import ( + TemplateMessageLanguage, + TemplateMessageTemplate, + WhatsAppTemplateMessageRequest, + ) + + template_request = WhatsAppTemplateMessageRequest( + to="+1234567890", + template=TemplateMessageTemplate(name="test_template", language=TemplateMessageLanguage(code="en_US")), + ) + + with pytest.raises(DevoValidationException, match="account_id is required"): + whatsapp_resource.send_template_message(account_id="", template_message=template_request) + + def test_send_template_message_catalog_offer(self, whatsapp_resource): + """Test sending catalog offer template message.""" + from devo_global_comms_python.models.whatsapp import ( + TemplateMessageComponent, + TemplateMessageLanguage, + TemplateMessageParameter, + TemplateMessageTemplate, + WhatsAppTemplateMessageRequest, + ) + + template_request = WhatsAppTemplateMessageRequest( + to="+1234567890", + template=TemplateMessageTemplate( + name="intro_catalog_offer", + language=TemplateMessageLanguage(code="en_US"), + components=[ + TemplateMessageComponent( + type="body", + parameters=[ + TemplateMessageParameter(type="text", text="100"), + TemplateMessageParameter(type="text", text="400"), + TemplateMessageParameter(type="text", text="3"), + ], + ), + TemplateMessageComponent( + type="button", + sub_type="url", + index="0", + parameters=[ + TemplateMessageParameter(type="text", text="View Catalog"), + TemplateMessageParameter(type="payload", payload="CATALOG_URL"), + ], + ), + ], + ), + ) + + mock_response = Mock() + mock_response.json.return_value = { + "message_id": "msg_catalog_161718", + "status": "sent", + "to": "+1234567890", + "account_id": "acc_161718", + "timestamp": "2024-01-01T12:00:00Z", + "success": True, + } + whatsapp_resource.client.post.return_value = mock_response + + result = whatsapp_resource.send_template_message(account_id="acc_161718", template_message=template_request) + + assert result.message_id == "msg_catalog_161718" + assert result.success is True