add optimistic concurrency guards via expected scheme version id protect draft editor, pricing snapshot, remap and publish flows from stale mutations protect version creation from stale current version state keep backward compatibility with optional query guards verify 409 conflict behavior for stale clients and 200 for valid flows
149 lines
5.2 KiB
Python
149 lines
5.2 KiB
Python
from fastapi import APIRouter, Depends, Query
|
|
|
|
from app.core.config import settings
|
|
from app.repositories.audit import create_audit_event
|
|
from app.repositories.scheme_version_pricing import replace_scheme_version_pricing_snapshot
|
|
from app.schemas.publish_preview import (
|
|
PublishPreviewResponse,
|
|
RemapApplyRequest,
|
|
RemapApplyResponse,
|
|
RemapPreviewRequest,
|
|
RemapPreviewResponse,
|
|
RemapPreviewSeatItem,
|
|
)
|
|
from app.security.auth import require_api_key
|
|
from app.services.draft_guard import get_current_draft_context
|
|
from app.services.publish_preview import get_or_build_publish_preview_bundle
|
|
from app.services.remap_service import apply_remap, preview_remap
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.post(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/draft/pricing/snapshot")
|
|
async def create_draft_pricing_snapshot(
|
|
scheme_id: str,
|
|
expected_scheme_version_id: str | None = Query(default=None),
|
|
role: str = Depends(require_api_key),
|
|
):
|
|
scheme, version = await get_current_draft_context(
|
|
scheme_id=scheme_id,
|
|
expected_scheme_version_id=expected_scheme_version_id,
|
|
)
|
|
result = await replace_scheme_version_pricing_snapshot(
|
|
scheme_id=scheme.scheme_id,
|
|
scheme_version_id=version.scheme_version_id,
|
|
)
|
|
|
|
await create_audit_event(
|
|
scheme_id=scheme.scheme_id,
|
|
event_type="draft.pricing.snapshot.created",
|
|
object_type="pricing_snapshot",
|
|
object_ref=version.scheme_version_id,
|
|
details=result,
|
|
)
|
|
|
|
return {
|
|
"scheme_id": scheme.scheme_id,
|
|
"scheme_version_id": version.scheme_version_id,
|
|
**result,
|
|
}
|
|
|
|
|
|
@router.get(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/draft/publish-preview", response_model=PublishPreviewResponse)
|
|
async def get_publish_preview(
|
|
scheme_id: str,
|
|
baseline_scheme_version_id: str | None = Query(default=None),
|
|
refresh: bool = Query(default=False),
|
|
expected_scheme_version_id: str | None = Query(default=None),
|
|
role: str = Depends(require_api_key),
|
|
):
|
|
scheme, version = await get_current_draft_context(
|
|
scheme_id=scheme_id,
|
|
expected_scheme_version_id=expected_scheme_version_id,
|
|
)
|
|
bundle = await get_or_build_publish_preview_bundle(
|
|
scheme_id=scheme.scheme_id,
|
|
scheme_version_id=version.scheme_version_id,
|
|
baseline_override_scheme_version_id=baseline_scheme_version_id,
|
|
refresh=refresh,
|
|
)
|
|
return PublishPreviewResponse(
|
|
scheme_id=scheme.scheme_id,
|
|
scheme_version_id=version.scheme_version_id,
|
|
artifacts=bundle["artifacts"],
|
|
validation=bundle["validation"],
|
|
structure_diff=bundle["structure_diff"],
|
|
pricing_coverage=bundle["pricing_coverage"],
|
|
summary=bundle["summary"],
|
|
)
|
|
|
|
|
|
@router.post(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/draft/remap/preview", response_model=RemapPreviewResponse)
|
|
async def preview_draft_remap(
|
|
scheme_id: str,
|
|
payload: RemapPreviewRequest,
|
|
expected_scheme_version_id: str | None = Query(default=None),
|
|
role: str = Depends(require_api_key),
|
|
):
|
|
scheme, version = await get_current_draft_context(
|
|
scheme_id=scheme_id,
|
|
expected_scheme_version_id=expected_scheme_version_id,
|
|
)
|
|
items = await preview_remap(
|
|
scheme_version_id=version.scheme_version_id,
|
|
seat_record_ids=payload.seat_record_ids,
|
|
from_sector_id=payload.from_sector_id,
|
|
to_sector_id=payload.to_sector_id,
|
|
from_group_id=payload.from_group_id,
|
|
to_group_id=payload.to_group_id,
|
|
)
|
|
|
|
return RemapPreviewResponse(
|
|
scheme_id=scheme.scheme_id,
|
|
scheme_version_id=version.scheme_version_id,
|
|
matched_count=len(items),
|
|
items=[RemapPreviewSeatItem(**item) for item in items],
|
|
)
|
|
|
|
|
|
@router.post(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/draft/remap/apply", response_model=RemapApplyResponse)
|
|
async def apply_draft_remap(
|
|
scheme_id: str,
|
|
payload: RemapApplyRequest,
|
|
expected_scheme_version_id: str | None = Query(default=None),
|
|
role: str = Depends(require_api_key),
|
|
):
|
|
scheme, version = await get_current_draft_context(
|
|
scheme_id=scheme_id,
|
|
expected_scheme_version_id=expected_scheme_version_id,
|
|
)
|
|
items = await apply_remap(
|
|
scheme_version_id=version.scheme_version_id,
|
|
seat_record_ids=payload.seat_record_ids,
|
|
from_sector_id=payload.from_sector_id,
|
|
to_sector_id=payload.to_sector_id,
|
|
from_group_id=payload.from_group_id,
|
|
to_group_id=payload.to_group_id,
|
|
)
|
|
|
|
await create_audit_event(
|
|
scheme_id=scheme.scheme_id,
|
|
event_type="draft.remap.applied",
|
|
object_type="draft_structure",
|
|
object_ref=version.scheme_version_id,
|
|
details={
|
|
"matched_count": len(items),
|
|
"from_sector_id": payload.from_sector_id,
|
|
"to_sector_id": payload.to_sector_id,
|
|
"from_group_id": payload.from_group_id,
|
|
"to_group_id": payload.to_group_id,
|
|
},
|
|
)
|
|
|
|
return RemapApplyResponse(
|
|
scheme_id=scheme.scheme_id,
|
|
scheme_version_id=version.scheme_version_id,
|
|
updated_count=len(items),
|
|
items=[RemapPreviewSeatItem(**item) for item in items],
|
|
)
|