freeze the current backend contract for frontend integration document the stabilized backend surface and handoff expectations mark the current state as the baseline for further frontend work
529 lines
12 KiB
Markdown
529 lines
12 KiB
Markdown
# Backend Integration Contract
|
|
|
|
This document is the frontend handoff contract for the `svg-service` backend. It is written as an integration baseline, not as an internal backend README.
|
|
|
|
## 1. Base URL and Auth
|
|
|
|
- Base URL: `http://<host>:9020`
|
|
- API prefix: `/api/v1`
|
|
- Auth header: `X-API-Key`
|
|
|
|
All non-`/healthz` routes require an API key.
|
|
|
|
Auth failure contract:
|
|
|
|
- missing API key -> `401` with string detail: `Missing API key`
|
|
- invalid API key -> `403` with string detail: `Invalid API key`
|
|
- valid non-admin key on admin-only route -> `403` with string detail: `Admin role required`
|
|
|
|
## 2. Roles and Access Boundaries
|
|
|
|
- `admin`
|
|
- full access to protected routes
|
|
- required for all `/api/v1/admin/...` routes
|
|
- `operator`
|
|
- allowed on non-admin protected routes
|
|
- denied on admin-only routes
|
|
- `viewer`
|
|
- allowed on non-admin protected routes
|
|
- denied on admin-only routes
|
|
|
|
Frontend implication:
|
|
|
|
- admin UI must treat admin routes as optional capabilities gated by role
|
|
- frontend must not assume `operator` or `viewer` can call cleanup, audit, backfill, or current-artifact admin routes
|
|
|
|
## 3. Core Entities
|
|
|
|
### Upload
|
|
|
|
Represents one uploaded SVG source and its normalized/sanitized artifacts.
|
|
|
|
Important fields:
|
|
|
|
- `upload_id`
|
|
- `original_filename`
|
|
- `content_type`
|
|
- `size_bytes`
|
|
- `original_storage_path`
|
|
- `sanitized_storage_path`
|
|
- `normalized_storage_path`
|
|
- `normalized_elements_count`
|
|
- `normalized_seats_count`
|
|
- `normalized_groups_count`
|
|
- `normalized_sectors_count`
|
|
|
|
### Scheme
|
|
|
|
Top-level business object created from upload.
|
|
|
|
Important fields:
|
|
|
|
- `scheme_id`
|
|
- `source_upload_id`
|
|
- `name`
|
|
- `status`
|
|
- `current_version_number`
|
|
- `published_at`
|
|
|
|
### Scheme Version
|
|
|
|
Versioned snapshot of the scheme structure and publish state.
|
|
|
|
Important fields:
|
|
|
|
- `scheme_version_id`
|
|
- `scheme_id`
|
|
- `version_number`
|
|
- `status`
|
|
- `normalized_storage_path`
|
|
- `normalized_*_count`
|
|
|
|
### Sector
|
|
|
|
Structure entity in a specific `scheme_version`.
|
|
|
|
Important fields:
|
|
|
|
- `sector_record_id`
|
|
- `sector_id`
|
|
- `element_id`
|
|
- `name`
|
|
|
|
Business identity priority:
|
|
|
|
- use `sector_id` when present
|
|
- fallback to `element_id`
|
|
- never treat `sector_record_id` as business identity across versions
|
|
|
|
### Group
|
|
|
|
Important fields:
|
|
|
|
- `group_record_id`
|
|
- `group_id`
|
|
- `element_id`
|
|
- `name`
|
|
|
|
Business identity priority:
|
|
|
|
- use `group_id` when present
|
|
- fallback to `element_id`
|
|
- never treat `group_record_id` as business identity across versions
|
|
|
|
### Seat
|
|
|
|
Important fields:
|
|
|
|
- `seat_record_id`
|
|
- `seat_id`
|
|
- `element_id`
|
|
- `sector_id`
|
|
- `group_id`
|
|
- `row_label`
|
|
- `seat_number`
|
|
|
|
Business identity priority:
|
|
|
|
- use `seat_id` when present
|
|
- fallback to `element_id`
|
|
- never treat `seat_record_id` as business identity across versions
|
|
|
|
### Pricing Category
|
|
|
|
Important fields:
|
|
|
|
- `pricing_category_id`
|
|
- `scheme_id`
|
|
- `name`
|
|
- `code`
|
|
|
|
### Price Rule
|
|
|
|
Important fields:
|
|
|
|
- `price_rule_id`
|
|
- `scheme_id`
|
|
- `pricing_category_id`
|
|
- `target_type`
|
|
- `target_ref`
|
|
- `amount`
|
|
- `currency`
|
|
|
|
### Artifact
|
|
|
|
Artifact registry row for generated backend files.
|
|
|
|
Important fields:
|
|
|
|
- `artifact_id`
|
|
- `artifact_type`
|
|
- `artifact_variant`
|
|
- `storage_path`
|
|
- `status`
|
|
- `meta_json`
|
|
|
|
Important artifact types currently exercised by regression:
|
|
|
|
- `sanitized_svg`
|
|
- `normalized_json`
|
|
- `display_svg`
|
|
- `publish_preview`
|
|
|
|
## 4. Lifecycle State Machine
|
|
|
|
### Fresh Upload
|
|
|
|
Flow:
|
|
|
|
1. `POST /api/v1/schemes/upload`
|
|
2. backend creates:
|
|
- `upload`
|
|
- `scheme`
|
|
- initial `scheme_version`
|
|
- structure rows
|
|
- initial artifacts
|
|
|
|
Expected initial state:
|
|
|
|
- `scheme.status = draft`
|
|
- `scheme.current_version_number = 1`
|
|
- current version status = `draft`
|
|
|
|
### Current Draft
|
|
|
|
If current scheme/version is still draft:
|
|
|
|
- editor works directly against current version
|
|
- `draft/ensure` is idempotent
|
|
- `draft/ensure` returns `created=false`
|
|
|
|
### Ensure Draft From Published Current
|
|
|
|
If current scheme/version is published:
|
|
|
|
- `POST /api/v1/schemes/{scheme_id}/draft/ensure`
|
|
- backend creates a new draft version
|
|
- current pointer switches to the new draft
|
|
- version number increments
|
|
|
|
### Publish
|
|
|
|
Preconditions:
|
|
|
|
- current scheme is draft
|
|
- current version is draft
|
|
- publish readiness must be satisfied
|
|
|
|
Publish path:
|
|
|
|
1. optional `draft/pricing/snapshot`
|
|
2. `GET draft/publish-readiness`
|
|
3. optional `GET draft/publish-preview`
|
|
4. `POST /api/v1/schemes/{scheme_id}/publish`
|
|
|
|
Expected result:
|
|
|
|
- scheme becomes `published`
|
|
- current version becomes `published`
|
|
|
|
### Rollback
|
|
|
|
Path:
|
|
|
|
- `POST /api/v1/schemes/{scheme_id}/rollback`
|
|
|
|
Effect:
|
|
|
|
- current pointer switches to requested historical `version_number`
|
|
- scheme returns to `draft`
|
|
- target version becomes current editable draft
|
|
|
|
### Unpublish
|
|
|
|
Path:
|
|
|
|
- `POST /api/v1/schemes/{scheme_id}/unpublish`
|
|
|
|
Effect:
|
|
|
|
- current scheme becomes `draft`
|
|
- current version becomes `draft`
|
|
|
|
## 5. Editor Flow
|
|
|
|
### Entry Point
|
|
|
|
- `GET /api/v1/schemes/{scheme_id}/editor/context`
|
|
|
|
Use it first to decide whether:
|
|
|
|
- current draft can be edited directly
|
|
- or a new draft must be created from published current
|
|
|
|
Important response fields:
|
|
|
|
- `current_scheme_version_id`
|
|
- `current_version_number`
|
|
- `scheme_status`
|
|
- `scheme_version_status`
|
|
- `current_is_draft`
|
|
- `create_draft_available`
|
|
- `recommended_action`
|
|
|
|
### Draft Read Models
|
|
|
|
- `POST /api/v1/schemes/{scheme_id}/draft/ensure`
|
|
- `GET /api/v1/schemes/{scheme_id}/draft/summary`
|
|
- `GET /api/v1/schemes/{scheme_id}/draft/structure`
|
|
- `GET /api/v1/schemes/{scheme_id}/draft/validation`
|
|
- `GET /api/v1/schemes/{scheme_id}/draft/compare-preview`
|
|
|
|
Frontend should treat `draft/structure` as the main editable read model.
|
|
|
|
### Patch Operations
|
|
|
|
Supported flows:
|
|
|
|
- single seat patch
|
|
- bulk seat patch
|
|
- sector create/patch/delete
|
|
- group create/patch/delete
|
|
- repair references
|
|
- remap preview/apply
|
|
|
|
Frontend rule:
|
|
|
|
- always send `expected_scheme_version_id` when mutating or reading draft state after editor entry
|
|
|
|
### Stale Conflict Handling
|
|
|
|
If backend returns a stale or draft editability conflict:
|
|
|
|
- stop optimistic local mutation flow
|
|
- re-read:
|
|
- `editor/context`
|
|
- `draft/summary`
|
|
- `draft/structure`
|
|
|
|
Do not keep editing against stale cached `scheme_version_id`.
|
|
|
|
## 6. Pricing Flow
|
|
|
|
### Categories
|
|
|
|
- `GET /api/v1/schemes/{scheme_id}/pricing`
|
|
- `POST /api/v1/schemes/{scheme_id}/pricing/categories`
|
|
- `PUT /api/v1/schemes/{scheme_id}/pricing/categories/{pricing_category_id}`
|
|
- `DELETE /api/v1/schemes/{scheme_id}/pricing/categories/{pricing_category_id}`
|
|
|
|
### Rules
|
|
|
|
- `POST /api/v1/schemes/{scheme_id}/pricing/rules`
|
|
- `PUT /api/v1/schemes/{scheme_id}/pricing/rules/{price_rule_id}`
|
|
- `DELETE /api/v1/schemes/{scheme_id}/pricing/rules/{price_rule_id}`
|
|
|
|
### Read Models
|
|
|
|
- `GET /api/v1/schemes/{scheme_id}/pricing`
|
|
- `GET /api/v1/schemes/{scheme_id}/pricing/coverage`
|
|
- `GET /api/v1/schemes/{scheme_id}/pricing/unpriced-seats`
|
|
- `GET /api/v1/schemes/{scheme_id}/pricing/explain/{seat_id}`
|
|
- `GET /api/v1/schemes/{scheme_id}/pricing/rules/diagnostics`
|
|
- `GET /api/v1/schemes/{scheme_id}/current/seats/{seat_id}/price`
|
|
- `GET /api/v1/schemes/{scheme_id}/test/seats/{seat_id}`
|
|
|
|
Frontend rule:
|
|
|
|
- empty pricing on a fresh upload is valid
|
|
- do not treat `categories=[]` and `rules=[]` as backend failure
|
|
|
|
## 7. Publish Flow
|
|
|
|
Main endpoints:
|
|
|
|
- `POST /api/v1/schemes/{scheme_id}/draft/pricing/snapshot`
|
|
- `GET /api/v1/schemes/{scheme_id}/draft/publish-readiness`
|
|
- `GET /api/v1/schemes/{scheme_id}/draft/publish-preview`
|
|
- `POST /api/v1/schemes/{scheme_id}/publish`
|
|
|
|
Frontend sequencing rule:
|
|
|
|
1. ensure draft
|
|
2. mutate if needed
|
|
3. create/refresh pricing
|
|
4. build pricing snapshot
|
|
5. read publish readiness
|
|
6. read publish preview if UI needs preview surface
|
|
7. publish
|
|
|
|
## 8. Admin/Ops Flow
|
|
|
|
Admin-only endpoints:
|
|
|
|
- `GET /api/v1/admin/schemes/{scheme_id}/current/artifacts`
|
|
- `GET /api/v1/admin/schemes/{scheme_id}/current/validation`
|
|
- `POST /api/v1/admin/schemes/{scheme_id}/current/display/regenerate`
|
|
- `POST /api/v1/admin/display/backfill`
|
|
- `GET /api/v1/admin/artifacts/publish-preview/audit`
|
|
- `POST /api/v1/admin/artifacts/publish-preview/cleanup`
|
|
- `GET /api/v1/admin/schemes/{scheme_id}/pricing/categories/cleanup-preview`
|
|
- `POST /api/v1/admin/schemes/{scheme_id}/pricing/categories/cleanup`
|
|
|
|
Healthy publish-preview audit contract:
|
|
|
|
- `orphan_files_count = 0`
|
|
- `missing_files_for_db_rows_count = 0`
|
|
- `db_rows_count == disk_files_count`
|
|
|
|
Frontend implication:
|
|
|
|
- admin tools must not be shown as generally available functionality
|
|
- admin cleanup/destructive flows must be role-gated on the client and still handle backend `403`
|
|
|
|
## 9. Typed Error Catalog
|
|
|
|
### Auth
|
|
|
|
- `401` string detail: `Missing API key`
|
|
- `403` string detail: `Invalid API key`
|
|
- `403` string detail: `Admin role required`
|
|
|
|
### Lifecycle / Draft / Publish
|
|
|
|
- `stale_draft_version`
|
|
- `stale_current_version`
|
|
- `current_version_inconsistent`
|
|
- `draft_not_editable`
|
|
- `publish_not_ready`
|
|
|
|
### Editor Uniqueness / References
|
|
|
|
- `editor_uniqueness_error`
|
|
- `editor_reference_error`
|
|
- `duplicate_seat_id`
|
|
- `duplicate_seat_id_in_payload`
|
|
- `duplicate_sector_id`
|
|
- `duplicate_group_id`
|
|
- `duplicate_sector_element_id`
|
|
- `duplicate_group_element_id`
|
|
- `unknown_sector_id`
|
|
- `unknown_group_id`
|
|
- `unknown_sector_ids`
|
|
- `unknown_group_ids`
|
|
- `unknown_target_sector_id`
|
|
- `unknown_target_group_id`
|
|
- `business_identifier_nullification_forbidden`
|
|
|
|
### Pricing / Remap / Test
|
|
|
|
- `invalid_amount`
|
|
- `remap_filter_required`
|
|
- `test_preview_failed`
|
|
|
|
### Validation Report Codes
|
|
|
|
These appear inside validation report payloads rather than as top-level HTTP conflict codes:
|
|
|
|
- `duplicate_seat_ids`
|
|
- `missing_seat_contract`
|
|
- `seats_without_sector_or_group`
|
|
- `seats_without_price`
|
|
|
|
Frontend rule:
|
|
|
|
- do not parse only HTTP status
|
|
- always inspect structured `detail.code` when `detail` is an object
|
|
|
|
## 10. Frontend Obligations
|
|
|
|
- always handle auth failures `401` and `403`
|
|
- always handle stale/conflict responses on draft, publish, and lifecycle operations
|
|
- never treat `*_record_id` as stable cross-version business identity
|
|
- always prefer business ids:
|
|
- seat -> `seat_id`, fallback `element_id`
|
|
- sector -> `sector_id`, fallback `element_id`
|
|
- group -> `group_id`, fallback `element_id`
|
|
- re-read current/draft state after:
|
|
- any `409`
|
|
- publish
|
|
- rollback
|
|
- unpublish
|
|
- `draft/ensure` returning a newly created draft
|
|
- do not assume current version remains stable across concurrent operator sessions
|
|
- do not assume publish-preview artifacts or display artifacts are frontend-owned resources
|
|
|
|
## 11. Non-Persistent Assumptions Frontend Must Avoid
|
|
|
|
The frontend must not assume that these remain stable forever:
|
|
|
|
- `scheme_version_id`
|
|
- `seat_record_id`
|
|
- `sector_record_id`
|
|
- `group_record_id`
|
|
- artifact `storage_path`
|
|
- publish-preview cache artifacts
|
|
|
|
These are safe to treat as business-stable:
|
|
|
|
- `scheme_id`
|
|
- `version_number` within one scheme
|
|
- `seat_id` when present
|
|
- `sector_id` when present
|
|
- `group_id` when present
|
|
|
|
## 12. Known Limitations / Deferred Tech Debt
|
|
|
|
- some lifecycle negative contracts still return mixed styles:
|
|
- typed object conflicts for `409`
|
|
- plain string details for some `404` and auth cases
|
|
- validation warnings and error code families are not yet unified into one single global error envelope
|
|
- admin/ops routes are backend-internal tools, not end-user product APIs
|
|
- corruption remediation smoke exists only for `publish_preview`, not for every artifact type
|
|
|
|
## 13. Regression Baseline Frontend Can Rely On
|
|
|
|
The frontend can rely on the following regression-backed flows:
|
|
|
|
- fresh upload on clean DB
|
|
- current/draft/editor read flow
|
|
- editor mutations and stale draft protection
|
|
- pricing setup and publish flow
|
|
- version lifecycle:
|
|
- publish
|
|
- ensure draft from published current
|
|
- rollback
|
|
- unpublish
|
|
- admin ops:
|
|
- audit
|
|
- cleanup
|
|
- destructive pricing cleanup for safe fixture categories
|
|
- full admin permission matrix on implemented admin endpoints
|
|
- controlled `publish_preview` corruption detection and remediation
|
|
- negative upload validation
|
|
- negative auth matrix
|
|
- negative lifecycle matrix
|
|
|
|
## 14. Recommended Frontend Integration Sequence
|
|
|
|
For normal editor work:
|
|
|
|
1. authenticate
|
|
2. upload or pick `scheme_id`
|
|
3. read `editor/context`
|
|
4. call `draft/ensure` if needed
|
|
5. read `draft/structure`
|
|
6. mutate using current `scheme_version_id`
|
|
7. on `409`, reload editor state before retry
|
|
8. configure pricing if needed
|
|
9. create pricing snapshot
|
|
10. read publish readiness / preview
|
|
11. publish
|
|
|
|
For admin UI:
|
|
|
|
1. verify admin role in client auth state
|
|
2. call admin endpoints
|
|
3. still handle backend `403`
|
|
4. treat cleanup and remediation as explicit operator actions, not background automation
|