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:
@@ -102,6 +102,37 @@ def test_create_booking_via_public_endpoint_sends_confirmation_email(client, mon
|
||||
assert "Haircut" in sent[0]["html"]
|
||||
|
||||
|
||||
def test_create_booking_notifies_owner(client, monkeypatch):
|
||||
"""#23's acceptance criterion: creating a booking via the public API
|
||||
triggers the owner notification webhook, carrying the client's
|
||||
notify_channel and business_name alongside the booking."""
|
||||
notified = []
|
||||
monkeypatch.setattr(
|
||||
"owner_notify.notify",
|
||||
lambda client, booking, event: notified.append((client, booking, event)))
|
||||
resource, service = _setup_resource_and_service(
|
||||
auto_confirm=True, min_notice_minutes=0, max_advance_days=365)
|
||||
day = _next_monday(date.today())
|
||||
slots = client.get("/api/booking/slots", query_string={
|
||||
"client_id": CLIENT_A, "resource_id": resource["resource_id"],
|
||||
"service_id": service["service_id"],
|
||||
"date_from": day.isoformat(), "date_to": day.isoformat()}).get_json()["slots"]
|
||||
|
||||
resp = client.post("/api/booking", json={
|
||||
"client_id": CLIENT_A, "resource_id": resource["resource_id"],
|
||||
"service_id": service["service_id"], "start_time": slots[0],
|
||||
"customer_name": "Nia", "customer_contact": "nia@example.com"})
|
||||
assert resp.status_code == 201
|
||||
booking_id = resp.get_json()["booking_id"]
|
||||
|
||||
assert len(notified) == 1
|
||||
notified_client, notified_booking, notified_event = notified[0]
|
||||
assert notified_client["client_id"] == CLIENT_A
|
||||
assert notified_booking["booking_id"] == booking_id
|
||||
assert notified_booking["customer_name"] == "Nia"
|
||||
assert notified_event == "created"
|
||||
|
||||
|
||||
def test_create_booking_auto_confirm_false_yields_pending(client):
|
||||
resource, service = _setup_resource_and_service(
|
||||
auto_confirm=False, min_notice_minutes=0, max_advance_days=365)
|
||||
@@ -180,6 +211,64 @@ def test_cancel_with_valid_token_cancels_booking(client):
|
||||
assert resp3.status_code == 400
|
||||
|
||||
|
||||
def test_cancel_notifies_owner(client, monkeypatch):
|
||||
notified = []
|
||||
monkeypatch.setattr(
|
||||
"owner_notify.notify",
|
||||
lambda client, booking, event: notified.append((client, booking, event)))
|
||||
resource, service = _setup_resource_and_service(
|
||||
min_notice_minutes=0, max_advance_days=365)
|
||||
day = _next_monday(date.today())
|
||||
slot = client.get("/api/booking/slots", query_string={
|
||||
"client_id": CLIENT_A, "resource_id": resource["resource_id"],
|
||||
"service_id": service["service_id"],
|
||||
"date_from": day.isoformat(), "date_to": day.isoformat()}).get_json()["slots"][0]
|
||||
created = client.post("/api/booking", json={
|
||||
"client_id": CLIENT_A, "resource_id": resource["resource_id"],
|
||||
"service_id": service["service_id"], "start_time": slot,
|
||||
"customer_name": "Omar", "customer_contact": "o@example.com"}).get_json()
|
||||
notified.clear() # drop the create-time notification, isolate the cancel one
|
||||
|
||||
resp = client.post("/api/booking/cancel", json={"token": created["token"]})
|
||||
assert resp.status_code == 200
|
||||
|
||||
assert len(notified) == 1
|
||||
notified_client, notified_booking, notified_event = notified[0]
|
||||
assert notified_client["client_id"] == CLIENT_A
|
||||
assert notified_booking["booking_id"] == created["booking_id"]
|
||||
assert notified_booking["status"] == "cancelled"
|
||||
assert notified_event == "cancelled"
|
||||
|
||||
|
||||
def test_reschedule_notifies_owner(client, monkeypatch):
|
||||
notified = []
|
||||
monkeypatch.setattr(
|
||||
"owner_notify.notify",
|
||||
lambda client, booking, event: notified.append((client, booking, event)))
|
||||
resource, service = _setup_resource_and_service(
|
||||
min_notice_minutes=0, max_advance_days=365)
|
||||
day = _next_monday(date.today())
|
||||
slot_list = client.get("/api/booking/slots", query_string={
|
||||
"client_id": CLIENT_A, "resource_id": resource["resource_id"],
|
||||
"service_id": service["service_id"],
|
||||
"date_from": day.isoformat(), "date_to": day.isoformat()}).get_json()["slots"]
|
||||
created = client.post("/api/booking", json={
|
||||
"client_id": CLIENT_A, "resource_id": resource["resource_id"],
|
||||
"service_id": service["service_id"], "start_time": slot_list[0],
|
||||
"customer_name": "Priya", "customer_contact": "p@example.com"}).get_json()
|
||||
notified.clear() # drop the create-time notification, isolate the reschedule one
|
||||
|
||||
resp = client.post("/api/booking/reschedule", json={
|
||||
"token": created["token"], "start_time": slot_list[2]})
|
||||
assert resp.status_code == 200
|
||||
|
||||
assert len(notified) == 1
|
||||
notified_client, notified_booking, notified_event = notified[0]
|
||||
assert notified_client["client_id"] == CLIENT_A
|
||||
assert notified_booking["start_time"].isoformat() == slot_list[2]
|
||||
assert notified_event == "rescheduled"
|
||||
|
||||
|
||||
def test_cancel_with_expired_token_is_rejected(client):
|
||||
import jwt as pyjwt
|
||||
from booking_api import TOKEN_SECRET
|
||||
|
||||
Reference in New Issue
Block a user