TorbatYar/backend/services/hospitality/app/tests/test_qr.py
Mortezakoohjani 5c6a2e78cf feat(platform): seed service registry, deploy all modules, and fix homepage catalog.
Register all active platform services and base features in core DB so admin can manage them, add delivery/hospitality/sports-center backend phases, update apps catalog and production deploy tooling.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 12:39:51 +03:30

283 lines
9.0 KiB
Python

"""Phase 12.2 QR Menu & QR Ordering API / tenant / validation tests."""
from __future__ import annotations
import pytest
from app.events.publisher import get_event_publisher
from app.events.types import HospitalityEventType
from app.tests.conftest import TENANT_A, TENANT_B, tenant_headers
async def _seed_menu_item(client, tenant_id, *, venue_code: str = "v1", item_code: str = "latte"):
h = tenant_headers(tenant_id)
venue = await client.post(
"/api/v1/venues",
json={"code": venue_code, "name": "Cafe", "status": "active", "venue_format": "cafe"},
headers=h,
)
assert venue.status_code == 201, venue.text
venue_id = venue.json()["id"]
menu = await client.post(
"/api/v1/menus",
json={"venue_id": venue_id, "code": "day", "name": "Day Menu"},
headers=h,
)
assert menu.status_code == 201, menu.text
menu_id = menu.json()["id"]
category = await client.post(
"/api/v1/menu-categories",
json={
"venue_id": venue_id,
"menu_id": menu_id,
"code": "drinks",
"name": "Drinks",
},
headers=h,
)
assert category.status_code == 201, category.text
item = await client.post(
"/api/v1/menu-items",
json={
"venue_id": venue_id,
"menu_id": menu_id,
"category_id": category.json()["id"],
"code": item_code,
"name": "Latte",
"base_price": 150000,
},
headers=h,
)
assert item.status_code == 201, item.text
return {
"headers": h,
"venue_id": venue_id,
"menu_id": menu_id,
"item_id": item.json()["id"],
"item": item.json(),
}
@pytest.mark.asyncio
async def test_qr_menu_ordering_happy_path(client):
seed = await _seed_menu_item(client, TENANT_A)
h = seed["headers"]
venue_id = seed["venue_id"]
menu_id = seed["menu_id"]
item_id = seed["item_id"]
qr_code = await client.post(
"/api/v1/qr-codes",
json={
"venue_id": venue_id,
"code": "table-1",
"name": "Table 1 QR",
"target_type": "menu",
"menu_id": menu_id,
"ordering_enabled": True,
},
headers=h,
)
assert qr_code.status_code == 201, qr_code.text
qr_code_id = qr_code.json()["id"]
qr_token = qr_code.json()["token"]
assert qr_token
by_token = await client.get(f"/api/v1/qr-codes/by-token/{qr_token}", headers=h)
assert by_token.status_code == 200, by_token.text
assert by_token.json()["id"] == qr_code_id
menu_session = await client.post(
"/api/v1/qr-menu-sessions",
json={"venue_id": venue_id, "qr_code_id": qr_code_id, "locale": "fa-IR"},
headers=h,
)
assert menu_session.status_code == 201, menu_session.text
menu_session_id = menu_session.json()["id"]
menu_session_token = menu_session.json()["session_token"]
by_token_ms = await client.get(
f"/api/v1/qr-menu-sessions/by-token/{menu_session_token}", headers=h
)
assert by_token_ms.status_code == 200, by_token_ms.text
ordering_session = await client.post(
"/api/v1/qr-ordering-sessions",
json={
"venue_id": venue_id,
"qr_code_id": qr_code_id,
"qr_menu_session_id": menu_session_id,
},
headers=h,
)
assert ordering_session.status_code == 201, ordering_session.text
ordering_session_id = ordering_session.json()["id"]
assert ordering_session.json()["status"] == "draft"
cart = await client.post(
"/api/v1/carts",
json={"venue_id": venue_id, "ordering_session_id": ordering_session_id},
headers=h,
)
assert cart.status_code == 201, cart.text
cart_id = cart.json()["id"]
assert cart.json()["status"] == "active"
line = await client.post(
"/api/v1/cart-lines",
json={
"venue_id": venue_id,
"cart_id": cart_id,
"menu_item_id": item_id,
"quantity": 2,
},
headers=h,
)
assert line.status_code == 201, line.text
assert line.json()["quantity"] == 2
assert line.json()["unit_price"] == 150000
submitted = await client.post(
f"/api/v1/qr-ordering-sessions/{ordering_session_id}/submit", headers=h
)
assert submitted.status_code == 200, submitted.text
assert submitted.json()["status"] == "submitted"
listed = await client.get("/api/v1/qr-codes", headers=h)
assert listed.status_code == 200
assert len(listed.json()) >= 1
published = {e.event_type for e in get_event_publisher().published}
assert HospitalityEventType.QR_CODE_CREATED.value in published
assert HospitalityEventType.QR_MENU_SESSION_CREATED.value in published
assert HospitalityEventType.QR_ORDERING_SESSION_CREATED.value in published
assert HospitalityEventType.CART_CREATED.value in published
assert HospitalityEventType.CART_LINE_ADDED.value in published
assert HospitalityEventType.QR_ORDERING_SESSION_SUBMITTED.value in published
@pytest.mark.asyncio
async def test_qr_code_tenant_isolation(client):
seed = await _seed_menu_item(client, TENANT_A, venue_code="iso-a")
qr_code = await client.post(
"/api/v1/qr-codes",
json={
"venue_id": seed["venue_id"],
"code": "iso-qr",
"name": "Iso QR",
"target_type": "menu",
"menu_id": seed["menu_id"],
},
headers=seed["headers"],
)
assert qr_code.status_code == 201
qr_code_id = qr_code.json()["id"]
denied = await client.get(
f"/api/v1/qr-codes/{qr_code_id}",
headers=tenant_headers(TENANT_B),
)
assert denied.status_code == 404
@pytest.mark.asyncio
async def test_qr_code_requires_menu_id_for_menu_target(client):
seed = await _seed_menu_item(client, TENANT_A, venue_code="val-a", item_code="tea")
bad = await client.post(
"/api/v1/qr-codes",
json={
"venue_id": seed["venue_id"],
"code": "bad-qr",
"name": "Bad QR",
"target_type": "menu",
},
headers=seed["headers"],
)
assert bad.status_code == 422
@pytest.mark.asyncio
async def test_qr_ordering_session_requires_ordering_enabled(client):
seed = await _seed_menu_item(client, TENANT_A, venue_code="ord-a", item_code="mocha")
qr_code = await client.post(
"/api/v1/qr-codes",
json={
"venue_id": seed["venue_id"],
"code": "no-order",
"name": "No Order QR",
"target_type": "menu",
"menu_id": seed["menu_id"],
"ordering_enabled": False,
},
headers=seed["headers"],
)
assert qr_code.status_code == 201, qr_code.text
bad = await client.post(
"/api/v1/qr-ordering-sessions",
json={"venue_id": seed["venue_id"], "qr_code_id": qr_code.json()["id"]},
headers=seed["headers"],
)
assert bad.status_code == 422
@pytest.mark.asyncio
async def test_cart_line_quantity_validation(client):
seed = await _seed_menu_item(client, TENANT_A, venue_code="qty-a", item_code="cake")
h = seed["headers"]
qr_code = await client.post(
"/api/v1/qr-codes",
json={
"venue_id": seed["venue_id"],
"code": "qty-qr",
"name": "Qty QR",
"target_type": "menu",
"menu_id": seed["menu_id"],
"ordering_enabled": True,
},
headers=h,
)
assert qr_code.status_code == 201, qr_code.text
ordering_session = await client.post(
"/api/v1/qr-ordering-sessions",
json={"venue_id": seed["venue_id"], "qr_code_id": qr_code.json()["id"]},
headers=h,
)
assert ordering_session.status_code == 201, ordering_session.text
cart = await client.post(
"/api/v1/carts",
json={
"venue_id": seed["venue_id"],
"ordering_session_id": ordering_session.json()["id"],
},
headers=h,
)
assert cart.status_code == 201, cart.text
bad = await client.post(
"/api/v1/cart-lines",
json={
"venue_id": seed["venue_id"],
"cart_id": cart.json()["id"],
"menu_item_id": seed["item_id"],
"quantity": 0,
},
headers=h,
)
assert bad.status_code == 422
@pytest.mark.asyncio
async def test_qr_menu_ordering_still_enabled_in_phase_12_3(client):
resp = await client.get("/capabilities")
assert resp.status_code == 200
body = resp.json()
assert body["features"]["qr_menu"] is True
assert body["features"]["qr_ordering"] is True
assert body["features"]["ordering_engine"] is False
assert body["features"]["pos_engine"] is False