""" 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_type : "TEXT" or "IMAGE" or "VIDEO" or "DOCUMENT" (default: "TEXT") - header_params : ordered list of variable keys sent in the HEADER component (empty list [] if the template has no header variables) - header_handle : media handle for IMAGE/VIDEO/DOCUMENT headers (optional) - body_params : ordered list of variable keys sent in the BODY component - button_type : "URL" or "PHONE_NUMBER" or "QUICK_REPLY" (optional) - button_text : button label/text (optional) - button_url : button URL (optional, for URL buttons) - 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, Optional from sqlalchemy.orm import Session def load_custom_templates(db: Session) -> Dict[str, Dict[str, Any]]: """Load user-created templates from the database.""" from models import WhatsAppTemplate try: templates = db.query(WhatsAppTemplate).all() result = {} for t in templates: result[t.template_key] = { "meta_name": t.meta_name, "language_code": t.language_code, "friendly_name": t.friendly_name, "description": t.description, "header_type": t.header_type, "header_text": t.header_text, "header_handle": t.header_handle, "body_text": t.body_text, "header_params": json.loads(t.header_params), "body_params": json.loads(t.body_params), "button_type": t.button_type, "button_text": t.button_text, "button_url": t.button_url, "button_param_key": t.button_url, # backward compat "fallbacks": json.loads(t.fallbacks), "guest_name_key": "", } return result except Exception as e: print(f"[WARNING] Failed to load custom templates from database: {e}") return {} def save_custom_templates(db: Session, data: Dict[str, Dict[str, Any]]) -> None: """Persist custom templates to the database.""" from models import WhatsAppTemplate try: # Clear old templates db.query(WhatsAppTemplate).delete() # Add new ones for key, tpl in data.items(): template_record = WhatsAppTemplate( template_key=key, meta_name=tpl.get("meta_name", key), friendly_name=tpl.get("friendly_name", key), language_code=tpl.get("language_code", "he"), description=tpl.get("description", ""), header_type=tpl.get("header_type", "TEXT"), header_text=tpl.get("header_text", ""), header_handle=tpl.get("header_handle", ""), body_text=tpl.get("body_text", ""), header_params=json.dumps(tpl.get("header_params", [])), body_params=json.dumps(tpl.get("body_params", [])), button_type=tpl.get("button_type", ""), button_text=tpl.get("button_text", ""), button_url=tpl.get("button_url", ""), fallbacks=json.dumps(tpl.get("fallbacks", {})), ) db.add(template_record) db.commit() except Exception as e: print(f"[WARNING] Failed to save custom templates to database: {e}") db.rollback() def get_all_templates(db: Session) -> Dict[str, Dict[str, Any]]: """Return merged dict: built-in TEMPLATES + user custom templates.""" merged = dict(TEMPLATES) merged.update(load_custom_templates(db)) return merged def add_custom_template(db: Session, 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(db) data[key] = template save_custom_templates(db, data) def delete_custom_template(db: Session, 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(db) if key not in data: raise KeyError(f"Custom template '{key}' not found.") del data[key] save_custom_templates(db, 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(db: Session, 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(db) 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(db: Session) -> 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(db) custom_keys = set(load_custom_templates(db).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", ""), "header_type": tpl.get("header_type", "TEXT"), "header_handle": tpl.get("header_handle", ""), "button_type": tpl.get("button_type", ""), "button_text": tpl.get("button_text", ""), "button_url": tpl.get("button_url", ""), "button_param_key": tpl.get("button_param_key", ""), "guest_name_key": tpl.get("guest_name_key", ""), "url_button": tpl.get("url_button", None), } for key, tpl in all_tpls.items() ] def build_params_list(db: Session, 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(db, 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