Перейти к содержанию

Seed и миграции

Операционный гайд по подготовке базы данных Payment Manager: применение миграций Drizzle, наполнение справочников через drizzle/seed.ts и порядок запуска при первом старте.


1. Обзор

PM хранит всё своё состояние в схеме pm.* PostgreSQL. Подготовка БД состоит из двух независимых шагов:

  1. Миграции — изменения схемы (DDL): таблицы, индексы, ограничения. Источник истины — файлы в drizzle/migrations/*.sql, генерируемые drizzle-kit из drizzle/schema.ts.
  2. 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

Миграции применяются строго по возрастанию имени (00000009 → …). drizzle-kit сам отслеживает применённые версии по таблице __drizzle_migrations в схеме drizzle — повторный запуск идемпотентен.

2.2. Команда применения

npm run db:migrate
# либо напрямую:
npx drizzle-kit migrate

Конфиг — drizzle.config.ts, читает DATABASE_URL из .env. Команда: - читает журнал применённых миграций, - последовательно применяет недостающие .sql-файлы, - останавливается при первой ошибке (нет автоматического rollback — это ответственность DBA).

2.3. Генерация новой миграции

После изменения drizzle/schema.ts:

npm run db:generate          # создаёт следующий 00NN_*.sql на основе diff'а 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. Команда запуска

npm run db:seed
# либо напрямую:
npx tsx drizzle/seed.ts

Требования к окружению: - 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_keyON CONFLICT (service_id) DO UPDATE SET permissions=…, active=true: permissions всегда приводятся к актуальным значениям из кода.
  • payment_route, fee_ruleON CONFLICT DO NOTHING: существующие строки не трогаются.
  • limit_ruleSELECT … 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.