Files
svg-backend/backend/app/api/routes/pricing_diagnostics.py
greebo 2af5e49b8c feat(backend): add pricing coverage, unpriced seats, and explain endpoints
add backend endpoints for pricing coverage analysis and unpriced seat inspection

add explain endpoint to make effective pricing decisions traceable
improve pricing diagnostics for admin and editor workflows
2026-03-19 20:10:14 +03:00

211 lines
6.8 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 (
PricingCoverageSummaryResponse,
PricingExplainMatchedRule,
PricingExplainResponse,
UnpricedSeatItem,
UnpricedSeatsResponse,
)
from app.security.auth import require_api_key
router = APIRouter()
def _build_unpriced_reason(*, seat_id: str | None) -> tuple[str, str]:
if not seat_id:
return (
"missing_seat_id",
"Seat has no seat_id, so price resolution is impossible.",
)
return (
"no_price_rule",
"No effective price rule was found for this seat.",
)
@router.get(
f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/pricing/coverage",
response_model=PricingCoverageSummaryResponse,
)
async def get_pricing_coverage_summary(
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 Exception:
unpriced_seats += 1
total_seats = len(seats)
coverage_percent = round((priced_seats / total_seats) * 100, 2) if total_seats else 0.0
return PricingCoverageSummaryResponse(
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=UnpricedSeatsResponse,
)
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 seat.seat_id:
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,
)
continue
except Exception:
pass
reason_code, reason_message = _build_unpriced_reason(seat_id=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=reason_code,
reason_message=reason_message,
)
)
return UnpricedSeatsResponse(
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=PricingExplainResponse,
)
async def explain_pricing_for_seat(
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:
reason_code, reason_message = _build_unpriced_reason(seat_id=seat.seat_id)
return PricingExplainResponse(
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=reason_code,
reason_message=reason_message,
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,
)
return PricingExplainResponse(
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=PricingExplainMatchedRule(
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:
reason_code, reason_message = _build_unpriced_reason(seat_id=seat.seat_id)
return PricingExplainResponse(
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=reason_code,
reason_message=reason_message,
matched_rule=None,
)