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. Пошаговый рецепт¶
- Сверь schema. Открой
src/shared/schema.ts, убедись, что unique-индексpayment_route_op_amount_uniqвсё ещё(operation_type, amount_min, amount_max). - Создай миграцию
drizzle/migrations/NNNN_<op>_<channel>_route.sql, гдеNNNN— следующий по порядку номер. ИспользуйINSERT ... ON CONFLICT (operation_type, amount_min, amount_max) DO NOTHING, чтобы миграция была идемпотентна. - Проверь, что канал реальный. Канал должен быть из числа поддерживаемых саг/воркеров:
INTERNAL,MERCHANT_INVOICE,IPPS,QP,ADMIN(именноADMIN, а неADMIN_TRANSFER). Несуществующий канал приведёт к падению саги, а неresolveChannel. - Учти диапазоны. Если суммы пересекаются с уже существующим маршрутом — побеждает
amount_minповыше. Это позволяет наслаивать «нишевые» правила поверх «широких». - Применить миграцию:
- Проверить:
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;"
- Обнови
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. См. также¶
src/intent/router.ts—resolveChannel, единственный потребитель.src/shared/schema.ts— определениеpaymentRoute.drizzle/migrations/0009_invoice_payment_route.sql— образец.docs/dev/modules/workers.md— список саг и поддерживаемых каналов.