a27ee59125
Test backoffice (smb-crm) / test (push) Has been cancelled
Flask fires a fire-and-forget internal webhook (owner_notify.py, mirroring
mailer.py's background-thread queue) on booking create/cancel/reschedule,
carrying the same {booking, business_name, notify_channel} shape the old
EA-driven "Build booking row" node produced, plus an event field so the
Telegram message can say what actually happened. n8n/booking-sync.json gets
a new webhook + IF node feeding the existing Telegram node directly, so it
no longer needs EA's API to build the notification payload; channel routing
(only telegram sends for now) lives in that IF node.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
80 lines
2.9 KiB
Python
80 lines
2.9 KiB
Python
"""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)
|