245 lines
9.6 KiB
Python
245 lines
9.6 KiB
Python
"""
|
|
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_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
|