Rate-limit public bookings per contact; add direct owner contact endpoint
Test backoffice (smb-crm) / test (push) Successful in 1m43s

Public booking API now rejects a 6th active booking from the same
customer_contact within 24h (429), stopping one contact from filling
every slot on every resource, while owner-entered manual bookings stay
unaffected.

Add POST /api/contact: client sites can reach their own owner's inbox
directly (via their existing login email) for general inquiries,
separate from the agency's leads/Telegram pipeline (n8n/lead-intake.json),
which stays reserved for actual prospects contacting the agency itself.
Paris Barber Shop's contact form and Rückruf widget now point here; the
Rückruf floating widget itself has been removed from the site.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-12 04:47:06 +02:00
parent bcb2672d9f
commit c3e520aabb
9 changed files with 299 additions and 108 deletions
+32
View File
@@ -185,6 +185,38 @@ def test_concurrent_booking_requests_only_one_succeeds(client):
assert len(bdb.list_bookings(CLIENT_A)) == 1
def test_contact_rate_limit_blocks_after_five_recent_bookings(client):
resource, service = _setup_resource_and_service(
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"]
assert len(slots) >= 6 # 09:00-17:00, 60min slots -- 8 available
for slot in slots[:5]:
resp = client.post("/api/booking", json={
"client_id": CLIENT_A, "resource_id": resource["resource_id"],
"service_id": service["service_id"], "start_time": slot,
"customer_name": "Serial Booker", "customer_contact": "serial@example.com"})
assert resp.status_code == 201
resp = client.post("/api/booking", json={
"client_id": CLIENT_A, "resource_id": resource["resource_id"],
"service_id": service["service_id"], "start_time": slots[5],
"customer_name": "Serial Booker", "customer_contact": "serial@example.com"})
assert resp.status_code == 429
assert len(bdb.list_bookings(CLIENT_A)) == 5
# A different contact is unaffected by the first contact's count.
resp = client.post("/api/booking", json={
"client_id": CLIENT_A, "resource_id": resource["resource_id"],
"service_id": service["service_id"], "start_time": slots[5],
"customer_name": "Someone Else", "customer_contact": "else@example.com"})
assert resp.status_code == 201
def test_cancel_with_valid_token_cancels_booking(client):
resource, service = _setup_resource_and_service(
min_notice_minutes=0, max_advance_days=365)