"""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