greebo 62550d5cb5 feat(backend): add stale draft guards and reference validation for draft mutations
add stale draft protection for mutation flows

validate referenced entities before applying draft changes
reduce invalid draft writes caused by stale state and broken references

keep mutation behavior explicit and version-aware
2026-03-19 19:25:44 +03:00
2026-03-19 13:39:32 +03:00
2026-03-19 13:39:32 +03:00
2026-03-19 13:39:32 +03:00
2026-03-19 13:39:32 +03:00

SVG Service

Backend-сервис для загрузки, sanitization, normalization, хранения и базовой эксплуатации SVG-схем с местами, секторами, группами и pricing.

Текущее состояние

Реализовано:

  • загрузка SVG-файла
  • безопасная первичная обработка SVG
  • sanitization запрещённых элементов и атрибутов
  • normalization в машиночитаемый JSON snapshot
  • хранение original / sanitized / normalized файлов
  • PostgreSQL persistence
  • registry uploads / schemes / versions
  • lifecycle схем:
    • draft
    • publish
    • unpublish
    • rollback
    • create next draft version
  • persistence доменных сущностей:
    • sectors
    • groups
    • seats
  • pricing v1:
    • pricing categories
    • price rules
    • effective price resolution
  • test mode preview API
  • audit/history API

Стек

  • Python 3.11+
  • FastAPI
  • Pydantic
  • SQLAlchemy Async
  • PostgreSQL
  • Alembic
  • Docker Compose

Запуск

1. Сборка и запуск

docker compose build --no-cache docker compose up -d

2. Применение миграций

docker compose run --rm svg-service alembic -c /app/alembic.ini upgrade head

3. Проверка

curl -s http://127.0.0.1:9020/healthz

Ожидаемый ответ:

{"status":"ok"}


Аутентификация

Во все защищённые эндпойнты передаётся заголовок:

X-API-Key:

Локально использовались ключи из .env.


Основные env-переменные

Примерно используются такие параметры:

  • APP_NAME
  • APP_ENV
  • APP_PORT
  • API_V1_PREFIX
  • AUTH_HEADER_NAME
  • ADMIN_API_KEY
  • VIEWER_API_KEY
  • SVG_MAX_FILE_SIZE_BYTES
  • SVG_MAX_ELEMENTS
  • SVG_ALLOW_INTERNAL_USE_REFERENCES_ONLY
  • SVG_FORBID_FOREIGN_OBJECT_V1
  • SVG_FORBID_STYLE_V1
  • SVG_FORBID_IMAGE_V1
  • POSTGRES_HOST
  • POSTGRES_PORT
  • POSTGRES_DB
  • POSTGRES_USER
  • POSTGRES_PASSWORD
  • DATABASE_URL

Storage layout

На файловой системе используются каталоги:

  • storage/original
  • storage/sanitized
  • storage/normalized

Каждая загрузка сохраняется в свой каталог по upload_id.


API

1. Service / auth / diagnostics

GET /

Базовая информация о сервисе.

GET /healthz

Healthcheck.

GET /api/v1/ping

Проверка API и auth.

GET /api/v1/auth/me

Возвращает текущую роль по API key.

GET /api/v1/db/ping

Проверка доступности PostgreSQL.

GET /api/v1/manifest

Возвращает manifest сервиса:

  • лимиты SVG
  • правила sanitization
  • extraction contract

2. Uploads

POST /api/v1/schemes/upload

Загружает SVG, выполняет:

  • validate
  • inspect
  • sanitize
  • normalize
  • storage save
  • create upload record
  • create scheme
  • create version 1
  • persist sectors/groups/seats
  • audit event

Form-data

  • file: SVG-файл

Ответ

Возвращает:

  • upload_id
  • имя файла
  • размеры
  • counts
  • storage paths
  • accepted flag

GET /api/v1/uploads

Список upload records.

GET /api/v1/uploads/{upload_id}

Детали upload record.

GET /api/v1/uploads/{upload_id}/normalized

Чтение normalized JSON snapshot.


3. Schemes

GET /api/v1/schemes

Список схем.

GET /api/v1/schemes/{scheme_id}

Детали схемы.

GET /api/v1/schemes/{scheme_id}/current

Текущая version схемы.

GET /api/v1/schemes/{scheme_id}/versions

Список всех version схемы.

POST /api/v1/schemes/{scheme_id}/versions

Создаёт новую draft version из current version.

Что делает:

  • создаёт version_number = current + 1
  • копирует metadata snapshot
  • копирует sectors/groups/seats
  • переключает current_version_number
  • сбрасывает схему в draft
  • пишет audit event

4. Scheme lifecycle

POST /api/v1/schemes/{scheme_id}/publish

Публикует current version.

Что делает:

  • scheme.status = published
  • published_at = now()
  • current version получает статус published
  • пишет audit event

POST /api/v1/schemes/{scheme_id}/unpublish

Снимает публикацию.

Что делает:

  • scheme.status = draft
  • published_at = null
  • current version переводится в draft
  • пишет audit event

POST /api/v1/schemes/{scheme_id}/rollback

Откатывает current version на указанную version.

JSON body

{ "target_version_number": 1 }

Что делает:

  • переключает current_version_number
  • сбрасывает публикацию
  • схема становится draft
  • пишет audit event

5. Current domain data

GET /api/v1/schemes/{scheme_id}/current/sectors

Сектора текущей версии.

GET /api/v1/schemes/{scheme_id}/current/groups

Группы текущей версии.

GET /api/v1/schemes/{scheme_id}/current/seats

Места текущей версии.

Seat содержит:

  • seat_id
  • sector_id
  • group_id
  • row_label
  • seat_number
  • geometry fields
  • tag / classes

6. Pricing

6.1 Pricing categories

POST /api/v1/schemes/{scheme_id}/pricing/categories

Создать pricing category.

JSON body

{ "name": "VIP", "code": "VIP" }

PUT /api/v1/schemes/{scheme_id}/pricing/categories/{pricing_category_id}

Обновить pricing category.

JSON body

{ "name": "Econom Plus", "code": "ECO_PLUS" }

DELETE /api/v1/schemes/{scheme_id}/pricing/categories/{pricing_category_id}

Удалить pricing category.


6.2 Price rules

POST /api/v1/schemes/{scheme_id}/pricing/rules

Создать price rule.

JSON body

{ "pricing_category_id": "....", "target_type": "sector", "target_ref": "vip", "amount": "2500.00", "currency": "RUB" }

Ограничения v1

  • target_type только:
    • sector
    • group
    • seat
  • currency только RUB
  • amount хранится как NUMERIC(12,2)

PUT /api/v1/schemes/{scheme_id}/pricing/rules/{price_rule_id}

Обновить rule.

DELETE /api/v1/schemes/{scheme_id}/pricing/rules/{price_rule_id}

Удалить rule.

GET /api/v1/schemes/{scheme_id}/pricing

Вернуть все pricing categories и rules схемы.


7. Effective pricing

GET /api/v1/schemes/{scheme_id}/current/seats/{seat_id}/price

Рассчитать effective price для места.

Приоритет поиска rules

  1. seat
  2. group
  3. sector

Ответ

Возвращает:

  • seat_id
  • sector_id
  • group_id
  • matched_rule_level
  • matched_target_ref
  • pricing_category_id
  • amount
  • currency

Если правило не найдено:

  • 404 No pricing rule matched current seat

Если место не найдено:

  • 404 Seat not found in current scheme version

8. Test mode

GET /api/v1/schemes/{scheme_id}/test/seats/{seat_id}

Preview для frontend test mode.

Возвращает:

  • seat metadata
  • selectable
  • has_price
  • pricing info, если найдено effective rule

Логика:

  • если price найден -> selectable = true
  • если price не найден -> selectable = false

9. Audit

GET /api/v1/schemes/{scheme_id}/audit

История событий схемы.

Сейчас в audit пишутся:

  • scheme.created_from_upload
  • scheme.published
  • scheme.unpublished
  • scheme.rolled_back
  • scheme.version.created
  • pricing.category.created
  • pricing.category.updated
  • pricing.category.deleted
  • pricing.rule.created
  • pricing.rule.updated
  • pricing.rule.deleted

Пример базового сценария

1. Загрузить SVG

curl -s -H 'X-API-Key: admin-local-dev-key'
-F 'file=@sample-contract.svg;type=image/svg+xml'
http://127.0.0.1:9020/api/v1/schemes/upload

2. Получить список схем

curl -s -H 'X-API-Key: admin-local-dev-key'
http://127.0.0.1:9020/api/v1/schemes

3. Создать pricing category

curl -s -X POST -H 'Content-Type: application/json' -H 'X-API-Key: admin-local-dev-key'
-d '{"name":"VIP","code":"VIP"}'
http://127.0.0.1:9020/api/v1/schemes/<scheme_id>/pricing/categories

4. Создать pricing rule

curl -s -X POST -H 'Content-Type: application/json' -H 'X-API-Key: admin-local-dev-key'
-d '{"pricing_category_id":"<category_id>","target_type":"sector","target_ref":"vip","amount":"2500.00","currency":"RUB"}'
http://127.0.0.1:9020/api/v1/schemes/<scheme_id>/pricing/rules

5. Проверить цену места

curl -s -H 'X-API-Key: admin-local-dev-key'
http://127.0.0.1:9020/api/v1/schemes/<scheme_id>/current/seats/seat-a1/price

6. Проверить test mode preview

curl -s -H 'X-API-Key: admin-local-dev-key'
http://127.0.0.1:9020/api/v1/schemes/<scheme_id>/test/seats/seat-a1


Ограничения текущего v1

  • редактирование seats/groups/sectors через API пока не реализовано
  • нет bulk update editor API
  • нет optimistic locking
  • нет soft delete policy
  • нет полноценной multi-user edit coordination
  • routes пока не разнесены по отдельным router-модулям
  • нет полного integration/e2e test набора
  • frontend отсутствует

Что делать дальше

Приоритет 1

Разработать frontend / test UI:

  • список схем
  • просмотр current seats/sectors/groups
  • test mode click preview
  • pricing management UI

Приоритет 2

Добавить editor operations:

  • update seat/group/sector in draft version
  • bulk updates
  • draft save flow

Приоритет 3

Усилить backend:

  • router decomposition
  • service-layer cleanup
  • pagination/filters for audit
  • integration tests
  • concurrency policy
  • publish validation rules
Description
No description provided
Readme 414 KiB
Languages
Python 79.4%
Shell 20.4%