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
274 lines
10 KiB
Python
274 lines
10 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_groups import clone_scheme_version_groups
|
|
from app.repositories.scheme_seats import clone_scheme_version_seats
|
|
from app.repositories.scheme_sectors import clone_scheme_version_sectors
|
|
from app.repositories.scheme_versions import (
|
|
count_scheme_versions,
|
|
create_next_scheme_version_from_current,
|
|
get_current_scheme_version,
|
|
list_scheme_versions,
|
|
)
|
|
from app.repositories.schemes import (
|
|
count_scheme_records,
|
|
get_scheme_record_by_scheme_id,
|
|
list_scheme_records,
|
|
rollback_scheme_to_version,
|
|
unpublish_scheme,
|
|
)
|
|
from app.schemas.publish_readiness import SchemePublishActionResponse
|
|
from app.schemas.scheme_registry import (
|
|
SchemeCurrentResponse,
|
|
SchemeDetailResponse,
|
|
SchemeListItem,
|
|
SchemeListResponse,
|
|
SchemePublishResponse,
|
|
SchemeRollbackRequest,
|
|
SchemeRollbackResponse,
|
|
)
|
|
from app.schemas.scheme_versions import (
|
|
SchemeVersionCreateResponse,
|
|
SchemeVersionListItem,
|
|
SchemeVersionListResponse,
|
|
)
|
|
from app.security.auth import require_api_key
|
|
from app.services.api_errors import raise_conflict
|
|
from app.services.publish_service import publish_current_draft_scheme
|
|
from app.services.scheme_validation import build_scheme_validation_report
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get(f"{settings.api_v1_prefix}/schemes", response_model=SchemeListResponse)
|
|
async def get_schemes(
|
|
limit: int = Query(default=50, ge=1, le=200),
|
|
offset: int = Query(default=0, ge=0),
|
|
role: str = Depends(require_api_key),
|
|
):
|
|
rows = await list_scheme_records(limit=limit, offset=offset)
|
|
total = await count_scheme_records()
|
|
|
|
items = [
|
|
SchemeListItem(
|
|
scheme_id=row.scheme_id,
|
|
source_upload_id=row.source_upload_id,
|
|
name=row.name,
|
|
status=row.status,
|
|
current_version_number=row.current_version_number,
|
|
published_at=row.published_at.isoformat() if row.published_at else None,
|
|
normalized_elements_count=row.normalized_elements_count,
|
|
normalized_seats_count=row.normalized_seats_count,
|
|
normalized_groups_count=row.normalized_groups_count,
|
|
normalized_sectors_count=row.normalized_sectors_count,
|
|
created_at=row.created_at.isoformat(),
|
|
)
|
|
for row in rows
|
|
]
|
|
return SchemeListResponse(items=items, total=total)
|
|
|
|
|
|
@router.get(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}", response_model=SchemeDetailResponse)
|
|
async def get_scheme(scheme_id: str, role: str = Depends(require_api_key)):
|
|
row = await get_scheme_record_by_scheme_id(scheme_id)
|
|
return SchemeDetailResponse(
|
|
scheme_id=row.scheme_id,
|
|
source_upload_id=row.source_upload_id,
|
|
name=row.name,
|
|
status=row.status,
|
|
current_version_number=row.current_version_number,
|
|
published_at=row.published_at.isoformat() if row.published_at else None,
|
|
normalized_elements_count=row.normalized_elements_count,
|
|
normalized_seats_count=row.normalized_seats_count,
|
|
normalized_groups_count=row.normalized_groups_count,
|
|
normalized_sectors_count=row.normalized_sectors_count,
|
|
created_at=row.created_at.isoformat(),
|
|
)
|
|
|
|
|
|
@router.get(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/current", response_model=SchemeCurrentResponse)
|
|
async def get_scheme_current(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,
|
|
)
|
|
return SchemeCurrentResponse(
|
|
scheme_id=version.scheme_id,
|
|
scheme_version_id=version.scheme_version_id,
|
|
version_number=version.version_number,
|
|
status=version.status,
|
|
normalized_storage_path=version.normalized_storage_path,
|
|
normalized_elements_count=version.normalized_elements_count,
|
|
normalized_seats_count=version.normalized_seats_count,
|
|
normalized_groups_count=version.normalized_groups_count,
|
|
normalized_sectors_count=version.normalized_sectors_count,
|
|
created_at=version.created_at.isoformat(),
|
|
)
|
|
|
|
|
|
@router.get(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/versions", response_model=SchemeVersionListResponse)
|
|
async def get_scheme_versions(
|
|
scheme_id: str,
|
|
limit: int = Query(default=100, ge=1, le=200),
|
|
offset: int = Query(default=0, ge=0),
|
|
role: str = Depends(require_api_key),
|
|
):
|
|
rows = await list_scheme_versions(scheme_id=scheme_id, limit=limit, offset=offset)
|
|
total = await count_scheme_versions(scheme_id=scheme_id)
|
|
|
|
items = [
|
|
SchemeVersionListItem(
|
|
scheme_version_id=row.scheme_version_id,
|
|
scheme_id=row.scheme_id,
|
|
version_number=row.version_number,
|
|
status=row.status,
|
|
normalized_storage_path=row.normalized_storage_path,
|
|
normalized_elements_count=row.normalized_elements_count,
|
|
normalized_seats_count=row.normalized_seats_count,
|
|
normalized_groups_count=row.normalized_groups_count,
|
|
normalized_sectors_count=row.normalized_sectors_count,
|
|
created_at=row.created_at.isoformat(),
|
|
)
|
|
for row in rows
|
|
]
|
|
return SchemeVersionListResponse(items=items, total=total)
|
|
|
|
|
|
@router.post(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/versions", response_model=SchemeVersionCreateResponse)
|
|
async def create_next_scheme_version_endpoint(
|
|
scheme_id: str,
|
|
expected_current_scheme_version_id: str | None = Query(default=None),
|
|
role: str = Depends(require_api_key),
|
|
):
|
|
current_scheme = await get_scheme_record_by_scheme_id(scheme_id)
|
|
current_version = await get_current_scheme_version(
|
|
scheme_id=current_scheme.scheme_id,
|
|
current_version_number=current_scheme.current_version_number,
|
|
)
|
|
|
|
if (
|
|
expected_current_scheme_version_id
|
|
and expected_current_scheme_version_id != current_version.scheme_version_id
|
|
):
|
|
raise_conflict(
|
|
code="stale_current_version",
|
|
message="Current scheme version changed. Reload scheme state before creating a new version.",
|
|
details={
|
|
"expected_scheme_version_id": expected_current_scheme_version_id,
|
|
"actual_scheme_version_id": current_version.scheme_version_id,
|
|
},
|
|
)
|
|
|
|
new_version = await create_next_scheme_version_from_current(scheme_id)
|
|
|
|
await clone_scheme_version_sectors(
|
|
source_scheme_version_id=current_version.scheme_version_id,
|
|
target_scheme_version_id=new_version.scheme_version_id,
|
|
)
|
|
await clone_scheme_version_groups(
|
|
source_scheme_version_id=current_version.scheme_version_id,
|
|
target_scheme_version_id=new_version.scheme_version_id,
|
|
)
|
|
await clone_scheme_version_seats(
|
|
source_scheme_version_id=current_version.scheme_version_id,
|
|
target_scheme_version_id=new_version.scheme_version_id,
|
|
)
|
|
|
|
await create_audit_event(
|
|
scheme_id=scheme_id,
|
|
event_type="scheme.version.created",
|
|
object_type="scheme_version",
|
|
object_ref=new_version.scheme_version_id,
|
|
details={
|
|
"source_scheme_version_id": current_version.scheme_version_id,
|
|
"version_number": new_version.version_number,
|
|
"normalized_storage_path": new_version.normalized_storage_path,
|
|
},
|
|
)
|
|
|
|
return SchemeVersionCreateResponse(
|
|
scheme_id=new_version.scheme_id,
|
|
scheme_version_id=new_version.scheme_version_id,
|
|
version_number=new_version.version_number,
|
|
status=new_version.status,
|
|
normalized_storage_path=new_version.normalized_storage_path,
|
|
)
|
|
|
|
|
|
@router.get(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/publish/validation")
|
|
async def get_publish_validation(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,
|
|
)
|
|
report = await build_scheme_validation_report(
|
|
scheme_id=scheme.scheme_id,
|
|
scheme_version_id=version.scheme_version_id,
|
|
)
|
|
return {
|
|
"scheme_id": scheme.scheme_id,
|
|
"scheme_version_id": version.scheme_version_id,
|
|
"report": report,
|
|
}
|
|
|
|
|
|
@router.post(
|
|
f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/publish",
|
|
response_model=SchemePublishActionResponse,
|
|
)
|
|
async def publish_scheme_endpoint(
|
|
scheme_id: str,
|
|
expected_scheme_version_id: str | None = Query(default=None),
|
|
role: str = Depends(require_api_key),
|
|
):
|
|
return await publish_current_draft_scheme(
|
|
scheme_id=scheme_id,
|
|
expected_scheme_version_id=expected_scheme_version_id,
|
|
)
|
|
|
|
|
|
@router.post(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/unpublish", response_model=SchemePublishResponse)
|
|
async def unpublish_scheme_endpoint(scheme_id: str, role: str = Depends(require_api_key)):
|
|
row = await unpublish_scheme(scheme_id)
|
|
await create_audit_event(
|
|
scheme_id=row.scheme_id,
|
|
event_type="scheme.unpublished",
|
|
object_type="scheme",
|
|
object_ref=row.scheme_id,
|
|
details={"current_version_number": row.current_version_number, "status": row.status},
|
|
)
|
|
return SchemePublishResponse(
|
|
scheme_id=row.scheme_id,
|
|
status=row.status,
|
|
current_version_number=row.current_version_number,
|
|
published_at=row.published_at.isoformat() if row.published_at else None,
|
|
)
|
|
|
|
|
|
@router.post(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/rollback", response_model=SchemeRollbackResponse)
|
|
async def rollback_scheme_endpoint(
|
|
scheme_id: str,
|
|
payload: SchemeRollbackRequest,
|
|
role: str = Depends(require_api_key),
|
|
):
|
|
row = await rollback_scheme_to_version(
|
|
scheme_id=scheme_id,
|
|
target_version_number=payload.target_version_number,
|
|
)
|
|
await create_audit_event(
|
|
scheme_id=row.scheme_id,
|
|
event_type="scheme.rolled_back",
|
|
object_type="scheme_version",
|
|
object_ref=str(payload.target_version_number),
|
|
details={"current_version_number": row.current_version_number, "status": row.status},
|
|
)
|
|
return SchemeRollbackResponse(
|
|
scheme_id=row.scheme_id,
|
|
status=row.status,
|
|
current_version_number=row.current_version_number,
|
|
published_at=row.published_at.isoformat() if row.published_at else None,
|
|
)
|