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 ( create_scheme_version_group, delete_scheme_version_group_by_record_id, get_scheme_version_group_by_record_id, list_scheme_version_groups, update_scheme_version_group_by_record_id, ) from app.repositories.scheme_seats import ( bulk_update_scheme_version_seats_by_record_id, cascade_update_seat_group_reference, cascade_update_seat_sector_reference, get_scheme_version_seat_by_record_id, list_scheme_version_seats, update_scheme_version_seat_by_record_id, ) from app.repositories.scheme_sectors import ( create_scheme_version_sector, delete_scheme_version_sector_by_record_id, get_scheme_version_sector_by_record_id, list_scheme_version_sectors, update_scheme_version_sector_by_record_id, ) from app.schemas.editor import ( BulkSeatPatchRequest, BulkSeatPatchResponse, BulkSeatPatchResultItem, CreateGroupRequest, CreateGroupResponse, CreateSectorRequest, CreateSectorResponse, DeleteEntityResponse, DraftGroupItem, DraftSeatItem, DraftSectorItem, DraftStructureResponse, GroupPatchRequest, GroupPatchResponse, RepairReferencesResponse, SeatPatchRequest, SeatPatchResponse, SectorPatchRequest, SectorPatchResponse, StructureDiffEntityItem, StructureDiffResponse, ) from app.security.auth import require_api_key from app.services.draft_guard import get_current_draft_context from app.services.editor_validation import ( validate_bulk_seat_patch_references, validate_bulk_seat_patch_uniqueness, validate_create_group_uniqueness, validate_create_sector_uniqueness, validate_group_patch_uniqueness, validate_sector_patch_uniqueness, validate_single_seat_patch_references, validate_single_seat_patch_uniqueness, ) from app.services.scheme_validation import build_scheme_validation_report from app.services.structure_diff import build_structure_diff from app.services.structure_sync import repair_structure_references router = APIRouter() @router.get(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/draft/structure", response_model=DraftStructureResponse) async def get_draft_structure( 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, ) seats = await list_scheme_version_seats(version.scheme_version_id) sectors = await list_scheme_version_sectors(version.scheme_version_id) groups = await list_scheme_version_groups(version.scheme_version_id) return DraftStructureResponse( scheme_id=scheme.scheme_id, scheme_version_id=version.scheme_version_id, status=version.status, seats=[ DraftSeatItem( seat_record_id=row.seat_record_id, scheme_id=row.scheme_id, scheme_version_id=row.scheme_version_id, element_id=row.element_id, seat_id=row.seat_id, sector_id=row.sector_id, group_id=row.group_id, row_label=row.row_label, seat_number=row.seat_number, tag=row.tag, classes_raw=row.classes_raw, x=row.x, y=row.y, cx=row.cx, cy=row.cy, width=row.width, height=row.height, created_at=row.created_at.isoformat(), ) for row in seats ], sectors=[ DraftSectorItem( sector_record_id=row.sector_record_id, scheme_id=row.scheme_id, scheme_version_id=row.scheme_version_id, element_id=row.element_id, sector_id=row.sector_id, name=row.name, classes_raw=row.classes_raw, created_at=row.created_at.isoformat(), ) for row in sectors ], groups=[ DraftGroupItem( group_record_id=row.group_record_id, scheme_id=row.scheme_id, scheme_version_id=row.scheme_version_id, element_id=row.element_id, group_id=row.group_id, name=row.name, classes_raw=row.classes_raw, created_at=row.created_at.isoformat(), ) for row in groups ], total_seats=len(seats), total_sectors=len(sectors), total_groups=len(groups), ) @router.get(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/draft/validation") async def get_draft_validation( 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, ) 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, "status": version.status, "report": report, } @router.get(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/draft/seats/records/{{seat_record_id}}", response_model=DraftSeatItem) async def get_draft_seat_by_record_id( scheme_id: str, seat_record_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, ) row = await get_scheme_version_seat_by_record_id( scheme_version_id=version.scheme_version_id, seat_record_id=seat_record_id, ) return DraftSeatItem( seat_record_id=row.seat_record_id, scheme_id=row.scheme_id, scheme_version_id=row.scheme_version_id, element_id=row.element_id, seat_id=row.seat_id, sector_id=row.sector_id, group_id=row.group_id, row_label=row.row_label, seat_number=row.seat_number, tag=row.tag, classes_raw=row.classes_raw, x=row.x, y=row.y, cx=row.cx, cy=row.cy, width=row.width, height=row.height, created_at=row.created_at.isoformat(), ) @router.get(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/draft/sectors/records/{{sector_record_id}}", response_model=DraftSectorItem) async def get_draft_sector_by_record_id( scheme_id: str, sector_record_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, ) row = await get_scheme_version_sector_by_record_id( scheme_version_id=version.scheme_version_id, sector_record_id=sector_record_id, ) return DraftSectorItem( sector_record_id=row.sector_record_id, scheme_id=row.scheme_id, scheme_version_id=row.scheme_version_id, element_id=row.element_id, sector_id=row.sector_id, name=row.name, classes_raw=row.classes_raw, created_at=row.created_at.isoformat(), ) @router.get(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/draft/groups/records/{{group_record_id}}", response_model=DraftGroupItem) async def get_draft_group_by_record_id( scheme_id: str, group_record_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, ) row = await get_scheme_version_group_by_record_id( scheme_version_id=version.scheme_version_id, group_record_id=group_record_id, ) return DraftGroupItem( group_record_id=row.group_record_id, scheme_id=row.scheme_id, scheme_version_id=row.scheme_version_id, element_id=row.element_id, group_id=row.group_id, name=row.name, classes_raw=row.classes_raw, created_at=row.created_at.isoformat(), ) @router.get(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/draft/compare-preview", response_model=StructureDiffResponse) async def get_draft_compare_preview( 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, ) diff = await build_structure_diff( scheme_id=scheme.scheme_id, draft_scheme_version_id=version.scheme_version_id, ) return StructureDiffResponse( scheme_id=scheme.scheme_id, draft_scheme_version_id=version.scheme_version_id, baseline_scheme_version_id=diff["baseline_scheme_version_id"], summary=diff["summary"], sectors=[StructureDiffEntityItem(**item) for item in diff["sectors"]], groups=[StructureDiffEntityItem(**item) for item in diff["groups"]], seats=[StructureDiffEntityItem(**item) for item in diff["seats"]], ) @router.post(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/draft/sectors", response_model=CreateSectorResponse) async def create_draft_sector( scheme_id: str, payload: CreateSectorRequest, 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, ) await validate_create_sector_uniqueness( scheme_version_id=version.scheme_version_id, sector_id=payload.sector_id, element_id=payload.element_id, ) row = await create_scheme_version_sector( scheme_id=scheme.scheme_id, scheme_version_id=version.scheme_version_id, element_id=payload.element_id, sector_id=payload.sector_id, name=payload.name, classes_raw=payload.classes_raw, ) await create_audit_event( scheme_id=scheme.scheme_id, event_type="draft.sector.created", object_type="sector", object_ref=row.sector_record_id, details={ "scheme_version_id": version.scheme_version_id, "sector_id": row.sector_id, "name": row.name, }, ) return CreateSectorResponse( scheme_id=scheme.scheme_id, scheme_version_id=version.scheme_version_id, sector_record_id=row.sector_record_id, element_id=row.element_id, sector_id=row.sector_id, name=row.name, ) @router.post(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/draft/groups", response_model=CreateGroupResponse) async def create_draft_group( scheme_id: str, payload: CreateGroupRequest, 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, ) await validate_create_group_uniqueness( scheme_version_id=version.scheme_version_id, group_id=payload.group_id, element_id=payload.element_id, ) row = await create_scheme_version_group( scheme_id=scheme.scheme_id, scheme_version_id=version.scheme_version_id, element_id=payload.element_id, group_id=payload.group_id, name=payload.name, classes_raw=payload.classes_raw, ) await create_audit_event( scheme_id=scheme.scheme_id, event_type="draft.group.created", object_type="group", object_ref=row.group_record_id, details={ "scheme_version_id": version.scheme_version_id, "group_id": row.group_id, "name": row.name, }, ) return CreateGroupResponse( scheme_id=scheme.scheme_id, scheme_version_id=version.scheme_version_id, group_record_id=row.group_record_id, element_id=row.element_id, group_id=row.group_id, name=row.name, ) @router.delete(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/draft/sectors/records/{{sector_record_id}}", response_model=DeleteEntityResponse) async def delete_draft_sector( scheme_id: str, sector_record_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, ) await delete_scheme_version_sector_by_record_id( scheme_version_id=version.scheme_version_id, sector_record_id=sector_record_id, ) await create_audit_event( scheme_id=scheme.scheme_id, event_type="draft.sector.deleted", object_type="sector", object_ref=sector_record_id, details={"scheme_version_id": version.scheme_version_id}, ) return DeleteEntityResponse( scheme_id=scheme.scheme_id, scheme_version_id=version.scheme_version_id, deleted=True, record_id=sector_record_id, ) @router.delete(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/draft/groups/records/{{group_record_id}}", response_model=DeleteEntityResponse) async def delete_draft_group( scheme_id: str, group_record_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, ) await delete_scheme_version_group_by_record_id( scheme_version_id=version.scheme_version_id, group_record_id=group_record_id, ) await create_audit_event( scheme_id=scheme.scheme_id, event_type="draft.group.deleted", object_type="group", object_ref=group_record_id, details={"scheme_version_id": version.scheme_version_id}, ) return DeleteEntityResponse( scheme_id=scheme.scheme_id, scheme_version_id=version.scheme_version_id, deleted=True, record_id=group_record_id, ) @router.patch(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/draft/seats/records/{{seat_record_id}}", response_model=SeatPatchResponse) async def patch_draft_seat( scheme_id: str, seat_record_id: str, payload: SeatPatchRequest, 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, ) await validate_single_seat_patch_uniqueness( scheme_version_id=version.scheme_version_id, seat_record_id=seat_record_id, new_seat_id=payload.seat_id, ) await validate_single_seat_patch_references( scheme_version_id=version.scheme_version_id, sector_id=payload.sector_id, group_id=payload.group_id, ) row = await update_scheme_version_seat_by_record_id( scheme_version_id=version.scheme_version_id, seat_record_id=seat_record_id, seat_id=payload.seat_id, sector_id=payload.sector_id, group_id=payload.group_id, row_label=payload.row_label, seat_number=payload.seat_number, ) await create_audit_event( scheme_id=scheme.scheme_id, event_type="draft.seat.updated", object_type="seat", object_ref=seat_record_id, details={ "scheme_version_id": version.scheme_version_id, "seat_id": payload.seat_id, "sector_id": payload.sector_id, "group_id": payload.group_id, "row_label": payload.row_label, "seat_number": payload.seat_number, }, ) return SeatPatchResponse( scheme_id=scheme.scheme_id, scheme_version_id=version.scheme_version_id, element_id=row.element_id, seat_id=row.seat_id, sector_id=row.sector_id, group_id=row.group_id, row_label=row.row_label, seat_number=row.seat_number, ) @router.post(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/draft/seats/bulk", response_model=BulkSeatPatchResponse) async def bulk_patch_draft_seats( scheme_id: str, payload: BulkSeatPatchRequest, 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 = [item.model_dump() for item in payload.items] await validate_bulk_seat_patch_uniqueness( scheme_version_id=version.scheme_version_id, items=items, ) await validate_bulk_seat_patch_references( scheme_version_id=version.scheme_version_id, items=items, ) rows = await bulk_update_scheme_version_seats_by_record_id( scheme_version_id=version.scheme_version_id, items=items, ) await create_audit_event( scheme_id=scheme.scheme_id, event_type="draft.seats.bulk_updated", object_type="seat_bulk", object_ref=version.scheme_version_id, details={ "scheme_version_id": version.scheme_version_id, "updated_count": len(rows), }, ) return BulkSeatPatchResponse( scheme_id=scheme.scheme_id, scheme_version_id=version.scheme_version_id, updated_count=len(rows), items=[ BulkSeatPatchResultItem( seat_record_id=payload.items[idx].seat_record_id, updated_seat_id=row.seat_id, sector_id=row.sector_id, group_id=row.group_id, row_label=row.row_label, seat_number=row.seat_number, ) for idx, row in enumerate(rows) ], ) @router.patch(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/draft/sectors/records/{{sector_record_id}}", response_model=SectorPatchResponse) async def patch_draft_sector( scheme_id: str, sector_record_id: str, payload: SectorPatchRequest, 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, ) await validate_sector_patch_uniqueness( scheme_version_id=version.scheme_version_id, sector_record_id=sector_record_id, new_sector_id=payload.sector_id, ) row, old_sector_id = await update_scheme_version_sector_by_record_id( scheme_version_id=version.scheme_version_id, sector_record_id=sector_record_id, sector_id=payload.sector_id, name=payload.name, ) cascaded_count = await cascade_update_seat_sector_reference( scheme_version_id=version.scheme_version_id, old_sector_id=old_sector_id, new_sector_id=payload.sector_id, ) repair_result = await repair_structure_references( scheme_version_id=version.scheme_version_id, ) await create_audit_event( scheme_id=scheme.scheme_id, event_type="draft.sector.updated", object_type="sector", object_ref=sector_record_id, details={ "scheme_version_id": version.scheme_version_id, "old_sector_id": old_sector_id, "new_sector_id": payload.sector_id, "name": payload.name, "cascaded_seats_count": cascaded_count, "repair_result": repair_result, }, ) return SectorPatchResponse( scheme_id=scheme.scheme_id, scheme_version_id=version.scheme_version_id, element_id=row.element_id, sector_id=row.sector_id, name=row.name, ) @router.patch(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/draft/groups/records/{{group_record_id}}", response_model=GroupPatchResponse) async def patch_draft_group( scheme_id: str, group_record_id: str, payload: GroupPatchRequest, 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, ) await validate_group_patch_uniqueness( scheme_version_id=version.scheme_version_id, group_record_id=group_record_id, new_group_id=payload.group_id, ) row, old_group_id = await update_scheme_version_group_by_record_id( scheme_version_id=version.scheme_version_id, group_record_id=group_record_id, group_id=payload.group_id, name=payload.name, ) cascaded_count = await cascade_update_seat_group_reference( scheme_version_id=version.scheme_version_id, old_group_id=old_group_id, new_group_id=payload.group_id, ) repair_result = await repair_structure_references( scheme_version_id=version.scheme_version_id, ) await create_audit_event( scheme_id=scheme.scheme_id, event_type="draft.group.updated", object_type="group", object_ref=group_record_id, details={ "scheme_version_id": version.scheme_version_id, "old_group_id": old_group_id, "new_group_id": payload.group_id, "name": payload.name, "cascaded_seats_count": cascaded_count, "repair_result": repair_result, }, ) return GroupPatchResponse( scheme_id=scheme.scheme_id, scheme_version_id=version.scheme_version_id, element_id=row.element_id, group_id=row.group_id, name=row.name, ) @router.post(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/draft/repair-references", response_model=RepairReferencesResponse) async def repair_draft_references( 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 repair_structure_references( scheme_version_id=version.scheme_version_id, ) await create_audit_event( scheme_id=scheme.scheme_id, event_type="draft.references.repaired", object_type="draft_structure", object_ref=version.scheme_version_id, details=result, ) return RepairReferencesResponse( scheme_id=scheme.scheme_id, scheme_version_id=version.scheme_version_id, repaired_sector_refs_count=result.get("repaired_sector_refs_count", 0), repaired_group_refs_count=result.get("repaired_group_refs_count", 0), details=result, )