Owner notification webhook: Telegram re-point (#23)
Test backoffice (smb-crm) / test (push) Has been cancelled
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>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
"""Unit tests for owner_notify.py's webhook-call shape (#23). Calls
|
||||
_send_now directly rather than going through the background-thread queue,
|
||||
so assertions are synchronous -- the queue itself is just plumbing.
|
||||
"""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
import owner_notify
|
||||
|
||||
|
||||
class _FakeResponse:
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc):
|
||||
return False
|
||||
|
||||
|
||||
def _booking():
|
||||
return {
|
||||
"booking_id": "BK-1",
|
||||
"client_id": "C-1",
|
||||
"customer_name": "Alice",
|
||||
"customer_contact": "alice@example.com",
|
||||
"service": "Haircut",
|
||||
"start_time": datetime(2026, 8, 10, 9, 0, tzinfo=timezone.utc),
|
||||
"end_time": datetime(2026, 8, 10, 10, 0, tzinfo=timezone.utc),
|
||||
"source": "public",
|
||||
"status": "confirmed",
|
||||
}
|
||||
|
||||
|
||||
def test_send_now_skips_when_webhook_url_not_configured(monkeypatch):
|
||||
monkeypatch.setattr(owner_notify, "NOTIFY_WEBHOOK_URL", "")
|
||||
calls = []
|
||||
monkeypatch.setattr(owner_notify.urllib.request, "urlopen",
|
||||
lambda *a, **kw: calls.append((a, kw)) or _FakeResponse())
|
||||
owner_notify._send_now({"booking": _booking(), "business_name": "Happy Nails",
|
||||
"notify_channel": "telegram"})
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_send_now_posts_json_when_configured(monkeypatch):
|
||||
monkeypatch.setattr(owner_notify, "NOTIFY_WEBHOOK_URL", "https://n8n.example.com/webhook/owner-notify")
|
||||
captured = []
|
||||
|
||||
def fake_urlopen(req, timeout=None):
|
||||
captured.append(req)
|
||||
return _FakeResponse()
|
||||
|
||||
monkeypatch.setattr(owner_notify.urllib.request, "urlopen", fake_urlopen)
|
||||
payload = {"booking": owner_notify._serialize_booking(_booking()),
|
||||
"business_name": "Happy Nails", "notify_channel": "telegram"}
|
||||
owner_notify._send_now(payload)
|
||||
assert len(captured) == 1
|
||||
req = captured[0]
|
||||
assert req.full_url == "https://n8n.example.com/webhook/owner-notify"
|
||||
assert req.get_header("Content-type") == "application/json"
|
||||
import json
|
||||
body = json.loads(req.data)
|
||||
assert body["booking"]["booking_id"] == "BK-1"
|
||||
assert body["booking"]["start_time"] == "2026-08-10T09:00:00+00:00"
|
||||
assert body["business_name"] == "Happy Nails"
|
||||
assert body["notify_channel"] == "telegram"
|
||||
|
||||
|
||||
def test_notify_enqueues_regardless_of_channel(monkeypatch):
|
||||
"""Channel routing (only "telegram" sends anything real) lives on the
|
||||
n8n side of this webhook, not in notify() -- an unset/other channel is
|
||||
still queued, not silently dropped here (#23)."""
|
||||
captured = []
|
||||
monkeypatch.setattr(owner_notify._queue, "put", lambda p: captured.append(p))
|
||||
client = {"business_name": "Happy Nails", "notify_channel": "email"}
|
||||
owner_notify.notify(client, _booking(), "created")
|
||||
assert len(captured) == 1
|
||||
assert captured[0]["notify_channel"] == "email"
|
||||
assert captured[0]["business_name"] == "Happy Nails"
|
||||
assert captured[0]["event"] == "created"
|
||||
|
||||
|
||||
def test_notify_defaults_business_name_and_channel_when_client_missing(monkeypatch):
|
||||
captured = []
|
||||
monkeypatch.setattr(owner_notify._queue, "put", lambda p: captured.append(p))
|
||||
owner_notify.notify(None, _booking(), "cancelled")
|
||||
assert captured[0]["business_name"] == ""
|
||||
assert captured[0]["notify_channel"] is None
|
||||
assert captured[0]["event"] == "cancelled"
|
||||
Reference in New Issue
Block a user