Files
smb-online/backoffice/app/owner_notify.py
T
mivanchenko a27ee59125
Test backoffice (smb-crm) / test (push) Has been cancelled
Owner notification webhook: Telegram re-point (#23)
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>
2026-08-04 12:09:39 +02:00

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)