Implement display artifacts, pricing integrity, draft base and publish preview bundle
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.routes.admin import router as admin_router
|
||||
from app.api.routes.audit import router as audit_router
|
||||
from app.api.routes.editor import router as editor_router
|
||||
from app.api.routes.pricing import router as pricing_router
|
||||
from app.api.routes.publish import router as publish_router
|
||||
from app.api.routes.schemes import router as schemes_router
|
||||
from app.api.routes.structure import router as structure_router
|
||||
from app.api.routes.system import router as system_router
|
||||
@@ -16,3 +19,6 @@ router.include_router(structure_router)
|
||||
router.include_router(pricing_router)
|
||||
router.include_router(test_mode_router)
|
||||
router.include_router(audit_router)
|
||||
router.include_router(admin_router)
|
||||
router.include_router(editor_router)
|
||||
router.include_router(publish_router)
|
||||
|
||||
162
backend/app/api/routes/admin.py
Normal file
162
backend/app/api/routes/admin.py
Normal file
@@ -0,0 +1,162 @@
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
|
||||
from app.core.config import settings
|
||||
from app.repositories.scheme_artifacts import artifact_exists, list_scheme_artifacts
|
||||
from app.repositories.scheme_versions import get_current_scheme_version
|
||||
from app.repositories.schemes import get_scheme_record_by_scheme_id, list_scheme_records
|
||||
from app.repositories.uploads import get_upload_record_by_upload_id
|
||||
from app.security.auth import require_api_key
|
||||
from app.services.display_regenerator import regenerate_display_artifact
|
||||
from app.services.scheme_validation import build_scheme_validation_report
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get(f"{settings.api_v1_prefix}/admin/schemes/{{scheme_id}}/current/artifacts")
|
||||
async def list_current_scheme_artifacts(
|
||||
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,
|
||||
)
|
||||
rows = await list_scheme_artifacts(scheme_version_id=version.scheme_version_id)
|
||||
|
||||
return {
|
||||
"scheme_id": scheme.scheme_id,
|
||||
"scheme_version_id": version.scheme_version_id,
|
||||
"items": [
|
||||
{
|
||||
"artifact_id": row.artifact_id,
|
||||
"artifact_type": row.artifact_type,
|
||||
"artifact_variant": row.artifact_variant,
|
||||
"status": row.status,
|
||||
"storage_path": row.storage_path,
|
||||
"meta_json": row.meta_json,
|
||||
"created_at": row.created_at.isoformat(),
|
||||
}
|
||||
for row in rows
|
||||
],
|
||||
"total": len(rows),
|
||||
}
|
||||
|
||||
|
||||
@router.get(f"{settings.api_v1_prefix}/admin/schemes/{{scheme_id}}/current/validation")
|
||||
async def validate_current_scheme(
|
||||
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}/admin/schemes/{{scheme_id}}/current/display/regenerate")
|
||||
async def regenerate_current_display(
|
||||
scheme_id: str,
|
||||
mode: str = Query(default="passthrough"),
|
||||
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,
|
||||
)
|
||||
upload = await get_upload_record_by_upload_id(scheme.source_upload_id)
|
||||
|
||||
return await regenerate_display_artifact(
|
||||
scheme_id=scheme.scheme_id,
|
||||
scheme_version_id=version.scheme_version_id,
|
||||
upload_id=upload.upload_id,
|
||||
original_filename=upload.original_filename,
|
||||
sanitized_storage_path=upload.sanitized_storage_path,
|
||||
mode=mode,
|
||||
)
|
||||
|
||||
|
||||
@router.post(f"{settings.api_v1_prefix}/admin/display/backfill")
|
||||
async def bulk_backfill_display_artifacts(
|
||||
mode: str = Query(default="passthrough"),
|
||||
limit: int = Query(default=100, ge=1, le=1000),
|
||||
only_missing: bool = Query(default=True),
|
||||
role: str = Depends(require_api_key),
|
||||
):
|
||||
schemes = await list_scheme_records(limit=limit, offset=0)
|
||||
|
||||
processed: list[dict] = []
|
||||
skipped: list[dict] = []
|
||||
failed: list[dict] = []
|
||||
|
||||
for scheme in schemes:
|
||||
try:
|
||||
version = await get_current_scheme_version(
|
||||
scheme_id=scheme.scheme_id,
|
||||
current_version_number=scheme.current_version_number,
|
||||
)
|
||||
|
||||
if only_missing:
|
||||
exists = await artifact_exists(
|
||||
scheme_version_id=version.scheme_version_id,
|
||||
artifact_type="display_svg",
|
||||
artifact_variant=mode,
|
||||
)
|
||||
if exists:
|
||||
skipped.append(
|
||||
{
|
||||
"scheme_id": scheme.scheme_id,
|
||||
"scheme_version_id": version.scheme_version_id,
|
||||
"reason": "artifact already exists",
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
upload = await get_upload_record_by_upload_id(scheme.source_upload_id)
|
||||
result = await regenerate_display_artifact(
|
||||
scheme_id=scheme.scheme_id,
|
||||
scheme_version_id=version.scheme_version_id,
|
||||
upload_id=upload.upload_id,
|
||||
original_filename=upload.original_filename,
|
||||
sanitized_storage_path=upload.sanitized_storage_path,
|
||||
mode=mode,
|
||||
)
|
||||
processed.append(
|
||||
{
|
||||
"scheme_id": scheme.scheme_id,
|
||||
"scheme_version_id": version.scheme_version_id,
|
||||
"artifact_variant": result["artifact_variant"],
|
||||
}
|
||||
)
|
||||
except Exception as exc:
|
||||
failed.append(
|
||||
{
|
||||
"scheme_id": scheme.scheme_id,
|
||||
"reason": f"{exc.__class__.__name__}: {exc}",
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"mode": mode,
|
||||
"limit": limit,
|
||||
"only_missing": only_missing,
|
||||
"processed_count": len(processed),
|
||||
"skipped_count": len(skipped),
|
||||
"failed_count": len(failed),
|
||||
"processed": processed,
|
||||
"skipped": skipped,
|
||||
"failed": failed,
|
||||
}
|
||||
517
backend/app/api/routes/editor.py
Normal file
517
backend/app/api/routes/editor.py
Normal file
@@ -0,0 +1,517 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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_uniqueness,
|
||||
validate_group_patch_uniqueness,
|
||||
validate_sector_patch_uniqueness,
|
||||
validate_single_seat_patch_uniqueness,
|
||||
)
|
||||
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,
|
||||
role: str = Depends(require_api_key),
|
||||
):
|
||||
scheme, version = await get_current_draft_context(scheme_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/compare-preview", response_model=StructureDiffResponse)
|
||||
async def get_draft_compare_preview(
|
||||
scheme_id: str,
|
||||
role: str = Depends(require_api_key),
|
||||
):
|
||||
scheme, version = await get_current_draft_context(scheme_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,
|
||||
role: str = Depends(require_api_key),
|
||||
):
|
||||
scheme, version = await get_current_draft_context(scheme_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,
|
||||
role: str = Depends(require_api_key),
|
||||
):
|
||||
scheme, version = await get_current_draft_context(scheme_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,
|
||||
role: str = Depends(require_api_key),
|
||||
):
|
||||
scheme, version = await get_current_draft_context(scheme_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,
|
||||
role: str = Depends(require_api_key),
|
||||
):
|
||||
scheme, version = await get_current_draft_context(scheme_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,
|
||||
role: str = Depends(require_api_key),
|
||||
):
|
||||
scheme, version = await get_current_draft_context(scheme_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,
|
||||
)
|
||||
|
||||
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,
|
||||
role: str = Depends(require_api_key),
|
||||
):
|
||||
scheme, version = await get_current_draft_context(scheme_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,
|
||||
)
|
||||
|
||||
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,
|
||||
role: str = Depends(require_api_key),
|
||||
):
|
||||
scheme, version = await get_current_draft_context(scheme_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,
|
||||
"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,
|
||||
role: str = Depends(require_api_key),
|
||||
):
|
||||
scheme, version = await get_current_draft_context(scheme_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,
|
||||
"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,
|
||||
role: str = Depends(require_api_key),
|
||||
):
|
||||
scheme, version = await get_current_draft_context(scheme_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["repaired_sector_refs_count"],
|
||||
repaired_group_refs_count=result["repaired_group_refs_count"],
|
||||
details=result["details"],
|
||||
)
|
||||
@@ -1,4 +1,6 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from decimal import Decimal
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
|
||||
from app.core.config import settings
|
||||
from app.repositories.audit import create_audit_event
|
||||
@@ -12,33 +14,44 @@ from app.repositories.pricing import (
|
||||
update_price_rule,
|
||||
update_pricing_category,
|
||||
)
|
||||
from app.repositories.scheme_version_pricing import replace_scheme_version_pricing_snapshot
|
||||
from app.repositories.schemes import get_scheme_record_by_scheme_id
|
||||
from app.repositories.scheme_versions import get_current_scheme_version
|
||||
from app.schemas.pricing import (
|
||||
DeleteResponse,
|
||||
PriceRuleCreateRequest,
|
||||
PriceRuleCreateResponse,
|
||||
PriceRuleItem,
|
||||
PriceRuleUpdateRequest,
|
||||
PriceRuleUpdateResponse,
|
||||
PricingBundleResponse,
|
||||
PricingCategoryCreateRequest,
|
||||
PricingCategoryCreateResponse,
|
||||
PricingCategoryItem,
|
||||
PricingCategoryUpdateRequest,
|
||||
PricingCategoryUpdateResponse,
|
||||
SchemePricingResponse,
|
||||
)
|
||||
from app.security.auth import require_api_key
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/pricing", response_model=SchemePricingResponse)
|
||||
async def get_scheme_pricing(scheme_id: str, role: str = Depends(require_api_key)):
|
||||
await get_scheme_record_by_scheme_id(scheme_id)
|
||||
async def _refresh_current_draft_snapshot_if_possible(scheme_id: str) -> dict | None:
|
||||
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,
|
||||
)
|
||||
if scheme.status != "draft" or version.status != "draft":
|
||||
return None
|
||||
|
||||
return await replace_scheme_version_pricing_snapshot(
|
||||
scheme_id=scheme.scheme_id,
|
||||
scheme_version_id=version.scheme_version_id,
|
||||
)
|
||||
|
||||
|
||||
@router.get(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/pricing", response_model=PricingBundleResponse)
|
||||
async def get_pricing_bundle(scheme_id: str, role: str = Depends(require_api_key)):
|
||||
categories = await list_pricing_categories(scheme_id)
|
||||
rules = await list_price_rules(scheme_id)
|
||||
|
||||
return SchemePricingResponse(
|
||||
return PricingBundleResponse(
|
||||
categories=[
|
||||
PricingCategoryItem(
|
||||
pricing_category_id=row.pricing_category_id,
|
||||
@@ -56,7 +69,7 @@ async def get_scheme_pricing(scheme_id: str, role: str = Depends(require_api_key
|
||||
pricing_category_id=row.pricing_category_id,
|
||||
target_type=row.target_type,
|
||||
target_ref=row.target_ref,
|
||||
amount=row.amount,
|
||||
amount=str(row.amount),
|
||||
currency=row.currency,
|
||||
created_at=row.created_at.isoformat(),
|
||||
)
|
||||
@@ -65,34 +78,36 @@ async def get_scheme_pricing(scheme_id: str, role: str = Depends(require_api_key
|
||||
)
|
||||
|
||||
|
||||
@router.post(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/pricing/categories", response_model=PricingCategoryCreateResponse)
|
||||
@router.post(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/pricing/categories")
|
||||
async def create_pricing_category_endpoint(
|
||||
scheme_id: str,
|
||||
payload: PricingCategoryCreateRequest,
|
||||
role: str = Depends(require_api_key),
|
||||
):
|
||||
await get_scheme_record_by_scheme_id(scheme_id)
|
||||
pricing_category_id = await create_pricing_category(
|
||||
scheme_id=scheme_id,
|
||||
name=payload.name,
|
||||
code=payload.code,
|
||||
)
|
||||
snapshot = await _refresh_current_draft_snapshot_if_possible(scheme_id)
|
||||
|
||||
await create_audit_event(
|
||||
scheme_id=scheme_id,
|
||||
event_type="pricing.category.created",
|
||||
object_type="pricing_category",
|
||||
object_ref=pricing_category_id,
|
||||
details={"name": payload.name, "code": payload.code},
|
||||
)
|
||||
return PricingCategoryCreateResponse(
|
||||
pricing_category_id=pricing_category_id,
|
||||
scheme_id=scheme_id,
|
||||
name=payload.name,
|
||||
code=payload.code,
|
||||
details={"name": payload.name, "code": payload.code, "snapshot": snapshot},
|
||||
)
|
||||
|
||||
return {
|
||||
"pricing_category_id": pricing_category_id,
|
||||
"scheme_id": scheme_id,
|
||||
"name": payload.name,
|
||||
"code": payload.code,
|
||||
}
|
||||
|
||||
@router.put(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/pricing/categories/{{pricing_category_id}}", response_model=PricingCategoryUpdateResponse)
|
||||
|
||||
@router.put(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/pricing/categories/{{pricing_category_id}}")
|
||||
async def update_pricing_category_endpoint(
|
||||
scheme_id: str,
|
||||
pricing_category_id: str,
|
||||
@@ -105,53 +120,71 @@ async def update_pricing_category_endpoint(
|
||||
name=payload.name,
|
||||
code=payload.code,
|
||||
)
|
||||
snapshot = await _refresh_current_draft_snapshot_if_possible(scheme_id)
|
||||
|
||||
await create_audit_event(
|
||||
scheme_id=scheme_id,
|
||||
event_type="pricing.category.updated",
|
||||
object_type="pricing_category",
|
||||
object_ref=pricing_category_id,
|
||||
details={"name": payload.name, "code": payload.code},
|
||||
)
|
||||
return PricingCategoryUpdateResponse(
|
||||
pricing_category_id=row.pricing_category_id,
|
||||
scheme_id=row.scheme_id,
|
||||
name=row.name,
|
||||
code=row.code,
|
||||
details={"name": payload.name, "code": payload.code, "snapshot": snapshot},
|
||||
)
|
||||
|
||||
return {
|
||||
"pricing_category_id": row.pricing_category_id,
|
||||
"scheme_id": row.scheme_id,
|
||||
"name": row.name,
|
||||
"code": row.code,
|
||||
}
|
||||
|
||||
@router.delete(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/pricing/categories/{{pricing_category_id}}", response_model=DeleteResponse)
|
||||
|
||||
@router.delete(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/pricing/categories/{{pricing_category_id}}")
|
||||
async def delete_pricing_category_endpoint(
|
||||
scheme_id: str,
|
||||
pricing_category_id: str,
|
||||
role: str = Depends(require_api_key),
|
||||
):
|
||||
await delete_pricing_category(scheme_id=scheme_id, pricing_category_id=pricing_category_id)
|
||||
await delete_pricing_category(
|
||||
scheme_id=scheme_id,
|
||||
pricing_category_id=pricing_category_id,
|
||||
)
|
||||
snapshot = await _refresh_current_draft_snapshot_if_possible(scheme_id)
|
||||
|
||||
await create_audit_event(
|
||||
scheme_id=scheme_id,
|
||||
event_type="pricing.category.deleted",
|
||||
object_type="pricing_category",
|
||||
object_ref=pricing_category_id,
|
||||
details=None,
|
||||
details={"snapshot": snapshot},
|
||||
)
|
||||
return DeleteResponse(status="deleted")
|
||||
|
||||
return {"deleted": True, "pricing_category_id": pricing_category_id}
|
||||
|
||||
|
||||
@router.post(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/pricing/rules", response_model=PriceRuleCreateResponse)
|
||||
@router.post(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/pricing/rules")
|
||||
async def create_price_rule_endpoint(
|
||||
scheme_id: str,
|
||||
payload: PriceRuleCreateRequest,
|
||||
role: str = Depends(require_api_key),
|
||||
):
|
||||
await get_scheme_record_by_scheme_id(scheme_id)
|
||||
try:
|
||||
amount = Decimal(payload.amount)
|
||||
except Exception:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail="Некорректная сумма",
|
||||
)
|
||||
|
||||
price_rule_id = await create_price_rule(
|
||||
scheme_id=scheme_id,
|
||||
pricing_category_id=payload.pricing_category_id,
|
||||
target_type=payload.target_type,
|
||||
target_ref=payload.target_ref,
|
||||
amount=payload.amount,
|
||||
amount=amount,
|
||||
currency=payload.currency,
|
||||
)
|
||||
snapshot = await _refresh_current_draft_snapshot_if_possible(scheme_id)
|
||||
|
||||
await create_audit_event(
|
||||
scheme_id=scheme_id,
|
||||
event_type="pricing.rule.created",
|
||||
@@ -161,37 +194,49 @@ async def create_price_rule_endpoint(
|
||||
"pricing_category_id": payload.pricing_category_id,
|
||||
"target_type": payload.target_type,
|
||||
"target_ref": payload.target_ref,
|
||||
"amount": str(payload.amount),
|
||||
"amount": payload.amount,
|
||||
"currency": payload.currency,
|
||||
"snapshot": snapshot,
|
||||
},
|
||||
)
|
||||
return PriceRuleCreateResponse(
|
||||
price_rule_id=price_rule_id,
|
||||
scheme_id=scheme_id,
|
||||
pricing_category_id=payload.pricing_category_id,
|
||||
target_type=payload.target_type,
|
||||
target_ref=payload.target_ref,
|
||||
amount=payload.amount,
|
||||
currency=payload.currency,
|
||||
)
|
||||
|
||||
return {
|
||||
"price_rule_id": price_rule_id,
|
||||
"scheme_id": scheme_id,
|
||||
"pricing_category_id": payload.pricing_category_id,
|
||||
"target_type": payload.target_type,
|
||||
"target_ref": payload.target_ref,
|
||||
"amount": payload.amount,
|
||||
"currency": payload.currency,
|
||||
}
|
||||
|
||||
|
||||
@router.put(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/pricing/rules/{{price_rule_id}}", response_model=PriceRuleUpdateResponse)
|
||||
@router.put(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/pricing/rules/{{price_rule_id}}")
|
||||
async def update_price_rule_endpoint(
|
||||
scheme_id: str,
|
||||
price_rule_id: str,
|
||||
payload: PriceRuleUpdateRequest,
|
||||
role: str = Depends(require_api_key),
|
||||
):
|
||||
try:
|
||||
amount = Decimal(payload.amount)
|
||||
except Exception:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail="Некорректная сумма",
|
||||
)
|
||||
|
||||
row = await update_price_rule(
|
||||
scheme_id=scheme_id,
|
||||
price_rule_id=price_rule_id,
|
||||
pricing_category_id=payload.pricing_category_id,
|
||||
target_type=payload.target_type,
|
||||
target_ref=payload.target_ref,
|
||||
amount=payload.amount,
|
||||
amount=amount,
|
||||
currency=payload.currency,
|
||||
)
|
||||
snapshot = await _refresh_current_draft_snapshot_if_possible(scheme_id)
|
||||
|
||||
await create_audit_event(
|
||||
scheme_id=scheme_id,
|
||||
event_type="pricing.rule.updated",
|
||||
@@ -201,33 +246,41 @@ async def update_price_rule_endpoint(
|
||||
"pricing_category_id": payload.pricing_category_id,
|
||||
"target_type": payload.target_type,
|
||||
"target_ref": payload.target_ref,
|
||||
"amount": str(payload.amount),
|
||||
"amount": payload.amount,
|
||||
"currency": payload.currency,
|
||||
"snapshot": snapshot,
|
||||
},
|
||||
)
|
||||
return PriceRuleUpdateResponse(
|
||||
price_rule_id=row.price_rule_id,
|
||||
scheme_id=row.scheme_id,
|
||||
pricing_category_id=row.pricing_category_id,
|
||||
target_type=row.target_type,
|
||||
target_ref=row.target_ref,
|
||||
amount=row.amount,
|
||||
currency=row.currency,
|
||||
)
|
||||
|
||||
return {
|
||||
"price_rule_id": row.price_rule_id,
|
||||
"scheme_id": row.scheme_id,
|
||||
"pricing_category_id": row.pricing_category_id,
|
||||
"target_type": row.target_type,
|
||||
"target_ref": row.target_ref,
|
||||
"amount": str(row.amount),
|
||||
"currency": row.currency,
|
||||
}
|
||||
|
||||
|
||||
@router.delete(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/pricing/rules/{{price_rule_id}}", response_model=DeleteResponse)
|
||||
@router.delete(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/pricing/rules/{{price_rule_id}}")
|
||||
async def delete_price_rule_endpoint(
|
||||
scheme_id: str,
|
||||
price_rule_id: str,
|
||||
role: str = Depends(require_api_key),
|
||||
):
|
||||
await delete_price_rule(scheme_id=scheme_id, price_rule_id=price_rule_id)
|
||||
await delete_price_rule(
|
||||
scheme_id=scheme_id,
|
||||
price_rule_id=price_rule_id,
|
||||
)
|
||||
snapshot = await _refresh_current_draft_snapshot_if_possible(scheme_id)
|
||||
|
||||
await create_audit_event(
|
||||
scheme_id=scheme_id,
|
||||
event_type="pricing.rule.deleted",
|
||||
object_type="price_rule",
|
||||
object_ref=price_rule_id,
|
||||
details=None,
|
||||
details={"snapshot": snapshot},
|
||||
)
|
||||
return DeleteResponse(status="deleted")
|
||||
|
||||
return {"deleted": True, "price_rule_id": price_rule_id}
|
||||
|
||||
132
backend/app/api/routes/publish.py
Normal file
132
backend/app/api/routes/publish.py
Normal file
@@ -0,0 +1,132 @@
|
||||
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.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.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,
|
||||
role: str = Depends(require_api_key),
|
||||
):
|
||||
scheme, version = await get_current_draft_context(scheme_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-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),
|
||||
role: str = Depends(require_api_key),
|
||||
):
|
||||
scheme, version = await get_current_draft_context(scheme_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,
|
||||
role: str = Depends(require_api_key),
|
||||
):
|
||||
scheme, version = await get_current_draft_context(scheme_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,
|
||||
role: str = Depends(require_api_key),
|
||||
):
|
||||
scheme, version = await get_current_draft_context(scheme_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],
|
||||
)
|
||||
@@ -15,7 +15,6 @@ from app.repositories.schemes import (
|
||||
count_scheme_records,
|
||||
get_scheme_record_by_scheme_id,
|
||||
list_scheme_records,
|
||||
publish_scheme,
|
||||
rollback_scheme_to_version,
|
||||
unpublish_scheme,
|
||||
)
|
||||
@@ -34,6 +33,8 @@ from app.schemas.scheme_versions import (
|
||||
SchemeVersionListResponse,
|
||||
)
|
||||
from app.security.auth import require_api_key
|
||||
from app.services.publish_service import publish_current_draft_scheme
|
||||
from app.services.scheme_validation import build_scheme_validation_report
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -180,22 +181,27 @@ async def create_next_scheme_version_endpoint(
|
||||
)
|
||||
|
||||
|
||||
@router.post(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/publish", response_model=SchemePublishResponse)
|
||||
@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, role: str = Depends(require_api_key)):
|
||||
row = await publish_scheme(scheme_id)
|
||||
await create_audit_event(
|
||||
scheme_id=row.scheme_id,
|
||||
event_type="scheme.published",
|
||||
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,
|
||||
)
|
||||
return await publish_current_draft_scheme(scheme_id=scheme_id)
|
||||
|
||||
|
||||
@router.post(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/unpublish", response_model=SchemePublishResponse)
|
||||
|
||||
@@ -8,6 +8,7 @@ from lxml import etree
|
||||
|
||||
from app.core.config import settings
|
||||
from app.repositories.pricing import find_effective_price_rule
|
||||
from app.repositories.scheme_artifacts import get_latest_scheme_artifact
|
||||
from app.repositories.scheme_groups import list_scheme_version_groups
|
||||
from app.repositories.scheme_seats import get_scheme_version_seat_by_seat_id, list_scheme_version_seats
|
||||
from app.repositories.scheme_sectors import list_scheme_version_sectors
|
||||
@@ -66,13 +67,29 @@ async def _load_current_context(scheme_id: str):
|
||||
return scheme, version, upload
|
||||
|
||||
|
||||
async def _generate_default_display_artifact_if_needed(scheme, version, upload) -> tuple[bytes, Path]:
|
||||
async def _load_default_display_artifact(scheme, version, upload) -> tuple[bytes, Path]:
|
||||
artifact = await get_latest_scheme_artifact(
|
||||
scheme_version_id=version.scheme_version_id,
|
||||
artifact_type="display_svg",
|
||||
artifact_variant=settings.svg_display_mode,
|
||||
)
|
||||
if artifact is not None:
|
||||
path = Path(artifact.storage_path)
|
||||
if path.exists() and path.is_file():
|
||||
return path.read_bytes(), path
|
||||
|
||||
if version.display_svg_status == "ready" and version.display_svg_storage_path:
|
||||
path = Path(version.display_svg_storage_path)
|
||||
if path.exists() and path.is_file():
|
||||
return path.read_bytes(), path
|
||||
|
||||
sanitized_path = Path(upload.sanitized_storage_path)
|
||||
sanitized_artifact = await get_latest_scheme_artifact(
|
||||
scheme_version_id=version.scheme_version_id,
|
||||
artifact_type="sanitized_svg",
|
||||
artifact_variant="source",
|
||||
)
|
||||
sanitized_path = Path(sanitized_artifact.storage_path if sanitized_artifact else upload.sanitized_storage_path)
|
||||
|
||||
if not sanitized_path.exists() or not sanitized_path.is_file():
|
||||
if version.display_svg_status == "pending":
|
||||
raise HTTPException(
|
||||
@@ -106,6 +123,16 @@ async def _generate_default_display_artifact_if_needed(scheme, version, upload)
|
||||
)
|
||||
display_path = Path(display_path_str)
|
||||
|
||||
from app.repositories.scheme_artifacts import create_scheme_artifact
|
||||
await create_scheme_artifact(
|
||||
scheme_id=scheme.scheme_id,
|
||||
scheme_version_id=version.scheme_version_id,
|
||||
artifact_type="display_svg",
|
||||
artifact_variant=settings.svg_display_mode,
|
||||
storage_path=display_path_str,
|
||||
status="ready",
|
||||
meta_json=meta,
|
||||
)
|
||||
await update_scheme_version_display_artifact(
|
||||
scheme_version_id=version.scheme_version_id,
|
||||
display_svg_storage_path=display_path_str,
|
||||
@@ -113,14 +140,6 @@ async def _generate_default_display_artifact_if_needed(scheme, version, upload)
|
||||
display_svg_generated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"display_svg.lazy_generate scheme_id=%s scheme_version_id=%s mode=%s view_box=%s",
|
||||
scheme.scheme_id,
|
||||
version.scheme_version_id,
|
||||
settings.svg_display_mode,
|
||||
meta.get("view_box"),
|
||||
)
|
||||
|
||||
return display_bytes, display_path
|
||||
|
||||
|
||||
@@ -253,11 +272,16 @@ async def get_scheme_current_display_svg(
|
||||
scheme, version, upload = await _load_current_context(scheme_id)
|
||||
|
||||
if resolved_mode == settings.svg_display_mode:
|
||||
display_bytes, display_path = await _generate_default_display_artifact_if_needed(scheme, version, upload)
|
||||
_display_bytes, display_path = await _load_default_display_artifact(scheme, version, upload)
|
||||
filename = f"{scheme.name or scheme.scheme_id}.{resolved_mode}.svg"
|
||||
return FileResponse(path=display_path, media_type="image/svg+xml", filename=filename)
|
||||
|
||||
sanitized_path = Path(upload.sanitized_storage_path)
|
||||
artifact = await get_latest_scheme_artifact(
|
||||
scheme_version_id=version.scheme_version_id,
|
||||
artifact_type="sanitized_svg",
|
||||
artifact_variant="source",
|
||||
)
|
||||
sanitized_path = Path(artifact.storage_path if artifact else upload.sanitized_storage_path)
|
||||
if not sanitized_path.exists() or not sanitized_path.is_file():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
@@ -292,11 +316,16 @@ async def get_scheme_current_display_svg_meta(
|
||||
scheme, version, upload = await _load_current_context(scheme_id)
|
||||
|
||||
if resolved_mode == settings.svg_display_mode:
|
||||
display_bytes, _display_path = await _generate_default_display_artifact_if_needed(scheme, version, upload)
|
||||
display_bytes, _display_path = await _load_default_display_artifact(scheme, version, upload)
|
||||
meta = _parse_svg_meta_from_bytes(display_bytes)
|
||||
generated_at = version.display_svg_generated_at
|
||||
else:
|
||||
sanitized_path = Path(upload.sanitized_storage_path)
|
||||
artifact = await get_latest_scheme_artifact(
|
||||
scheme_version_id=version.scheme_version_id,
|
||||
artifact_type="sanitized_svg",
|
||||
artifact_variant="source",
|
||||
)
|
||||
sanitized_path = Path(artifact.storage_path if artifact else upload.sanitized_storage_path)
|
||||
if not sanitized_path.exists() or not sanitized_path.is_file():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
@@ -305,7 +334,7 @@ async def get_scheme_current_display_svg_meta(
|
||||
|
||||
sanitized_bytes = sanitized_path.read_bytes()
|
||||
try:
|
||||
display_bytes, meta = generate_display_svg(sanitized_bytes, resolved_mode)
|
||||
display_bytes, _meta = generate_display_svg(sanitized_bytes, resolved_mode)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"display_svg.meta_on_demand failed scheme_id=%s scheme_version_id=%s mode=%s",
|
||||
|
||||
@@ -6,6 +6,7 @@ from pathlib import Path
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile, status
|
||||
|
||||
from app.core.config import settings
|
||||
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
|
||||
@@ -222,6 +223,33 @@ async def upload_scheme_svg(
|
||||
display_svg_generated_at=display_svg_generated_at,
|
||||
)
|
||||
|
||||
await create_scheme_artifact(
|
||||
scheme_id=scheme_id,
|
||||
scheme_version_id=scheme_version_id,
|
||||
artifact_type="sanitized_svg",
|
||||
artifact_variant="source",
|
||||
storage_path=sanitized_storage_path,
|
||||
status="ready",
|
||||
)
|
||||
await create_scheme_artifact(
|
||||
scheme_id=scheme_id,
|
||||
scheme_version_id=scheme_version_id,
|
||||
artifact_type="normalized_json",
|
||||
artifact_variant="default",
|
||||
storage_path=normalized_storage_path,
|
||||
status="ready",
|
||||
)
|
||||
if display_svg_storage_path:
|
||||
await create_scheme_artifact(
|
||||
scheme_id=scheme_id,
|
||||
scheme_version_id=scheme_version_id,
|
||||
artifact_type="display_svg",
|
||||
artifact_variant=settings.svg_display_mode,
|
||||
storage_path=display_svg_storage_path,
|
||||
status="ready",
|
||||
meta_json=display_meta,
|
||||
)
|
||||
|
||||
normalized_payload_from_file = read_normalized_payload_from_path(normalized_storage_path)
|
||||
|
||||
await replace_scheme_version_sectors(
|
||||
|
||||
Reference in New Issue
Block a user