From c50544d4bd9f74610950a08d7b75a42d684136dd Mon Sep 17 00:00:00 2001 From: dvirlabs Date: Sun, 1 Mar 2026 03:17:24 +0200 Subject: [PATCH] fix: add missing send_by_template_key method to WhatsAppService --- backend/whatsapp.py | 117 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/backend/whatsapp.py b/backend/whatsapp.py index aaa00ad..8452def 100644 --- a/backend/whatsapp.py +++ b/backend/whatsapp.py @@ -411,6 +411,123 @@ class WhatsAppService: parameters=parameters ) + async def send_by_template_key( + self, + template_key: str, + to_phone: str, + params: dict, + ) -> dict: + """ + Send a WhatsApp template message using the template registry. + + Looks up *template_key* in whatsapp_templates.py, resolves header and + body parameter lists (with fallbacks) from *params*, then builds and + sends the Meta API payload dynamically. + + Args: + template_key: Registry key (e.g. "wedding_invitation"). + to_phone: Recipient phone number (normalized to E.164). + params: Dict of {param_key: value} for all placeholders. + + Returns: + dict with message_id and status. + """ + from whatsapp_templates import get_template, build_params_list + + tpl = get_template(template_key) + meta_name = tpl["meta_name"] + language_code = tpl.get("language_code", "he") + + header_values, body_values = build_params_list(template_key, params) + + to_e164 = self.normalize_phone_to_e164(to_phone) + if not self.validate_phone(to_e164): + raise WhatsAppError(f"Invalid phone number: {to_phone}") + + components = [] + if header_values: + components.append({ + "type": "header", + "parameters": [{"type": "text", "text": str(v)} for v in header_values], + }) + if body_values: + components.append({ + "type": "body", + "parameters": [{"type": "text", "text": str(v)} for v in body_values], + }) + + # Handle url_button component if defined in template + url_btn = tpl.get("url_button", {}) + if url_btn and url_btn.get("enabled"): + param_key = url_btn.get("param_key", "event_id") + btn_value = str(params.get(param_key, "")).strip() + if btn_value: + components.append({ + "type": "button", + "sub_type": "url", + "index": str(url_btn.get("button_index", 0)), + "parameters": [{"type": "text", "text": btn_value}], + }) + + payload = { + "messaging_product": "whatsapp", + "to": to_e164, + "type": "template", + "template": { + "name": meta_name, + "language": {"code": language_code}, + "components": components, + }, + } + + import json + logger.info( + f"[WhatsApp] send_by_template_key '{template_key}' → meta='{meta_name}' " + f"lang={language_code} to={to_e164} " + f"header_params={header_values} body_params={body_values}" + ) + logger.debug( + "[WhatsApp] payload: %s", + json.dumps(payload, ensure_ascii=False), + ) + + url = f"{self.base_url}/{self.phone_number_id}/messages" + try: + async with httpx.AsyncClient() as client: + response = await client.post( + url, + json=payload, + headers=self.headers, + timeout=30.0, + ) + + if response.status_code not in (200, 201): + error_data = response.json() + error_msg = error_data.get("error", {}).get("message", "Unknown error") + logger.error(f"[WhatsApp] API error ({response.status_code}): {error_msg}") + raise WhatsAppError( + f"WhatsApp API error ({response.status_code}): {error_msg}" + ) + + result = response.json() + message_id = result.get("messages", [{}])[0].get("id") + logger.info(f"[WhatsApp] Message sent successfully via template key. ID: {message_id}") + return { + "message_id": message_id, + "status": "sent", + "to": to_e164, + "timestamp": datetime.utcnow().isoformat(), + "type": "template", + "template": meta_name, + } + + except httpx.HTTPError as e: + raise WhatsAppError(f"HTTP request failed: {str(e)}") + except WhatsAppError: + raise + except Exception as e: + raise WhatsAppError(f"Failed to send WhatsApp template: {str(e)}") + def handle_webhook_verification(self, challenge: str) -> str: """ Handle webhook verification challenge from Meta