feat(backend): add operational smoke tooling and safe pricing cleanup endpoints
- add backend README and refresh API map and smoke regression docs - add full backend smoke regression script - add admin pricing cleanup preview and dry-run endpoints - add helper script for test pricing cleanup - verify typed error contracts, draft flow, publish readiness and preview flows - verify publish preview retention and clean backend startup behavior
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# svg-service backend
|
# svg-service backend
|
||||||
|
|
||||||
Backend for SVG scheme upload, draft editing, pricing, publish preview, and publish lifecycle.
|
Backend for SVG scheme upload, draft editing, pricing, diagnostics, publish preview, and publish lifecycle.
|
||||||
|
|
||||||
## Stack
|
## Stack
|
||||||
|
|
||||||
@@ -32,16 +32,15 @@ Default local admin key:
|
|||||||
|
|
||||||
## Core lifecycle
|
## Core lifecycle
|
||||||
|
|
||||||
The backend works with a scheme lifecycle:
|
|
||||||
|
|
||||||
1. Upload SVG
|
1. Upload SVG
|
||||||
2. Normalize and persist structure
|
2. Normalize and persist structure
|
||||||
3. Work in current draft
|
3. Enter editor flow through context + ensure draft
|
||||||
4. Create / update pricing
|
4. Edit sectors / groups / seats in current draft
|
||||||
5. Build pricing snapshot
|
5. Configure pricing and inspect diagnostics
|
||||||
6. Inspect publish preview / readiness
|
6. Build pricing snapshot
|
||||||
7. Publish current draft
|
7. Inspect publish readiness and publish preview
|
||||||
8. If editing is needed after publish, create or ensure a new draft again
|
8. Publish current draft
|
||||||
|
9. If editing is needed after publish, create or ensure a new draft again
|
||||||
|
|
||||||
## Main concepts
|
## Main concepts
|
||||||
|
|
||||||
@@ -49,23 +48,19 @@ The backend works with a scheme lifecycle:
|
|||||||
Top-level business entity.
|
Top-level business entity.
|
||||||
|
|
||||||
### Scheme version
|
### Scheme version
|
||||||
Concrete version of the scheme.
|
Concrete version of the scheme. A version can be `draft` or `published`.
|
||||||
A version can be `draft` or `published`.
|
|
||||||
|
|
||||||
### Current version
|
### Current version
|
||||||
The version referenced by the scheme registry as active current.
|
The version referenced by the scheme registry as active current.
|
||||||
|
|
||||||
### Draft
|
### Draft
|
||||||
Editable current version.
|
Editable current version. All editor mutations and draft pricing operations must target a current draft version only.
|
||||||
All editor mutations and draft pricing operations must target a current draft version only.
|
|
||||||
|
|
||||||
### Published version
|
### Published version
|
||||||
Non-editable current version.
|
Non-editable current version. If current version is published, editor flow must first create or ensure a new draft.
|
||||||
If current version is published, editor flow must first create or ensure a new draft.
|
|
||||||
|
|
||||||
### Upload artifacts
|
### Upload artifacts
|
||||||
Stored technical artifacts, including:
|
Stored technical artifacts, including:
|
||||||
|
|
||||||
- original svg
|
- original svg
|
||||||
- sanitized svg
|
- sanitized svg
|
||||||
- normalized json
|
- normalized json
|
||||||
@@ -74,30 +69,23 @@ Stored technical artifacts, including:
|
|||||||
|
|
||||||
## Editor entry flow
|
## Editor entry flow
|
||||||
|
|
||||||
Use this flow from frontend or operator scripts.
|
|
||||||
|
|
||||||
### 1. Inspect editor state
|
### 1. Inspect editor state
|
||||||
|
|
||||||
`GET /api/v1/schemes/{scheme_id}/editor/context`
|
`GET /api/v1/schemes/{scheme_id}/editor/context`
|
||||||
|
|
||||||
Response tells whether:
|
Response tells whether:
|
||||||
|
|
||||||
- current version is draft
|
- current version is draft
|
||||||
- editor is available
|
- editor is available
|
||||||
- a new draft should be created
|
- a new draft should be created
|
||||||
- recommended action is `use_current_draft` or `create_draft`
|
- recommended action is `use_current_draft` or `create_draft`
|
||||||
|
|
||||||
### 2. Ensure editable draft
|
### 2. Ensure editable draft
|
||||||
|
|
||||||
`POST /api/v1/schemes/{scheme_id}/draft/ensure`
|
`POST /api/v1/schemes/{scheme_id}/draft/ensure`
|
||||||
|
|
||||||
Behavior:
|
Behavior:
|
||||||
|
|
||||||
- if current version is already draft: returns it with `created=false`
|
- 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`
|
- 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:
|
Returned `scheme_version_id` should be reused as:
|
||||||
|
|
||||||
- `expected_scheme_version_id`
|
- `expected_scheme_version_id`
|
||||||
|
|
||||||
for draft reads and mutations.
|
for draft reads and mutations.
|
||||||
@@ -105,14 +93,10 @@ for draft reads and mutations.
|
|||||||
## Optimistic concurrency
|
## Optimistic concurrency
|
||||||
|
|
||||||
Mutable draft flows support optimistic concurrency through query params:
|
Mutable draft flows support optimistic concurrency through query params:
|
||||||
|
|
||||||
- `expected_current_scheme_version_id`
|
- `expected_current_scheme_version_id`
|
||||||
- `expected_scheme_version_id`
|
- `expected_scheme_version_id`
|
||||||
|
|
||||||
These guards prevent frontend/editor from mutating a stale draft after another version switch.
|
Typical typed conflicts:
|
||||||
|
|
||||||
Typical typed conflict payload:
|
|
||||||
|
|
||||||
- `stale_current_version`
|
- `stale_current_version`
|
||||||
- `stale_draft_version`
|
- `stale_draft_version`
|
||||||
- `draft_not_editable`
|
- `draft_not_editable`
|
||||||
@@ -120,22 +104,19 @@ Typical typed conflict payload:
|
|||||||
|
|
||||||
## Main operator routes
|
## Main operator routes
|
||||||
|
|
||||||
## System
|
### System
|
||||||
|
|
||||||
- `GET /healthz`
|
- `GET /healthz`
|
||||||
- `GET /api/v1/ping`
|
- `GET /api/v1/ping`
|
||||||
- `GET /api/v1/db/ping`
|
- `GET /api/v1/db/ping`
|
||||||
- `GET /api/v1/manifest`
|
- `GET /api/v1/manifest`
|
||||||
|
|
||||||
## Uploads
|
### Uploads
|
||||||
|
|
||||||
- `POST /api/v1/schemes/upload`
|
- `POST /api/v1/schemes/upload`
|
||||||
- `GET /api/v1/uploads`
|
- `GET /api/v1/uploads`
|
||||||
- `GET /api/v1/uploads/{upload_id}`
|
- `GET /api/v1/uploads/{upload_id}`
|
||||||
- `GET /api/v1/uploads/{upload_id}/normalized`
|
- `GET /api/v1/uploads/{upload_id}/normalized`
|
||||||
|
|
||||||
## Scheme registry
|
### Scheme registry
|
||||||
|
|
||||||
- `GET /api/v1/schemes`
|
- `GET /api/v1/schemes`
|
||||||
- `GET /api/v1/schemes/{scheme_id}`
|
- `GET /api/v1/schemes/{scheme_id}`
|
||||||
- `GET /api/v1/schemes/{scheme_id}/current`
|
- `GET /api/v1/schemes/{scheme_id}/current`
|
||||||
@@ -147,8 +128,7 @@ Typical typed conflict payload:
|
|||||||
- `POST /api/v1/schemes/{scheme_id}/unpublish`
|
- `POST /api/v1/schemes/{scheme_id}/unpublish`
|
||||||
- `POST /api/v1/schemes/{scheme_id}/rollback`
|
- `POST /api/v1/schemes/{scheme_id}/rollback`
|
||||||
|
|
||||||
## Editor / draft
|
### Editor / draft
|
||||||
|
|
||||||
- `GET /api/v1/schemes/{scheme_id}/editor/context`
|
- `GET /api/v1/schemes/{scheme_id}/editor/context`
|
||||||
- `POST /api/v1/schemes/{scheme_id}/draft/ensure`
|
- `POST /api/v1/schemes/{scheme_id}/draft/ensure`
|
||||||
- `GET /api/v1/schemes/{scheme_id}/draft/summary`
|
- `GET /api/v1/schemes/{scheme_id}/draft/summary`
|
||||||
@@ -168,8 +148,7 @@ Typical typed conflict payload:
|
|||||||
- `PATCH /api/v1/schemes/{scheme_id}/draft/groups/records/{group_record_id}`
|
- `PATCH /api/v1/schemes/{scheme_id}/draft/groups/records/{group_record_id}`
|
||||||
- `POST /api/v1/schemes/{scheme_id}/draft/repair-references`
|
- `POST /api/v1/schemes/{scheme_id}/draft/repair-references`
|
||||||
|
|
||||||
## Pricing
|
### Pricing
|
||||||
|
|
||||||
- `GET /api/v1/schemes/{scheme_id}/pricing`
|
- `GET /api/v1/schemes/{scheme_id}/pricing`
|
||||||
- `POST /api/v1/schemes/{scheme_id}/pricing/categories`
|
- `POST /api/v1/schemes/{scheme_id}/pricing/categories`
|
||||||
- `PUT /api/v1/schemes/{scheme_id}/pricing/categories/{pricing_category_id}`
|
- `PUT /api/v1/schemes/{scheme_id}/pricing/categories/{pricing_category_id}`
|
||||||
@@ -178,22 +157,19 @@ Typical typed conflict payload:
|
|||||||
- `PUT /api/v1/schemes/{scheme_id}/pricing/rules/{price_rule_id}`
|
- `PUT /api/v1/schemes/{scheme_id}/pricing/rules/{price_rule_id}`
|
||||||
- `DELETE /api/v1/schemes/{scheme_id}/pricing/rules/{price_rule_id}`
|
- `DELETE /api/v1/schemes/{scheme_id}/pricing/rules/{price_rule_id}`
|
||||||
|
|
||||||
## Pricing diagnostics
|
### Pricing diagnostics
|
||||||
|
|
||||||
- `GET /api/v1/schemes/{scheme_id}/pricing/coverage`
|
- `GET /api/v1/schemes/{scheme_id}/pricing/coverage`
|
||||||
- `GET /api/v1/schemes/{scheme_id}/pricing/unpriced-seats`
|
- `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/explain/{seat_id}`
|
||||||
- `GET /api/v1/schemes/{scheme_id}/pricing/rules/diagnostics`
|
- `GET /api/v1/schemes/{scheme_id}/pricing/rules/diagnostics`
|
||||||
|
|
||||||
## Publish preview
|
### Publish preview
|
||||||
|
|
||||||
- `POST /api/v1/schemes/{scheme_id}/draft/pricing/snapshot`
|
- `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-preview`
|
||||||
- `POST /api/v1/schemes/{scheme_id}/draft/remap/preview`
|
- `POST /api/v1/schemes/{scheme_id}/draft/remap/preview`
|
||||||
- `POST /api/v1/schemes/{scheme_id}/draft/remap/apply`
|
- `POST /api/v1/schemes/{scheme_id}/draft/remap/apply`
|
||||||
|
|
||||||
## Structure read model
|
### Structure read model
|
||||||
|
|
||||||
- `GET /api/v1/schemes/{scheme_id}/current/sectors`
|
- `GET /api/v1/schemes/{scheme_id}/current/sectors`
|
||||||
- `GET /api/v1/schemes/{scheme_id}/current/groups`
|
- `GET /api/v1/schemes/{scheme_id}/current/groups`
|
||||||
- `GET /api/v1/schemes/{scheme_id}/current/seats`
|
- `GET /api/v1/schemes/{scheme_id}/current/seats`
|
||||||
@@ -202,129 +178,85 @@ Typical typed conflict payload:
|
|||||||
- `GET /api/v1/schemes/{scheme_id}/current/svg/display`
|
- `GET /api/v1/schemes/{scheme_id}/current/svg/display`
|
||||||
- `GET /api/v1/schemes/{scheme_id}/current/svg/display/meta`
|
- `GET /api/v1/schemes/{scheme_id}/current/svg/display/meta`
|
||||||
|
|
||||||
## Test mode
|
### Test mode
|
||||||
|
|
||||||
- `GET /api/v1/schemes/{scheme_id}/test/seats/{seat_id}`
|
- `GET /api/v1/schemes/{scheme_id}/test/seats/{seat_id}`
|
||||||
|
|
||||||
## Audit
|
### Audit
|
||||||
|
|
||||||
- `GET /api/v1/schemes/{scheme_id}/audit`
|
- `GET /api/v1/schemes/{scheme_id}/audit`
|
||||||
|
|
||||||
## Admin / ops
|
### Admin / ops
|
||||||
|
|
||||||
- `GET /api/v1/admin/schemes/{scheme_id}/current/artifacts`
|
- `GET /api/v1/admin/schemes/{scheme_id}/current/artifacts`
|
||||||
- `GET /api/v1/admin/schemes/{scheme_id}/current/validation`
|
- `GET /api/v1/admin/schemes/{scheme_id}/current/validation`
|
||||||
- `POST /api/v1/admin/schemes/{scheme_id}/current/display/regenerate`
|
- `POST /api/v1/admin/schemes/{scheme_id}/current/display/regenerate`
|
||||||
- `POST /api/v1/admin/display/backfill`
|
- `POST /api/v1/admin/display/backfill`
|
||||||
- `GET /api/v1/admin/artifacts/publish-preview/audit`
|
- `GET /api/v1/admin/artifacts/publish-preview/audit`
|
||||||
- `POST /api/v1/admin/artifacts/publish-preview/cleanup`
|
- `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`
|
||||||
|
|
||||||
|
## Cleanup of test pricing data
|
||||||
|
|
||||||
|
Cleanup endpoints are intended for removing diagnostic / test categories accidentally accumulated in a shared scheme.
|
||||||
|
|
||||||
|
Preview candidates:
|
||||||
|
`GET /api/v1/admin/schemes/{scheme_id}/pricing/categories/cleanup-preview`
|
||||||
|
|
||||||
|
Execute cleanup:
|
||||||
|
`POST /api/v1/admin/schemes/{scheme_id}/pricing/categories/cleanup`
|
||||||
|
|
||||||
|
Safety notes:
|
||||||
|
- use `dry_run=true` first
|
||||||
|
- keep `delete_only_without_rules=true` unless you intentionally want a harder cleanup
|
||||||
|
- prefer matching by prefixes instead of raw ids for repetitive test artifacts
|
||||||
|
|
||||||
|
Helper script:
|
||||||
|
- `backend/scripts/cleanup_test_pricing_data.sh`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
`SCHEME_ID=... DRY_RUN=true ./backend/scripts/cleanup_test_pricing_data.sh`
|
||||||
|
|
||||||
## Typical local flow
|
## Typical local flow
|
||||||
|
|
||||||
## 1. Read current version
|
### 1. Read current version
|
||||||
|
|
||||||
Use:
|
|
||||||
|
|
||||||
`GET /api/v1/schemes/{scheme_id}/current`
|
`GET /api/v1/schemes/{scheme_id}/current`
|
||||||
|
|
||||||
## 2. Ensure draft
|
### 2. Ensure draft
|
||||||
|
|
||||||
Use:
|
|
||||||
|
|
||||||
`POST /api/v1/schemes/{scheme_id}/draft/ensure`
|
`POST /api/v1/schemes/{scheme_id}/draft/ensure`
|
||||||
|
|
||||||
Store returned:
|
Store returned:
|
||||||
|
|
||||||
- `scheme_version_id`
|
- `scheme_version_id`
|
||||||
|
|
||||||
## 3. Read draft state
|
### 3. Read draft state
|
||||||
|
|
||||||
Use:
|
|
||||||
|
|
||||||
- `GET /draft/summary?expected_scheme_version_id=...`
|
- `GET /draft/summary?expected_scheme_version_id=...`
|
||||||
- `GET /draft/structure?expected_scheme_version_id=...`
|
- `GET /draft/structure?expected_scheme_version_id=...`
|
||||||
- `GET /draft/validation?expected_scheme_version_id=...`
|
- `GET /draft/validation?expected_scheme_version_id=...`
|
||||||
- `GET /draft/compare-preview?expected_scheme_version_id=...`
|
- `GET /draft/compare-preview?expected_scheme_version_id=...`
|
||||||
|
|
||||||
## 4. Perform editor mutations
|
### 4. Perform editor mutations
|
||||||
|
|
||||||
Pass:
|
Pass:
|
||||||
|
|
||||||
- `expected_scheme_version_id={draft_scheme_version_id}`
|
- `expected_scheme_version_id={draft_scheme_version_id}`
|
||||||
|
|
||||||
on every mutation route.
|
on every mutation route.
|
||||||
|
|
||||||
## 5. Inspect pricing quality
|
### 5. Inspect pricing quality
|
||||||
|
|
||||||
Use:
|
|
||||||
|
|
||||||
- `GET /pricing/coverage`
|
- `GET /pricing/coverage`
|
||||||
- `GET /pricing/unpriced-seats`
|
- `GET /pricing/unpriced-seats`
|
||||||
- `GET /pricing/explain/{seat_id}`
|
- `GET /pricing/explain/{seat_id}`
|
||||||
- `GET /pricing/rules/diagnostics`
|
- `GET /pricing/rules/diagnostics`
|
||||||
|
|
||||||
## 6. Create pricing snapshot
|
### 6. Build snapshot and inspect readiness
|
||||||
|
- `POST /draft/pricing/snapshot`
|
||||||
|
- `GET /draft/publish-readiness`
|
||||||
|
- `GET /draft/publish-preview?refresh=true`
|
||||||
|
|
||||||
Use:
|
### 7. Publish
|
||||||
|
- `POST /publish?expected_scheme_version_id=...`
|
||||||
|
|
||||||
`POST /draft/pricing/snapshot?expected_scheme_version_id=...`
|
## Regression
|
||||||
|
|
||||||
## 7. Inspect readiness
|
Main operator regression:
|
||||||
|
- `backend/scripts/smoke_regression.sh`
|
||||||
|
|
||||||
Use:
|
Run:
|
||||||
|
`API_URL=http://127.0.0.1:9020 API_KEY=admin-local-dev-key SCHEME_ID=... ./backend/scripts/smoke_regression.sh`
|
||||||
|
|
||||||
`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.
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from app.api.routes.admin import router as admin_router
|
from app.api.routes.admin import router as admin_router
|
||||||
|
from app.api.routes.admin_cleanup import router as admin_cleanup_router
|
||||||
from app.api.routes.audit import router as audit_router
|
from app.api.routes.audit import router as audit_router
|
||||||
from app.api.routes.editor import router as editor_router
|
from app.api.routes.editor import router as editor_router
|
||||||
from app.api.routes.pricing import router as pricing_router
|
from app.api.routes.pricing import router as pricing_router
|
||||||
@@ -22,5 +23,6 @@ router.include_router(pricing_diagnostics_router)
|
|||||||
router.include_router(test_mode_router)
|
router.include_router(test_mode_router)
|
||||||
router.include_router(audit_router)
|
router.include_router(audit_router)
|
||||||
router.include_router(admin_router)
|
router.include_router(admin_router)
|
||||||
|
router.include_router(admin_cleanup_router)
|
||||||
router.include_router(editor_router)
|
router.include_router(editor_router)
|
||||||
router.include_router(publish_router)
|
router.include_router(publish_router)
|
||||||
|
|||||||
55
backend/app/api/routes/admin_cleanup.py
Normal file
55
backend/app/api/routes/admin_cleanup.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
from fastapi import APIRouter, Depends, Query
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
|
from app.schemas.admin_cleanup import (
|
||||||
|
PricingCleanupExecuteRequest,
|
||||||
|
PricingCleanupExecuteResponse,
|
||||||
|
PricingCleanupPreviewResponse,
|
||||||
|
)
|
||||||
|
from app.security.auth import require_api_key
|
||||||
|
from app.services.pricing_cleanup import (
|
||||||
|
build_pricing_cleanup_preview,
|
||||||
|
execute_pricing_cleanup,
|
||||||
|
)
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
f"{settings.api_v1_prefix}/admin/schemes/{{scheme_id}}/pricing/categories/cleanup-preview",
|
||||||
|
response_model=PricingCleanupPreviewResponse,
|
||||||
|
)
|
||||||
|
async def get_pricing_cleanup_preview(
|
||||||
|
scheme_id: str,
|
||||||
|
code_prefix: list[str] = Query(default_factory=list),
|
||||||
|
name_prefix: list[str] = Query(default_factory=list),
|
||||||
|
pricing_category_id: list[str] = Query(default_factory=list),
|
||||||
|
delete_only_without_rules: bool = Query(default=True),
|
||||||
|
role: str = Depends(require_api_key),
|
||||||
|
):
|
||||||
|
return await build_pricing_cleanup_preview(
|
||||||
|
scheme_id=scheme_id,
|
||||||
|
code_prefixes=code_prefix,
|
||||||
|
name_prefixes=name_prefix,
|
||||||
|
pricing_category_ids=pricing_category_id,
|
||||||
|
delete_only_without_rules=delete_only_without_rules,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
f"{settings.api_v1_prefix}/admin/schemes/{{scheme_id}}/pricing/categories/cleanup",
|
||||||
|
response_model=PricingCleanupExecuteResponse,
|
||||||
|
)
|
||||||
|
async def post_pricing_cleanup(
|
||||||
|
scheme_id: str,
|
||||||
|
payload: PricingCleanupExecuteRequest,
|
||||||
|
role: str = Depends(require_api_key),
|
||||||
|
):
|
||||||
|
return await execute_pricing_cleanup(
|
||||||
|
scheme_id=scheme_id,
|
||||||
|
code_prefixes=payload.code_prefixes,
|
||||||
|
name_prefixes=payload.name_prefixes,
|
||||||
|
pricing_category_ids=payload.pricing_category_ids,
|
||||||
|
delete_only_without_rules=payload.delete_only_without_rules,
|
||||||
|
dry_run=payload.dry_run,
|
||||||
|
)
|
||||||
95
backend/app/repositories/pricing_cleanup.py
Normal file
95
backend/app/repositories/pricing_cleanup.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from importlib import import_module
|
||||||
|
|
||||||
|
from sqlalchemy import delete, func, outerjoin, select
|
||||||
|
|
||||||
|
from app.db.session import AsyncSessionLocal
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_model(module_path: str, *candidate_names: str):
|
||||||
|
module = import_module(module_path)
|
||||||
|
for name in candidate_names:
|
||||||
|
model = getattr(module, name, None)
|
||||||
|
if model is not None:
|
||||||
|
return model
|
||||||
|
raise ImportError(
|
||||||
|
f"Unable to resolve model from {module_path}. "
|
||||||
|
f"Tried: {', '.join(candidate_names)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
PricingCategoryModel = _resolve_model(
|
||||||
|
"app.models.pricing_category",
|
||||||
|
"PricingCategory",
|
||||||
|
"PricingCategoryRecord",
|
||||||
|
)
|
||||||
|
PriceRuleModel = _resolve_model(
|
||||||
|
"app.models.price_rule",
|
||||||
|
"PriceRule",
|
||||||
|
"PriceRuleRecord",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def list_pricing_categories_with_rule_counts(
|
||||||
|
*,
|
||||||
|
scheme_id: str,
|
||||||
|
) -> list[dict]:
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
stmt = (
|
||||||
|
select(
|
||||||
|
PricingCategoryModel.pricing_category_id,
|
||||||
|
PricingCategoryModel.scheme_id,
|
||||||
|
PricingCategoryModel.name,
|
||||||
|
PricingCategoryModel.code,
|
||||||
|
func.count(PriceRuleModel.price_rule_id).label("rules_count"),
|
||||||
|
)
|
||||||
|
.select_from(
|
||||||
|
outerjoin(
|
||||||
|
PricingCategoryModel,
|
||||||
|
PriceRuleModel,
|
||||||
|
PricingCategoryModel.pricing_category_id == PriceRuleModel.pricing_category_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.where(PricingCategoryModel.scheme_id == scheme_id)
|
||||||
|
.group_by(
|
||||||
|
PricingCategoryModel.pricing_category_id,
|
||||||
|
PricingCategoryModel.scheme_id,
|
||||||
|
PricingCategoryModel.name,
|
||||||
|
PricingCategoryModel.code,
|
||||||
|
)
|
||||||
|
.order_by(
|
||||||
|
PricingCategoryModel.name.asc(),
|
||||||
|
PricingCategoryModel.code.asc(),
|
||||||
|
PricingCategoryModel.pricing_category_id.asc(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
rows = (await session.execute(stmt)).all()
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"pricing_category_id": row.pricing_category_id,
|
||||||
|
"scheme_id": row.scheme_id,
|
||||||
|
"name": row.name,
|
||||||
|
"code": row.code,
|
||||||
|
"rules_count": int(row.rules_count or 0),
|
||||||
|
}
|
||||||
|
for row in rows
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_pricing_categories_by_ids(
|
||||||
|
*,
|
||||||
|
scheme_id: str,
|
||||||
|
pricing_category_ids: list[str],
|
||||||
|
) -> int:
|
||||||
|
if not pricing_category_ids:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
stmt = delete(PricingCategoryModel).where(
|
||||||
|
PricingCategoryModel.scheme_id == scheme_id,
|
||||||
|
PricingCategoryModel.pricing_category_id.in_(pricing_category_ids),
|
||||||
|
)
|
||||||
|
result = await session.execute(stmt)
|
||||||
|
await session.commit()
|
||||||
|
return int(result.rowcount or 0)
|
||||||
51
backend/app/schemas/admin_cleanup.py
Normal file
51
backend/app/schemas/admin_cleanup.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class PricingCleanupPreviewItem(BaseModel):
|
||||||
|
pricing_category_id: str
|
||||||
|
name: str
|
||||||
|
code: str
|
||||||
|
rules_count: int = Field(ge=0)
|
||||||
|
matched_by: list[str]
|
||||||
|
deletable: bool
|
||||||
|
|
||||||
|
|
||||||
|
class PricingCleanupPreviewResponse(BaseModel):
|
||||||
|
scheme_id: str
|
||||||
|
code_prefixes: list[str]
|
||||||
|
name_prefixes: list[str]
|
||||||
|
pricing_category_ids: list[str]
|
||||||
|
delete_only_without_rules: bool
|
||||||
|
total_candidates: int = Field(ge=0)
|
||||||
|
safe_to_delete_count: int = Field(ge=0)
|
||||||
|
items: list[PricingCleanupPreviewItem]
|
||||||
|
|
||||||
|
|
||||||
|
class PricingCleanupExecuteRequest(BaseModel):
|
||||||
|
code_prefixes: list[str] = Field(default_factory=list)
|
||||||
|
name_prefixes: list[str] = Field(default_factory=list)
|
||||||
|
pricing_category_ids: list[str] = Field(default_factory=list)
|
||||||
|
delete_only_without_rules: bool = True
|
||||||
|
dry_run: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
class PricingCleanupSkippedItem(BaseModel):
|
||||||
|
pricing_category_id: str
|
||||||
|
reason: str
|
||||||
|
|
||||||
|
|
||||||
|
class PricingCleanupExecuteResponse(BaseModel):
|
||||||
|
scheme_id: str
|
||||||
|
dry_run: bool
|
||||||
|
delete_only_without_rules: bool
|
||||||
|
|
||||||
|
requested_total: int = Field(ge=0)
|
||||||
|
matched_total: int = Field(ge=0)
|
||||||
|
|
||||||
|
would_delete_count: int = Field(ge=0)
|
||||||
|
deleted_count: int = Field(ge=0)
|
||||||
|
skipped_count: int = Field(ge=0)
|
||||||
|
|
||||||
|
would_delete_category_ids: list[str]
|
||||||
|
deleted_category_ids: list[str]
|
||||||
|
skipped: list[PricingCleanupSkippedItem]
|
||||||
127
backend/app/services/pricing_cleanup.py
Normal file
127
backend/app/services/pricing_cleanup.py
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from app.repositories.pricing_cleanup import (
|
||||||
|
delete_pricing_categories_by_ids,
|
||||||
|
list_pricing_categories_with_rule_counts,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _matches_any_prefix(value: str | None, prefixes: list[str]) -> list[str]:
|
||||||
|
if not value:
|
||||||
|
return []
|
||||||
|
matches: list[str] = []
|
||||||
|
lower_value = value.lower()
|
||||||
|
for prefix in prefixes:
|
||||||
|
if lower_value.startswith(prefix.lower()):
|
||||||
|
matches.append(prefix)
|
||||||
|
return matches
|
||||||
|
|
||||||
|
|
||||||
|
async def build_pricing_cleanup_preview(
|
||||||
|
*,
|
||||||
|
scheme_id: str,
|
||||||
|
code_prefixes: list[str],
|
||||||
|
name_prefixes: list[str],
|
||||||
|
pricing_category_ids: list[str],
|
||||||
|
delete_only_without_rules: bool,
|
||||||
|
) -> dict:
|
||||||
|
rows = await list_pricing_categories_with_rule_counts(scheme_id=scheme_id)
|
||||||
|
requested_ids = set(pricing_category_ids)
|
||||||
|
|
||||||
|
items: list[dict] = []
|
||||||
|
safe_to_delete_count = 0
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
matched_by: list[str] = []
|
||||||
|
|
||||||
|
for prefix in _matches_any_prefix(row["code"], code_prefixes):
|
||||||
|
matched_by.append(f"code_prefix:{prefix}")
|
||||||
|
|
||||||
|
for prefix in _matches_any_prefix(row["name"], name_prefixes):
|
||||||
|
matched_by.append(f"name_prefix:{prefix}")
|
||||||
|
|
||||||
|
if row["pricing_category_id"] in requested_ids:
|
||||||
|
matched_by.append("pricing_category_id")
|
||||||
|
|
||||||
|
if not matched_by:
|
||||||
|
continue
|
||||||
|
|
||||||
|
deletable = True
|
||||||
|
if delete_only_without_rules and row["rules_count"] > 0:
|
||||||
|
deletable = False
|
||||||
|
|
||||||
|
if deletable:
|
||||||
|
safe_to_delete_count += 1
|
||||||
|
|
||||||
|
items.append(
|
||||||
|
{
|
||||||
|
"pricing_category_id": row["pricing_category_id"],
|
||||||
|
"name": row["name"],
|
||||||
|
"code": row["code"],
|
||||||
|
"rules_count": row["rules_count"],
|
||||||
|
"matched_by": matched_by,
|
||||||
|
"deletable": deletable,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"scheme_id": scheme_id,
|
||||||
|
"code_prefixes": code_prefixes,
|
||||||
|
"name_prefixes": name_prefixes,
|
||||||
|
"pricing_category_ids": pricing_category_ids,
|
||||||
|
"delete_only_without_rules": delete_only_without_rules,
|
||||||
|
"total_candidates": len(items),
|
||||||
|
"safe_to_delete_count": safe_to_delete_count,
|
||||||
|
"items": items,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def execute_pricing_cleanup(
|
||||||
|
*,
|
||||||
|
scheme_id: str,
|
||||||
|
code_prefixes: list[str],
|
||||||
|
name_prefixes: list[str],
|
||||||
|
pricing_category_ids: list[str],
|
||||||
|
delete_only_without_rules: bool,
|
||||||
|
dry_run: bool,
|
||||||
|
) -> dict:
|
||||||
|
preview = await build_pricing_cleanup_preview(
|
||||||
|
scheme_id=scheme_id,
|
||||||
|
code_prefixes=code_prefixes,
|
||||||
|
name_prefixes=name_prefixes,
|
||||||
|
pricing_category_ids=pricing_category_ids,
|
||||||
|
delete_only_without_rules=delete_only_without_rules,
|
||||||
|
)
|
||||||
|
|
||||||
|
deletable_items = [item for item in preview["items"] if item["deletable"]]
|
||||||
|
skipped_items = [item for item in preview["items"] if not item["deletable"]]
|
||||||
|
|
||||||
|
would_delete_ids = [item["pricing_category_id"] for item in deletable_items]
|
||||||
|
deleted_ids: list[str] = []
|
||||||
|
|
||||||
|
if not dry_run and would_delete_ids:
|
||||||
|
await delete_pricing_categories_by_ids(
|
||||||
|
scheme_id=scheme_id,
|
||||||
|
pricing_category_ids=would_delete_ids,
|
||||||
|
)
|
||||||
|
deleted_ids = list(would_delete_ids)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"scheme_id": scheme_id,
|
||||||
|
"dry_run": dry_run,
|
||||||
|
"delete_only_without_rules": delete_only_without_rules,
|
||||||
|
"requested_total": len(pricing_category_ids) + len(code_prefixes) + len(name_prefixes),
|
||||||
|
"matched_total": preview["total_candidates"],
|
||||||
|
"would_delete_count": len(would_delete_ids),
|
||||||
|
"deleted_count": 0 if dry_run else len(deleted_ids),
|
||||||
|
"skipped_count": len(skipped_items),
|
||||||
|
"would_delete_category_ids": would_delete_ids,
|
||||||
|
"deleted_category_ids": deleted_ids,
|
||||||
|
"skipped": [
|
||||||
|
{
|
||||||
|
"pricing_category_id": item["pricing_category_id"],
|
||||||
|
"reason": "category_has_rules",
|
||||||
|
}
|
||||||
|
for item in skipped_items
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -90,6 +90,10 @@
|
|||||||
- GET /api/v1/admin/artifacts/publish-preview/audit
|
- GET /api/v1/admin/artifacts/publish-preview/audit
|
||||||
- POST /api/v1/admin/artifacts/publish-preview/cleanup
|
- POST /api/v1/admin/artifacts/publish-preview/cleanup
|
||||||
|
|
||||||
|
## app/api/routes/admin_cleanup.py
|
||||||
|
- GET /api/v1/admin/schemes/{scheme_id}/pricing/categories/cleanup-preview
|
||||||
|
- POST /api/v1/admin/schemes/{scheme_id}/pricing/categories/cleanup
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
- This file is an operational route index, not a generated OpenAPI export.
|
- 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.
|
- Update this map in the same change set when adding, removing, renaming, or moving routes.
|
||||||
|
|||||||
@@ -17,6 +17,14 @@ export API_URL="http://127.0.0.1:9020"
|
|||||||
export API_KEY="admin-local-dev-key"
|
export API_KEY="admin-local-dev-key"
|
||||||
export SCHEME_ID="82086336d385427f9d56244f9e1dd772"
|
export SCHEME_ID="82086336d385427f9d56244f9e1dd772"
|
||||||
|
|
||||||
|
## Main script
|
||||||
|
|
||||||
|
Primary operator regression:
|
||||||
|
|
||||||
|
`backend/scripts/smoke_regression.sh`
|
||||||
|
|
||||||
|
The script is expected to fail fast on any contract break or unexpected 5xx.
|
||||||
|
|
||||||
## 1. Health / system
|
## 1. Health / system
|
||||||
|
|
||||||
- GET /healthz -> 200
|
- GET /healthz -> 200
|
||||||
@@ -175,14 +183,14 @@ Validate:
|
|||||||
- GET /api/v1/admin/schemes/{scheme_id}/current/validation -> 200
|
- GET /api/v1/admin/schemes/{scheme_id}/current/validation -> 200
|
||||||
- GET /api/v1/admin/artifacts/publish-preview/audit -> 200
|
- GET /api/v1/admin/artifacts/publish-preview/audit -> 200
|
||||||
- POST /api/v1/admin/artifacts/publish-preview/cleanup?dry_run=true -> 200
|
- POST /api/v1/admin/artifacts/publish-preview/cleanup?dry_run=true -> 200
|
||||||
|
- GET /api/v1/admin/schemes/{scheme_id}/pricing/categories/cleanup-preview -> 200
|
||||||
Optional:
|
- POST /api/v1/admin/schemes/{scheme_id}/pricing/categories/cleanup with dry_run=true -> 200
|
||||||
- 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:
|
Validate:
|
||||||
- audit endpoint does not report orphan files or missing files for DB rows in normal state
|
- artifact audit does not report orphan files or missing files for DB rows in normal state
|
||||||
- validation report is readable and deterministic
|
- validation report is readable and deterministic
|
||||||
|
- pricing cleanup preview returns matched candidates and safe_to_delete_count
|
||||||
|
- pricing cleanup dry-run returns deleted_count=0 and would_delete_count>0
|
||||||
- admin routes do not produce 500 for healthy scheme state
|
- admin routes do not produce 500 for healthy scheme state
|
||||||
|
|
||||||
## 13. Audit trail
|
## 13. Audit trail
|
||||||
@@ -207,6 +215,7 @@ Regression is considered failed if any of the following happen:
|
|||||||
- draft summary / structure / validation / compare-preview returns 500
|
- draft summary / structure / validation / compare-preview returns 500
|
||||||
- pricing bundle or diagnostics contract changes unexpectedly
|
- pricing bundle or diagnostics contract changes unexpectedly
|
||||||
- admin audit/cleanup endpoints fail on healthy environment
|
- admin audit/cleanup endpoints fail on healthy environment
|
||||||
|
- pricing cleanup dry-run mutates data
|
||||||
- artifact retention grows without bound for repeated preview refresh on same variant
|
- artifact retention grows without bound for repeated preview refresh on same variant
|
||||||
|
|
||||||
## 15. Operator note
|
## 15. Operator note
|
||||||
@@ -220,3 +229,4 @@ Run this checklist after:
|
|||||||
- startup/import/config changes
|
- startup/import/config changes
|
||||||
- draft lifecycle changes
|
- draft lifecycle changes
|
||||||
- publish readiness changes
|
- publish readiness changes
|
||||||
|
- admin cleanup changes
|
||||||
|
|||||||
38
backend/scripts/cleanup_test_pricing_data.sh
Executable file
38
backend/scripts/cleanup_test_pricing_data.sh
Executable file
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
API_URL="${API_URL:-http://127.0.0.1:9020}"
|
||||||
|
API_KEY="${API_KEY:-admin-local-dev-key}"
|
||||||
|
SCHEME_ID="${SCHEME_ID:-}"
|
||||||
|
DRY_RUN="${DRY_RUN:-true}"
|
||||||
|
|
||||||
|
if [[ -z "${SCHEME_ID}" ]]; then
|
||||||
|
echo "SCHEME_ID is required"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
REQUEST_BODY=$(cat <<JSON
|
||||||
|
{
|
||||||
|
"code_prefixes": ["FAIL_", "DIAG_", "AUTO_", "TYPED_"],
|
||||||
|
"name_prefixes": ["should-fail-", "diag-", "auto ", "typed-response-"],
|
||||||
|
"pricing_category_ids": [],
|
||||||
|
"delete_only_without_rules": true,
|
||||||
|
"dry_run": ${DRY_RUN}
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "===== CLEANUP PREVIEW ====="
|
||||||
|
curl -sS \
|
||||||
|
-H "X-API-Key: ${API_KEY}" \
|
||||||
|
"${API_URL}/api/v1/admin/schemes/${SCHEME_ID}/pricing/categories/cleanup-preview?code_prefix=FAIL_&code_prefix=DIAG_&code_prefix=AUTO_&code_prefix=TYPED_&name_prefix=should-fail-&name_prefix=diag-&name_prefix=auto%20&name_prefix=typed-response-" \
|
||||||
|
| python3 -m json.tool
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "===== CLEANUP EXECUTE (DRY_RUN=${DRY_RUN}) ====="
|
||||||
|
curl -sS -X POST \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: ${API_KEY}" \
|
||||||
|
-d "${REQUEST_BODY}" \
|
||||||
|
"${API_URL}/api/v1/admin/schemes/${SCHEME_ID}/pricing/categories/cleanup" \
|
||||||
|
| python3 -m json.tool
|
||||||
@@ -1,201 +1,203 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -Eeuo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
API_URL="${API_URL:-http://127.0.0.1:9020}"
|
API_URL="${API_URL:-http://127.0.0.1:9020}"
|
||||||
API_KEY="${API_KEY:-admin-local-dev-key}"
|
API_KEY="${API_KEY:-admin-local-dev-key}"
|
||||||
SCHEME_ID="${SCHEME_ID:-82086336d385427f9d56244f9e1dd772}"
|
SCHEME_ID="${SCHEME_ID:-82086336d385427f9d56244f9e1dd772}"
|
||||||
|
|
||||||
WORKDIR="$(mktemp -d)"
|
TMP_DIR="$(mktemp -d)"
|
||||||
trap 'rm -rf "$WORKDIR"' EXIT
|
trap 'rm -rf "${TMP_DIR}"' 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() {
|
request() {
|
||||||
local name="$1"
|
local name="$1"
|
||||||
local method="$2"
|
local method="$2"
|
||||||
local url="$3"
|
local url="$3"
|
||||||
local expected="$4"
|
local expected_status="$4"
|
||||||
local body="${5:-}"
|
local body="${5:-}"
|
||||||
local outfile="${WORKDIR}/${name}.body"
|
local out_file="${TMP_DIR}/${name}.body"
|
||||||
local codefile="${WORKDIR}/${name}.code"
|
local status_file="${TMP_DIR}/${name}.status"
|
||||||
|
|
||||||
if [[ -n "$body" ]]; then
|
echo
|
||||||
|
echo "===== ${name} ====="
|
||||||
|
|
||||||
|
if [[ -n "${body}" ]]; then
|
||||||
curl -sS \
|
curl -sS \
|
||||||
-X "$method" \
|
-X "${method}" \
|
||||||
"${HDR_AUTH[@]}" \
|
-H "X-API-Key: ${API_KEY}" \
|
||||||
"${HDR_JSON[@]}" \
|
-H "Content-Type: application/json" \
|
||||||
-o "$outfile" \
|
-o "${out_file}" \
|
||||||
-w "%{http_code}" \
|
-w "%{http_code}" \
|
||||||
"$url" \
|
"${url}" \
|
||||||
--data "$body" > "$codefile"
|
--data "${body}" > "${status_file}"
|
||||||
else
|
else
|
||||||
curl -sS \
|
curl -sS \
|
||||||
-X "$method" \
|
-X "${method}" \
|
||||||
"${HDR_AUTH[@]}" \
|
-H "X-API-Key: ${API_KEY}" \
|
||||||
-o "$outfile" \
|
-o "${out_file}" \
|
||||||
-w "%{http_code}" \
|
-w "%{http_code}" \
|
||||||
"$url" > "$codefile"
|
"${url}" > "${status_file}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local code
|
local actual_status
|
||||||
code="$(cat "$codefile")"
|
actual_status="$(cat "${status_file}")"
|
||||||
|
|
||||||
echo "[$method] $url -> $code"
|
echo "[${method}] ${url} -> ${actual_status}"
|
||||||
cat "$outfile"
|
cat "${out_file}"
|
||||||
echo
|
echo
|
||||||
|
|
||||||
if [[ "$code" != "$expected" ]]; then
|
if [[ "${actual_status}" != "${expected_status}" ]]; then
|
||||||
fail "Unexpected HTTP status for ${name}: expected ${expected}, got ${code}"
|
echo "[FAIL] Unexpected HTTP status for ${name}: expected ${expected_status}, got ${actual_status}" >&2
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
json_get() {
|
json_get() {
|
||||||
local file="$1"
|
local file="$1"
|
||||||
local expr="$2"
|
local expr="$2"
|
||||||
python3 - <<PY
|
python3 - "$file" "$expr" <<'PY'
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
import sys
|
||||||
|
|
||||||
payload = json.loads(Path("$file").read_text())
|
path = sys.argv[2].split(".")
|
||||||
value = payload
|
value = json.load(open(sys.argv[1], "r", encoding="utf-8"))
|
||||||
for part in "$expr".split("."):
|
for part in path:
|
||||||
|
if not part:
|
||||||
|
continue
|
||||||
if part.isdigit():
|
if part.isdigit():
|
||||||
value = value[int(part)]
|
value = value[int(part)]
|
||||||
else:
|
else:
|
||||||
value = value[part]
|
value = value[part]
|
||||||
print(value if value is not None else "")
|
if isinstance(value, bool):
|
||||||
|
print("true" if value else "false")
|
||||||
|
elif value is None:
|
||||||
|
print("null")
|
||||||
|
else:
|
||||||
|
print(value)
|
||||||
PY
|
PY
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_json_value() {
|
assert_json_eq() {
|
||||||
local file="$1"
|
local file="$1"
|
||||||
local expr="$2"
|
local expr="$2"
|
||||||
local expected="$3"
|
local expected="$3"
|
||||||
local actual
|
local actual
|
||||||
actual="$(json_get "$file" "$expr")"
|
actual="$(json_get "${file}" "${expr}")"
|
||||||
if [[ "$actual" != "$expected" ]]; then
|
if [[ "${actual}" != "${expected}" ]]; then
|
||||||
fail "JSON assertion failed for ${expr}: expected '${expected}', got '${actual}'"
|
echo "[FAIL] ${expr}: expected '${expected}', got '${actual}'" >&2
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "[OK] ${expr}=${actual}"
|
echo "[OK] ${expr}=${actual}"
|
||||||
}
|
}
|
||||||
|
|
||||||
step "health"
|
assert_json_int_gt() {
|
||||||
curl -sS -i "${API_URL}/healthz" || fail "Health endpoint is unavailable"
|
local file="$1"
|
||||||
|
local expr="$2"
|
||||||
|
local threshold="$3"
|
||||||
|
local actual
|
||||||
|
actual="$(json_get "${file}" "${expr}")"
|
||||||
|
if ! [[ "${actual}" =~ ^[0-9]+$ ]]; then
|
||||||
|
echo "[FAIL] ${expr}: expected integer, got '${actual}'" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if (( actual <= threshold )); then
|
||||||
|
echo "[FAIL] ${expr}: expected > ${threshold}, got ${actual}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "[OK] ${expr}=${actual} (> ${threshold})"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "===== health ====="
|
||||||
|
curl -i "${API_URL}/healthz"
|
||||||
|
|
||||||
step "system"
|
|
||||||
request "ping" "GET" "${API_URL}/api/v1/ping" "200"
|
request "ping" "GET" "${API_URL}/api/v1/ping" "200"
|
||||||
request "db_ping" "GET" "${API_URL}/api/v1/db/ping" "200"
|
request "db_ping" "GET" "${API_URL}/api/v1/db/ping" "200"
|
||||||
request "manifest" "GET" "${API_URL}/api/v1/manifest" "200"
|
request "manifest" "GET" "${API_URL}/api/v1/manifest" "200"
|
||||||
|
|
||||||
step "scheme current"
|
request "scheme_current" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/current" "200"
|
||||||
request "current" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/current" "200"
|
CURRENT_VERSION_ID="$(json_get "${TMP_DIR}/scheme_current.body" "scheme_version_id")"
|
||||||
CURRENT_VERSION_ID="$(json_get "${WORKDIR}/current.body" "scheme_version_id")"
|
CURRENT_STATUS="$(json_get "${TMP_DIR}/scheme_current.body" "status")"
|
||||||
CURRENT_STATUS="$(json_get "${WORKDIR}/current.body" "status")"
|
|
||||||
echo "CURRENT_VERSION_ID=${CURRENT_VERSION_ID}"
|
echo "CURRENT_VERSION_ID=${CURRENT_VERSION_ID}"
|
||||||
echo "CURRENT_STATUS=${CURRENT_STATUS}"
|
echo "CURRENT_STATUS=${CURRENT_STATUS}"
|
||||||
|
|
||||||
step "editor context"
|
|
||||||
request "editor_context" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/editor/context" "200"
|
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"
|
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_VERSION_ID="$(json_get "${TMP_DIR}/ensure_draft.body" "scheme_version_id")"
|
||||||
DRAFT_CREATED="$(json_get "${WORKDIR}/ensure_draft.body" "created")"
|
DRAFT_CREATED="$(json_get "${TMP_DIR}/ensure_draft.body" "created")"
|
||||||
echo "DRAFT_VERSION_ID=${DRAFT_VERSION_ID}"
|
echo "DRAFT_VERSION_ID=${DRAFT_VERSION_ID}"
|
||||||
echo "DRAFT_CREATED=${DRAFT_CREATED}"
|
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"
|
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_eq "${TMP_DIR}/draft_summary.body" "scheme_version_id" "${DRAFT_VERSION_ID}"
|
||||||
assert_json_value "${WORKDIR}/draft_summary.body" "status" "draft"
|
assert_json_eq "${TMP_DIR}/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"
|
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}"
|
assert_json_eq "${TMP_DIR}/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")"
|
SEAT_RECORD_ID="$(json_get "${TMP_DIR}/draft_structure.body" "seats.0.seat_record_id")"
|
||||||
GROUP_RECORD_ID="$(json_get "${WORKDIR}/draft_structure.body" "groups.0.group_record_id")"
|
SECTOR_RECORD_ID="$(json_get "${TMP_DIR}/draft_structure.body" "sectors.0.sector_record_id")"
|
||||||
PRICED_SEAT_ID="$(json_get "${WORKDIR}/draft_structure.body" "seats.0.seat_id")"
|
GROUP_RECORD_ID="$(json_get "${TMP_DIR}/draft_structure.body" "groups.0.group_record_id")"
|
||||||
UNPRICED_SEAT_ID="$(json_get "${WORKDIR}/draft_structure.body" "seats.2.seat_id")"
|
PRICED_SEAT_ID="$(json_get "${TMP_DIR}/draft_structure.body" "seats.0.seat_id")"
|
||||||
|
UNPRICED_SEAT_ID="$(json_get "${TMP_DIR}/draft_structure.body" "seats.2.seat_id")"
|
||||||
|
|
||||||
echo "SEAT_RECORD_ID=${SEAT_RECORD_ID}"
|
echo "SEAT_RECORD_ID=${SEAT_RECORD_ID}"
|
||||||
echo "SECTOR_RECORD_ID=${SECTOR_RECORD_ID}"
|
echo "SECTOR_RECORD_ID=${SECTOR_RECORD_ID}"
|
||||||
echo "GROUP_RECORD_ID=${GROUP_RECORD_ID}"
|
echo "GROUP_RECORD_ID=${GROUP_RECORD_ID}"
|
||||||
echo "PRICED_SEAT_ID=${PRICED_SEAT_ID}"
|
echo "PRICED_SEAT_ID=${PRICED_SEAT_ID}"
|
||||||
echo "UNPRICED_SEAT_ID=${UNPRICED_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"
|
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"
|
assert_json_eq "${TMP_DIR}/draft_validation.body" "status" "draft"
|
||||||
|
|
||||||
step "draft compare preview"
|
request "draft_compare_preview" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/compare-preview?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
|
||||||
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 "stale_draft_conflict" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/summary?expected_scheme_version_id=deadbeefdeadbeefdeadbeefdeadbeef" "409"
|
||||||
request "draft_summary_stale" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/summary?expected_scheme_version_id=deadbeefdeadbeefdeadbeefdeadbeef" "409"
|
assert_json_eq "${TMP_DIR}/stale_draft_conflict.body" "detail.code" "stale_draft_version"
|
||||||
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_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_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_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"
|
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_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_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"
|
request "current_seats" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/current/seats" "200"
|
||||||
|
request "display_meta" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/current/svg/display/meta" "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_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_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_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_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_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 "pricing_rule_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 "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_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"
|
request "test_mode_unpriced" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/test/seats/${UNPRICED_SEAT_ID}" "200"
|
||||||
|
|
||||||
step "typed validation errors"
|
request "typed_invalid_amount" "POST" "${API_URL}/api/v1/schemes/${SCHEME_ID}/pricing/rules?expected_scheme_version_id=${DRAFT_VERSION_ID}" "422" '{"pricing_category_id":"4ef9e2b78fe0447f9f5db02714c7cad5","target_type":"sector","target_ref":"vip","amount":"bad","currency":"RUB"}'
|
||||||
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_eq "${TMP_DIR}/typed_invalid_amount.body" "detail.code" "invalid_amount"
|
||||||
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}'
|
request "typed_remap_filter_required" "POST" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/remap/preview?expected_scheme_version_id=${DRAFT_VERSION_ID}" "422" '{}'
|
||||||
assert_json_value "${WORKDIR}/remap_no_filters.body" "detail.code" "remap_filter_required"
|
assert_json_eq "${TMP_DIR}/typed_remap_filter_required.body" "detail.code" "remap_filter_required"
|
||||||
|
|
||||||
step "draft pricing snapshot"
|
request "draft_pricing_snapshot" "POST" "${API_URL}/api/v1/schemes/${SCHEME_ID}/draft/pricing/snapshot?expected_scheme_version_id=${DRAFT_VERSION_ID}" "200"
|
||||||
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"
|
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_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"
|
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_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_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_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"
|
request "admin_preview_cleanup_dry_run" "POST" "${API_URL}/api/v1/admin/artifacts/publish-preview/cleanup?dry_run=true" "200"
|
||||||
|
|
||||||
step "audit trail"
|
request "admin_cleanup_preview" "GET" "${API_URL}/api/v1/admin/schemes/${SCHEME_ID}/pricing/categories/cleanup-preview?code_prefix=FAIL_&code_prefix=DIAG_&code_prefix=AUTO_&code_prefix=TYPED_&name_prefix=should-fail-&name_prefix=diag-&name_prefix=auto%20&name_prefix=typed-response-" "200"
|
||||||
request "audit" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/audit" "200"
|
request "admin_cleanup_dry_run" "POST" "${API_URL}/api/v1/admin/schemes/${SCHEME_ID}/pricing/categories/cleanup" "200" '{"code_prefixes":["FAIL_","DIAG_","AUTO_","TYPED_"],"name_prefixes":["should-fail-","diag-","auto ","typed-response-"],"delete_only_without_rules":true,"dry_run":true}'
|
||||||
|
|
||||||
step "done"
|
assert_json_eq "${TMP_DIR}/admin_cleanup_dry_run.body" "dry_run" "true"
|
||||||
|
assert_json_eq "${TMP_DIR}/admin_cleanup_dry_run.body" "deleted_count" "0"
|
||||||
|
assert_json_int_gt "${TMP_DIR}/admin_cleanup_dry_run.body" "would_delete_count" "0"
|
||||||
|
|
||||||
|
request "audit_trail" "GET" "${API_URL}/api/v1/schemes/${SCHEME_ID}/audit" "200"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "===== done ====="
|
||||||
echo "[OK] smoke regression completed successfully"
|
echo "[OK] smoke regression completed successfully"
|
||||||
|
|||||||
Reference in New Issue
Block a user