Files
svg-backend/backend/app/api/routes/publish.py
greebo 7b6c12f924 feat(backend): add publish readiness endpoint and enforce publish gate contract
add backend endpoint for publish readiness checks

enforce publish gate contract before version publication
make publish preconditions explicit and consistent for clients
2026-03-19 20:15:48 +03:00

180 lines
6.3 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.schemas.publish_readiness import PublishReadinessResponse
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.publish_readiness import build_publish_readiness
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,
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-readiness",
response_model=PublishReadinessResponse,
)
async def get_draft_publish_readiness(
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,
expected_scheme_version_id=expected_scheme_version_id,
)
readiness = await build_publish_readiness(
scheme_id=scheme.scheme_id,
scheme_version_id=version.scheme_version_id,
require_full_pricing_coverage=settings.publish_require_full_pricing_coverage,
)
return PublishReadinessResponse(
scheme_id=scheme.scheme_id,
scheme_version_id=version.scheme_version_id,
status=version.status,
validation_summary=readiness["validation_summary"],
pricing_coverage=readiness["pricing_coverage"],
snapshot=readiness["snapshot"],
readiness=readiness["readiness"],
)
@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,
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,
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,
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],
)