feat(frontend): add editor integration shell and draft read-model views
- add editor integration shell - add editor context read flow - add draft flow entry handling - add summary, structure, validation and compare-preview views - render backend read models for draft and published states - verify sample-contract entities: 3 seats, 1 group, 1 sector
This commit is contained in:
528
doc/frontend-integration-contract.md
Normal file
528
doc/frontend-integration-contract.md
Normal file
@@ -0,0 +1,528 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user