- add editor entry flow with editor context and ensure-draft bootstrap - add draft summary read model and single-record draft read endpoints - add typed draft, edit and publish conflicts with validation errors - add pricing diagnostics and publish readiness endpoints - fix Decimal serialization in seat price and test preview flows - harden draft lifecycle guards for published vs draft current version - update API map and smoke regression checklist - add backend README and smoke regression script
8.3 KiB
svg-service backend
Backend for SVG scheme upload, draft editing, pricing, publish preview, and publish lifecycle.
Stack
- Python 3.11
- FastAPI
- SQLAlchemy async
- PostgreSQL 16
- Docker Compose
Runtime
Default backend port: 9020
Health check:
GET /healthz
Main API prefix:
/api/v1
Auth header:
X-API-Key
Default local admin key:
admin-local-dev-key
Core lifecycle
The backend works with a scheme lifecycle:
- Upload SVG
- Normalize and persist structure
- Work in current draft
- Create / update pricing
- Build pricing snapshot
- Inspect publish preview / readiness
- Publish current draft
- If editing is needed after publish, create or ensure a new draft again
Main concepts
Scheme
Top-level business entity.
Scheme version
Concrete version of the scheme.
A version can be draft or published.
Current version
The version referenced by the scheme registry as active current.
Draft
Editable current version.
All editor mutations and draft pricing operations must target a current draft version only.
Published version
Non-editable current version.
If current version is published, editor flow must first create or ensure a new draft.
Upload artifacts
Stored technical artifacts, including:
- original svg
- sanitized svg
- normalized json
- display svg
- publish preview json
Editor entry flow
Use this flow from frontend or operator scripts.
1. Inspect editor state
GET /api/v1/schemes/{scheme_id}/editor/context
Response tells whether:
- current version is draft
- editor is available
- a new draft should be created
- recommended action is
use_current_draftorcreate_draft
2. Ensure editable draft
POST /api/v1/schemes/{scheme_id}/draft/ensure
Behavior:
- if current version is already draft: returns it with
created=false - if current version is published: clones current version into a new current draft and returns it with
created=true
Returned scheme_version_id must be reused as:
expected_scheme_version_id
for draft reads and mutations.
Optimistic concurrency
Mutable draft flows support optimistic concurrency through query params:
expected_current_scheme_version_idexpected_scheme_version_id
These guards prevent frontend/editor from mutating a stale draft after another version switch.
Typical typed conflict payload:
stale_current_versionstale_draft_versiondraft_not_editablepublish_not_ready
Main operator routes
System
GET /healthzGET /api/v1/pingGET /api/v1/db/pingGET /api/v1/manifest
Uploads
POST /api/v1/schemes/uploadGET /api/v1/uploadsGET /api/v1/uploads/{upload_id}GET /api/v1/uploads/{upload_id}/normalized
Scheme registry
GET /api/v1/schemesGET /api/v1/schemes/{scheme_id}GET /api/v1/schemes/{scheme_id}/currentGET /api/v1/schemes/{scheme_id}/versionsPOST /api/v1/schemes/{scheme_id}/versionsGET /api/v1/schemes/{scheme_id}/publish/validationGET /api/v1/schemes/{scheme_id}/draft/publish-readinessPOST /api/v1/schemes/{scheme_id}/publishPOST /api/v1/schemes/{scheme_id}/unpublishPOST /api/v1/schemes/{scheme_id}/rollback
Editor / draft
GET /api/v1/schemes/{scheme_id}/editor/contextPOST /api/v1/schemes/{scheme_id}/draft/ensureGET /api/v1/schemes/{scheme_id}/draft/summaryGET /api/v1/schemes/{scheme_id}/draft/structureGET /api/v1/schemes/{scheme_id}/draft/validationGET /api/v1/schemes/{scheme_id}/draft/compare-previewGET /api/v1/schemes/{scheme_id}/draft/seats/records/{seat_record_id}GET /api/v1/schemes/{scheme_id}/draft/sectors/records/{sector_record_id}GET /api/v1/schemes/{scheme_id}/draft/groups/records/{group_record_id}POST /api/v1/schemes/{scheme_id}/draft/sectorsPOST /api/v1/schemes/{scheme_id}/draft/groupsDELETE /api/v1/schemes/{scheme_id}/draft/sectors/records/{sector_record_id}DELETE /api/v1/schemes/{scheme_id}/draft/groups/records/{group_record_id}PATCH /api/v1/schemes/{scheme_id}/draft/seats/records/{seat_record_id}POST /api/v1/schemes/{scheme_id}/draft/seats/bulkPATCH /api/v1/schemes/{scheme_id}/draft/sectors/records/{sector_record_id}PATCH /api/v1/schemes/{scheme_id}/draft/groups/records/{group_record_id}POST /api/v1/schemes/{scheme_id}/draft/repair-references
Pricing
GET /api/v1/schemes/{scheme_id}/pricingPOST /api/v1/schemes/{scheme_id}/pricing/categoriesPUT /api/v1/schemes/{scheme_id}/pricing/categories/{pricing_category_id}DELETE /api/v1/schemes/{scheme_id}/pricing/categories/{pricing_category_id}POST /api/v1/schemes/{scheme_id}/pricing/rulesPUT /api/v1/schemes/{scheme_id}/pricing/rules/{price_rule_id}DELETE /api/v1/schemes/{scheme_id}/pricing/rules/{price_rule_id}
Pricing diagnostics
GET /api/v1/schemes/{scheme_id}/pricing/coverageGET /api/v1/schemes/{scheme_id}/pricing/unpriced-seatsGET /api/v1/schemes/{scheme_id}/pricing/explain/{seat_id}GET /api/v1/schemes/{scheme_id}/pricing/rules/diagnostics
Publish preview
POST /api/v1/schemes/{scheme_id}/draft/pricing/snapshotGET /api/v1/schemes/{scheme_id}/draft/publish-previewPOST /api/v1/schemes/{scheme_id}/draft/remap/previewPOST /api/v1/schemes/{scheme_id}/draft/remap/apply
Structure read model
GET /api/v1/schemes/{scheme_id}/current/sectorsGET /api/v1/schemes/{scheme_id}/current/groupsGET /api/v1/schemes/{scheme_id}/current/seatsGET /api/v1/schemes/{scheme_id}/current/seats/{seat_id}/priceGET /api/v1/schemes/{scheme_id}/current/svgGET /api/v1/schemes/{scheme_id}/current/svg/displayGET /api/v1/schemes/{scheme_id}/current/svg/display/meta
Test mode
GET /api/v1/schemes/{scheme_id}/test/seats/{seat_id}
Audit
GET /api/v1/schemes/{scheme_id}/audit
Admin / ops
GET /api/v1/admin/schemes/{scheme_id}/current/artifactsGET /api/v1/admin/schemes/{scheme_id}/current/validationPOST /api/v1/admin/schemes/{scheme_id}/current/display/regeneratePOST /api/v1/admin/display/backfillGET /api/v1/admin/artifacts/publish-preview/auditPOST /api/v1/admin/artifacts/publish-preview/cleanup
Typical local flow
1. Read current version
Use:
GET /api/v1/schemes/{scheme_id}/current
2. Ensure draft
Use:
POST /api/v1/schemes/{scheme_id}/draft/ensure
Store returned:
scheme_version_id
3. Read draft state
Use:
GET /draft/summary?expected_scheme_version_id=...GET /draft/structure?expected_scheme_version_id=...GET /draft/validation?expected_scheme_version_id=...GET /draft/compare-preview?expected_scheme_version_id=...
4. Perform editor mutations
Pass:
expected_scheme_version_id={draft_scheme_version_id}
on every mutation route.
5. Inspect pricing quality
Use:
GET /pricing/coverageGET /pricing/unpriced-seatsGET /pricing/explain/{seat_id}GET /pricing/rules/diagnostics
6. Create pricing snapshot
Use:
POST /draft/pricing/snapshot?expected_scheme_version_id=...
7. Inspect readiness
Use:
GET /draft/publish-readiness?expected_scheme_version_id=...
8. Publish
Use:
POST /publish?expected_scheme_version_id=...
Typed error expectations
Examples of stable typed errors already used in the service:
Draft concurrency/state
stale_current_versionstale_draft_versiondraft_not_editable
Editor validation
duplicate_seat_idduplicate_seat_id_in_payloadduplicate_sector_idduplicate_group_idduplicate_sector_element_idduplicate_group_element_idunknown_sector_idunknown_group_idunknown_sector_idsunknown_group_idsunknown_target_sector_idunknown_target_group_idremap_filter_required
Pricing / publish
invalid_amountpublish_not_ready
Draft summary semantics
GET /draft/summary is the compact route for editor bootstrap.
It returns:
- current draft counters
- validation summary
- structure diff summary
- publish readiness summary
This route is intended for frontend side panels / header status / quick preflight.
Notes
- Draft-only routes must not mutate a published current version.
- Published current version should require
draft/ensurebefore any editor mutation. - Publish readiness can fail even if validation passes, for example when pricing snapshot is missing.
api-map.mdandsmoke-regression.mdmust be updated together with route changes.