diff --git a/backend/app/api/routes/test_mode.py b/backend/app/api/routes/test_mode.py index af46bd9..da74913 100644 --- a/backend/app/api/routes/test_mode.py +++ b/backend/app/api/routes/test_mode.py @@ -30,12 +30,6 @@ async def preview_test_seat( seat_id=seat_id, ) - if not seat.seat_id: - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Невозможно построить preview: у места отсутствует seat_id", - ) - matched_rule_level = None matched_target_ref = None pricing_category_id = None @@ -43,6 +37,27 @@ async def preview_test_seat( currency = None has_price = False + if not seat.seat_id: + return TestSeatPreviewResponse( + scheme_id=scheme.scheme_id, + scheme_version_id=version.scheme_version_id, + seat_id=seat.seat_id, + element_id=seat.element_id, + sector_id=seat.sector_id, + group_id=seat.group_id, + row_label=seat.row_label, + seat_number=seat.seat_number, + selectable=False, + has_price=False, + matched_rule_level=None, + matched_target_ref=None, + pricing_category_id=None, + amount=None, + currency=None, + reason_code="missing_seat_id", + reason_message="Seat is not sellable because seat_id is missing.", + ) + try: matched_rule_level, rule = await find_effective_price_rule( scheme_id=scheme.scheme_id, @@ -52,7 +67,7 @@ async def preview_test_seat( ) matched_target_ref = rule["target_ref"] pricing_category_id = rule["pricing_category_id"] - amount = rule["amount"] + amount = str(rule["amount"]) currency = rule["currency"] has_price = True except HTTPException as exc: @@ -66,15 +81,25 @@ async def preview_test_seat( ) raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail=f"Не удалось построить preview: {exc.__class__.__name__}: {exc}", + detail={ + "code": "test_preview_failed", + "message": f"Не удалось построить preview: {exc.__class__.__name__}: {exc}", + }, ) + if has_price: + reason_code = "ok" + reason_message = "Seat is sellable." + else: + reason_code = "no_price_rule" + reason_message = "Seat is not sellable because no effective price rule was found." + return TestSeatPreviewResponse( scheme_id=scheme.scheme_id, scheme_version_id=version.scheme_version_id, seat_id=seat.seat_id, element_id=seat.element_id, - sector_id=seat.sector_id, + sector_id=seat.seat_id and seat.sector_id, group_id=seat.group_id, row_label=seat.row_label, seat_number=seat.seat_number, @@ -85,4 +110,6 @@ async def preview_test_seat( pricing_category_id=pricing_category_id, amount=amount, currency=currency, + reason_code=reason_code, + reason_message=reason_message, ) diff --git a/backend/app/schemas/test_mode.py b/backend/app/schemas/test_mode.py index 61335da..25d1e2c 100644 --- a/backend/app/schemas/test_mode.py +++ b/backend/app/schemas/test_mode.py @@ -1,12 +1,10 @@ -from decimal import Decimal - from pydantic import BaseModel class TestSeatPreviewResponse(BaseModel): scheme_id: str scheme_version_id: str - seat_id: str + seat_id: str | None element_id: str | None sector_id: str | None group_id: str | None @@ -17,5 +15,7 @@ class TestSeatPreviewResponse(BaseModel): matched_rule_level: str | None matched_target_ref: str | None pricing_category_id: str | None - amount: Decimal | None + amount: str | None currency: str | None + reason_code: str + reason_message: str diff --git a/backend/docs/api-map.md b/backend/docs/api-map.md index d4401aa..2f7a3f9 100644 --- a/backend/docs/api-map.md +++ b/backend/docs/api-map.md @@ -84,3 +84,5 @@ - This file is an operational route index, not a generated OpenAPI export. - Update this map in the same change set when adding, removing, renaming, or moving routes. - Query guards such as expected_current_scheme_version_id / expected_scheme_version_id are part of the operational contract for optimistic concurrency on mutable flows. + +- Test mode response now includes reason_code and reason_message for sellability diagnostics. diff --git a/backend/docs/smoke-regression.md b/backend/docs/smoke-regression.md index ecd3887..b709098 100644 --- a/backend/docs/smoke-regression.md +++ b/backend/docs/smoke-regression.md @@ -73,7 +73,7 @@ Validate: Validate: - pricing bundle contains categories and rules arrays - effective seat price resolves according to domain priority -- test seat preview explains selectable / has_price state +- test seat preview explains selectable / has_price state and returns reason_code / reason_message - pricing write responses are stable and typed - stale pricing mutation returns `detail.code = stale_draft_version`