feat(backend): add sellability reason codes and string price serialization to test seat preview

extend test seat preview with explicit sellability reason codes

serialize preview price amount as string for a stable API contract
improve diagnosis of non-sellable states for preview consumers
This commit is contained in:
greebo
2026-03-19 20:07:19 +03:00
parent af175d88dd
commit aab5a51654
4 changed files with 43 additions and 14 deletions

View File

@@ -30,12 +30,6 @@ async def preview_test_seat(
seat_id=seat_id, 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_rule_level = None
matched_target_ref = None matched_target_ref = None
pricing_category_id = None pricing_category_id = None
@@ -43,6 +37,27 @@ async def preview_test_seat(
currency = None currency = None
has_price = False 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: try:
matched_rule_level, rule = await find_effective_price_rule( matched_rule_level, rule = await find_effective_price_rule(
scheme_id=scheme.scheme_id, scheme_id=scheme.scheme_id,
@@ -52,7 +67,7 @@ async def preview_test_seat(
) )
matched_target_ref = rule["target_ref"] matched_target_ref = rule["target_ref"]
pricing_category_id = rule["pricing_category_id"] pricing_category_id = rule["pricing_category_id"]
amount = rule["amount"] amount = str(rule["amount"])
currency = rule["currency"] currency = rule["currency"]
has_price = True has_price = True
except HTTPException as exc: except HTTPException as exc:
@@ -66,15 +81,25 @@ async def preview_test_seat(
) )
raise HTTPException( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, 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( return TestSeatPreviewResponse(
scheme_id=scheme.scheme_id, scheme_id=scheme.scheme_id,
scheme_version_id=version.scheme_version_id, scheme_version_id=version.scheme_version_id,
seat_id=seat.seat_id, seat_id=seat.seat_id,
element_id=seat.element_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, group_id=seat.group_id,
row_label=seat.row_label, row_label=seat.row_label,
seat_number=seat.seat_number, seat_number=seat.seat_number,
@@ -85,4 +110,6 @@ async def preview_test_seat(
pricing_category_id=pricing_category_id, pricing_category_id=pricing_category_id,
amount=amount, amount=amount,
currency=currency, currency=currency,
reason_code=reason_code,
reason_message=reason_message,
) )

View File

@@ -1,12 +1,10 @@
from decimal import Decimal
from pydantic import BaseModel from pydantic import BaseModel
class TestSeatPreviewResponse(BaseModel): class TestSeatPreviewResponse(BaseModel):
scheme_id: str scheme_id: str
scheme_version_id: str scheme_version_id: str
seat_id: str seat_id: str | None
element_id: str | None element_id: str | None
sector_id: str | None sector_id: str | None
group_id: str | None group_id: str | None
@@ -17,5 +15,7 @@ class TestSeatPreviewResponse(BaseModel):
matched_rule_level: str | None matched_rule_level: str | None
matched_target_ref: str | None matched_target_ref: str | None
pricing_category_id: str | None pricing_category_id: str | None
amount: Decimal | None amount: str | None
currency: str | None currency: str | None
reason_code: str
reason_message: str

View File

@@ -84,3 +84,5 @@
- This file is an operational route index, not a generated OpenAPI export. - 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. - 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. - 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.

View File

@@ -73,7 +73,7 @@ Validate:
Validate: Validate:
- pricing bundle contains categories and rules arrays - pricing bundle contains categories and rules arrays
- effective seat price resolves according to domain priority - 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 - pricing write responses are stable and typed
- stale pricing mutation returns `detail.code = stale_draft_version` - stale pricing mutation returns `detail.code = stale_draft_version`