""" WhatsApp Template Registry -------------------------- Single source of truth for ALL approved Meta WhatsApp templates. How to add a new template: 1. Get the template approved in Meta Business Manager. 2. Add an entry under TEMPLATES with: - meta_name : exact name as it appears in Meta - language_code : he / he_IL / en / en_US … - friendly_name : shown in the frontend dropdown - description : optional, for documentation - header_params : ordered list of variable keys sent in the HEADER component (empty list [] if the template has no header variables) - body_params : ordered list of variable keys sent in the BODY component - fallbacks : dict {key: default_string} used when the caller doesn't provide a value for that key The backend will: - Look up the template by its registry key (e.g. "wedding_invitation") - Build the Meta payload header/body param lists in exact declaration order - Apply fallbacks for any missing keys - Validate total param count == len(header_params) + len(body_params) IMPORTANT: param order in header_params / body_params MUST match the {{1}}, {{2}}, … placeholder order inside the Meta template. """ import json import os from typing import Dict, Any # ── Custom templates file ───────────────────────────────────────────────────── CUSTOM_TEMPLATES_FILE = os.path.join(os.path.dirname(__file__), "custom_templates.json") def load_custom_templates() -> Dict[str, Dict[str, Any]]: """Load user-created templates from the JSON store.""" try: with open(CUSTOM_TEMPLATES_FILE, "r", encoding="utf-8") as f: return json.load(f) except (FileNotFoundError, json.JSONDecodeError): return {} def save_custom_templates(data: Dict[str, Dict[str, Any]]) -> None: """Persist custom templates to the JSON store.""" with open(CUSTOM_TEMPLATES_FILE, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) def get_all_templates() -> Dict[str, Dict[str, Any]]: """Return merged dict: built-in TEMPLATES + user custom templates.""" merged = dict(TEMPLATES) merged.update(load_custom_templates()) return merged def add_custom_template(key: str, template: Dict[str, Any]) -> None: """Add or overwrite a custom template (cannot replace built-ins).""" if key in TEMPLATES: raise ValueError(f"Template key '{key}' is a built-in and cannot be overwritten.") data = load_custom_templates() data[key] = template save_custom_templates(data) def delete_custom_template(key: str) -> None: """Delete a custom template by key. Raises KeyError if not found.""" if key in TEMPLATES: raise ValueError(f"Template key '{key}' is a built-in and cannot be deleted.") data = load_custom_templates() if key not in data: raise KeyError(f"Custom template '{key}' not found.") del data[key] save_custom_templates(data) # ── Template registry ───────────────────────────────────────────────────────── TEMPLATES: Dict[str, Dict[str, Any]] = { # ── wedding_invitation ──────────────────────────────────────────────────── # Approved Hebrew wedding invitation template. # Header {{1}} = guest name (greeting) # Body {{1}} = guest name (same, repeated inside body) # Body {{2}} = groom name # Body {{3}} = bride name # Body {{4}} = venue / hall name # Body {{5}} = event date (DD/MM) # Body {{6}} = event time (HH:mm) # Body {{7}} = RSVP / guest link URL "wedding_invitation": { "meta_name": "wedding_invitation", "language_code": "he", "friendly_name": "הזמנה לחתונה", "description": "הזמנה רשמית לאירוע חתונה עם כל פרטי האירוע וקישור RSVP", "header_params": ["contact_name"], # 1 header variable "body_params": [ # 7 body variables "contact_name", # body {{1}} "groom_name", # body {{2}} "bride_name", # body {{3}} "venue", # body {{4}} "event_date", # body {{5}} "event_time", # body {{6}} "guest_link", # body {{7}} ], "fallbacks": { "contact_name": "חבר", "groom_name": "החתן", "bride_name": "הכלה", "venue": "האולם", "event_date": "—", "event_time": "—", "guest_link": "https://invy.dvirlabs.com/guest", }, }, # ── save_the_date ───────────────────────────────────────────────────────── # Shorter "save the date" template — no venue/time details. # Create & approve this template in Meta before using it. # Header {{1}} = guest name # Body {{1}} = guest name (repeated) # Body {{2}} = groom name # Body {{3}} = bride name # Body {{4}} = event date (DD/MM/YYYY) # Body {{5}} = guest link "save_the_date": { "meta_name": "save_the_date", "language_code": "he", "friendly_name": "שמור את התאריך", "description": "הודעת 'שמור את התאריך' קצרה לפני ההזמנה הרשמית", "header_params": ["contact_name"], "body_params": [ "contact_name", "groom_name", "bride_name", "event_date", "guest_link", ], "fallbacks": { "contact_name": "חבר", "groom_name": "החתן", "bride_name": "הכלה", "event_date": "—", "guest_link": "https://invy.dvirlabs.com/guest", }, }, # ── reminder_1 ──────────────────────────────────────────────────────────── # Reminder template sent ~1 week before the event. # Header {{1}} = guest name # Body {{1}} = guest name # Body {{2}} = event date (DD/MM) # Body {{3}} = event time (HH:mm) # Body {{4}} = venue # Body {{5}} = guest link "reminder_1": { "meta_name": "reminder_1", "language_code": "he", "friendly_name": "תזכורת לאירוע", "description": "תזכורת שתשלח שבוע לפני האירוע", "header_params": ["contact_name"], "body_params": [ "contact_name", "event_date", "event_time", "venue", "guest_link", ], "fallbacks": { "contact_name": "חבר", "event_date": "—", "event_time": "—", "venue": "האולם", "guest_link": "https://invy.dvirlabs.com/guest", }, }, } # ── Helper functions ────────────────────────────────────────────────────────── def get_template(key: str) -> Dict[str, Any]: """ Return the template definition for *key* (checks both built-in + custom). Raises KeyError with a helpful message if not found. """ all_tpls = get_all_templates() if key not in all_tpls: available = ", ".join(all_tpls.keys()) raise KeyError( f"Unknown template key '{key}'. " f"Available templates: {available}" ) return all_tpls[key] def list_templates_for_frontend() -> list: """ Return a list suitable for the frontend dropdown (built-in + custom). Each item: {key, friendly_name, meta_name, param_count, language_code, description, is_custom} """ all_tpls = get_all_templates() custom_keys = set(load_custom_templates().keys()) return [ { "key": key, "friendly_name": tpl["friendly_name"], "meta_name": tpl["meta_name"], "language_code": tpl["language_code"], "description": tpl.get("description", ""), "param_count": len(tpl["header_params"]) + len(tpl["body_params"]), "header_param_count": len(tpl["header_params"]), "body_param_count": len(tpl["body_params"]), "is_custom": key in custom_keys, "body_params": tpl["body_params"], "header_params": tpl["header_params"], "body_text": tpl.get("body_text", ""), "header_text": tpl.get("header_text", ""), } for key, tpl in all_tpls.items() ] def build_params_list(key: str, values: dict) -> tuple: """ Given a template key and a dict of {param_key: value}, return (header_params_list, body_params_list) after applying fallbacks. Both lists contain plain string values in correct order. """ tpl = get_template(key) # checks built-in + custom fallbacks = tpl.get("fallbacks", {}) def resolve(param_key: str) -> str: raw = values.get(param_key, "") val = str(raw).strip() if raw else "" if not val: val = str(fallbacks.get(param_key, "—")).strip() return val header_values = [resolve(k) for k in tpl["header_params"]] body_values = [resolve(k) for k in tpl["body_params"]] return header_values, body_values