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
216 lines
7.2 KiB
Python
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,
|
|
)
|