Files
svg-backend/backend/app/api/routes/pricing_diagnostics.py
greebo 8d4255181b feat(backend): add publish readiness contract and pricing diagnostics
add backend readiness contract for publish prechecks

add pricing diagnostics to explain publish-blocking conditions
make publish decisions more explicit and easier to debug for clients
2026-03-19 20:29:58 +03:00

216 lines
7.2 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, status
from app.core.config import settings
from app.repositories.pricing import find_effective_price_rule
from app.repositories.scheme_seats import get_scheme_version_seat_by_seat_id, list_scheme_version_seats
from app.repositories.scheme_versions import get_current_scheme_version
from app.repositories.schemes import get_scheme_record_by_scheme_id
from app.schemas.pricing_diagnostics import (
ExplainMatchedRule,
ExplainSeatPriceResponse,
PricingCoverageResponse,
UnpricedSeatItem,
UnpricedSeatListResponse,
)
from app.security.auth import require_api_key
router = APIRouter()
@router.get(
f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/pricing/coverage",
response_model=PricingCoverageResponse,
)
async def get_pricing_coverage(
scheme_id: str,
role: str = Depends(require_api_key),
):
scheme = await get_scheme_record_by_scheme_id(scheme_id)
version = await get_current_scheme_version(
scheme_id=scheme.scheme_id,
current_version_number=scheme.current_version_number,
)
seats = await list_scheme_version_seats(version.scheme_version_id)
priced_seats = 0
unpriced_seats = 0
for seat in seats:
if not seat.seat_id:
unpriced_seats += 1
continue
try:
await find_effective_price_rule(
scheme_id=scheme.scheme_id,
seat_id=seat.seat_id,
group_id=seat.group_id,
sector_id=seat.sector_id,
)
priced_seats += 1
except HTTPException as exc:
if exc.status_code != status.HTTP_404_NOT_FOUND:
raise
unpriced_seats += 1
total_seats = len(seats)
coverage_percent = round((priced_seats / total_seats) * 100, 2) if total_seats else 100.0
return PricingCoverageResponse(
scheme_id=scheme.scheme_id,
scheme_version_id=version.scheme_version_id,
total_seats=total_seats,
priced_seats=priced_seats,
unpriced_seats=unpriced_seats,
coverage_percent=coverage_percent,
)
@router.get(
f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/pricing/unpriced-seats",
response_model=UnpricedSeatListResponse,
)
async def get_unpriced_seats(
scheme_id: str,
role: str = Depends(require_api_key),
):
scheme = await get_scheme_record_by_scheme_id(scheme_id)
version = await get_current_scheme_version(
scheme_id=scheme.scheme_id,
current_version_number=scheme.current_version_number,
)
seats = await list_scheme_version_seats(version.scheme_version_id)
items: list[UnpricedSeatItem] = []
for seat in seats:
if not seat.seat_id:
items.append(
UnpricedSeatItem(
seat_record_id=seat.seat_record_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,
reason_code="missing_seat_id",
reason_message="Seat has no seat_id, so price resolution is not possible.",
)
)
continue
try:
await find_effective_price_rule(
scheme_id=scheme.scheme_id,
seat_id=seat.seat_id,
group_id=seat.group_id,
sector_id=seat.sector_id,
)
except HTTPException as exc:
if exc.status_code != status.HTTP_404_NOT_FOUND:
raise
items.append(
UnpricedSeatItem(
seat_record_id=seat.seat_record_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,
reason_code="no_price_rule",
reason_message="No effective price rule was found for this seat.",
)
)
return UnpricedSeatListResponse(
scheme_id=scheme.scheme_id,
scheme_version_id=version.scheme_version_id,
total=len(items),
items=items,
)
@router.get(
f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/pricing/explain/{{seat_id}}",
response_model=ExplainSeatPriceResponse,
)
async def explain_seat_price(
scheme_id: str,
seat_id: str,
role: str = Depends(require_api_key),
):
scheme = await get_scheme_record_by_scheme_id(scheme_id)
version = await get_current_scheme_version(
scheme_id=scheme.scheme_id,
current_version_number=scheme.current_version_number,
)
seat = await get_scheme_version_seat_by_seat_id(
scheme_version_id=version.scheme_version_id,
seat_id=seat_id,
)
if not seat.seat_id:
return ExplainSeatPriceResponse(
scheme_id=scheme.scheme_id,
scheme_version_id=version.scheme_version_id,
seat_id=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,
has_price=False,
reason_code="missing_seat_id",
reason_message="Seat has no seat_id, so price resolution is not possible.",
matched_rule=None,
)
try:
matched_rule_level, rule = await find_effective_price_rule(
scheme_id=scheme.scheme_id,
seat_id=seat.seat_id,
group_id=seat.group_id,
sector_id=seat.sector_id,
)
matched_rule = ExplainMatchedRule(
matched_rule_level=matched_rule_level,
matched_target_ref=rule["target_ref"],
pricing_category_id=rule["pricing_category_id"],
amount=str(rule["amount"]),
currency=rule["currency"],
)
return ExplainSeatPriceResponse(
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,
has_price=True,
reason_code="ok",
reason_message="Effective price rule resolved successfully.",
matched_rule=matched_rule,
)
except HTTPException as exc:
if exc.status_code != status.HTTP_404_NOT_FOUND:
raise
return ExplainSeatPriceResponse(
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,
has_price=False,
reason_code="no_price_rule",
reason_message="No effective price rule was found for this seat.",
matched_rule=None,
)