Seed и миграции¶
Операционный гайд по подготовке базы данных Payment Manager: применение миграций Drizzle, наполнение справочников через drizzle/seed.ts и порядок запуска при первом старте.
1. Обзор¶
PM хранит всё своё состояние в схеме pm.* PostgreSQL. Подготовка БД состоит из двух независимых шагов:
- Миграции — изменения схемы (DDL): таблицы, индексы, ограничения. Источник истины — файлы в
drizzle/migrations/*.sql, генерируемыеdrizzle-kitизdrizzle/schema.ts. - Seed — наполнение справочников начальными данными: ключи сервисов, маршруты платежей, fee-правила, лимиты NFC. Источник —
drizzle/seed.ts.
Эти шаги не пересекаются: миграции никогда не вставляют данные, seed никогда не меняет схему. Любые DML-вставки начальных строк делаются только из seed.ts.
NO-GO: PM пишет только в
pm.*. Любое прямое SQL-вмешательство вpublic.*(схема Auth Center / Serverpod) запрещено.
2. Миграции¶
2.1. Расположение¶
drizzle/
├── schema.ts # источник истины — Drizzle ORM
├── migrations/
│ ├── 0000_init.sql
│ ├── 0001_perf-tuning.sql
│ ├── 0002_magenta_forgotten_one.sql
│ ├── 0003_fix_limit_rule_direction_char.sql
│ ├── 0004_fix_tx_history_direction_char.sql
│ ├── 0005_skinny_skreet.sql
│ ├── 0006_rapid_millenium_guard.sql
│ ├── 0007_intents_invoice_fields.sql
│ ├── 0008_auth_policies.sql
│ ├── 0009_invoice_payment_route.sql
│ └── meta/ # journal-файлы drizzle-kit
Миграции применяются строго по возрастанию имени (0000 → 0009 → …). drizzle-kit сам отслеживает применённые версии по таблице __drizzle_migrations в схеме drizzle — повторный запуск идемпотентен.
2.2. Команда применения¶
Конфиг — drizzle.config.ts, читает DATABASE_URL из .env. Команда:
- читает журнал применённых миграций,
- последовательно применяет недостающие .sql-файлы,
- останавливается при первой ошибке (нет автоматического rollback — это ответственность DBA).
2.3. Генерация новой миграции¶
После изменения drizzle/schema.ts:
После генерации обязательно прочитать получившийся .sql-файл и убедиться, что diff корректен; только после ревью — коммитить.
Подробное описание правил миграций, именования файлов и инвариантов схемы —
../reference/database/13-migrations.md.
3. Seed¶
3.1. Что и зачем¶
drizzle/seed.ts наполняет 4 типа справочных данных, без которых API не сможет принять ни одного POST /intents:
| Таблица | Назначение | Идемпотентность |
|---|---|---|
service_key |
Permissions HMAC-клиентов (без секретов — секреты только в SERVICE_SECRETS) |
ON CONFLICT (service_id) DO UPDATE |
payment_route |
Маппинг operationType + диапазон сумм → channel |
ON CONFLICT (operation_type, amount_min, amount_max) DO NOTHING |
fee_rule |
Выражения комиссий, считаемые rule-engine/ |
ON CONFLICT DO NOTHING |
limit_rule |
Лимиты на сумму (NFC per-tap, daily) | проверка существования по name перед INSERT |
Все секреты HMAC хранятся только в переменной окружения SERVICE_SECRETS (JSON-объект). Seed выполняет fail-fast проверку на старте: если для обязательных сервисов нет секретов, процесс завершается с кодом 1.
3.2. Service keys (5 обязательных + 1 опциональный)¶
service_id |
Назначение | allowedOperationTypes |
|---|---|---|
auth-center |
Основной клиент — все клиентские интенты от Flutter | P2P_TRANSFER, IPPS_WITHDRAWAL, THAI_QR_PAY, WITHDRAWAL, QP_TOPUP, MINIAPP_CHARGE, MINIAPP_CREDIT, INVOICE_PAYMENT |
auth-center-merchant |
Мерчантский поток (NFC pull-charge) | NFC_CHARGE |
nginx-gateway |
Read-only: только GET /accounts/balance и history |
[] |
admin-panel |
Бухгалтерские проводки | ADMIN_TRANSFER |
admin-tool |
CLI/скрипты с forceResolve=true |
[] |
exchange-webhook (опционально) |
Сидится только если SERVICE_SECRETS.exchange-webhook задан |
SERVICE_DEPOSIT |
Каждый ключ дополнен fromAccountOverride / toAccountOverride для жёсткого ограничения, на какие типы TB-аккаунтов разрешены входящие интенты (защита от чужих ключей в чужих каналах).
3.3. Payment routes (6 строк)¶
Маршруты вставляются тремя группами:
| Группа | operationType |
channel |
Диапазон (satang) |
|---|---|---|---|
| IPPS | IPPS_WITHDRAWAL |
IPPS_TRANSFER |
100 — 20 000 000 |
| IPPS | THAI_QR_PAY |
IPPS_TRANSFER |
100 — 20 000 000 |
| Service | SERVICE_DEPOSIT |
SERVICE_TRANSFER |
1 — 999 999 999 |
| Service | ADMIN_TRANSFER |
ADMIN |
1 — 999 999 999 |
| Service | NFC_CHARGE |
INTERNAL_P2P |
0 — BIGINT_MAX (ограничения через limit_rule) |
| Invoice | INVOICE_PAYMENT |
MERCHANT_INVOICE |
1 — 100 000 000 |
Уникальный индекс (operation_type, amount_min, amount_max) гарантирует, что повторный seed не задвоит строки.
3.4. Fee rules (3 правила)¶
| Имя | operationType |
timing |
Tags | Выражение (доля от amount) |
|---|---|---|---|---|
| P2P базовая комиссия 1.5% | P2P_TRANSFER |
PRE |
exclude: vip, fee_exempt |
1.5% → system.revenue.THB |
| P2P VIP — сниженная 0.5% | P2P_TRANSFER |
PRE |
include: vip, exclude: fee_exempt |
0.5% → system.revenue.THB |
| MINIAPP_CHARGE 2% | MINIAPP_CHARGE |
POST |
exclude: fee_exempt |
2% → system.revenue.THB |
Все суммы — в satang (1 THB = 100 satang). rule-engine/ выбирает самое приоритетное подходящее правило.
3.5. Limit rules (NFC, 2 правила)¶
| Имя | operationType |
channel |
direction |
window |
amountLimit (satang) |
|---|---|---|---|---|---|
nfc_per_tap |
NFC_CHARGE |
INTERNAL_P2P |
DEBIT |
PER_TX |
30 000 (300 THB) |
nfc_daily |
NFC_CHARGE |
INTERNAL_P2P |
DEBIT |
DAILY |
150 000 (1 500 THB) |
Downstream contract: оба правила —
direction='DEBIT'.handler.tsопределяетlimitDirectionчерезfrom.tb_account_map.userId === X-User-Id ? DEBIT : CREDIT. Auth Center обязан передавать userId покупателя в заголовкеX-User-Idдля NFC-интентов, иначе лимиты молча обойдутся.
limit_rule не имеет уникального индекса, поэтому seed проверяет существование по name перед INSERT — это и есть форма идемпотентности.
3.6. Команда запуска¶
Требования к окружению:
- DATABASE_URL — строка подключения к PostgreSQL с search_path=pm,public.
- SERVICE_SECRETS — JSON со всеми 5 обязательными ключами (auth-center, auth-center-merchant, nginx-gateway, admin-panel, admin-tool).
Скрипт ничего не пишет в public.*, не дёргает TigerBeetle, не открывает Redis-стримов — это чисто DDL-агностичная вставка справочников.
3.7. Идемпотентность¶
Запуск npm run db:seed несколько раз подряд безопасен:
service_key—ON CONFLICT (service_id) DO UPDATE SET permissions=…, active=true: permissions всегда приводятся к актуальным значениям из кода.payment_route,fee_rule—ON CONFLICT DO NOTHING: существующие строки не трогаются.limit_rule—SELECT … WHERE name=$1+ условныйINSERT: повторные запуски не плодят дубликаты.
Это позволяет безопасно перезапускать seed после каждого деплоя без выпуска отдельной миграции для изменения справочника.
4. Порядок запуска при первом старте¶
# 1. Подготовить .env (минимум: DATABASE_URL, SERVICE_SECRETS, TB_*, REDIS_URL)
cp .env.example .env
$EDITOR .env
# 2. Применить миграции (создаст схему pm.* и все таблицы)
npm run db:migrate
# 3. Наполнить справочники (service_key, payment_route, fee_rule, limit_rule)
npm run db:seed
# 4. Запустить сервис
npm run dev # для локальной разработки
# либо
npm run build && npm start # для production
После шага 3 PM готов принимать запросы. На шаге 4 он сам инициализирует пул соединений с PostgreSQL, проверит доступность TigerBeetle и Redis, поднимет HTTP-сервер.
Подробный сценарий локального запуска (включая запуск TigerBeetle, PostgreSQL и Redis в Docker) —
../cookbook/run-locally.md.
5. Сводка команд из package.json¶
| Команда | Что делает |
|---|---|
npm run db:generate |
Сгенерировать новый .sql-файл миграции из schema.ts |
npm run db:migrate |
Применить миграции (drizzle-kit migrate) |
npm run db:seed |
Запустить drizzle/seed.ts (через tsx) |
npm run dev |
Старт API в режиме hot-reload (tsx watch) |
npm run build |
Компиляция TypeScript в dist/ |
npm start |
Запуск собранной версии (node dist/server.js) |
npm test |
Прогон тестов (vitest run) |
6. Чек-лист перед prod-деплоем¶
-
npm run db:migrateвыполнен без ошибок; журнал__drizzle_migrationsсодержит все версии до0009включительно. -
SERVICE_SECRETSсодержит все 5 обязательных ключей (HMAC-секреты разной длины, минимум 32 байта). -
npm run db:seedотработал — в логах видны строки[seed] upserted service_key: …,[seed] payment_route seeded: …,[seed] inserted fee_rule: …,[seed] inserted limit_rule: …,[seed] done. - Повторный запуск
npm run db:seedзавершается без ошибок и не плодит дубликаты (sanity-check идемпотентности). -
psqlзапросSELECT service_id, active FROM pm.service_keyвозвращает все ожидаемые строки сactive=true.