"""Owner Telegram notification webhook (#23): fires an internal webhook call on booking create/cancel/reschedule, with the same {booking, business_name, notify_channel} payload shape n8n/booking-sync.json's "Build booking row" step used to hand the Telegram node -- plus an "event" field (that node never needed, since it only ever fired for a new EA booking) so the message text can say what actually happened instead of always "new booking". Fire-and-forget over HTTP, mirroring mailer.py's background-thread queue: a slow/unreachable webhook must never block or fail a booking mutation. """ import json import os import queue import threading import traceback import urllib.request # Left blank, sending is skipped (logged, not fatal) -- same convention as # mailer.py's SMTP_HOST. Channel routing (only "telegram" does anything at # launch) lives on the n8n side of this webhook, not here -- see #23. NOTIFY_WEBHOOK_URL = os.environ.get("OWNER_NOTIFY_WEBHOOK_URL", "") _queue = queue.Queue() def _serialize_booking(booking): return {k: (v.isoformat() if hasattr(v, "isoformat") else v) for k, v in booking.items()} def _send_now(payload): if not NOTIFY_WEBHOOK_URL: print(f"[owner_notify] OWNER_NOTIFY_WEBHOOK_URL not configured, " f"skipping notify for booking {payload['booking'].get('booking_id')}", flush=True) return req = urllib.request.Request( NOTIFY_WEBHOOK_URL, data=json.dumps(payload).encode("utf-8"), headers={"Content-Type": "application/json"}, method="POST", ) with urllib.request.urlopen(req, timeout=10): pass def _worker(): while True: payload = _queue.get() try: _send_now(payload) except Exception: # noqa: BLE001 print(f"[owner_notify] send for booking " f"{payload['booking'].get('booking_id')} failed:", flush=True) traceback.print_exc() finally: _queue.task_done() threading.Thread(target=_worker, daemon=True).start() def notify(client, booking, event): """Queue a best-effort owner notification for a booking create/cancel/ reschedule (#23). event is "created"/"cancelled"/"rescheduled" -- booking status alone can't distinguish a fresh booking from a rescheduled one (both land as "confirmed"), and the n8n message text needs to say which happened. Fires for every notify_channel value, including one that isn't "telegram" (or is unset) -- that's not an error, the n8n workflow this webhook feeds is what decides whether a given channel actually sends anything, which keeps this call site the same regardless of how many channels exist in the future.""" payload = { "event": event, "booking": _serialize_booking(booking), "business_name": (client or {}).get("business_name") or "", "notify_channel": (client or {}).get("notify_channel"), } _queue.put(payload)