Implement display artifacts, pricing integrity, draft base and publish preview bundle

This commit is contained in:
greebo
2026-03-19 17:58:17 +03:00
parent 85fb2f4bb9
commit c91c5abf15
35 changed files with 3283 additions and 302 deletions

View File

@@ -0,0 +1,45 @@
from __future__ import annotations
from fastapi import HTTPException, status
from app.repositories.scheme_versions import list_scheme_versions
async def select_baseline_scheme_version(
*,
scheme_id: str,
draft_scheme_version_id: str,
override_scheme_version_id: str | None = None,
):
versions = await list_scheme_versions(scheme_id=scheme_id, limit=200, offset=0)
if override_scheme_version_id:
for row in versions:
if row.scheme_version_id == override_scheme_version_id:
if row.scheme_version_id == draft_scheme_version_id:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Baseline override must differ from current draft scheme version",
)
return row, "override"
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Baseline override scheme version not found",
)
published_candidates = [
row for row in versions
if row.scheme_version_id != draft_scheme_version_id and row.status == "published"
]
if published_candidates:
return published_candidates[0], "published"
previous_candidates = [
row for row in versions
if row.scheme_version_id != draft_scheme_version_id
]
if previous_candidates:
return previous_candidates[0], "previous"
return None, "none"

View File

@@ -0,0 +1,89 @@
from __future__ import annotations
import logging
from datetime import datetime, timezone
from pathlib import Path
from fastapi import HTTPException, status
from app.core.config import settings
from app.repositories.scheme_artifacts import create_scheme_artifact
from app.repositories.scheme_versions import update_scheme_version_display_artifact
from app.services.storage import save_display_svg
from app.services.svg_display_processor import ALLOWED_MODES, generate_display_svg
logger = logging.getLogger(__name__)
async def regenerate_display_artifact(
*,
scheme_id: str,
scheme_version_id: str,
upload_id: str,
original_filename: str,
sanitized_storage_path: str,
mode: str,
) -> dict:
if mode not in ALLOWED_MODES:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Unsupported display mode: {mode}",
)
svg_path = Path(sanitized_storage_path)
if not svg_path.exists() or not svg_path.is_file():
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Sanitized SVG not found for current scheme version",
)
sanitized_bytes = svg_path.read_bytes()
try:
display_bytes, meta = generate_display_svg(sanitized_bytes, mode)
except Exception as exc:
logger.exception(
"display_svg.regenerate failed scheme_id=%s scheme_version_id=%s mode=%s",
scheme_id,
scheme_version_id,
mode,
)
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"Current scheme version is not ready for display rendering: {exc.__class__.__name__}",
)
storage_path = save_display_svg(
upload_id=upload_id,
filename=original_filename,
content=display_bytes,
)
await create_scheme_artifact(
scheme_id=scheme_id,
scheme_version_id=scheme_version_id,
artifact_type="display_svg",
artifact_variant=mode,
storage_path=storage_path,
status="ready",
meta_json=meta,
)
if mode == settings.svg_display_mode:
await update_scheme_version_display_artifact(
scheme_version_id=scheme_version_id,
display_svg_storage_path=storage_path,
display_svg_status="ready",
display_svg_generated_at=datetime.now(timezone.utc),
)
return {
"scheme_id": scheme_id,
"scheme_version_id": scheme_version_id,
"artifact_type": "display_svg",
"artifact_variant": mode,
"storage_path": storage_path,
"meta": meta,
"generated_at": datetime.now(timezone.utc).isoformat(),
}

View File

@@ -0,0 +1,20 @@
from fastapi import HTTPException, status
from app.repositories.scheme_versions import get_current_scheme_version
from app.repositories.schemes import get_scheme_record_by_scheme_id
async def get_current_draft_context(scheme_id: str):
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 version.status != "draft" or scheme.status != "draft":
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Current scheme version is not editable because it is not in draft state",
)
return scheme, version

View File

@@ -0,0 +1,96 @@
from fastapi import HTTPException, status
from app.repositories.scheme_groups import list_scheme_version_groups
from app.repositories.scheme_seats import list_scheme_version_seats
from app.repositories.scheme_sectors import list_scheme_version_sectors
async def validate_single_seat_patch_uniqueness(
*,
scheme_version_id: str,
seat_record_id: str,
new_seat_id: str | None,
) -> None:
if not new_seat_id:
return
seats = await list_scheme_version_seats(scheme_version_id)
for seat in seats:
if seat.seat_id == new_seat_id and seat.seat_record_id != seat_record_id:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"seat_id already exists in draft version: {new_seat_id}",
)
async def validate_bulk_seat_patch_uniqueness(
*,
scheme_version_id: str,
items: list[dict],
) -> None:
seats = await list_scheme_version_seats(scheme_version_id)
existing = {seat.seat_id: seat.seat_record_id for seat in seats if seat.seat_id}
payload_new_ids = [item.get("seat_id") for item in items if item.get("seat_id")]
duplicates_inside_payload = sorted(
{
seat_id
for seat_id in payload_new_ids
if payload_new_ids.count(seat_id) > 1
}
)
if duplicates_inside_payload:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"Duplicate seat_id values inside bulk payload: {', '.join(duplicates_inside_payload)}",
)
for item in items:
new_seat_id = item.get("seat_id")
seat_record_id = item["seat_record_id"]
if not new_seat_id:
continue
existing_record_id = existing.get(new_seat_id)
if existing_record_id and existing_record_id != seat_record_id:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"seat_id already exists in draft version: {new_seat_id}",
)
async def validate_sector_patch_uniqueness(
*,
scheme_version_id: str,
sector_record_id: str,
new_sector_id: str | None,
) -> None:
if not new_sector_id:
return
sectors = await list_scheme_version_sectors(scheme_version_id)
for sector in sectors:
if sector.sector_id == new_sector_id and sector.sector_record_id != sector_record_id:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"sector_id already exists in draft version: {new_sector_id}",
)
async def validate_group_patch_uniqueness(
*,
scheme_version_id: str,
group_record_id: str,
new_group_id: str | None,
) -> None:
if not new_group_id:
return
groups = await list_scheme_version_groups(scheme_version_id)
for group in groups:
if group.group_id == new_group_id and group.group_record_id != group_record_id:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"group_id already exists in draft version: {new_group_id}",
)

View File

@@ -0,0 +1,135 @@
from __future__ import annotations
import json
from pathlib import Path
from app.repositories.scheme_artifacts import list_scheme_artifacts
from app.repositories.scheme_seats import list_scheme_version_seats
from app.repositories.scheme_version_pricing import (
find_effective_snapshot_price_rule,
list_scheme_version_snapshot_categories,
list_scheme_version_snapshot_rules,
)
from app.services.publish_preview_cache import (
get_latest_publish_preview_artifact,
save_publish_preview_artifact,
)
from app.services.scheme_validation import build_scheme_validation_report
from app.services.structure_diff import build_structure_diff
async def build_publish_preview_bundle(
*,
scheme_id: str,
scheme_version_id: str,
baseline_override_scheme_version_id: str | None = None,
) -> dict:
validation = await build_scheme_validation_report(
scheme_id=scheme_id,
scheme_version_id=scheme_version_id,
)
structure_diff = await build_structure_diff(
scheme_id=scheme_id,
draft_scheme_version_id=scheme_version_id,
baseline_override_scheme_version_id=baseline_override_scheme_version_id,
)
artifacts_rows = await list_scheme_artifacts(scheme_version_id=scheme_version_id)
seats = await list_scheme_version_seats(scheme_version_id)
snapshot_categories = await list_scheme_version_snapshot_categories(scheme_version_id)
snapshot_rules = await list_scheme_version_snapshot_rules(scheme_version_id)
priced = 0
unpriced = 0
snapshot_available = len(snapshot_rules) > 0 or len(snapshot_categories) > 0
for seat in seats:
if not seat.seat_id:
unpriced += 1
continue
if not snapshot_available:
unpriced += 1
continue
try:
await find_effective_snapshot_price_rule(
scheme_version_id=scheme_version_id,
seat_id=seat.seat_id,
group_id=seat.group_id,
sector_id=seat.sector_id,
)
priced += 1
except Exception:
unpriced += 1
artifacts = {
"total": len(artifacts_rows),
"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 artifacts_rows
],
}
pricing_coverage = {
"snapshot_available": snapshot_available,
"snapshot_categories_count": len(snapshot_categories),
"snapshot_rules_count": len(snapshot_rules),
"total_seats": len(seats),
"priced_seats": priced,
"unpriced_seats": unpriced,
}
summary = {
"is_publishable": validation["summary"]["is_publishable"],
"has_structure_changes": any(value > 0 for value in structure_diff["summary"].values()),
"has_artifacts": len(artifacts_rows) > 0,
"has_unpriced_seats": unpriced > 0,
"snapshot_available": snapshot_available,
}
return {
"artifacts": artifacts,
"validation": validation,
"structure_diff": structure_diff,
"pricing_coverage": pricing_coverage,
"summary": summary,
}
async def get_or_build_publish_preview_bundle(
*,
scheme_id: str,
scheme_version_id: str,
baseline_override_scheme_version_id: str | None = None,
refresh: bool = False,
) -> dict:
if not refresh:
artifact = await get_latest_publish_preview_artifact(
scheme_version_id=scheme_version_id,
baseline_scheme_version_id=baseline_override_scheme_version_id,
)
if artifact:
path = Path(artifact.storage_path)
if path.exists():
return json.loads(path.read_text(encoding="utf-8"))
payload = await build_publish_preview_bundle(
scheme_id=scheme_id,
scheme_version_id=scheme_version_id,
baseline_override_scheme_version_id=baseline_override_scheme_version_id,
)
await save_publish_preview_artifact(
scheme_id=scheme_id,
scheme_version_id=scheme_version_id,
payload=payload,
baseline_scheme_version_id=payload["structure_diff"]["baseline_scheme_version_id"],
)
return payload

View File

@@ -0,0 +1,59 @@
from __future__ import annotations
import json
from pathlib import Path
from uuid import uuid4
from app.core.config import settings
from app.repositories.scheme_artifacts import create_scheme_artifact, list_scheme_artifacts
def _preview_storage_dir() -> Path:
path = Path(settings.storage_preview_dir)
path.mkdir(parents=True, exist_ok=True)
return path
async def save_publish_preview_artifact(
*,
scheme_id: str,
scheme_version_id: str,
payload: dict,
baseline_scheme_version_id: str | None,
) -> dict:
artifact_dir = _preview_storage_dir() / uuid4().hex
artifact_dir.mkdir(parents=True, exist_ok=True)
storage_path = artifact_dir / "publish-preview.json"
storage_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
artifact = await create_scheme_artifact(
scheme_id=scheme_id,
scheme_version_id=scheme_version_id,
artifact_type="publish_preview",
artifact_variant=baseline_scheme_version_id or "default",
storage_path=str(storage_path),
meta_json={
"baseline_scheme_version_id": baseline_scheme_version_id,
"summary": payload.get("summary"),
},
)
return artifact
async def get_latest_publish_preview_artifact(
*,
scheme_version_id: str,
baseline_scheme_version_id: str | None,
):
rows = await list_scheme_artifacts(scheme_version_id=scheme_version_id)
variant = baseline_scheme_version_id or "default"
matching = [
row for row in rows
if row.artifact_type == "publish_preview" and row.artifact_variant == variant
]
if not matching:
return None
matching.sort(key=lambda row: (row.created_at, row.id), reverse=True)
return matching[0]

View File

@@ -0,0 +1,65 @@
from fastapi import HTTPException, status
from app.repositories.audit import create_audit_event
from app.repositories.scheme_version_pricing import replace_scheme_version_pricing_snapshot
from app.repositories.scheme_versions import get_current_scheme_version
from app.repositories.schemes import get_scheme_record_by_scheme_id, publish_scheme
from app.services.scheme_validation import build_scheme_validation_report
async def publish_current_draft_scheme(
*,
scheme_id: str,
) -> dict:
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":
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Current scheme version is not publishable because it is not in draft state",
)
validation = await build_scheme_validation_report(
scheme_id=scheme.scheme_id,
scheme_version_id=version.scheme_version_id,
)
if not validation["summary"]["is_publishable"]:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Scheme is not publishable in current state",
)
snapshot = await replace_scheme_version_pricing_snapshot(
scheme_id=scheme.scheme_id,
scheme_version_id=version.scheme_version_id,
)
published_row = await publish_scheme(scheme.scheme_id)
await create_audit_event(
scheme_id=published_row.scheme_id,
event_type="scheme.published",
object_type="scheme",
object_ref=published_row.scheme_id,
details={
"current_version_number": published_row.current_version_number,
"status": published_row.status,
"pricing_snapshot": snapshot,
"scheme_version_id": version.scheme_version_id,
},
)
return {
"scheme_id": published_row.scheme_id,
"scheme_version_id": version.scheme_version_id,
"status": published_row.status,
"current_version_number": published_row.current_version_number,
"published_at": published_row.published_at.isoformat() if published_row.published_at else None,
"pricing_snapshot": snapshot,
"validation_summary": validation["summary"],
}

View File

@@ -0,0 +1,95 @@
from __future__ import annotations
from fastapi import HTTPException, status
from app.repositories.scheme_seats import (
bulk_remap_scheme_version_seats,
list_scheme_version_seats,
)
def _match_seat(
seat,
*,
seat_record_ids: set[str] | None,
from_sector_id: str | None,
from_group_id: str | None,
) -> bool:
if seat_record_ids is not None and seat.seat_record_id not in seat_record_ids:
return False
if from_sector_id is not None and seat.sector_id != from_sector_id:
return False
if from_group_id is not None and seat.group_id != from_group_id:
return False
return True
async def preview_remap(
*,
scheme_version_id: str,
seat_record_ids: list[str] | None,
from_sector_id: str | None,
to_sector_id: str | None,
from_group_id: str | None,
to_group_id: str | None,
) -> list[dict]:
if not any([seat_record_ids, from_sector_id, from_group_id]):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="At least one remap filter must be provided",
)
seats = await list_scheme_version_seats(scheme_version_id)
seat_record_id_set = set(seat_record_ids) if seat_record_ids else None
matched: list[dict] = []
for seat in seats:
if not _match_seat(
seat,
seat_record_ids=seat_record_id_set,
from_sector_id=from_sector_id,
from_group_id=from_group_id,
):
continue
matched.append(
{
"seat_record_id": seat.seat_record_id,
"seat_id": seat.seat_id,
"before_sector_id": seat.sector_id,
"after_sector_id": to_sector_id if to_sector_id is not None else seat.sector_id,
"before_group_id": seat.group_id,
"after_group_id": to_group_id if to_group_id is not None else seat.group_id,
}
)
return matched
async def apply_remap(
*,
scheme_version_id: str,
seat_record_ids: list[str] | None,
from_sector_id: str | None,
to_sector_id: str | None,
from_group_id: str | None,
to_group_id: str | None,
) -> list[dict]:
preview_items = await preview_remap(
scheme_version_id=scheme_version_id,
seat_record_ids=seat_record_ids,
from_sector_id=from_sector_id,
to_sector_id=to_sector_id,
from_group_id=from_group_id,
to_group_id=to_group_id,
)
if not preview_items:
return []
await bulk_remap_scheme_version_seats(
scheme_version_id=scheme_version_id,
items=preview_items,
)
return preview_items

View File

@@ -0,0 +1,107 @@
from __future__ import annotations
from collections import Counter
from app.repositories.pricing import find_effective_price_rule
from app.repositories.scheme_groups import list_scheme_version_groups
from app.repositories.scheme_seats import list_scheme_version_seats
from app.repositories.scheme_sectors import list_scheme_version_sectors
async def build_scheme_validation_report(
*,
scheme_id: str,
scheme_version_id: str,
) -> dict:
sectors = await list_scheme_version_sectors(scheme_version_id)
groups = await list_scheme_version_groups(scheme_version_id)
seats = await list_scheme_version_seats(scheme_version_id)
seat_ids = [row.seat_id for row in seats if row.seat_id]
duplicate_seat_ids = sorted([seat_id for seat_id, count in Counter(seat_ids).items() if count > 1])
seats_without_price: list[str] = []
seats_without_sector_or_group: list[str] = []
seats_with_missing_contract: list[str] = []
priced_seats_count = 0
for seat in seats:
if not seat.seat_id or not seat.element_id:
seats_with_missing_contract.append(seat.element_id or seat.seat_id or "unknown")
continue
if not seat.sector_id and not seat.group_id:
seats_without_sector_or_group.append(seat.seat_id)
try:
await find_effective_price_rule(
scheme_id=scheme_id,
seat_id=seat.seat_id,
group_id=seat.group_id,
sector_id=seat.sector_id,
)
priced_seats_count += 1
except Exception:
seats_without_price.append(seat.seat_id)
errors: list[dict] = []
warnings: list[dict] = []
if duplicate_seat_ids:
errors.append(
{
"code": "duplicate_seat_ids",
"message": "Duplicate seat_id values found",
"items": duplicate_seat_ids,
}
)
if seats_with_missing_contract:
errors.append(
{
"code": "missing_seat_contract",
"message": "Some seats have missing contract fields",
"items": seats_with_missing_contract,
}
)
if seats_without_sector_or_group:
warnings.append(
{
"code": "seats_without_sector_or_group",
"message": "Some seats do not belong to sector or group",
"items": sorted(seats_without_sector_or_group),
}
)
if seats_without_price:
warnings.append(
{
"code": "seats_without_price",
"message": "Some seats have no pricing rule",
"items": sorted(seats_without_price),
}
)
is_publishable = len(errors) == 0
return {
"summary": {
"sectors_count": len(sectors),
"groups_count": len(groups),
"seats_count": len(seats),
"priced_seats_count": priced_seats_count,
"unpriced_seats_count": len(seats_without_price),
"duplicate_seat_ids_count": len(duplicate_seat_ids),
"seats_with_missing_contract_count": len(seats_with_missing_contract),
"is_publishable": is_publishable,
},
"errors": errors,
"warnings": warnings,
"issues": {
"duplicate_seat_ids": duplicate_seat_ids,
"seats_without_price": sorted(seats_without_price),
"seats_without_sector_or_group": sorted(seats_without_sector_or_group),
"seats_with_missing_contract": seats_with_missing_contract,
},
}

View File

@@ -0,0 +1,130 @@
from __future__ import annotations
from app.repositories.scheme_groups import list_scheme_version_groups
from app.repositories.scheme_seats import list_scheme_version_seats
from app.repositories.scheme_sectors import list_scheme_version_sectors
from app.services.baseline_selector import select_baseline_scheme_version
def _serialize_sector(row) -> dict:
return {
"sector_record_id": row.sector_record_id,
"element_id": row.element_id,
"sector_id": row.sector_id,
"name": row.name,
"classes_raw": row.classes_raw,
}
def _serialize_group(row) -> dict:
return {
"group_record_id": row.group_record_id,
"element_id": row.element_id,
"group_id": row.group_id,
"name": row.name,
"classes_raw": row.classes_raw,
}
def _serialize_seat(row) -> dict:
return {
"seat_record_id": row.seat_record_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,
}
def _build_diff(before_map: dict, after_map: dict) -> list[dict]:
keys = sorted(set(before_map.keys()) | set(after_map.keys()))
result: list[dict] = []
for key in keys:
before = before_map.get(key)
after = after_map.get(key)
if before is None and after is not None:
status = "added"
elif before is not None and after is None:
status = "removed"
elif before != after:
status = "changed"
else:
status = "unchanged"
result.append(
{
"key": key,
"status": status,
"before": before,
"after": after,
}
)
return result
async def build_structure_diff(
*,
scheme_id: str,
draft_scheme_version_id: str,
baseline_override_scheme_version_id: str | None = None,
) -> dict:
baseline, baseline_strategy = await select_baseline_scheme_version(
scheme_id=scheme_id,
draft_scheme_version_id=draft_scheme_version_id,
override_scheme_version_id=baseline_override_scheme_version_id,
)
draft_sectors = await list_scheme_version_sectors(draft_scheme_version_id)
draft_groups = await list_scheme_version_groups(draft_scheme_version_id)
draft_seats = await list_scheme_version_seats(draft_scheme_version_id)
if baseline is None:
baseline_sector_map = {}
baseline_group_map = {}
baseline_seat_map = {}
baseline_scheme_version_id = None
else:
baseline_scheme_version_id = baseline.scheme_version_id
baseline_sector_map = {
row.sector_record_id: _serialize_sector(row)
for row in await list_scheme_version_sectors(baseline.scheme_version_id)
}
baseline_group_map = {
row.group_record_id: _serialize_group(row)
for row in await list_scheme_version_groups(baseline.scheme_version_id)
}
baseline_seat_map = {
row.seat_record_id: _serialize_seat(row)
for row in await list_scheme_version_seats(baseline.scheme_version_id)
}
draft_sector_map = {row.sector_record_id: _serialize_sector(row) for row in draft_sectors}
draft_group_map = {row.group_record_id: _serialize_group(row) for row in draft_groups}
draft_seat_map = {row.seat_record_id: _serialize_seat(row) for row in draft_seats}
sector_diff = _build_diff(baseline_sector_map, draft_sector_map)
group_diff = _build_diff(baseline_group_map, draft_group_map)
seat_diff = _build_diff(baseline_seat_map, draft_seat_map)
return {
"baseline_scheme_version_id": baseline_scheme_version_id,
"baseline_strategy": baseline_strategy,
"summary": {
"sectors_added": sum(1 for item in sector_diff if item["status"] == "added"),
"sectors_removed": sum(1 for item in sector_diff if item["status"] == "removed"),
"sectors_changed": sum(1 for item in sector_diff if item["status"] == "changed"),
"groups_added": sum(1 for item in group_diff if item["status"] == "added"),
"groups_removed": sum(1 for item in group_diff if item["status"] == "removed"),
"groups_changed": sum(1 for item in group_diff if item["status"] == "changed"),
"seats_added": sum(1 for item in seat_diff if item["status"] == "added"),
"seats_removed": sum(1 for item in seat_diff if item["status"] == "removed"),
"seats_changed": sum(1 for item in seat_diff if item["status"] == "changed"),
},
"sectors": sector_diff,
"groups": group_diff,
"seats": seat_diff,
}

View File

@@ -0,0 +1,62 @@
from app.repositories.scheme_groups import list_scheme_version_groups
from app.repositories.scheme_seats import (
list_scheme_version_seats,
repair_orphan_group_refs,
repair_orphan_sector_refs,
)
from app.repositories.scheme_sectors import list_scheme_version_sectors
async def repair_structure_references(
*,
scheme_version_id: str,
) -> dict:
sectors = await list_scheme_version_sectors(scheme_version_id)
groups = await list_scheme_version_groups(scheme_version_id)
seats = await list_scheme_version_seats(scheme_version_id)
valid_sector_ids = {item.sector_id for item in sectors if item.sector_id}
valid_group_ids = {item.group_id for item in groups if item.group_id}
orphan_sector_values = sorted(
{
row.sector_id
for row in seats
if row.sector_id and row.sector_id not in valid_sector_ids
}
)
orphan_group_values = sorted(
{
row.group_id
for row in seats
if row.group_id and row.group_id not in valid_group_ids
}
)
repaired_sector_refs_count = 0
repaired_group_refs_count = 0
if len(valid_sector_ids) == 1 and orphan_sector_values:
repaired_sector_refs_count = await repair_orphan_sector_refs(
scheme_version_id=scheme_version_id,
new_sector_id=next(iter(valid_sector_ids)),
orphan_values=orphan_sector_values,
)
if len(valid_group_ids) == 1 and orphan_group_values:
repaired_group_refs_count = await repair_orphan_group_refs(
scheme_version_id=scheme_version_id,
new_group_id=next(iter(valid_group_ids)),
orphan_values=orphan_group_values,
)
return {
"repaired_sector_refs_count": repaired_sector_refs_count,
"repaired_group_refs_count": repaired_group_refs_count,
"details": {
"valid_sector_ids": sorted(valid_sector_ids),
"valid_group_ids": sorted(valid_group_ids),
"orphan_sector_values": orphan_sector_values,
"orphan_group_values": orphan_group_values,
},
}