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.scheme_registry import ( SchemeCurrentResponse, SchemeDetailResponse, SchemeListItem, SchemeListResponse, SchemePublishResponse, SchemeRollbackRequest, SchemeRollbackResponse, ) from app.schemas.scheme_versions import ( EnsureDraftResponse, 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() def _build_stale_current_version_detail( *, expected_scheme_version_id: str, actual_scheme_version_id: str, ) -> dict: return { "code": "stale_current_version", "message": "Current scheme version changed. Reload scheme state before creating a new version.", "expected_scheme_version_id": expected_scheme_version_id, "actual_scheme_version_id": actual_scheme_version_id, } @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( _build_stale_current_version_detail( 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.post(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/draft/ensure", response_model=EnsureDraftResponse) async def ensure_draft_scheme_version( scheme_id: str, expected_current_scheme_version_id: str | None = Query(default=None), role: str = Depends(require_api_key), ): scheme = await get_scheme_record_by_scheme_id(scheme_id) current_version = await get_current_scheme_version( scheme_id=scheme.scheme_id, current_version_number=scheme.current_version_number, ) if ( expected_current_scheme_version_id and expected_current_scheme_version_id != current_version.scheme_version_id ): raise_conflict( _build_stale_current_version_detail( expected_scheme_version_id=expected_current_scheme_version_id, actual_scheme_version_id=current_version.scheme_version_id, ) ) if scheme.status == "draft" and current_version.status == "draft": return EnsureDraftResponse( scheme_id=scheme.scheme_id, scheme_version_id=current_version.scheme_version_id, version_number=current_version.version_number, status=current_version.status, normalized_storage_path=current_version.normalized_storage_path, created=False, source_scheme_version_id=None, ) 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, "reason": "ensure_draft", }, ) return EnsureDraftResponse( 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, created=True, source_scheme_version_id=current_version.scheme_version_id, ) @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") 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, )