- unify typed API errors across draft, pricing and publish flows - add stale draft and publish-state mutation guards - add publish readiness contract and guarded publish flow - add sellability reason codes to test seat preview - add pricing diagnostics and strengthen snapshot/publish lifecycle consistency
197 lines
6.7 KiB
Python
197 lines
6.7 KiB
Python
from fastapi import APIRouter, Depends
|
|
|
|
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 PricingRuleDiagnosticsResponse
|
|
from app.security.auth import require_api_key
|
|
from app.services.pricing_rule_diagnostics import build_pricing_rule_diagnostics
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get(
|
|
f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/pricing/rules/diagnostics",
|
|
response_model=PricingRuleDiagnosticsResponse,
|
|
)
|
|
async def get_pricing_rule_diagnostics(
|
|
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,
|
|
)
|
|
payload = await build_pricing_rule_diagnostics(
|
|
scheme_id=scheme.scheme_id,
|
|
scheme_version_id=version.scheme_version_id,
|
|
)
|
|
return PricingRuleDiagnosticsResponse(**payload)
|
|
|
|
|
|
@router.get(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/pricing/coverage")
|
|
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 = 0
|
|
unpriced = 0
|
|
|
|
for seat in seats:
|
|
if not seat.seat_id:
|
|
unpriced += 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 += 1
|
|
except Exception:
|
|
unpriced += 1
|
|
|
|
total = len(seats)
|
|
coverage_percent = round((priced / total) * 100, 2) if total else 100.0
|
|
|
|
return {
|
|
"scheme_id": scheme.scheme_id,
|
|
"scheme_version_id": version.scheme_version_id,
|
|
"total_seats": total,
|
|
"priced_seats": priced,
|
|
"unpriced_seats": unpriced,
|
|
"coverage_percent": coverage_percent,
|
|
}
|
|
|
|
|
|
@router.get(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/pricing/unpriced-seats")
|
|
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[dict] = []
|
|
for seat in seats:
|
|
if not seat.seat_id:
|
|
items.append(
|
|
{
|
|
"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 and cannot be priced.",
|
|
}
|
|
)
|
|
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 Exception:
|
|
items.append(
|
|
{
|
|
"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 {
|
|
"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}}")
|
|
async def explain_seat_pricing(
|
|
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,
|
|
)
|
|
|
|
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,
|
|
)
|
|
return {
|
|
"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_level": matched_rule_level,
|
|
"matched_target_ref": rule["target_ref"],
|
|
"pricing_category_id": rule["pricing_category_id"],
|
|
"amount": str(rule["amount"]),
|
|
"currency": rule["currency"],
|
|
},
|
|
}
|
|
except Exception:
|
|
return {
|
|
"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,
|
|
}
|