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>
283 lines
9.0 KiB
Python
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
|