diff --git a/WHATSAPP_FIX_SUMMARY.md b/WHATSAPP_FIX_SUMMARY.md index e4b39cc..9360fe4 100644 --- a/WHATSAPP_FIX_SUMMARY.md +++ b/WHATSAPP_FIX_SUMMARY.md @@ -36,7 +36,7 @@ This error occurred because: "components": [{ // ✅ Correct structure "type": "body", "parameters": [ - {"type": "text", "text": "דוד"}, + {"type": "text", "text": "דביר"}, {"type": "text", "text": "שרה"}, ... ] @@ -53,8 +53,8 @@ Your template has **7 variables** that MUST be sent in this EXACT order: | Placeholder | Field | Example | Fallback | |------------|-------|---------|----------| -| `{{1}}` | Guest name | "דוד" | "חבר" | -| `{{2}}` | Groom name | "דוד" | "החתן" | +| `{{1}}` | Guest name | "דביר" | "חבר" | +| `{{2}}` | Groom name | "דביר" | "החתן" | | `{{3}}` | Bride name | "שרה" | "הכלה" | | `{{4}}` | Hall/Venue | "אולם בן-גוריון" | "האולם" | | `{{5}}` | Event date | "15/06" | "—" | @@ -106,7 +106,7 @@ Before sending to Meta API, logs show: ``` [WhatsApp] Sending template 'wedding_invitation' Language: he, To: +972541234567, -Params (7): ['דוד', 'דוד', 'שרה', 'אולם בן-גוריון', '15/06', '18:30', 'https://invy...guest'] +Params (7): ['דביר', 'דביר', 'שרה', 'אולם בן-גוריון', '15/06', '18:30', 'https://invy...guest'] ``` On success: diff --git a/WHATSAPP_INTEGRATION.md b/WHATSAPP_INTEGRATION.md index 1d3a4be..c3e57bd 100644 --- a/WHATSAPP_INTEGRATION.md +++ b/WHATSAPP_INTEGRATION.md @@ -111,7 +111,7 @@ The approved Meta template body (in Hebrew): **Auto-filled by system:** - `{{1}}` = Guest first name (or "חבר" if empty) -- `{{2}}` = `event.partner1_name` (e.g., "דוד") +- `{{2}}` = `event.partner1_name` (e.g., "דביר") - `{{3}}` = `event.partner2_name` (e.g., "וורד") - `{{4}}` = `event.venue` (e.g., "אולם כלות גן עדן") - `{{5}}` = `event.date` formatted as DD/MM (e.g., "25/02") @@ -157,7 +157,7 @@ Content-Type: application/json Response: { "guest_id": "uuid", - "guest_name": "דוד", + "guest_name": "דביר", "phone": "+972541234567", "status": "sent" | "failed", "message_id": "wamid.xxx...", diff --git a/backend/custom_templates.json b/backend/custom_templates.json index 86dc5fe..b9f53c9 100644 --- a/backend/custom_templates.json +++ b/backend/custom_templates.json @@ -20,8 +20,8 @@ "שם החתן" ], "fallbacks": { - "contact_name": "דוד", - "groom_name": "דוד", + "contact_name": "דביר", + "groom_name": "דביר", "bride_name": "ורד", "venue": "אולם הגן", "event_date": "15/06", diff --git a/backend/main.py b/backend/main.py index dcc31e0..790f631 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1249,15 +1249,17 @@ def get_event_guest_by_phone( ).first() if not guest: - raise HTTPException( - status_code=404, - detail="לא נמצאת ברשימת האורחים לאירוע זה. אנא בדוק את מספר הטלפון.", - ) + # Guest not in list — allow self-service registration instead of blocking + return { + "found": False, + "phone_number": normalized or phone, + } return { + "found": True, "guest_id": str(guest.id), - "first_name": guest.first_name, - "last_name": guest.last_name, + # NOTE: first_name / last_name intentionally omitted so the guest + # never sees the host's contact nickname — they enter their own name. "rsvp_status": guest.rsvp_status, "meal_preference": guest.meal_preference, "has_plus_one": guest.has_plus_one, @@ -1290,10 +1292,52 @@ def submit_event_rsvp( ).first() if not guest: - raise HTTPException( - status_code=404, - detail="לא נמצאת ברשימת האורחים לאירוע זה.", + # Guest not pre-imported — create them as a self-service entry + event_obj = db.query(models.Event).filter(models.Event.id == event_id).first() + if not event_obj: + raise HTTPException(status_code=404, detail="האירוע לא נמצא.") + + # Find the event admin to use as added_by_user_id + admin_member = ( + db.query(models.EventMember) + .filter( + models.EventMember.event_id == event_id, + models.EventMember.role == models.RoleEnum.admin, + ) + .first() ) + if not admin_member: + admin_member = ( + db.query(models.EventMember) + .filter(models.EventMember.event_id == event_id) + .first() + ) + if not admin_member: + raise HTTPException(status_code=404, detail="האירוע לא נמצא.") + + guest = models.Guest( + event_id=event_id, + added_by_user_id=admin_member.user_id, + first_name=data.first_name or "", + last_name=data.last_name or "", + phone_number=normalized, + phone=normalized, + rsvp_status=data.rsvp_status or models.GuestStatus.invited, + meal_preference=data.meal_preference, + has_plus_one=data.has_plus_one or False, + plus_one_name=data.plus_one_name, + source="self-service", + ) + db.add(guest) + db.commit() + db.refresh(guest) + + return { + "success": True, + "message": "תודה! אישור ההגעה שלך נשמר.", + "guest_id": str(guest.id), + "rsvp_status": guest.rsvp_status, + } if data.rsvp_status is not None: guest.rsvp_status = data.rsvp_status diff --git a/backend/test_combinations.py b/backend/test_combinations.py index a612b71..85e5b5b 100644 --- a/backend/test_combinations.py +++ b/backend/test_combinations.py @@ -35,7 +35,7 @@ async def test_combinations(): "language": {"code": "he"}, "components": [{ "type": "header", - "parameters": [{"type": "text", "text": "דוד"}] + "parameters": [{"type": "text", "text": "דביר"}] }] } }), @@ -47,7 +47,7 @@ async def test_combinations(): "name": "wedding_invitation", "language": {"code": "he"}, "components": [ - {"type": "header", "parameters": [{"type": "text", "text": "דוד"}]}, + {"type": "header", "parameters": [{"type": "text", "text": "דביר"}]}, {"type": "body", "parameters": [ {"type": "text", "text": "p1"}, {"type": "text", "text": "p2"}, @@ -68,7 +68,7 @@ async def test_combinations(): "language": {"code": "he"}, "components": [{ "type": "body", - "parameters": [{"type": "text", "text": "דוד"}] + "parameters": [{"type": "text", "text": "דביר"}] }] } }), diff --git a/backend/test_direct_whatsapp.py b/backend/test_direct_whatsapp.py index f09e0cf..da3b9fd 100644 --- a/backend/test_direct_whatsapp.py +++ b/backend/test_direct_whatsapp.py @@ -39,8 +39,8 @@ async def test_whatsapp_send(): # Test data phone = "0504370045" # Israeli format - should be converted to +972504370045 - guest_name = "דוד" - groom_name = "דוד" + guest_name = "דביר" + groom_name = "דביר" bride_name = "שרה" venue = "אולם בן-גוריון" event_date = "15/06" diff --git a/backend/test_header_variants.py b/backend/test_header_variants.py index 16e4a1e..69374f0 100644 --- a/backend/test_header_variants.py +++ b/backend/test_header_variants.py @@ -21,13 +21,13 @@ test_cases = [ { "type": "header", "parameters": [ - {"type": "text", "text": "דוד"} + {"type": "text", "text": "דביר"} ] }, { "type": "body", "parameters": [ - {"type": "text", "text": "דוד"}, + {"type": "text", "text": "דביר"}, {"type": "text", "text": "שרה"}, {"type": "text", "text": "אולם בן-גוריון"}, {"type": "text", "text": "15/06"}, @@ -42,12 +42,12 @@ test_cases = [ "components": [ { "type": "header", - "parameters": ["דוד"] + "parameters": ["דביר"] }, { "type": "body", "parameters": [ - {"type": "text", "text": "דוד"}, + {"type": "text", "text": "דביר"}, {"type": "text", "text": "שרה"}, {"type": "text", "text": "אולם בן-גוריון"}, {"type": "text", "text": "15/06"}, @@ -63,8 +63,8 @@ test_cases = [ { "type": "body", "parameters": [ - {"type": "text", "text": "דוד"}, - {"type": "text", "text": "דוד"}, + {"type": "text", "text": "דביר"}, + {"type": "text", "text": "דביר"}, {"type": "text", "text": "שרה"}, {"type": "text", "text": "אולם בן-גוריון"}, {"type": "text", "text": "15/06"}, @@ -80,7 +80,7 @@ test_cases = [ { "type": "header", "parameters": [ - {"type": "text", "text": "דוד"}, + {"type": "text", "text": "דביר"}, {"type": "text", "text": "כללי"} ] }, diff --git a/backend/test_language_code.py b/backend/test_language_code.py index 30b6bff..e5ffa66 100644 --- a/backend/test_language_code.py +++ b/backend/test_language_code.py @@ -31,8 +31,8 @@ async def test_language_code(): template_name="wedding_invitation", language_code="he_IL", # Try with locale parameters=[ - "דוד", - "דוד", + "דביר", + "דביר", "שרה", "אולם בן-גוריון", "15/06", diff --git a/backend/test_param_counts.py b/backend/test_param_counts.py index 4ade388..4ade97a 100644 --- a/backend/test_param_counts.py +++ b/backend/test_param_counts.py @@ -31,10 +31,10 @@ async def test_counts(): # Test different parameter counts test_params = [ - (5, ["דוד", "דוד", "שרה", "אולם", "15/06"]), - (6, ["דוד", "דוד", "שרה", "אולם", "15/06", "18:30"]), - (7, ["דוד", "דוד", "שרה", "אולם", "15/06", "18:30", "https://link"]), - (8, ["דוד", "דוד", "שרה", "אולם", "15/06", "18:30", "https://link", "extra"]), + (5, ["דביר", "דביר", "שרה", "אולם", "15/06"]), + (6, ["דביר", "דביר", "שרה", "אולם", "15/06", "18:30"]), + (7, ["דביר", "דביר", "שרה", "אולם", "15/06", "18:30", "https://link"]), + (8, ["דביר", "דביר", "שרה", "אולם", "15/06", "18:30", "https://link", "extra"]), ] print("Testing different parameter counts...") diff --git a/backend/test_payload_structure.py b/backend/test_payload_structure.py index 39a606c..4a11f07 100644 --- a/backend/test_payload_structure.py +++ b/backend/test_payload_structure.py @@ -11,8 +11,8 @@ import json # Sample template parameters (7 required) parameters = [ - "דוד", # {{1}} contact_name - "דוד", # {{2}} groom_name + "דביר", # {{1}} contact_name + "דביר", # {{2}} groom_name "שרה", # {{3}} bride_name "אולם בן-גוריון", # {{4}} hall_name "15/06", # {{5}} event_date diff --git a/backend/test_payload_variants.py b/backend/test_payload_variants.py index 1fbb4d0..0374e63 100644 --- a/backend/test_payload_variants.py +++ b/backend/test_payload_variants.py @@ -43,8 +43,8 @@ async def test_payload(): { "type": "body", "parameters": [ - {"type": "text", "text": "דוד"}, - {"type": "text", "text": "דוד"}, + {"type": "text", "text": "דביר"}, + {"type": "text", "text": "דביר"}, {"type": "text", "text": "שרה"}, {"type": "text", "text": "אולם בן-גוריון"}, {"type": "text", "text": "15/06"}, @@ -72,8 +72,8 @@ async def test_payload(): { "type": "body", "parameters": [ - {"type": "text", "text": "דוד"}, - {"type": "text", "text": "דוד"}, + {"type": "text", "text": "דביר"}, + {"type": "text", "text": "דביר"}, {"type": "text", "text": "שרה"}, {"type": "text", "text": "אולם בן-גוריון"}, {"type": "text", "text": "15/06"}, @@ -97,8 +97,8 @@ async def test_payload(): { "type": "body", "parameters": [ - {"type": "text", "text": "דוד"}, - {"type": "text", "text": "דוד"}, + {"type": "text", "text": "דביר"}, + {"type": "text", "text": "דביר"}, {"type": "text", "text": "שרה"}, {"type": "text", "text": "אולם בן-גוריון"}, {"type": "text", "text": "15/06"}, diff --git a/backend/whatsapp.py b/backend/whatsapp.py index b288d30..aaa00ad 100644 --- a/backend/whatsapp.py +++ b/backend/whatsapp.py @@ -1,11 +1,6 @@ """ WhatsApp Cloud API Service -Handles sending WhatsApp messages via Meta's Cloud API. - -Template system: - - All approved templates are declared in whatsapp_templates.py - - Use send_by_template_key() to send any registered template - - send_wedding_invitation() remains as a backward-compatible wrapper +Handles sending WhatsApp messages via Meta's API """ import os import httpx @@ -14,8 +9,6 @@ import logging from typing import Optional from datetime import datetime -from whatsapp_templates import get_template, build_params_list, list_templates_for_frontend - # Setup logging logger = logging.getLogger(__name__) @@ -92,203 +85,251 @@ class WhatsAppService: except Exception: return False - # ── Low-level: raw template sender ───────────────────────────────────── - - async def _send_raw_template( - self, - to_e164: str, - meta_name: str, - language_code: str, - header_values: list, - body_values: list, - button_values: list = None, - ) -> dict: - """Build and POST a template message to Meta.""" - components = [] - if header_values: - components.append({ - "type": "header", - "parameters": [{"type": "text", "text": v} for v in header_values], - }) - if body_values: - components.append({ - "type": "body", - "parameters": [{"type": "text", "text": v} for v in body_values], - }) - # button_values is a list of (sub_type, index, payload) tuples - # e.g. [("url", 0, "abc123token")] - if button_values: - for sub_type, btn_index, payload in button_values: - components.append({ - "type": "button", - "sub_type": sub_type, - "index": str(btn_index), - "parameters": [{"type": "text", "text": payload}], - }) - - payload = { - "messaging_product": "whatsapp", - "to": to_e164, - "type": "template", - "template": { - "name": meta_name, - "language": {"code": language_code}, - "components": components, - }, - } - - def _mask(v: str) -> str: - return f"{v[:30]}...{v[-10:]}" if len(v) > 50 and v.startswith("http") else v - - logger.info( - "[WhatsApp] template=%s lang=%s to=%s header_params=%d body_params=%d button_components=%d", - meta_name, language_code, to_e164, len(header_values), len(body_values), - len(button_values) if button_values else 0, - ) - logger.debug( - "[WhatsApp] params header=%s body=%s buttons=%s", - [_mask(v) for v in header_values], - [_mask(v) for v in body_values], - button_values or [], - ) - - 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("[WhatsApp] API Error %d: %s", 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("[WhatsApp] Sent OK message_id=%s", 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 exc: - logger.error("[WhatsApp] HTTP error: %s", exc) - raise WhatsAppError(f"HTTP request failed: {exc}") from exc - except WhatsAppError: - raise - except Exception as exc: - logger.error("[WhatsApp] Unexpected error: %s", exc) - raise WhatsAppError(f"Failed to send WhatsApp template: {exc}") from exc - - # ── Primary API: registry-driven ───────────────────────────────────────── - - async def send_by_template_key( - self, - template_key: str, - to_phone: str, - params: dict, - ) -> dict: + @staticmethod + def validate_template_params(params: list, expected_count: int = 8) -> bool: """ - Send any registered template by its registry key. - + Validate template parameters + Args: - template_key : key in TEMPLATES registry, e.g. "wedding_invitation" - to_phone : recipient phone (normalized to E.164 automatically) - params : dict {param_key: value}; missing keys use registry fallbacks - + params: List of parameters to send + expected_count: Expected number of parameters (default: 8) + Wedding template = 1 header param + 7 body params = 8 total + Returns: - dict with message_id, status, to, timestamp, template + True if valid, otherwise raises WhatsAppError """ - to_e164 = self.normalize_phone_to_e164(to_phone) - if not self.validate_phone(to_e164): - raise WhatsAppError(f"Invalid phone number: {to_phone}") - - tpl = get_template(template_key) - header_values, body_values = build_params_list(template_key, params) - - expected = len(tpl["header_params"]) + len(tpl["body_params"]) - actual = len(header_values) + len(body_values) - if actual != expected: + if not params: + raise WhatsAppError(f"Parameters list is empty, expected {expected_count}") + + if len(params) != expected_count: raise WhatsAppError( - f"Template '{template_key}': expected {expected} params, got {actual}" + f"Parameter count mismatch: got {len(params)}, expected {expected_count}. " + f"Parameters: {params}" ) - for i, v in enumerate(header_values + body_values, 1): - if not v.strip(): + + # Ensure all params are strings and non-empty + for i, param in enumerate(params, 1): + param_str = str(param).strip() + if not param_str: raise WhatsAppError( - f"Template '{template_key}': param #{i} is empty after fallbacks" + f"Parameter #{i} is empty or None. " + f"All {expected_count} parameters must have values. Parameters: {params}" ) - - # Build button components when the template has a url_button declaration - button_values = None - url_btn_cfg = tpl.get("url_button") - if url_btn_cfg and url_btn_cfg.get("enabled"): - param_key = url_btn_cfg.get("param_key", "rsvp_token") - btn_index = url_btn_cfg.get("index", 0) - btn_payload = params.get(param_key, "") - if btn_payload: - button_values = [("url", btn_index, str(btn_payload))] - - return await self._send_raw_template( - to_e164=to_e164, - meta_name=tpl["meta_name"], - language_code=tpl["language_code"], - header_values=header_values, - body_values=body_values, - button_values=button_values, - ) + + return True - # ── Plain text ──────────────────────────────────────────────────────────── - async def send_text_message( self, to_phone: str, message_text: str, - context_message_id: Optional[str] = None, + context_message_id: Optional[str] = None ) -> dict: - """Send a plain text message.""" + """ + Send a text message via WhatsApp Cloud API + + Args: + to_phone: Recipient phone number (will be normalized to E.164) + message_text: Message body + context_message_id: Optional message ID to reply to + + Returns: + dict with message_id and status + + Raises: + WhatsAppError: If message fails to send + """ + # Normalize phone number to_e164 = self.normalize_phone_to_e164(to_phone) + if not self.validate_phone(to_e164): raise WhatsAppError(f"Invalid phone number: {to_phone}") - + + # Build payload payload = { "messaging_product": "whatsapp", "to": to_e164, "type": "text", - "text": {"body": message_text}, + "text": { + "body": message_text + } } + + # Add context if provided (for replies) if context_message_id: - payload["context"] = {"message_id": context_message_id} - + payload["context"] = { + "message_id": context_message_id + } + + # Send to API 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 + 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") - raise WhatsAppError(f"WhatsApp API error ({response.status_code}): {error_msg}") + raise WhatsAppError( + f"WhatsApp API error ({response.status_code}): {error_msg}" + ) + result = response.json() return { "message_id": result.get("messages", [{}])[0].get("id"), "status": "sent", "to": to_e164, "timestamp": datetime.utcnow().isoformat(), - "type": "text", + "type": "text" } - except httpx.HTTPError as exc: - raise WhatsAppError(f"HTTP request failed: {exc}") from exc - except WhatsAppError: - raise - except Exception as exc: - raise WhatsAppError(f"Failed to send WhatsApp message: {exc}") from exc + + except httpx.HTTPError as e: + raise WhatsAppError(f"HTTP request failed: {str(e)}") + except Exception as e: + raise WhatsAppError(f"Failed to send WhatsApp message: {str(e)}") + + async def send_template_message( + self, + to_phone: str, + template_name: str, + language_code: str = "en", + parameters: Optional[list] = None + ) -> dict: + """ + Send a pre-approved template message via WhatsApp Cloud API + + Args: + to_phone: Recipient phone number + template_name: Template name (must be approved by Meta) + language_code: Language code (default: en) + parameters: List of parameter values for template placeholders (must be 7 for wedding template) + + Returns: + dict with message_id and status + """ + to_e164 = self.normalize_phone_to_e164(to_phone) + + if not self.validate_phone(to_e164): + raise WhatsAppError(f"Invalid phone number: {to_phone}") + + # Validate parameters + if not parameters: + raise WhatsAppError("Parameters list is required for template messages") + + self.validate_template_params(parameters, expected_count=8) + + # Convert all parameters to strings + param_list = [str(p).strip() for p in parameters] + + # Build payload with correct Meta structure (includes "components" array) + # Template structure: Header (1 param) + Body (7 params) + # param_list[0] = guest_name (header) + # param_list[1] = guest_name (body {{1}} - repeated from header) + # param_list[2] = groom_name + # param_list[3] = bride_name + # param_list[4] = hall_name + # param_list[5] = event_date + # param_list[6] = event_time + # param_list[7] = guest_link + payload = { + "messaging_product": "whatsapp", + "to": to_e164, + "type": "template", + "template": { + "name": template_name, + "language": { + "code": language_code + }, + "components": [ + { + "type": "header", + "parameters": [ + {"type": "text", "text": param_list[0]} # {{1}} - guest_name (header) + ] + }, + { + "type": "body", + "parameters": [ + {"type": "text", "text": param_list[1]}, # {{1}} - guest_name (repeated) + {"type": "text", "text": param_list[2]}, # {{2}} - groom_name + {"type": "text", "text": param_list[3]}, # {{3}} - bride_name + {"type": "text", "text": param_list[4]}, # {{4}} - hall_name + {"type": "text", "text": param_list[5]}, # {{5}} - event_date + {"type": "text", "text": param_list[6]}, # {{6}} - event_time + {"type": "text", "text": param_list[7]} # {{7}} - guest_link + ] + } + ] + } + } + + # DEBUG: Log what we're sending (mask long URLs) + masked_params = [] + for p in param_list: + if len(p) > 50 and p.startswith("http"): + masked_params.append(f"{p[:30]}...{p[-10:]}") + else: + masked_params.append(p) + + logger.info( + f"[WhatsApp] Sending template '{template_name}' " + f"Language: {language_code}, " + f"To: {to_e164}, " + f"Params ({len(param_list)}): {masked_params}" + ) + + url = f"{self.base_url}/{self.phone_number_id}/messages" + + # DEBUG: Print the full payload + import json + print("\n" + "=" * 80) + print("[DEBUG] Full Payload Being Sent to Meta:") + print("=" * 80) + print(json.dumps(payload, indent=2, ensure_ascii=False)) + print("=" * 80 + "\n") + + 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! ID: {message_id}") + + return { + "message_id": message_id, + "status": "sent", + "to": to_e164, + "timestamp": datetime.utcnow().isoformat(), + "type": "template", + "template": template_name + } + + except httpx.HTTPError as e: + logger.error(f"[WhatsApp] HTTP Error: {str(e)}") + raise WhatsAppError(f"HTTP request failed: {str(e)}") + except Exception as e: + logger.error(f"[WhatsApp] Unexpected error: {str(e)}") + raise WhatsAppError(f"Failed to send WhatsApp template: {str(e)}") - # ── Backward-compat wedding invitation ─────────────────────────────────── - async def send_wedding_invitation( self, to_phone: str, @@ -296,55 +337,127 @@ class WhatsAppService: partner1_name: str, partner2_name: str, venue: str, - event_date: str, - event_time: str, + event_date: str, # Should be formatted as DD/MM + event_time: str, # Should be formatted as HH:mm guest_link: str, template_name: Optional[str] = None, - language_code: Optional[str] = None, - template_key: Optional[str] = None, + language_code: Optional[str] = None ) -> dict: """ - Send a wedding invitation using the template registry. - template_key takes precedence; falls back to env WHATSAPP_TEMPLATE_KEY - or "wedding_invitation". + Send wedding invitation template message + + IMPORTANT: Always sends exactly 7 parameters in this order: + {{1}} = contact_name (guest first name or fallback) + {{2}} = groom_name (partner1) + {{3}} = bride_name (partner2) + {{4}} = hall_name (venue) + {{5}} = event_date (DD/MM format) + {{6}} = event_time (HH:mm format) + {{7}} = guest_link (RSVP link) + + Args: + to_phone: Recipient phone number + guest_name: Guest first name + partner1_name: First partner name (groom) + partner2_name: Second partner name (bride) + venue: Wedding venue/hall name + event_date: Event date in DD/MM format + event_time: Event time in HH:mm format + guest_link: RSVP/guest link + template_name: Meta template name (uses env var if not provided) + language_code: Language code (uses env var if not provided) + + Returns: + dict with message_id and status """ - key = ( - template_key - or os.getenv("WHATSAPP_TEMPLATE_KEY", "wedding_invitation") + # Use environment defaults if not provided + template_name = template_name or os.getenv("WHATSAPP_TEMPLATE_NAME", "wedding_invitation") + language_code = language_code or os.getenv("WHATSAPP_LANGUAGE_CODE", "he") + + # Build 8 parameters with safe fallbacks + # The template requires: 1 header param + 7 body params + # Body {{1}} = guest_name (same as header, repeated) + param_1_contact_name = (guest_name or "").strip() or "חבר" + param_2_groom_name = (partner1_name or "").strip() or "החתן" + param_3_bride_name = (partner2_name or "").strip() or "הכלה" + param_4_hall_name = (venue or "").strip() or "האולם" + param_5_event_date = (event_date or "").strip() or "—" + param_6_event_time = (event_time or "").strip() or "—" + param_7_guest_link = (guest_link or "").strip() or f"{os.getenv('FRONTEND_URL', 'http://localhost:5174')}/guest?event_id=unknown" + + parameters = [ + param_1_contact_name, # header {{1}} + param_1_contact_name, # body {{1}} - guest name repeated + param_2_groom_name, # body {{2}} + param_3_bride_name, # body {{3}} + param_4_hall_name, # body {{4}} + param_5_event_date, # body {{5}} + param_6_event_time, # body {{6}} + param_7_guest_link # body {{7}} + ] + + logger.info( + f"[WhatsApp Invitation] Building params for {to_phone}: " + f"guest={param_1_contact_name}, groom={param_2_groom_name}, " + f"bride={param_3_bride_name}, venue={param_4_hall_name}, " + f"date={param_5_event_date}, time={param_6_event_time}" ) - params = { - "contact_name": (guest_name or "").strip(), - "groom_name": (partner1_name or "").strip(), - "bride_name": (partner2_name or "").strip(), - "venue": (venue or "").strip(), - "event_date": (event_date or "").strip(), - "event_time": (event_time or "").strip(), - "guest_link": (guest_link or "").strip(), - } - return await self.send_by_template_key(key, to_phone, params) - - # ── Webhook helpers ──────────────────────────────────────────────────── - + + # Use standard template sending with validated parameters + return await self.send_template_message( + to_phone=to_phone, + template_name=template_name, + language_code=language_code, + parameters=parameters + ) + def handle_webhook_verification(self, challenge: str) -> str: + """ + Handle webhook verification challenge from Meta + + Args: + challenge: The challenge string from Meta + + Returns: + The challenge string to echo back + """ return challenge - + def verify_webhook_signature(self, body: str, signature: str) -> bool: + """ + Verify webhook signature from Meta + + Args: + body: Raw request body + signature: x-hub-signature header value + + Returns: + True if signature is valid + """ import hmac import hashlib + if not self.verify_token: return False + + # Extract signature from header (format: sha1=...) try: - _, hash_value = signature.split("=") + hash_algo, hash_value = signature.split("=") except ValueError: return False - expected = hmac.new( - self.verify_token.encode(), body.encode(), hashlib.sha1 + + # Compute expected signature + expected_signature = hmac.new( + self.verify_token.encode(), + body.encode(), + hashlib.sha1 ).hexdigest() - return hmac.compare_digest(hash_value, expected) + + # Constant-time comparison + return hmac.compare_digest(hash_value, expected_signature) -# ── Singleton ───────────────────────────────────────────────────────────────── - +# Singleton instance _whatsapp_service: Optional[WhatsAppService] = None @@ -354,9 +467,3 @@ def get_whatsapp_service() -> WhatsAppService: if _whatsapp_service is None: _whatsapp_service = WhatsAppService() return _whatsapp_service - - -def reset_whatsapp_service() -> None: - """Force recreation of the singleton (useful after env-var changes in tests)""" - global _whatsapp_service - _whatsapp_service = None diff --git a/frontend/src/components/GuestSelfService.jsx b/frontend/src/components/GuestSelfService.jsx index b84ddab..5538f1e 100644 --- a/frontend/src/components/GuestSelfService.jsx +++ b/frontend/src/components/GuestSelfService.jsx @@ -54,17 +54,21 @@ function GuestSelfService({ eventId }) { setLoading(true) try { const guestData = await getGuestForEvent(eventId, phoneNumber) - setGuest(guestData) + // Always present the form regardless of whether the guest was pre-imported. + // Never pre-fill the name — the host may have saved a nickname in their + // contacts that the guest should not see. + setGuest(guestData) // found:true or found:false — both show the RSVP form setFormData({ - first_name: guestData.first_name || '', - last_name: guestData.last_name || '', + first_name: '', // guest enters their own name + last_name: '', rsvp_status: guestData.rsvp_status || 'invited', meal_preference: guestData.meal_preference || '', has_plus_one: guestData.has_plus_one || false, plus_one_name: guestData.plus_one_name || '', }) } catch { - setError('לא נמצאת ברשימת האורחים לאירוע זה. אנא בדוק את מספר הטלפון ונסה שוב.') + // Only real network / server errors reach here + setError('אירעה שגיאה. אנא נסה שוב.') } finally { setLoading(false) } @@ -260,7 +264,7 @@ function GuestSelfService({ eventId }) { /* ── Step 2: RSVP form ── */
-

שלום {guest.first_name || ''}! 👋

+

שלום! 👋

אנא אשר את הגעתך והעדפותיך

{!success && (