fix(core): stabilize editor lifecycle, transactional versions, and runtime config
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from fastapi import APIRouter, Depends, Query, Request
|
||||
|
||||
from app.core.config import settings
|
||||
from app.repositories.audit import create_audit_event
|
||||
@@ -508,6 +508,7 @@ async def delete_draft_group(
|
||||
|
||||
@router.patch(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/draft/seats/records/{{seat_record_id}}", response_model=SeatPatchResponse)
|
||||
async def patch_draft_seat(
|
||||
request: Request,
|
||||
scheme_id: str,
|
||||
seat_record_id: str,
|
||||
payload: SeatPatchRequest,
|
||||
@@ -530,14 +531,20 @@ async def patch_draft_seat(
|
||||
group_id=payload.group_id,
|
||||
)
|
||||
|
||||
raw_json = await request.json()
|
||||
update_data = {k: v for k, v in payload.model_dump(exclude_unset=True).items() if k in raw_json}
|
||||
for field in ("seat_id", "sector_id", "group_id"):
|
||||
if field in update_data and (update_data[field] is None or update_data[field] == ""):
|
||||
from app.services.api_errors import raise_unprocessable
|
||||
raise_unprocessable(
|
||||
code="business_identifier_nullification_forbidden",
|
||||
message=f"{field} cannot be nullified or explicitly cleared",
|
||||
)
|
||||
|
||||
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,
|
||||
**update_data,
|
||||
)
|
||||
|
||||
await create_audit_event(
|
||||
@@ -569,6 +576,7 @@ async def patch_draft_seat(
|
||||
|
||||
@router.post(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/draft/seats/bulk", response_model=BulkSeatPatchResponse)
|
||||
async def bulk_patch_draft_seats(
|
||||
request: Request,
|
||||
scheme_id: str,
|
||||
payload: BulkSeatPatchRequest,
|
||||
expected_scheme_version_id: str | None = Query(default=None),
|
||||
@@ -579,7 +587,20 @@ async def bulk_patch_draft_seats(
|
||||
expected_scheme_version_id=expected_scheme_version_id,
|
||||
)
|
||||
|
||||
items = [item.model_dump() for item in payload.items]
|
||||
raw_json = await request.json()
|
||||
items = []
|
||||
for i, item in enumerate(payload.items):
|
||||
item_raw = raw_json.get("items", [])[i] if "items" in raw_json else {}
|
||||
items.append({k: item.model_dump(exclude_unset=True).get(k) for k in item_raw})
|
||||
|
||||
for item in items:
|
||||
for field in ("seat_id", "sector_id", "group_id"):
|
||||
if field in item and (item[field] is None or item[field] == ""):
|
||||
from app.services.api_errors import raise_unprocessable
|
||||
raise_unprocessable(
|
||||
code="business_identifier_nullification_forbidden",
|
||||
message=f"{field} cannot be nullified or explicitly cleared",
|
||||
)
|
||||
await validate_bulk_seat_patch_uniqueness(
|
||||
scheme_version_id=version.scheme_version_id,
|
||||
items=items,
|
||||
@@ -625,6 +646,7 @@ async def bulk_patch_draft_seats(
|
||||
|
||||
@router.patch(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/draft/sectors/records/{{sector_record_id}}", response_model=SectorPatchResponse)
|
||||
async def patch_draft_sector(
|
||||
request: Request,
|
||||
scheme_id: str,
|
||||
sector_record_id: str,
|
||||
payload: SectorPatchRequest,
|
||||
@@ -642,20 +664,28 @@ async def patch_draft_sector(
|
||||
new_sector_id=payload.sector_id,
|
||||
)
|
||||
|
||||
raw_json = await request.json()
|
||||
update_data = {k: v for k, v in payload.model_dump(exclude_unset=True).items() if k in raw_json}
|
||||
for field in ("sector_id",):
|
||||
if field in update_data and (update_data[field] is None or update_data[field] == ""):
|
||||
from app.services.api_errors import raise_unprocessable
|
||||
raise_unprocessable(
|
||||
code="business_identifier_nullification_forbidden",
|
||||
message=f"{field} cannot be nullified or explicitly cleared",
|
||||
)
|
||||
|
||||
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,
|
||||
**update_data,
|
||||
)
|
||||
cascaded_count = 0
|
||||
if "sector_id" in update_data and update_data["sector_id"] and update_data["sector_id"] != old_sector_id:
|
||||
cascaded_count = await cascade_update_seat_sector_reference(
|
||||
scheme_version_id=version.scheme_version_id,
|
||||
old_sector_id=old_sector_id,
|
||||
new_sector_id=update_data["sector_id"],
|
||||
)
|
||||
|
||||
await create_audit_event(
|
||||
scheme_id=scheme.scheme_id,
|
||||
@@ -668,7 +698,6 @@ async def patch_draft_sector(
|
||||
"new_sector_id": payload.sector_id,
|
||||
"name": payload.name,
|
||||
"cascaded_seats_count": cascaded_count,
|
||||
"repair_result": repair_result,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -683,6 +712,7 @@ async def patch_draft_sector(
|
||||
|
||||
@router.patch(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/draft/groups/records/{{group_record_id}}", response_model=GroupPatchResponse)
|
||||
async def patch_draft_group(
|
||||
request: Request,
|
||||
scheme_id: str,
|
||||
group_record_id: str,
|
||||
payload: GroupPatchRequest,
|
||||
@@ -700,20 +730,28 @@ async def patch_draft_group(
|
||||
new_group_id=payload.group_id,
|
||||
)
|
||||
|
||||
raw_json = await request.json()
|
||||
update_data = {k: v for k, v in payload.model_dump(exclude_unset=True).items() if k in raw_json}
|
||||
for field in ("group_id",):
|
||||
if field in update_data and (update_data[field] is None or update_data[field] == ""):
|
||||
from app.services.api_errors import raise_unprocessable
|
||||
raise_unprocessable(
|
||||
code="business_identifier_nullification_forbidden",
|
||||
message=f"{field} cannot be nullified or explicitly cleared",
|
||||
)
|
||||
|
||||
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,
|
||||
**update_data,
|
||||
)
|
||||
cascaded_count = 0
|
||||
if "group_id" in update_data and update_data["group_id"] and update_data["group_id"] != old_group_id:
|
||||
cascaded_count = await cascade_update_seat_group_reference(
|
||||
scheme_version_id=version.scheme_version_id,
|
||||
old_group_id=old_group_id,
|
||||
new_group_id=update_data["group_id"],
|
||||
)
|
||||
|
||||
await create_audit_event(
|
||||
scheme_id=scheme.scheme_id,
|
||||
@@ -726,7 +764,6 @@ async def patch_draft_group(
|
||||
"new_group_id": payload.group_id,
|
||||
"name": payload.name,
|
||||
"cascaded_seats_count": cascaded_count,
|
||||
"repair_result": repair_result,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -2,12 +2,10 @@ 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,
|
||||
create_next_scheme_version_from_current_checked,
|
||||
ensure_draft_scheme_version_consistent,
|
||||
get_current_scheme_version,
|
||||
list_scheme_versions,
|
||||
)
|
||||
@@ -34,26 +32,12 @@ from app.schemas.scheme_versions import (
|
||||
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),
|
||||
@@ -155,36 +139,9 @@ async def create_next_scheme_version_endpoint(
|
||||
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,
|
||||
current_version, new_version = await create_next_scheme_version_from_current_checked(
|
||||
scheme_id=scheme_id,
|
||||
expected_current_scheme_version_id=expected_current_scheme_version_id,
|
||||
)
|
||||
|
||||
await create_audit_event(
|
||||
@@ -214,26 +171,14 @@ async def ensure_draft_scheme_version(
|
||||
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,
|
||||
current_version, created, source_scheme_version_id = await ensure_draft_scheme_version_consistent(
|
||||
scheme_id=scheme_id,
|
||||
expected_current_scheme_version_id=expected_current_scheme_version_id,
|
||||
)
|
||||
|
||||
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":
|
||||
if not created:
|
||||
return EnsureDraftResponse(
|
||||
scheme_id=scheme.scheme_id,
|
||||
scheme_id=current_version.scheme_id,
|
||||
scheme_version_id=current_version.scheme_version_id,
|
||||
version_number=current_version.version_number,
|
||||
status=current_version.status,
|
||||
@@ -242,42 +187,27 @@ async def ensure_draft_scheme_version(
|
||||
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,
|
||||
object_ref=current_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,
|
||||
"source_scheme_version_id": source_scheme_version_id,
|
||||
"version_number": current_version.version_number,
|
||||
"normalized_storage_path": current_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,
|
||||
scheme_id=current_version.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=True,
|
||||
source_scheme_version_id=current_version.scheme_version_id,
|
||||
source_scheme_version_id=source_scheme_version_id,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -10,8 +10,7 @@ from app.repositories.scheme_artifacts import create_scheme_artifact
|
||||
from app.repositories.scheme_groups import replace_scheme_version_groups
|
||||
from app.repositories.scheme_seats import replace_scheme_version_seats
|
||||
from app.repositories.scheme_sectors import replace_scheme_version_sectors
|
||||
from app.repositories.scheme_versions import create_initial_scheme_version
|
||||
from app.repositories.schemes import create_scheme_from_upload
|
||||
from app.repositories.schemes import create_scheme_from_upload_with_initial_version
|
||||
from app.repositories.uploads import (
|
||||
count_upload_records,
|
||||
create_upload_record,
|
||||
@@ -202,17 +201,9 @@ async def upload_scheme_svg(
|
||||
processing_status="completed",
|
||||
)
|
||||
|
||||
scheme_id = await create_scheme_from_upload(
|
||||
scheme_id, scheme_version_id = await create_scheme_from_upload_with_initial_version(
|
||||
source_upload_id=upload_id,
|
||||
name=Path(filename).stem or filename,
|
||||
normalized_elements_count=summary["elements_count"],
|
||||
normalized_seats_count=summary["seats_count"],
|
||||
normalized_groups_count=summary["groups_count"],
|
||||
normalized_sectors_count=summary["sectors_count"],
|
||||
)
|
||||
|
||||
scheme_version_id = await create_initial_scheme_version(
|
||||
scheme_id=scheme_id,
|
||||
normalized_storage_path=normalized_storage_path,
|
||||
normalized_elements_count=summary["elements_count"],
|
||||
normalized_seats_count=summary["seats_count"],
|
||||
|
||||
Reference in New Issue
Block a user