feat(backend): stabilize draft editor flow and complete smoke regression baseline
- 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
This commit is contained in:
330
backend/README.md
Normal file
330
backend/README.md
Normal file
@@ -0,0 +1,330 @@
|
||||
# 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:
|
||||
|
||||
1. Upload SVG
|
||||
2. Normalize and persist structure
|
||||
3. Work in current draft
|
||||
4. Create / update pricing
|
||||
5. Build pricing snapshot
|
||||
6. Inspect publish preview / readiness
|
||||
7. Publish current draft
|
||||
8. 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_draft` or `create_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_id`
|
||||
- `expected_scheme_version_id`
|
||||
|
||||
These guards prevent frontend/editor from mutating a stale draft after another version switch.
|
||||
|
||||
Typical typed conflict payload:
|
||||
|
||||
- `stale_current_version`
|
||||
- `stale_draft_version`
|
||||
- `draft_not_editable`
|
||||
- `publish_not_ready`
|
||||
|
||||
## Main operator routes
|
||||
|
||||
## System
|
||||
|
||||
- `GET /healthz`
|
||||
- `GET /api/v1/ping`
|
||||
- `GET /api/v1/db/ping`
|
||||
- `GET /api/v1/manifest`
|
||||
|
||||
## Uploads
|
||||
|
||||
- `POST /api/v1/schemes/upload`
|
||||
- `GET /api/v1/uploads`
|
||||
- `GET /api/v1/uploads/{upload_id}`
|
||||
- `GET /api/v1/uploads/{upload_id}/normalized`
|
||||
|
||||
## Scheme registry
|
||||
|
||||
- `GET /api/v1/schemes`
|
||||
- `GET /api/v1/schemes/{scheme_id}`
|
||||
- `GET /api/v1/schemes/{scheme_id}/current`
|
||||
- `GET /api/v1/schemes/{scheme_id}/versions`
|
||||
- `POST /api/v1/schemes/{scheme_id}/versions`
|
||||
- `GET /api/v1/schemes/{scheme_id}/publish/validation`
|
||||
- `GET /api/v1/schemes/{scheme_id}/draft/publish-readiness`
|
||||
- `POST /api/v1/schemes/{scheme_id}/publish`
|
||||
- `POST /api/v1/schemes/{scheme_id}/unpublish`
|
||||
- `POST /api/v1/schemes/{scheme_id}/rollback`
|
||||
|
||||
## Editor / draft
|
||||
|
||||
- `GET /api/v1/schemes/{scheme_id}/editor/context`
|
||||
- `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`
|
||||
- `GET /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/sectors`
|
||||
- `POST /api/v1/schemes/{scheme_id}/draft/groups`
|
||||
- `DELETE /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/bulk`
|
||||
- `PATCH /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}/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}`
|
||||
- `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}`
|
||||
|
||||
## Pricing diagnostics
|
||||
|
||||
- `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`
|
||||
|
||||
## Publish preview
|
||||
|
||||
- `POST /api/v1/schemes/{scheme_id}/draft/pricing/snapshot`
|
||||
- `GET /api/v1/schemes/{scheme_id}/draft/publish-preview`
|
||||
- `POST /api/v1/schemes/{scheme_id}/draft/remap/preview`
|
||||
- `POST /api/v1/schemes/{scheme_id}/draft/remap/apply`
|
||||
|
||||
## Structure read model
|
||||
|
||||
- `GET /api/v1/schemes/{scheme_id}/current/sectors`
|
||||
- `GET /api/v1/schemes/{scheme_id}/current/groups`
|
||||
- `GET /api/v1/schemes/{scheme_id}/current/seats`
|
||||
- `GET /api/v1/schemes/{scheme_id}/current/seats/{seat_id}/price`
|
||||
- `GET /api/v1/schemes/{scheme_id}/current/svg`
|
||||
- `GET /api/v1/schemes/{scheme_id}/current/svg/display`
|
||||
- `GET /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/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`
|
||||
|
||||
## 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/coverage`
|
||||
- `GET /pricing/unpriced-seats`
|
||||
- `GET /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_version`
|
||||
- `stale_draft_version`
|
||||
- `draft_not_editable`
|
||||
|
||||
### Editor validation
|
||||
- `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`
|
||||
- `remap_filter_required`
|
||||
|
||||
### Pricing / publish
|
||||
- `invalid_amount`
|
||||
- `publish_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/ensure` before any editor mutation.
|
||||
- Publish readiness can fail even if validation passes, for example when pricing snapshot is missing.
|
||||
- `api-map.md` and `smoke-regression.md` must be updated together with route changes.
|
||||
@@ -244,7 +244,7 @@ async def get_effective_seat_price(scheme_id: str, seat_id: str, role: str = Dep
|
||||
matched_rule_level=matched_rule_level,
|
||||
matched_target_ref=rule["target_ref"],
|
||||
pricing_category_id=rule["pricing_category_id"],
|
||||
amount=rule["amount"],
|
||||
amount=str(rule["amount"]),
|
||||
currency=rule["currency"],
|
||||
)
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
- GET /api/v1/schemes/{scheme_id}/versions
|
||||
- POST /api/v1/schemes/{scheme_id}/versions
|
||||
- GET /api/v1/schemes/{scheme_id}/publish/validation
|
||||
- GET /api/v1/schemes/{scheme_id}/draft/publish-readiness
|
||||
- POST /api/v1/schemes/{scheme_id}/publish
|
||||
- POST /api/v1/schemes/{scheme_id}/unpublish
|
||||
- POST /api/v1/schemes/{scheme_id}/rollback
|
||||
@@ -58,7 +59,6 @@
|
||||
## app/api/routes/publish.py
|
||||
- POST /api/v1/schemes/{scheme_id}/draft/pricing/snapshot
|
||||
- GET /api/v1/schemes/{scheme_id}/draft/publish-preview
|
||||
- GET /api/v1/schemes/{scheme_id}/draft/publish-readiness
|
||||
- POST /api/v1/schemes/{scheme_id}/draft/remap/preview
|
||||
- POST /api/v1/schemes/{scheme_id}/draft/remap/apply
|
||||
|
||||
@@ -94,4 +94,4 @@
|
||||
- This file is an operational route index, not a generated OpenAPI export.
|
||||
- Update this map in the same change set when adding, removing, renaming, or moving routes.
|
||||
- Query guards such as expected_current_scheme_version_id / expected_scheme_version_id are part of the operational contract for optimistic concurrency on mutable flows.
|
||||
- Draft/editor routes may legally return 409 draft_not_editable when current version is already published and no editable draft exists yet.
|
||||
- Draft editor flow starts from editor/context and draft/ensure, not from direct blind mutation calls.
|
||||
|
||||
@@ -31,19 +31,78 @@ export SCHEME_ID="82086336d385427f9d56244f9e1dd772"
|
||||
- GET /api/v1/schemes/{scheme_id}/current -> 200
|
||||
- GET /api/v1/schemes/{scheme_id}/versions -> 200
|
||||
|
||||
## 3. Structure read model
|
||||
Validate:
|
||||
- scheme_id is stable
|
||||
- current version exists
|
||||
- version list contains current version
|
||||
- status and counts are consistent
|
||||
|
||||
## 3. Editor entry flow
|
||||
|
||||
- GET /api/v1/schemes/{scheme_id}/editor/context -> 200
|
||||
- POST /api/v1/schemes/{scheme_id}/draft/ensure -> 200
|
||||
|
||||
Validate:
|
||||
- editor context returns current_scheme_version_id
|
||||
- editor context distinguishes draft vs published state correctly
|
||||
- ensure endpoint is idempotent on current draft
|
||||
- ensure endpoint creates a new draft from published current when needed
|
||||
- returned scheme_version_id is reusable as expected_scheme_version_id
|
||||
|
||||
## 4. Draft read model
|
||||
|
||||
Using current draft version id from draft/ensure:
|
||||
|
||||
- GET /api/v1/schemes/{scheme_id}/draft/summary?expected_scheme_version_id={draft_version_id} -> 200
|
||||
- GET /api/v1/schemes/{scheme_id}/draft/structure?expected_scheme_version_id={draft_version_id} -> 200
|
||||
- GET /api/v1/schemes/{scheme_id}/draft/validation?expected_scheme_version_id={draft_version_id} -> 200
|
||||
- GET /api/v1/schemes/{scheme_id}/draft/compare-preview?expected_scheme_version_id={draft_version_id} -> 200
|
||||
|
||||
Validate:
|
||||
- summary returns total_seats / total_sectors / total_groups
|
||||
- summary returns validation_summary / structure_diff_summary / publish_readiness
|
||||
- structure returns lists for seats / sectors / groups
|
||||
- validation is deterministic
|
||||
- compare preview returns stable diff structure
|
||||
- stale expected_scheme_version_id returns typed 409 conflict
|
||||
|
||||
## 5. Draft entity reads
|
||||
|
||||
- GET /api/v1/schemes/{scheme_id}/draft/seats/records/{seat_record_id} -> 200
|
||||
- GET /api/v1/schemes/{scheme_id}/draft/sectors/records/{sector_record_id} -> 200
|
||||
- GET /api/v1/schemes/{scheme_id}/draft/groups/records/{group_record_id} -> 200
|
||||
|
||||
Validate:
|
||||
- record endpoints return exact draft entities
|
||||
- unknown record id returns 404
|
||||
- stale expected_scheme_version_id returns typed 409 conflict
|
||||
|
||||
## 6. Structure read model
|
||||
|
||||
- GET /api/v1/schemes/{scheme_id}/current/sectors -> 200
|
||||
- GET /api/v1/schemes/{scheme_id}/current/groups -> 200
|
||||
- GET /api/v1/schemes/{scheme_id}/current/seats -> 200
|
||||
|
||||
## 4. SVG / display pipeline
|
||||
Validate:
|
||||
- total counts are non-negative
|
||||
- known sample scheme returns expected object lists
|
||||
- seats contain seat_id / sector_id / group_id contract where applicable
|
||||
|
||||
## 7. SVG / display pipeline
|
||||
|
||||
- GET /api/v1/schemes/{scheme_id}/current/svg -> 200
|
||||
- GET /api/v1/schemes/{scheme_id}/current/svg/display -> 200
|
||||
- GET /api/v1/schemes/{scheme_id}/current/svg/display/meta -> 200
|
||||
- GET /api/v1/schemes/{scheme_id}/current/svg/display?mode=optimized -> 200 or explicit controlled failure
|
||||
- GET /api/v1/schemes/{scheme_id}/current/svg/display/meta?mode=optimized -> 200 or explicit controlled failure
|
||||
|
||||
## 5. Pricing read model / diagnostics
|
||||
Validate:
|
||||
- response content type for svg endpoints is image/svg+xml
|
||||
- meta returns scheme_id, scheme_version_id, view_box, width, height
|
||||
- no 500 on passthrough mode
|
||||
- unsupported mode returns 422
|
||||
|
||||
## 8. Pricing read model
|
||||
|
||||
- GET /api/v1/schemes/{scheme_id}/pricing -> 200
|
||||
- GET /api/v1/schemes/{scheme_id}/pricing/coverage -> 200
|
||||
@@ -51,71 +110,113 @@ export SCHEME_ID="82086336d385427f9d56244f9e1dd772"
|
||||
- GET /api/v1/schemes/{scheme_id}/pricing/explain/{seat_id} -> 200
|
||||
- GET /api/v1/schemes/{scheme_id}/pricing/rules/diagnostics -> 200
|
||||
- GET /api/v1/schemes/{scheme_id}/current/seats/{seat_id}/price -> 200 for priced seat
|
||||
- GET /api/v1/schemes/{scheme_id}/test/seats/{seat_id} -> 200 for priced and unpriced seat
|
||||
|
||||
## 6. Editor entry workflow
|
||||
|
||||
- GET /api/v1/schemes/{scheme_id}/editor/context -> 200 always
|
||||
- if context.needs_new_draft=true -> POST /api/v1/schemes/{scheme_id}/draft/ensure -> 200
|
||||
- after ensure -> GET /api/v1/schemes/{scheme_id}/editor/context -> 200 and editable=true
|
||||
- GET /api/v1/schemes/{scheme_id}/test/seats/{seat_id} -> 200 for known seat
|
||||
|
||||
Validate:
|
||||
- published current version does not break editor bootstrap
|
||||
- ensure returns created_new_draft=true when current was published
|
||||
- ensure returns created_new_draft=false when current was already draft
|
||||
- pricing bundle contains categories and rules arrays
|
||||
- coverage values are internally consistent
|
||||
- unpriced seats list explains reason_code / reason_message
|
||||
- explain endpoint shows matched rule for priced seat and null for unpriced seat
|
||||
- diagnostics returns orphan/active rule visibility
|
||||
- test seat preview explains selectable / has_price state
|
||||
- priced test seat amount is serialized as string
|
||||
|
||||
## 7. Draft editor read model
|
||||
## 9. Draft mutations and validation guards
|
||||
|
||||
- GET /api/v1/schemes/{scheme_id}/draft/summary -> 200 when current version is draft
|
||||
- GET /api/v1/schemes/{scheme_id}/draft/structure -> 200 when current version is draft
|
||||
- GET /api/v1/schemes/{scheme_id}/draft/validation -> 200 when current version is draft
|
||||
- GET /api/v1/schemes/{scheme_id}/draft/compare-preview -> 200 when current version is draft
|
||||
- GET /api/v1/schemes/{scheme_id}/draft/seats/records/{seat_record_id} -> 200
|
||||
- GET /api/v1/schemes/{scheme_id}/draft/sectors/records/{sector_record_id} -> 200
|
||||
- GET /api/v1/schemes/{scheme_id}/draft/groups/records/{group_record_id} -> 200
|
||||
For current draft version:
|
||||
|
||||
## 8. Draft mutations
|
||||
|
||||
- POST /api/v1/schemes/{scheme_id}/draft/sectors -> 200 or typed 422 conflict
|
||||
- POST /api/v1/schemes/{scheme_id}/draft/groups -> 200 or typed 422 conflict
|
||||
- PATCH /api/v1/schemes/{scheme_id}/draft/seats/records/{seat_record_id} -> 200 or typed 422 validation error
|
||||
- POST /api/v1/schemes/{scheme_id}/draft/seats/bulk -> 200 or typed 422 validation error
|
||||
- PATCH /api/v1/schemes/{scheme_id}/draft/sectors/records/{sector_record_id} -> 200
|
||||
- PATCH /api/v1/schemes/{scheme_id}/draft/groups/records/{group_record_id} -> 200
|
||||
- POST /api/v1/schemes/{scheme_id}/draft/sectors -> 200 or 422
|
||||
- POST /api/v1/schemes/{scheme_id}/draft/groups -> 200 or 422
|
||||
- PATCH /api/v1/schemes/{scheme_id}/draft/seats/records/{seat_record_id} -> 200 or 422
|
||||
- POST /api/v1/schemes/{scheme_id}/draft/seats/bulk -> 200 or 422
|
||||
- POST /api/v1/schemes/{scheme_id}/draft/remap/preview -> 200 or 422
|
||||
- POST /api/v1/schemes/{scheme_id}/draft/remap/apply -> 200 or 422
|
||||
- POST /api/v1/schemes/{scheme_id}/draft/repair-references -> 200
|
||||
- POST /api/v1/schemes/{scheme_id}/draft/remap/preview -> 200 or typed 422 validation error
|
||||
- POST /api/v1/schemes/{scheme_id}/draft/remap/apply -> 200 or typed 422 validation error
|
||||
|
||||
## 9. Publish preview / readiness
|
||||
Validate:
|
||||
- duplicate ids return typed 422
|
||||
- duplicate element binding returns typed 422
|
||||
- unknown sector/group references return typed 422
|
||||
- remap without filters returns typed 422
|
||||
- stale expected_scheme_version_id returns typed 409
|
||||
- published current version rejects draft mutations with typed draft_not_editable conflict
|
||||
|
||||
## 10. Draft publish preview
|
||||
|
||||
- GET /api/v1/schemes/{scheme_id}/publish/validation -> 200
|
||||
- GET /api/v1/schemes/{scheme_id}/draft/publish-readiness -> 200 when current version is draft
|
||||
- POST /api/v1/schemes/{scheme_id}/draft/pricing/snapshot -> 200 when scheme is in draft
|
||||
- GET /api/v1/schemes/{scheme_id}/draft/publish-preview?refresh=true -> 200
|
||||
- GET /api/v1/schemes/{scheme_id}/draft/publish-preview -> 200
|
||||
- GET /api/v1/schemes/{scheme_id}/draft/publish-preview?refresh=true&baseline_scheme_version_id={published_version_id} -> 200
|
||||
|
||||
## 10. Publish lifecycle
|
||||
Validate:
|
||||
- refresh and cached read both succeed
|
||||
- preview summary contains is_publishable / has_structure_changes / has_artifacts / snapshot_available
|
||||
- pricing_coverage is internally consistent
|
||||
- baseline override returns override strategy when explicit baseline is provided
|
||||
- preview retention does not grow unbounded for same version+variant
|
||||
|
||||
- POST /api/v1/schemes/{scheme_id}/publish -> 200 when draft is ready
|
||||
- POST /api/v1/schemes/{scheme_id}/publish with stale expected_scheme_version_id -> 409
|
||||
- POST /api/v1/schemes/{scheme_id}/unpublish -> 200
|
||||
- POST /api/v1/schemes/{scheme_id}/rollback -> 200
|
||||
## 11. Publish readiness and publish flow
|
||||
|
||||
## 11. Admin / ops
|
||||
For current draft version:
|
||||
|
||||
- GET /api/v1/schemes/{scheme_id}/draft/publish-readiness -> 200
|
||||
- POST /api/v1/schemes/{scheme_id}/publish?expected_scheme_version_id={draft_version_id} -> 200 or 409
|
||||
|
||||
Validate:
|
||||
- readiness explicitly shows snapshot_available and pricing gate state
|
||||
- publish with stale expected version returns typed 409
|
||||
- publish without draft state returns typed 409
|
||||
- publish success updates current status to published
|
||||
- audit trail contains scheme.published event
|
||||
|
||||
## 12. Admin / ops
|
||||
|
||||
- GET /api/v1/admin/schemes/{scheme_id}/current/artifacts -> 200
|
||||
- GET /api/v1/admin/schemes/{scheme_id}/current/validation -> 200
|
||||
- GET /api/v1/admin/artifacts/publish-preview/audit -> 200
|
||||
- POST /api/v1/admin/artifacts/publish-preview/cleanup?dry_run=true -> 200
|
||||
|
||||
## 12. Audit trail
|
||||
Optional:
|
||||
- POST /api/v1/admin/schemes/{scheme_id}/current/display/regenerate?mode=passthrough -> 200
|
||||
- POST /api/v1/admin/display/backfill?mode=passthrough&limit=10&only_missing=true -> 200
|
||||
|
||||
Validate:
|
||||
- audit endpoint does not report orphan files or missing files for DB rows in normal state
|
||||
- validation report is readable and deterministic
|
||||
- admin routes do not produce 500 for healthy scheme state
|
||||
|
||||
## 13. Audit trail
|
||||
|
||||
- GET /api/v1/schemes/{scheme_id}/audit -> 200
|
||||
|
||||
## 13. Fail criteria
|
||||
Validate:
|
||||
- recent publish preview / pricing / version / publish events are present when corresponding operations were run
|
||||
- audit total is non-negative
|
||||
- event payloads stay JSON-serializable
|
||||
|
||||
Regression is considered failed if:
|
||||
## 14. Fail criteria
|
||||
|
||||
Regression is considered failed if any of the following happen:
|
||||
|
||||
- health or db ping fails
|
||||
- any stable read endpoint returns 500
|
||||
- published current version cannot be converted to draft through ensure flow
|
||||
- draft editor summary/read endpoints return inconsistent data
|
||||
- publish-state mutation guard is bypassed
|
||||
- passthrough display endpoint fails on known-good sample
|
||||
- publish preview refresh or cached read returns 500
|
||||
- publish readiness returns 500
|
||||
- editor context or draft ensure returns 500
|
||||
- draft summary / structure / validation / compare-preview returns 500
|
||||
- pricing bundle or diagnostics contract changes unexpectedly
|
||||
- admin audit/cleanup endpoints fail on healthy environment
|
||||
- artifact retention grows without bound for repeated preview refresh on same variant
|
||||
|
||||
## 15. Operator note
|
||||
|
||||
Run this checklist after:
|
||||
- schema changes
|
||||
- pricing schema/repository refactors
|
||||
- artifact lifecycle changes
|
||||
- display pipeline changes
|
||||
- route reorganization
|
||||
- startup/import/config changes
|
||||
- draft lifecycle changes
|
||||
- publish readiness changes
|
||||
|
||||
201
backend/scripts/smoke_regression.sh
Executable file
201
backend/scripts/smoke_regression.sh
Executable file
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
API_URL="${API_URL:-http://127.0.0.1:9020}"
|
||||
API_KEY="${API_KEY:-admin-local-dev-key}"
|
||||
SCHEME_ID="${SCHEME_ID:-82086336d385427f9d56244f9e1dd772}"
|
||||
|
||||
WORKDIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORKDIR"' EXIT
|
||||
|
||||
HDR_AUTH=(-H "X-API-Key: ${API_KEY}")
|
||||
HDR_JSON=(-H "Content-Type: application/json")
|
||||
|
||||
step() {
|
||||
echo
|
||||
echo "===== $1 ====="
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo
|
||||
echo "[FAIL] $1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
request() {
|
||||
local name="$1"
|
||||
local method="$2"
|
||||
local url="$3"
|
||||
local expected="$4"
|
||||
local body="${5:-}"
|
||||
local outfile="${WORKDIR}/${name}.body"
|
||||
local codefile="${WORKDIR}/${name}.code"
|
||||
|
||||
if [[ -n "$body" ]]; then
|
||||
curl -sS \
|
||||
-X "$method" \
|
||||
"${HDR_AUTH[@]}" \
|
||||
"${HDR_JSON[@]}" \
|
||||
-o "$outfile" \
|
||||
-w "%{http_code}" \
|
||||
"$url" \
|
||||
--data "$body" > "$codefile"
|
||||
else
|
||||
curl -sS \
|
||||
-X "$method" \
|
||||
"${HDR_AUTH[@]}" \
|
||||
-o "$outfile" \
|
||||
-w "%{http_code}" \
|
||||
"$url" > "$codefile"
|
||||
fi
|
||||
|
||||
local code
|
||||
code="$(cat "$codefile")"
|
||||
|
||||
echo "[$method] $url -> $code"
|
||||
cat "$outfile"
|
||||
echo
|
||||
|
||||
if [[ "$code" != "$expected" ]]; then
|
||||
fail "Unexpected HTTP status for ${name}: expected ${expected}, got ${code}"
|
||||
fi
|
||||
}
|
||||
|
||||
json_get() {
|
||||
local file="$1"
|
||||
local expr="$2"
|
||||
python3 - <<PY
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
payload = json.loads(Path("$file").read_text())
|
||||
value = payload
|
||||
for part in "$expr".split("."):
|
||||
if part.isdigit():
|
||||
value = value[int(part)]
|
||||
else:
|
||||
value = value[part]
|
||||
print(value if value is not None else "")
|
||||
PY
|
||||
}
|
||||
|
||||
assert_json_value() {
|
||||
local file="$1"
|
||||
local expr="$2"
|
||||
local expected="$3"
|
||||
local actual
|
||||
actual="$(json_get "$file" "$expr")"
|
||||
if [[ "$actual" != "$expected" ]]; then
|
||||
fail "JSON assertion failed for ${expr}: expected '${expected}', got '${actual}'"
|
||||
fi
|
||||
echo "[OK] ${expr}=${actual}"
|
||||
}
|
||||
|
||||
step "health"
|
||||
curl -sS -i "${API_URL}/healthz" || fail "Health endpoint is unavailable"
|
||||
|
||||
step "system"
|
||||
request "ping" "GET" "${API_URL}/api/v1/ping" "200"
|
||||
request "db_ping" "GET" "${API_URL}/api/v1/db/ping" "200"
|
||||
request "manifest" "GET" "${API_URL}/api/v1/manifest" "200"
|
||||
|
||||
step "scheme current"
|
||||
request "current" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/current" "200"
|
||||
CURRENT_VERSION_ID="$(json_get "${WORKDIR}/current.body" "scheme_version_id")"
|
||||
CURRENT_STATUS="$(json_get "${WORKDIR}/current.body" "status")"
|
||||
echo "CURRENT_VERSION_ID=${CURRENT_VERSION_ID}"
|
||||
echo "CURRENT_STATUS=${CURRENT_STATUS}"
|
||||
|
||||
step "editor context"
|
||||
request "editor_context" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/editor/context" "200"
|
||||
|
||||
step "ensure draft"
|
||||
request "ensure_draft" "POST" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/ensure" "200"
|
||||
DRAFT_VERSION_ID="$(json_get "${WORKDIR}/ensure_draft.body" "scheme_version_id")"
|
||||
DRAFT_CREATED="$(json_get "${WORKDIR}/ensure_draft.body" "created")"
|
||||
echo "DRAFT_VERSION_ID=${DRAFT_VERSION_ID}"
|
||||
echo "DRAFT_CREATED=${DRAFT_CREATED}"
|
||||
|
||||
step "draft summary"
|
||||
request "draft_summary" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/summary?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
|
||||
assert_json_value "${WORKDIR}/draft_summary.body" "scheme_version_id" "${DRAFT_VERSION_ID}"
|
||||
assert_json_value "${WORKDIR}/draft_summary.body" "status" "draft"
|
||||
|
||||
step "draft structure"
|
||||
request "draft_structure" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/structure?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
|
||||
assert_json_value "${WORKDIR}/draft_structure.body" "scheme_version_id" "${DRAFT_VERSION_ID}"
|
||||
SEAT_RECORD_ID="$(json_get "${WORKDIR}/draft_structure.body" "seats.0.seat_record_id")"
|
||||
SECTOR_RECORD_ID="$(json_get "${WORKDIR}/draft_structure.body" "sectors.0.sector_record_id")"
|
||||
GROUP_RECORD_ID="$(json_get "${WORKDIR}/draft_structure.body" "groups.0.group_record_id")"
|
||||
PRICED_SEAT_ID="$(json_get "${WORKDIR}/draft_structure.body" "seats.0.seat_id")"
|
||||
UNPRICED_SEAT_ID="$(json_get "${WORKDIR}/draft_structure.body" "seats.2.seat_id")"
|
||||
echo "SEAT_RECORD_ID=${SEAT_RECORD_ID}"
|
||||
echo "SECTOR_RECORD_ID=${SECTOR_RECORD_ID}"
|
||||
echo "GROUP_RECORD_ID=${GROUP_RECORD_ID}"
|
||||
echo "PRICED_SEAT_ID=${PRICED_SEAT_ID}"
|
||||
echo "UNPRICED_SEAT_ID=${UNPRICED_SEAT_ID}"
|
||||
|
||||
step "draft validation"
|
||||
request "draft_validation" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/validation?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
|
||||
assert_json_value "${WORKDIR}/draft_validation.body" "status" "draft"
|
||||
|
||||
step "draft compare preview"
|
||||
request "draft_compare" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/compare-preview?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
|
||||
|
||||
step "stale draft conflict"
|
||||
request "draft_summary_stale" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/summary?expected_scheme_version_id=deadbeefdeadbeefdeadbeefdeadbeef" "409"
|
||||
assert_json_value "${WORKDIR}/draft_summary_stale.body" "detail.code" "stale_draft_version"
|
||||
|
||||
step "draft record reads"
|
||||
request "draft_seat_record" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/seats/records/${SEAT_RECORD_ID}?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
|
||||
request "draft_sector_record" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/sectors/records/${SECTOR_RECORD_ID}?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
|
||||
request "draft_group_record" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/groups/records/${GROUP_RECORD_ID}?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
|
||||
request "draft_unknown_record" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/seats/records/deadbeefdeadbeefdeadbeefdeadbeef" "404"
|
||||
|
||||
step "structure read model"
|
||||
request "current_sectors" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/current/sectors" "200"
|
||||
request "current_groups" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/current/groups" "200"
|
||||
request "current_seats" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/current/seats" "200"
|
||||
|
||||
step "svg display pipeline"
|
||||
request "current_svg_meta" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/current/svg/display/meta" "200"
|
||||
|
||||
step "pricing read model"
|
||||
request "pricing_bundle" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing" "200"
|
||||
request "pricing_coverage" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing/coverage" "200"
|
||||
request "pricing_unpriced" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing/unpriced-seats" "200"
|
||||
request "pricing_explain_priced" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing/explain/${PRICED_SEAT_ID}" "200"
|
||||
request "pricing_explain_unpriced" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing/explain/${UNPRICED_SEAT_ID}" "200"
|
||||
request "pricing_diagnostics" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing/rules/diagnostics" "200"
|
||||
request "seat_price" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/current/seats/${PRICED_SEAT_ID}/price" "200"
|
||||
request "test_mode_priced" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/test/seats/${PRICED_SEAT_ID}" "200"
|
||||
request "test_mode_unpriced" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/test/seats/${UNPRICED_SEAT_ID}" "200"
|
||||
|
||||
step "typed validation errors"
|
||||
request "invalid_amount" "POST" "${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing/rules?expected_scheme_version_id=${DRAFT_VERSION_ID}" "422" '{"pricing_category_id":"deadbeefdeadbeefdeadbeefdeadbeef","target_type":"seat","target_ref":"seat-x","amount":"bad","currency":"RUB"}'
|
||||
assert_json_value "${WORKDIR}/invalid_amount.body" "detail.code" "invalid_amount"
|
||||
|
||||
request "remap_no_filters" "POST" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/remap/preview?expected_scheme_version_id=${DRAFT_VERSION_ID}" "422" '{"seat_record_ids":null,"from_sector_id":null,"to_sector_id":"vip","from_group_id":null,"to_group_id":null}'
|
||||
assert_json_value "${WORKDIR}/remap_no_filters.body" "detail.code" "remap_filter_required"
|
||||
|
||||
step "draft pricing snapshot"
|
||||
request "draft_snapshot" "POST" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/pricing/snapshot?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
|
||||
|
||||
step "publish readiness"
|
||||
request "publish_readiness" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/publish-readiness?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
|
||||
|
||||
step "publish preview"
|
||||
request "publish_preview_refresh" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/publish-preview?refresh=true&expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
|
||||
request "publish_preview_cached" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/publish-preview?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
|
||||
|
||||
step "admin ops"
|
||||
request "admin_artifacts" "GET" "${API_URL}/api/v1/admin/schemes/${SCHEME_ID}/current/artifacts" "200"
|
||||
request "admin_validation" "GET" "${API_URL}/api/v1/admin/schemes/${SCHEME_ID}/current/validation" "200"
|
||||
request "admin_preview_audit" "GET" "${API_URL}/api/v1/admin/artifacts/publish-preview/audit" "200"
|
||||
request "admin_preview_cleanup" "POST" "${API_URL}/api/v1/admin/artifacts/publish-preview/cleanup?dry_run=true" "200"
|
||||
|
||||
step "audit trail"
|
||||
request "audit" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/audit" "200"
|
||||
|
||||
step "done"
|
||||
echo "[OK] smoke regression completed successfully"
|
||||
Reference in New Issue
Block a user