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

Cookbook: добавить payment_route

Практическое руководство: как добавить новую запись в таблицу pm.payment_route для маршрутизации платежей по operation_type и сумме.

Важно. Речь идёт о маршрутах платёжного канала (channel), которые читает resolveChannel() в Intent-роутере. Это не HTTP-routes Fastify — те живут в src/*/routes.ts и регистрируются в Fastify-плагинах.

1. Контекст: что такое payment_route

pm.payment_route — таблица-справочник, по которой PM выбирает канал расчёта (MERCHANT_INVOICE, INTERNAL, IPPS, QP, ADMIN и т. п.) для конкретного intent в зависимости от:

  • operation_type — тип операции (INVOICE_PAYMENT, P2P, TOPUP, WITHDRAW, ...);
  • диапазона суммы [amount_min, amount_max] (в минорных единицах, bigint);
  • флага active.

Схема (src/shared/schema.ts, секция B3):

pm.payment_route(
  id            serial PK,
  operation_type varchar(50),
  amount_min     bigint  default 0,
  amount_max     bigint  default 9223372036854775807,
  channel        varchar(50),
  active         boolean default true,
  UNIQUE (operation_type, amount_min, amount_max)  -- payment_route_op_amount_uniq
)

Unique key: (operation_type, amount_min, amount_max) — нельзя завести два маршрута с одинаковыми типом операции и диапазоном суммы. Это ключ для ON CONFLICT ... DO NOTHING в миграциях.

2. Кто потребитель

Единственный потребитель — resolveChannel(operationType, amount, db) в src/intent/router.ts:

// src/intent/router.ts
const rows = await db
  .select({ channel: paymentRoute.channel })
  .from(paymentRoute)
  .where(and(
    eq(paymentRoute.operationType, operationType),
    lte(paymentRoute.amountMin, amount),
    gte(paymentRoute.amountMax, amount),
    eq(paymentRoute.active, true),
  ))
  .orderBy(desc(paymentRoute.amountMin))
  .limit(1)

Логика:

  • ищется активный маршрут, подходящий по типу операции и сумме;
  • при пересечении диапазонов побеждает тот, у которого amount_min выше (более узкий диапазон);
  • если подходящего маршрута нет — бросается NoRouteError и intent не создаётся.

3. Образец миграции (0009_invoice_payment_route.sql)

-- drizzle/migrations/0009_invoice_payment_route.sql
INSERT INTO pm.payment_route (operation_type, amount_min, amount_max, channel, active)
VALUES ('INVOICE_PAYMENT', 1, 100000000, 'MERCHANT_INVOICE', true)
ON CONFLICT (operation_type, amount_min, amount_max) DO NOTHING;

Берём этот файл как шаблон для своих маршрутов.

4. Пошаговый рецепт

  1. Сверь schema. Открой src/shared/schema.ts, убедись, что unique-индекс payment_route_op_amount_uniq всё ещё (operation_type, amount_min, amount_max).
  2. Создай миграцию drizzle/migrations/NNNN_<op>_<channel>_route.sql, где NNNN — следующий по порядку номер. Используй INSERT ... ON CONFLICT (operation_type, amount_min, amount_max) DO NOTHING, чтобы миграция была идемпотентна.
  3. Проверь, что канал реальный. Канал должен быть из числа поддерживаемых саг/воркеров: INTERNAL, MERCHANT_INVOICE, IPPS, QP, ADMIN (именно ADMIN, а не ADMIN_TRANSFER). Несуществующий канал приведёт к падению саги, а не resolveChannel.
  4. Учти диапазоны. Если суммы пересекаются с уже существующим маршрутом — побеждает amount_min повыше. Это позволяет наслаивать «нишевые» правила поверх «широких».
  5. Применить миграцию:
npx drizzle-kit migrate
  1. Проверить:
psql "$DATABASE_URL" -c "SELECT id, operation_type, amount_min, amount_max, channel, active
                         FROM pm.payment_route
                         WHERE operation_type='<NAME>'
                         ORDER BY amount_min;"
  1. Обнови CHANGELOG.md в projects/payment-manager/ — кратко: какой operation_type и channel добавлен и зачем.

5. Типовые ошибки

  • «Создаю Fastify-роут». Нет. payment_route ≠ HTTP-route. HTTP-эндпоинты регистрируются плагинами в src/*/routes.ts. Если задача — добавить новый HTTP-эндпоинт, эта инструкция тебе не подойдёт.
  • Канал ADMIN_TRANSFER вместо ADMIN. Корректное имя канала — ADMIN. Сверяйся со списком саг в docs/dev/modules/workers.md.
  • Перекрытие диапазонов без понимания приоритета. resolveChannel берёт строку с наибольшим amount_min — это «более специфичный» маршрут. Не полагайся на id или порядок вставки.
  • Ручное редактирование SQL мимо drizzle-kit migrate. Запрещено правилами PM (см. корневой CLAUDE.md).

6. Чек-лист перед merge

  • Файл миграции лежит в drizzle/migrations/ с верным префиксом NNNN_.
  • Использован ON CONFLICT (operation_type, amount_min, amount_max) DO NOTHING.
  • Канал реально существует в коде саг/роутера.
  • npx drizzle-kit migrate отработал локально без ошибок.
  • SQL-verify показал нужную строку.
  • CHANGELOG.md обновлён.

7. См. также