АБС «на минималках» — Видение и рамки проекта
Документ 0 из 6. Остальные разделы: Архитектура, Техстек, Модель данных, Дорожная карта, Миграция с Elcoin. Дата: 2026-07-19. Статус: утверждён план, идёт проектирование.
1. Зачем мы строим свою АБС
Сегодня проект MD работает поверх Elcoin ABS (SaaS):
- Свой Client Bank (React + Express/TypeScript) проксирует 3-step flow Elcoin
(
PREPARE → VALIDATE → PUTDB). - Данные зеркалируются в собственный PostgreSQL 18 (64 таблицы).
- Платёжная логика, GL, комиссии, холды, курсы — всё живёт внутри Elcoin, мы видим только API и зеркало.
Триггер изменений: получена банковская лицензия в одной из стран Африки. Под этой эгидой создаётся АБС для двух проектов (MD + AB). Зависимость от Elcoin становится стратегическим риском и статьёй расходов:
| Проблема с Elcoin | Последствие |
|---|---|
| Vendor lock-in: ядро, GL и правила — чёрный ящик | Не можем менять продуктовую логику, тарифы, каналы без вендора |
| Стоимость SaaS растёт с оборотом | Расходы масштабируются вместе с бизнесом |
| Нет контроля над данными и резидентностью | Регулятор африканской страны требует контроля данных |
| Один вендор на два разных регуляторных профиля | Elcoin заточен под EMI/UK-профиль, не под африканскую лицензию |
Вывод исследования рынка: покупка стороннего ядра (Mambu, Tuum, Thought Machine) — это те же деньги и тот же lock-in. Наш масштаб (сотни клиентов, тысячи проводок) позволяет построить минимальное ядро самим: современные cloud-native ядра — это по сути «правильный double-entry леджер + API + интеграции», и именно эту часть мы уже понимаем досконально по опыту интеграции с Elcoin.
2. Что такое «АБС на минималках»
Ядро из пяти неразделимых вещей, без которых банк не банк:
- Леджер — неизменяемый журнал двойной записи (проводки: дебет = кредит, всегда).
- План счетов (GL) — иерархия бухгалтерских счетов (245 GL импортируем из Elcoin).
- Клиентские счета — привязка клиент → счёт → валюта → GL, балансы как производные от проводок.
- Клиенты — карточка, статусы, группы, контакты, контрагенты (бенефициары).
- Операционный контур — холды, комиссии, валюты и курсы, статусная машина платежей.
Всё остальное — модули, подключаемые поэтапно (см. Дорожную карту).
3. Два тенанта
| MD | Африканский банк (AB) | |
|---|---|---|
| Регуляторный профиль | EMI-подобный (существующий бизнес) | Полная банковская лицензия |
| Валюты | EUR, USD, AED, GBP, USDT и др. | Локальная валюта + USD/EUR |
| Каналы | SEPA, SWIFT, UAEFTS через кор-банки (Clear Junction и др.) | SWIFT + локальные рейлы (уточняется) |
| Данные | Импорт всей истории из Elcoin | Чистый старт |
| Клиентская база | ~190 клиентов, ~220 счетов | С нуля |
| Приоритет | Миграция без потери истории и сервиса | Быстрый запуск на готовом ядре |
Решение: одна кодовая база, отдельная база данных на каждый банк (database-per-tenant). Общий control plane (управление тенантами, деплой, мониторинг). Подробнее — в разделе Архитектура.
4. Что входит в MVP (фазы 0–4, ~4–6 месяцев)
- Ядро: леджер, GL, счета, клиенты, валюты/курсы, холды, комиссии.
- Платежи: внутренние переводы, конвертации, исходящие через кор-банк.
- 3-step API (
prepare / validate / commit) — совместимый по духу с текущим Client Bank, чтобы фронт переехал с минимальными правками. - Импорт данных: полная история из PostgreSQL-зеркала Elcoin + текущие операции через выписки кор-банков (camt.053 / MT940).
- Реконсиляция ностро-счетов (сверка нашего леджера с выписками кор-банков).
- Банк-клиент: существующий React-фронт, переключённый на своё API.
- Админ-панель оператора: клиенты, счета, ручные проводки, авторизация платежей (maker-checker).
5. Что НЕ входит в MVP (отложено, фаза 5+)
| Отложено | Как закрываем потребность до тех пор |
|---|---|
| Карточный процессинг | Текущая схема Intercash + card-abs-sync переносится на новое ядро на фазе 5 |
| Кредиты, депозиты как продукты | Не нужны на старте ни одному из проектов |
| Собственный AML/sanctions-скрининг | Интеграция с внешним провайдером (SumSub / ComplyAdvantage), в MVP — blocklist + ручная авторизация |
| Прямое членство в SEPA/SWIFT | Работаем через кор-банки, как сейчас |
| Отчётность регулятору (авто) | Ручная выгрузка из леджера SQL-отчётами; автоматизация — фаза 5 по требованиям конкретного регулятора |
| Мобильные приложения | Web-first |
6. Принципы (не подлежат пересмотру в ходе реализации)
- Леджер неизменяем. Проводка не редактируется и не удаляется — только сторнируется новой проводкой. Баланс никогда не хранится как «истина» — он выводим из проводок (кэшируется, но всегда пересчитываем).
- Дебет = кредит в каждой транзакции. Проверяется на уровне БД (constraint/триггер), а не только в коде.
- Деньги — не float.
NUMERICв БД, строки/decimal в коде и API. - Идемпотентность всех записывающих операций. Повторный запрос с тем же idempotency key не создаёт дубль.
- Аудит всего. Каждое действие оператора и API-клиента — в append-only аудит-лог.
- Maker-checker. Операции выше порога требуют второй авторизации оператора.
- Совместимость важнее красоты. 3-step flow и знакомые поля
(
debtor_account,creditor_account_iban,fee_amount_num…) сохраняем, чтобы фронт и накопленные знания команды переиспользовались.
7. Критерии успеха проекта
- Client Bank MD работает на своей АБС, Elcoin отключён; клиенты не заметили миграции.
- Балансы всех счетов после миграции сходятся с Elcoin и с выписками кор-банков до цента.
- Африканский банк запущен как второй тенант без изменений в ядре.
- Ежемесячные расходы на инфраструктуру существенно ниже подписки Elcoin.
- Новый платёжный канал (новый кор-банк / рейл) подключается силами команды без вендора.
8. Ключевые риски (детально — в Дорожной карте)
- Точность миграции — главный риск. Смягчение: параллельный прогон (shadow ledger), ежедневная сверка, откат на Elcoin до момента cutover.
- Неполнота зеркала Elcoin (таблицы, упёршиеся в pageSize=1000). Смягчение: допаганация экспорта до cutover, сверка контрольных сумм.
- Регуляторные требования африканской страны ещё не формализованы. Смягчение: ядро проектируем переносимым (Docker), отчётность — отдельный модуль фазы 5.
- Малая команда (1–3 разработчика). Смягчение: модульный монолит вместо микросервисов, скучные проверенные технологии, минимум движущихся частей.
АБС — Архитектура системы
Документ 1 из 6. Контекст и цели — в разделе Видение и рамки.
1. Стиль архитектуры: модульный монолит
Микросервисы для команды из 1–3 разработчиков — самоубийство: сетевые границы, распределённые транзакции, зоопарк деплоев. Мы строим модульный монолит на NestJS:
- Один деплоймент, одна база данных на тенант, одна транзакция БД на бизнес-операцию.
- Жёсткие границы модулей внутри процесса: модуль общается с другим только через его публичный сервис-интерфейс, никогда — через чужие таблицы напрямую.
- Если когда-нибудь понадобится выделить модуль в сервис (например, Statement Import) — граница уже готова.
2. Контекстная диаграмма
3. Модули и их ответственность
| Модуль | Ответственность | Зависит от |
|---|---|---|
ledger |
Журнал двойной записи, GL, балансы, сторно, закрытие дня | — (ядро, ни от кого) |
clients |
Карточки клиентов, контакты, адреса, группы, статусы, контрагенты | — |
accounts |
Клиентские счета, IBAN-генерация, статусы, схемы, привязка к GL | ledger, clients |
holds |
Блокировки средств: создание, снятие, истечение | ledger, accounts |
fx |
Валюты, валютные пары, курсы (загрузка + история), расчёт конвертаций | — |
fees |
Тарифные планы, подписки клиентов, расчёт комиссии по событию | clients |
payments |
Оркестрация платежей: internal / outward / inward / conversion; статусная машина; 3-step flow | ledger, accounts, holds, fx, fees |
corr-gateway |
Отправка исходящих в кор-банки (Clear Junction API и др.), маппинг форматов | payments |
statements |
Импорт выписок (camt.053, MT940, CSV), матчинг с ожидаемыми операциями, создание inward-платежей | payments, ledger |
recon |
Реконсиляция ностро: наш леджер vs выписка кор-банка, отчёт о расхождениях | ledger, statements |
auth |
Пользователи (клиенты и операторы), JWT, роли, 2FA | — |
audit |
Append-only лог всех действий | — (подписан на все) |
notifications |
Вебхуки наружу, email/SMS клиентам, шаблоны | — |
tenancy |
Control plane: реестр тенантов, роутинг подключений к БД | — |
import-elcoin |
Одноразовый модуль миграции из зеркала Elcoin (см. Миграция с Elcoin) | все |
4. Ядро: леджер двойной записи
Самая важная часть системы. Три таблицы (детали схемы — в разделе Модель данных):
Правила:
journal_entry— атомарная транзакция: 2+ postings,SUM(debit) = SUM(credit)по каждой валюте. Проверяется constraint-триггером в PostgreSQL приCOMMIT.- Postings append-only: ни
UPDATE, ниDELETE(запрещены грантами БД). Исправление — сторнирующая проводка со ссылкой на оригинал. - Каждый posting ссылается либо на клиентский счёт, либо на GL-счёт. Клиентский счёт всегда привязан к GL (пассивы банка), поэтому баланс банка сходится на любом уровне.
- Баланс =
SUM(credits) - SUM(debits)(для пассивных счетов). Кэш вaccount_balancesобновляется в той же транзакции; ночной job пересчитывает и сверяет кэш с сырыми postings. - Available balance = ledger balance − активные холды.
balance_date(операционный день) на каждой проводке; закрытие дня фиксируетend-of-dayснапшоты для отчётности.
5. Платёжный поток: 3-step flow сохраняем
Совместимость с текущим Client Bank — осознанное решение (см. принцип 7 в разделе Видение и рамки). API повторяет семантику Elcoin:
POST /api/v1/payments/{model}/prepare → дефолты: GL, канал, комиссия, имя отправителя
POST /api/v1/payments/{model}/validate → бизнес-правила: баланс, лимиты, IBAN, blocklist
POST /api/v1/payments/{model}/commit → создание платежа + hold, необратимо (бывш. putDb)
где model ∈ internal | outward | conversion (+ inward создаётся импортом выписок).
Статусная машина (совпадает с текущей, к которой привык фронт и операторы):
Проводки по этапам (пример outward 1000 EUR + fee 25):
| Этап | Действие в леджере |
|---|---|
commit |
hold 1025 EUR на счёте клиента (не проводка — запись в holds) |
ACCEPTED → PROCESSING |
Dr клиент 1025 / Cr транзитный GL 1000 / Cr доход 25; hold снят |
COMPLETED (по выписке) |
Dr транзитный GL 1000 / Cr ностро GL 1000 |
RETURNED |
сторно + Dr ностро / Cr счёт клиента (за минусом комиссии возврата) |
Транзитный GL между «отправили» и «увидели в выписке» — то, что делает реконсиляцию ностро тривиальной: остаток на транзитном счёте = платежи в пути.
6. Асинхронная обработка
Всё, что не обязано случиться синхронно, идёт через transactional outbox:
- Бизнес-транзакция пишет событие (
payment.accepted,statement.line.matched…) в таблицуoutbox_eventsв той же транзакции БД. - Диспетчер читает outbox и кладёт задачи в BullMQ (Redis): отправка в кор-банк, вебхуки, email, пересчёт кэшей.
- Обработчики идемпотентны; ретраи с backoff; DLQ для разбора вручную.
Так мы не теряем события при падении процесса и не тащим Kafka в проект на 3 человека.
7. Мульти-тенантность
Database-per-tenant. Один инстанс приложения (или по инстансу на регион), у каждого банка своя PostgreSQL-база:
Почему не schema-per-tenant и не row-level:
- изоляция данных абсолютная (требование резидентности: базы физически в разных регионах);
- бэкап/рестор и миграция независимы;
- невозможно случайно смешать данные банков одним
WHERE, забытым в запросе.
Цена — прогон Prisma-миграций по N баз; при N=2 это не цена.
8. Безопасность
- API клиентов: JWT (короткоживущий access + refresh), 2FA на логин и подтверждение платежей, rate limiting, idempotency keys на все POST.
- API операторов: отдельный issuer, роли (viewer / operator / authorizer / admin), maker-checker на платежи выше порога и на ручные проводки — всегда.
- Секреты: переменные окружения из секрет-менеджера облака, никаких секретов в репо.
- БД: приложение работает под ролью без прав
UPDATE/DELETEнаpostingsиaudit_log; TLS до БД; шифрование дисков. - Аудит: каждый запрос, изменивший состояние — запись (кто, что, когда, до/после,
IP, tenant) в append-only
audit_log.
9. Наблюдаемость и эксплуатация
- Структурированные логи (pino) с
tenant_id,request_id,payment_uid. - Метрики Prometheus: платежи по статусам, лаг очередей, расхождения реконсиляции.
- Алерты: расхождение в реконсиляции ≠ 0, DLQ не пустая, кэш балансов разошёлся с postings.
- Бэкапы: WAL-архивирование + ежедневный полный дамп, регулярная проверка восстановления.
- RTO ≤ 4 часа, RPO ≤ 5 минут (WAL) для MVP.
10. Что мы сознательно НЕ делаем
| Соблазн | Почему нет |
|---|---|
| Микросервисы | Команда 1–3 человека; границы модулей дают ту же эволюцию без распределённых транзакций |
| Kafka / event sourcing всего | Outbox + BullMQ покрывает наши объёмы (тысячи платежей/день) на порядки дешевле |
| Свой AML-скоринг | Blocklist + внешний провайдер + ручная авторизация; своё — никогда не пройдёт аудит |
| Балансы как поле «истины» | Источник истины — postings; поле — только кэш |
| GraphQL | REST + OpenAPI: проще, фронт уже написан под REST |
АБС — Технологический стек
Документ 2 из 6. Общее устройство системы — в разделе Архитектура. Главный критерий выбора: команда 1–3 разработчика уже пишет на TypeScript (Express + React + PostgreSQL), стек должен быть скучным, проверенным и единым.
1. Сводная таблица
| Слой | Выбор | Альтернатива, которую отклонили | Почему |
|---|---|---|---|
| Язык | TypeScript (strict) везде: ядро, воркеры, фронт | Java/Kotlin, Go | Компетенция команды; один язык = один разработчик может трогать всё |
| Backend-фреймворк | NestJS 11 | Express (текущий), Fastify голый | Модули NestJS = границы модулей монолита; DI, guards, встроенная OpenAPI |
| СУБД | PostgreSQL 18 | MySQL, CockroachDB | Уже используется (зеркало Elcoin); NUMERIC, транзакции, триггеры, partitioning |
| ORM / миграции | Prisma для схемы, миграций и CRUD-модулей | TypeORM, Knex | Декларативная схема + генерация типов; конвенции проекта уже под Prisma |
| Леджер-запросы | Сырой SQL ($queryRaw / kysely для сложных отчётов) |
Всё через ORM | Postings, балансы, реконсиляция — это SQL с оконными функциями и FOR UPDATE; ORM тут мешает |
| Очереди | BullMQ + Redis 7 | Kafka, RabbitMQ, SQS | Достаточно для тысяч задач/день; ретраи, DLQ, cron из коробки; знакомый JS-инструмент |
| Кэш/сессии | тот же Redis | — | Одна зависимость на две роли |
| Фронт (Client Bank) | React 18 + Vite + TailwindCSS + React Query — существующий | Переписывание | Фронт уже написан; меняется только API-слой (elcoin-api.ts → abs-api.ts) |
| Фронт (Admin) | React + Vite + shadcn/ui | Retool/низкокод | Админка — конкурентное преимущество (operations tooling), низкокод упрётся в maker-checker |
| API-контракт | REST + OpenAPI 3 (генерация из NestJS-декораторов) | GraphQL, gRPC | Фронт под REST; OpenAPI = автоклиенты и документация |
| Валидация | zod (единые схемы фронт/бек) | class-validator | Схемы шарятся с фронтом через общий пакет |
| Деньги | NUMERIC(20,4) в БД; строки в API; decimal.js в расчётах |
number/float | Копейка не теряется. float запрещён линтером в модулях денег |
| Парсинг выписок | camt.053 (ISO 20022 XML) — fast-xml-parser; MT940 — свой парсер (формат простой); CSV — papaparse | Покупной конвертер | Форматы стабильны, парсеры пишутся и тестируются за дни |
| Тесты | Vitest + testcontainers (PostgreSQL) для леджера; property-based тесты баланса | Только юниты с моками | Леджер тестируем ТОЛЬКО на реальном PostgreSQL — триггеры и constraint'ы часть логики |
| Контейнеризация | Docker + docker-compose (dev), образы OCI (prod) | — | Требование переносимости между облаками/регионами |
| Оркестрация prod | Старт: docker-compose / systemd на 1–2 VM на регион; рост: managed K8s | Сразу K8s | Два тенанта и небольшой RPS не оправдывают K8s с первого дня |
| Облако | Регион ЕС для MD; регион/ЦОД страны лицензии для AB (уточняется у регулятора) | Один регион на всё | Требование резидентности данных |
| CI/CD | GitHub Actions: lint → test → build → push image → deploy | — | Репо уже на GitHub |
| Логи | pino (JSON) → Loki или облачный сток | — | Структурированные, с tenant_id/request_id |
| Метрики/алерты | Prometheus + Grafana (или managed-аналог) | — | Стандарт де-факто |
| Ошибки | Sentry | — | Уже в инструментарии команды |
| Секреты | Секрет-менеджер облака / Doppler | .env в репо | Банковская система |
2. Структура репозитория (монорепо)
abs/
├── apps/
│ ├── core/ # NestJS: API + модули (ledger, payments, ...)
│ ├── worker/ # BullMQ-воркеры (тот же код, другой entrypoint)
│ ├── client-bank/ # React фронт клиента (переносится из MD)
│ └── admin/ # React админ-панель оператора
├── packages/
│ ├── contracts/ # zod-схемы + типы API (шарятся фронт/бек)
│ ├── decimal/ # обёртка денежной арифметики
│ └── statements/ # парсеры camt.053 / MT940 / CSV (чистые функции)
├── prisma/
│ ├── schema.prisma # схема тенант-базы
│ └── migrations/
├── tools/
│ └── import-elcoin/ # одноразовый мигратор (см. раздел про миграцию)
├── docker-compose.yml
└── .github/workflows/
pnpm workspaces + turborepo (опционально). Один pnpm test гоняет всё.
3. Ключевые технические решения
3.1 Деньги и точность
- БД:
NUMERIC(20, 4)для сумм,NUMERIC(20, 10)для курсов. - API: суммы — строки (
"1030.00"), как делает Elcoin. JSON number запрещён. - Код:
decimal.js; ESLint-правило запрещает арифметические операторы над типомMoneyвне пакетаpackages/decimal. - Округление: banker's rounding по умолчанию, per-currency
decimal_pointsиз справочника валют (у Elcoin есть крипта с 8 знаками — берёмNUMERIC(30,10)где нужно).
3.2 Целостность леджера на уровне БД
-- Роль приложения не может изменять историю
REVOKE UPDATE, DELETE ON postings, journal_entries, audit_log FROM abs_app;
-- Constraint-триггер: транзакция сбалансирована по каждой валюте
CREATE CONSTRAINT TRIGGER trg_entry_balanced
AFTER INSERT ON postings
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW EXECUTE FUNCTION check_entry_balanced();
DEFERRABLE INITIALLY DEFERRED — проверка в момент COMMIT, когда все postings
транзакции уже вставлены.
3.3 Идемпотентность
Таблица idempotency_keys (key, endpoint, request_hash, response, created_at).
Все POST принимают Idempotency-Key; повтор с тем же ключом возвращает сохранённый
ответ. Для commit платежа ключ обязателен.
3.4 Конкурентность балансов
Списание: SELECT ... FOR UPDATE строки счёта в account_balances внутри
транзакции → проверка available (баланс − холды) → insert postings → update кэша.
Никаких «проверили баланс, потом записали» без блокировки.
3.5 Почему не готовые леджер-библиотеки (TigerBeetle, Formance)
Рассматривались. TigerBeetle — отдельная СУБД со своей моделью эксплуатации; Formance — ещё один вендор (от вендора мы и уходим). Наши объёмы (десятки тысяч проводок в месяц) PostgreSQL обрабатывает с огромным запасом, а весь стек остаётся одной технологией. Пересмотреть при >100 платежей/сек.
4. Версии и фиксация
| Компонент | Версия на старте |
|---|---|
| Node.js | 22 LTS |
| TypeScript | 5.x strict |
| NestJS | 11 |
| PostgreSQL | 18 |
| Prisma | 7 |
| Redis | 7 |
| React | 18 (как в текущем Client Bank) |
Renovate/Dependabot для обновлений; мажоры — только осознанно.
АБС — Модель данных ядра
Документ 3 из 6. Основан на анализе 64 экспортированных таблиц Elcoin и реверс-инжиниринге платёжных схем. Схема — одна на тенант-базу (database-per-tenant, см. раздел Архитектура).
1. Обзор: ER-диаграмма ядра
2. Блоки модели
2.1 Леджер (сердце системы)
gl_accounts — план счетов
| Поле | Тип | Комментарий |
|---|---|---|
id |
int PK | |
glNumber |
varchar unique | Номер GL (импортируем все 245 из gl_gl Elcoin) |
name |
text | |
glType |
enum | ASSET / LIABILITY / INCOME / EXPENSE / EQUITY / OFF_BALANCE |
parentId |
int FK self | Иерархия плана счетов |
currency |
char(3) nullable | null = мультивалютный узел |
isPostable |
boolean | Проводки только в листья иерархии |
journal_entries — транзакции (заголовки проводок)
| Поле | Тип | Комментарий |
|---|---|---|
id |
bigint PK | |
uid |
uuid unique | Внешний идентификатор |
documentReference |
varchar unique | Человекочитаемый номер документа |
entryType |
enum | PAYMENT / FEE / FX / MANUAL / REVERSAL / MIGRATION / EOD |
paymentOrderId |
FK nullable | Ссылка на бизнес-документ |
reversalOfId |
FK self nullable | Для сторно — ссылка на оригинал |
balanceDate |
date | Операционный день |
description |
text | |
createdBy |
FK users | Оператор или system |
authorizedBy |
FK users nullable | Maker-checker |
Аналог Elcoin: transactionEntryHeader / transactionViewHeader (27 колонок → сжимаем до нужного).
postings — строки проводок (append-only)
| Поле | Тип | Комментарий |
|---|---|---|
id |
bigint PK | |
journalEntryId |
FK | |
accountId |
FK nullable | Клиентский счёт (ровно одно из двух полей заполнено) |
glAccountId |
FK nullable | GL-счёт |
side |
enum | DEBIT / CREDIT |
amount |
numeric(30,10) | Всегда > 0; знак определяется side |
currency |
char(3) | |
balanceDate |
date | Денормализовано для отчётов |
Инварианты (constraint-триггеры БД):
SUM(debit) = SUM(credit)по каждой валюте внутриjournalEntryIdна момент COMMIT;UPDATE/DELETEзапрещены грантами;- ровно один из
accountId/glAccountIdне null.
Аналог Elcoin: transactionViewLine (37 колонок; их debet_num/credit_num в одной строке
разносим на две postings-строки — классическая нормализация двойной записи).
account_balances — кэш балансов
| Поле | Тип | Комментарий |
|---|---|---|
accountId |
FK PK | |
ledgerBalance |
numeric | = SUM postings |
holdAmount |
numeric | = SUM активных holds |
availableBalance |
numeric | ledger − holds (генерируемая колонка) |
lastPostingId |
bigint | До какого posting кэш актуален |
Обновляется в транзакции проводки (SELECT FOR UPDATE); ночной job пересчитывает с нуля и
алертит при расхождении.
eod_balances — снапшоты на конец дня
(accountId | glAccountId, balanceDate, balance) — для отчётности и быстрых выписок.
2.2 Клиенты
| Таблица | Источник в Elcoin | Ключевые поля |
|---|---|---|
clients |
client_client (36 колонок) |
clientCode (MDxxxxxxx, сохраняем формат), entityType (PERSON/BUSINESS), status, group, residenceCountry, riskGroup, KYC-статус |
contacts |
contact_contact |
clientId, type (EMAIL/PHONE/...), value |
postal_addresses |
clientPostalAddress |
clientId, addressType, страна/город/улица/индекс |
client_attributes |
additionalInformation |
key-value KYC-метаданные, clientId, name, value |
counterparties |
clientCounterparty (32 кол.) |
clientId, fullName, iban, account, bankBic, country, entityType, blocklistId |
client_messages |
clientMessage |
уведомления; в MVP — упрощённо через notifications |
2.3 Счета
accounts — клиентские счета
| Поле | Тип | Комментарий |
|---|---|---|
id |
int PK | |
accountNumber |
varchar unique | Формат Elcoin 6100005V1A3USD3 сохраняем для мигрированных; новые — своя схема |
iban |
varchar unique nullable | Публичный идентификатор |
clientId |
FK | |
currency |
char(3) FK | |
glAccountId |
FK | GL-привязка (пассивы «средства клиентов») |
scheme |
enum | IBAN / BBAN / CARD / SUBLEDGER... (из enum_account_scheme) |
subtype |
enum | CURRENT / SAVINGS / TRANSIT |
status |
enum | ACTIVE / BLOCKED / CLOSED... (из enum_account_status) |
isDirectPaymentAllowed |
boolean | Аналог DFB-ограничения Elcoin |
masterAccountId |
FK self nullable | Для сабледжеров |
Источник: remoteAccount_remoteAccount (43 колонки). Балансовые поля
(available_amount, ledger_balance) НЕ переносятся как данные — пересчитываются
из мигрированных проводок и сверяются (см. раздел Миграция с Elcoin).
nostro_accounts — наши счета в кор-банках
| Поле | Комментарий |
|---|---|
corrBankCode |
CLEAR_JUNCTION, ... |
iban / accountNumber, currency, bic |
|
glAccountId |
GL «ностро» (актив) |
transitGlAccountId |
Транзитный GL «в пути» для этого ностро |
channels |
Какие рейлы ходят через этот счёт (SEPA/SWIFT/UAEFTS) |
2.4 Холды
holds
id, accountId, amount, currency, holdType (PAYMENT/COURT/AML/MANUAL), status (ACTIVE/RELEASED/EXPIRED/CONSUMED), paymentOrderId nullable, expiresAt, reason
Источник: holdInstruction_holdInstruction (37 колонок, 21 тип из enum — оставляем
востребованные, остальные заводим по мере надобности).
2.5 Платежи
Elcoin держит 3 таблицы по ~100 колонок (paymentInward/Internal/Convertation).
Мы сводим в одну payment_orders + JSONB для канальных полей:
payment_orders
| Поле | Тип | Комментарий |
|---|---|---|
id |
bigint PK | |
uid |
uuid unique | |
documentReference |
varchar unique | |
orderType |
enum | INTERNAL / OUTWARD / INWARD / CONVERSION |
channel |
enum nullable | SEPA / SWIFT / UAEFTS / LOCAL_XX (из enum_payment_outgoing_chanel) |
status |
enum | NEW / PENDING / ACCEPTED / PROCESSING / COMPLETED / REJECTED / CANCELLED / RETURNED |
debtorAccountId |
FK nullable | |
creditorAccountId |
FK nullable | Для internal/inward |
counterpartyId |
FK nullable | Сохранённый бенефициар |
amount, currency |
||
feeAmount, feeCurrency, externalFeeAmount |
Формула Elcoin: total_debit = amount + fee + external_fee |
|
rate, counterAmount, counterCurrency, fixedSide |
Для CONVERSION | |
creditorDetails |
JSONB | name, iban, bic, agent country, address — канальные поля без 100 колонок |
valueDate, balanceDate |
date | |
idempotencyKey |
varchar unique nullable | |
holdId |
FK nullable | Hold, созданный на commit |
statementLineId |
FK nullable | Матч с выпиской (для OUTWARD-подтверждения и INWARD-происхождения) |
payment_status_history
paymentOrderId, fromStatus, toStatus, changedBy, reason, createdAt — полный след статусов.
2.6 Валюты и курсы
| Таблица | Источник Elcoin | Поля |
|---|---|---|
currencies |
currency_currency (28 строк) |
code ISO 4217 + крипто-коды (USDT_TRC20), type (FIAT/CRYPTO), decimalPoints, status |
currency_pairs |
currencyPairs (60 строк) |
baseCurrency, quoteCurrency, isActive |
fx_rates |
currencyRate |
pairId, bid, ask, mid, spread, source, validFrom — история хранится вечно |
2.7 Комиссии и тарифы
| Таблица | Источник Elcoin | Комментарий |
|---|---|---|
subscriptions |
subscription_subscription (19 тарифов) |
Тарифные планы |
client_subscriptions |
clientSubscription |
Привязка клиент → тариф, даты действия |
fee_rules |
нет прямого (логика была внутри Elcoin) | subscriptionId, eventCode (49 событий из enum_fee_event_code: перевод SEPA, открытие счёта...), channel, currency, тип (FIXED/PERCENT/MIN_MAX), revenueGlAccountId |
Расчёт комиссии = функция (client, eventCode, channel, amount, currency) → fee,
детерминированная и покрытая тестами.
2.8 Справочники и инфраструктура
| Таблица | Источник Elcoin |
|---|---|
countries |
country_country (250) |
bank_codes |
bankcode_bankcode — BIC-справочник кор-банков |
sepa_participants |
sepaParticipants (допаганировать: >1000 строк) |
blocklist |
blocklist_blocklist (478) |
products |
product_product (13) — типы счетов/продуктов с GL-привязками |
enums |
27 enum-таблиц Elcoin → нативные enum PostgreSQL + таблица enum_labels для локализации |
users |
user_user — операторы; клиентские логины — отдельно в auth_identities |
system_settings |
systemSettings — key-value конфиг тенанта |
audit_log |
нет (новое) — append-only: actor, action, entity, before/after JSONB, ip |
idempotency_keys |
нет (новое) |
outbox_events |
нет (новое) — transactional outbox |
webhooks, webhook_deliveries |
remoteWebhook |
2.9 Выписки и реконсиляция
| Таблица | Поля |
|---|---|
statement_files |
nostroAccountId, format (CAMT053/MT940/CSV), fileName, sha256 unique (дедуп), periodFrom/To, openingBalance, closingBalance, status (RECEIVED/PARSED/RECONCILED/FAILED) |
statement_lines |
fileId, valueDate, amount, currency, direction (CREDIT/DEBIT), counterpartyName/iban/bic, endToEndId, reference, rawData JSONB, matchStatus (UNMATCHED/MATCHED/MANUAL/IGNORED), paymentOrderId nullable |
recon_runs |
nostroAccountId, balanceDate, ledgerBalance, statementBalance, difference, status, отчёт о нематченных строках |
3. Prisma-схема ядра леджера (выдержка)
model GlAccount {
id Int @id @default(autoincrement())
glNumber String @unique
name String
glType GlType
currency String? @db.Char(3)
isPostable Boolean @default(true)
parentId Int?
parent GlAccount? @relation("GlHierarchy", fields: [parentId], references: [id])
children GlAccount[] @relation("GlHierarchy")
postings Posting[]
accounts Account[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([glType])
@@map("gl_accounts")
}
model JournalEntry {
id BigInt @id @default(autoincrement())
uid String @unique @default(uuid()) @db.Uuid
documentReference String @unique
entryType EntryType
balanceDate DateTime @db.Date
description String?
paymentOrderId BigInt?
paymentOrder PaymentOrder? @relation(fields: [paymentOrderId], references: [id])
reversalOfId BigInt?
reversalOf JournalEntry? @relation("Reversal", fields: [reversalOfId], references: [id])
reversals JournalEntry[] @relation("Reversal")
postings Posting[]
createdById Int
createdBy User @relation("EntryCreator", fields: [createdById], references: [id])
authorizedById Int?
authorizedBy User? @relation("EntryAuthorizer", fields: [authorizedById], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([balanceDate])
@@index([paymentOrderId])
@@map("journal_entries")
}
model Posting {
id BigInt @id @default(autoincrement())
journalEntryId BigInt
journalEntry JournalEntry @relation(fields: [journalEntryId], references: [id])
accountId Int?
account Account? @relation(fields: [accountId], references: [id])
glAccountId Int?
glAccount GlAccount? @relation(fields: [glAccountId], references: [id])
side PostingSide
amount Decimal @db.Decimal(30, 10)
currency String @db.Char(3)
balanceDate DateTime @db.Date
createdAt DateTime @default(now())
@@index([accountId, balanceDate])
@@index([glAccountId, balanceDate])
@@index([journalEntryId])
@@map("postings")
}
enum GlType {
ASSET
LIABILITY
INCOME
EXPENSE
EQUITY
OFF_BALANCE
}
enum PostingSide {
DEBIT
CREDIT
}
enum EntryType {
PAYMENT
FEE
FX
MANUAL
REVERSAL
MIGRATION
EOD
}
Полная схема (~30 моделей) пишется на фазе 0 по этому документу.
Инварианты, которые Prisma выразить не может (сбалансированность транзакции,
запрет UPDATE/DELETE, accountId XOR glAccountId), добавляются raw-SQL миграцией
поверх сгенерированной (prisma migrate dev --create-only → дописать SQL).
4. Сводный маппинг Elcoin → АБС (основа импорта)
| Elcoin (зеркало PG) | Новая таблица | Трансформация |
|---|---|---|
client_client |
clients |
Прямой маппинг, client_code сохраняем |
contact_contact |
contacts |
Прямой |
clientPostalAddress_* |
postal_addresses |
Прямой |
additionalInformation_* |
client_attributes |
key-value как есть |
clientCounterparty_* |
counterparties |
Прямой + связка с blocklist |
clientSubscription_*, subscription_* |
client_subscriptions, subscriptions |
Прямой; fee_rules — заполняем вручную по действующим тарифам (в Elcoin логика скрыта) |
gl_gl |
gl_accounts |
245 строк; gl_type → наш enum, построить иерархию по level |
remoteAccount_* |
accounts |
Реквизиты — да; балансы — НЕТ (пересчёт из проводок) |
transactionViewHeader_* |
journal_entries |
entryType = MIGRATION, оригинальный header_id в migrationSourceId |
transactionViewLine_* |
postings |
Строка с debet_num → posting DEBIT; с credit_num → posting CREDIT |
paymentInward/Internal/Convertation |
payment_orders |
100+ колонок → ядро полей + creditorDetails JSONB |
holdInstruction_* |
holds |
Только активные на момент cutover |
currency_*, currencyPairs_*, currencyRate_* |
currencies, currency_pairs, fx_rates |
Прямой |
country_country, bankcode_*, sepaParticipants_*, blocklist_*, product_* |
одноимённые справочники | Прямой (sepaParticipants — допаганировать) |
enum_* (27 таблиц) |
enum PostgreSQL + enum_labels |
Коды — в enum, локализация — в таблицу |
user_user |
users |
Операторы; пароли переустанавливаются |
remoteWebhook_* |
webhooks |
Конфигурация endpoint'ов |
remoteClient/remoteCounterparty/remoteProcessing |
— | Не переносим: это внутренняя кухня remote-API Elcoin |
accountStatisticAmtReport, remoteStatement, remittance, clientMessage, application, notificationTemplate, clientExternalRegistration |
архив | Кладём в схему elcoin_archive тенант-базы как есть, для истории и разборов |
Детальная процедура импорта и сверки — в разделе Миграция с Elcoin.
АБС — Дорожная карта
Документ 4 из 6. Сроки рассчитаны на команду 1–3 разработчика с активным использованием AI-разработки. Горизонт MVP: 4–6 месяцев (фазы 0–4). Каждая фаза заканчивается работающим, демонстрируемым результатом.
1. Диаграмма Ганта
2. Фазы подробно
Фаза 0 — Фундамент (2–3 недели)
Строим «рельсы», по которым поедет всё остальное.
| # | Задача |
|---|---|
| 0.1 | Монорепо (pnpm workspaces): apps/core, apps/worker, packages/* |
| 0.2 | NestJS-скелет, конфигурация, healthcheck, OpenAPI |
| 0.3 | Prisma-схема v1 по разделу Модель данных + raw-SQL инварианты леджера |
| 0.4 | Tenancy: control plane, резолв тенанта, пул подключений per-tenant |
| 0.5 | Auth: JWT (клиенты и операторы), роли, guards |
| 0.6 | audit_log, idempotency_keys, outbox_events + диспетчер BullMQ |
| 0.7 | CI (GitHub Actions): lint, test (testcontainers), build, образы |
| 0.8 | docker-compose dev-окружения; стейджинг в облаке |
Definition of Done: пустая АБС деплоится в облако одной командой; тест на constraint «дебет = кредит» зелёный; два тенанта видны в control plane.
Фаза 1 — Ядро (6–8 недель)
| # | Задача |
|---|---|
| 1.1 | Модуль ledger: journal entries, postings, балансы, сторно, EOD-снапшоты |
| 1.2 | Модуль clients: CRUD, статусы, контакты, адреса, атрибуты, контрагенты |
| 1.3 | Модуль accounts: открытие/блокировка/закрытие, GL-привязка, сабледжеры |
| 1.4 | Модуль fx: валюты, пары, загрузка курсов (источник + ручной ввод) |
| 1.5 | Справочники: страны, банки, SEPA-участники, blocklist, продукты, enum'ы |
| 1.6 | tools/import-elcoin v1: справочники, клиенты, GL, счета, история проводок |
| 1.7 | Admin-панель: поиск клиентов/счетов, просмотр проводок, ручная проводка с maker-checker |
| 1.8 | Property-based тесты: любой сценарий → банк сбалансирован по всем валютам |
DoD: вся история MD импортирована в стейджинг; баланс каждого счёта
совпадает с remoteAccount.ledger_balance Elcoin (отчёт сверки = 0 расхождений);
оператор может сделать ручную проводку через админку.
Фаза 2 — Платежи (6–8 недель)
| # | Задача |
|---|---|
| 2.1 | payment_orders, статусная машина, payment_status_history |
| 2.2 | 3-step API: prepare (дефолты, канал, комиссия) / validate (баланс, лимиты, IBAN checksum, blocklist, DFB-правило) / commit (hold + запись) |
| 2.3 | Internal-платежи: полный цикл с проводками |
| 2.4 | Конвертации: fixed_side, курс из fx, проводки через FX-позицию GL |
| 2.5 | Модуль fees: fee_rules, расчёт по событию, проводки комиссий на revenue GL |
| 2.6 | Модуль holds: жизненный цикл, expiry-job |
| 2.7 | Авторизация платежей оператором (maker-checker, пороги per-tenant) |
| 2.8 | Вебхуки о смене статусов; account lookup (авто-детект internal по IBAN) |
DoD: через API (Postman/тестовый фронт) проходит internal-платёж и конвертация с комиссией: hold → авторизация → проводки → COMPLETED; отчёт «баланс банка» сходится.
Фаза 3 — Кор-банки (6–8 недель, параллелится с хвостом фазы 2)
| # | Задача |
|---|---|
| 3.1 | nostro_accounts + транзитные GL |
| 3.2 | Парсеры camt.053 и MT940 (packages/statements, чистые функции + golden-file тесты на реальных выписках Clear Junction) |
| 3.3 | Импорт выписок: файл → строки → матчинг (endToEndId / reference / сумма+дата) |
| 3.4 | Inward-платежи из нематченных кредитовых строк: определение счёта получателя по IBAN, зачисление с проводками, комиссия |
| 3.5 | Подтверждение outward по выписке: транзит → ностро, статус COMPLETED |
| 3.6 | recon: ежедневная сверка ностро (наш GL vs closing balance выписки), отчёт расхождений, алерт |
| 3.7 | Outward через Clear Junction API: маппинг полей, отправка из очереди, обработка ответов/возвратов |
| 3.8 | Ручной канал для кор-банков без API (оператор отправляет в интернет-банке, статус проставляется по выписке) |
DoD: боевая выписка Clear Junction импортируется без ошибок; inward зачисляется автоматически; реконсиляция за тестовый месяц = 0 расхождений; тестовый outward уходит и подтверждается выпиской.
Фаза 3П — Параллельный трек: запуск AB (ноябрь 2026 – январь 2027)
AB — greenfield-тенант: без миграции Elcoin ему нужны только ядро (фаза 1), платежи (фаза 2) и кор-счёт (фаза 3). Ждать cutover MD незачем — запускаемся параллельно с фазой 3 и принимаем первого клиента на M3. Трек преимущественно операционный (настройка, договорённости с банками, KYC-процесс), разработчиков почти не отвлекает.
| # | Задача |
|---|---|
| 3П.1 | Создание тенанта в control plane — первая боевая проверка мульти-тенантности (отдельная БД, регион страны лицензии) |
| 3П.2 | План счетов (GL) под требования локального регулятора; продукты и типы счетов |
| 3П.3 | Тарифные планы и fee_rules для стартовой линейки |
| 3П.4 | KYC-процесс онбординга (внешний провайдер + ручная проверка), анкеты, чек-листы комплаенса |
| 3П.5 | Открытие кор-счёта / подключение рейла: локальная валюта + USD/EUR (SWIFT-провайдер или региональный кор-банк) |
| 3П.6 | Импорт выписок и реконсиляция для нового кор-банка (переиспользуем модуль statements/recon фазы 3) |
| 3П.7 | Пилотный онбординг в ручном режиме: низкие лимиты, maker-checker на 100% платежей, ежедневная реконсиляция с первого дня; перевод в авто-режим — на M5 |
DoD: первый реальный клиент AB открыл счёт, получил входящий платёж и провёл исходящий — всё в ручном режиме (авторизация оператором); реконсиляция кор-счёта за пилотный период = 0 расхождений; данные тенантов физически изолированы (проверено).
Фаза 4 — Миграция и cutover (4–6 недель)
Детальная процедура — в разделе Миграция с Elcoin.
| # | Задача |
|---|---|
| 4.1 | Client Bank: замена elcoin-api.ts на abs-api.ts, адаптация payment rails и хуков |
| 4.2 | Импортер v2: инкрементальная дозагрузка дельты из Elcoin (ежедневно) |
| 4.3 | Параллельный прогон 2–4 недели: Elcoin — мастер, наша АБС — shadow; ежедневная сверка балансов и платежей |
| 4.4 | Прогон операционных сценариев командой (чек-лист: все типы платежей, отмены, возвраты, комиссии) |
| 4.5 | Cutover (выходные): финальная дельта → сверка → переключение DNS/конфига фронта → Elcoin read-only |
| 4.6 | Гиперзабота 2 недели: ежедневная реконсиляция, быстрые фиксы |
DoD: MD работает на своей АБС; сверка cutover-балансов подписана; подписка Elcoin переведена в режим завершения.
Фаза 5 — Устойчивость и развитие (после cutover, приоритеты уточняются)
Первый блок — подтверждение боевой устойчивости: оба банка уже в проде, и ядро должно доказать надёжность формальными проверками, а не только штатной работой.
| Направление | Содержание |
|---|---|
| Регресс-пакет | Автоматизированный e2e-регресс всех типов платежей (internal, outward, inward, конвертации, возвраты, отмены, комиссии) — гоняется в CI на каждый релиз |
| Нагрузочные тесты | Профиль пиковой нагрузки x10 от текущей; замер деградации, узкие места леджера |
| Pen-test | Внешний пентест API и админки; устранение находок |
| DR-учение | Восстановление стейджинга из бэкапа на время (проверка RTO ≤ 4 ч / RPO ≤ 5 мин); отработка сценария потери основной БД |
| Чистая реконсиляция | 30 дней подряд без расхождений по всем ностро обоих тенантов |
| AB в авто-режим | Снятие ручной авторизации со 100% платежей AB: авто-обработка в пределах лимитов, maker-checker только выше порогов; критерий готовности для M5 |
| Карты | Перенос контуров TopUp/Purchase с Intercash на новое ядро (наработки по карточным контурам уже задокументированы) |
| AML/Compliance | Интеграция screening-провайдера в validate; кейс-менеджмент в админке |
| Отчётность | Регуляторные отчёты африканской страны; management-отчёты из EOD-снапшотов |
| Клиентские фичи | Выписки PDF/CSV, шаблоны платежей, мультиподпись для юрлиц |
3. Порядок работ и зависимости
Импорт данных начинается внутри фазы 1, как только готов леджер: реальные данные — лучший тест ядра, и именно на них ловятся все сюрпризы модели Elcoin.
4. Риски
| Риск | Вероятность | Удар | Смягчение |
|---|---|---|---|
| Расхождение балансов при миграции | Высокая | Критический | Импорт запускаем рано (фаза 1); сверка на каждом прогоне; неполные таблицы зеркала допаганируем заранее; cutover только при нулевом расхождении |
| Elcoin ограничит API/экспорт при уведомлении об уходе | Средняя | Высокий | Полный экспорт и допагинацию сделать ДО уведомления вендора; зеркало уже у нас |
| Скрытая бизнес-логика Elcoin (комиссии, правила каналов) всплывёт поздно | Высокая | Средний | Логика уже частично реверс-инжинирена; параллельный прогон сравнивает результаты prepare/validate двух систем |
| Требования африканского регулятора к АБС/отчётности неизвестны | Средняя | Средний | Запросить требования сейчас; ядро переносимо; отчётность — изолированный модуль |
| Команда 1–3 человека, bus factor | Высокая | Высокий | Документация (этот пакет), простые технологии, полная воспроизводимость окружения, тесты как спецификация |
| Clear Junction API: сюрпризы в форматах/лимитах | Средняя | Средний | Ручной канал (3.8) как fallback; ранний тестовый доступ |
| Расползание скоупа («давайте сразу и кредиты») | Высокая | Средний | Раздел «Что НЕ входит в MVP» (Видение и рамки) утверждён; изменения только через пересмотр roadmap |
| AB запускается на ядре, ещё не прошедшем боевую обкатку MD (cutover позже) | Средняя | Высокий | Пилотный режим: низкие лимиты, maker-checker на 100% платежей, малый объём операций, ежедневная реконсиляция с первого дня; масштабирование объёмов — только после M4 и чистой реконсиляции |
5. Контрольные точки для руководства
| Веха | Ожидаемый срок | Проверка |
|---|---|---|
| M1: ядро держит историю | конец октября 2026 | Балансы импорта = Elcoin, 0 расхождений |
| M2: платежи ходят внутри | середина декабря 2026 | Демо: internal + конвертация с комиссией end-to-end |
| M3: кор-банк подключён + первый клиент AB в ручном режиме | конец января 2027 | Выписка импортируется, реконсиляция = 0, тестовый outward прошёл; первый клиент AB открыл счёт и провёл платежи с ручной авторизацией оператором |
| M4: cutover MD | февраль 2027 | Elcoin отключён, клиенты работают |
| M5: боевая устойчивость подтверждена + первый клиент AB в авто режиме | март–апрель 2027 | Pen-test пройден, DR-учение проведено (RTO/RPO в норме), 30 дней чистой реконсиляции по обоим тенантам, регресс-пакет зелёный в CI; платежи AB обрабатываются автоматически (maker-checker только выше порогов) |
| M6: карты и отчётность | Q2 2027 | Карточные контуры на новом ядре; первый регуляторный отчёт сдан |
АБС — План миграции данных с Elcoin
Документ 5 из 6. Маппинг таблиц — в разделе Модель данных (п. 4). Сроки — фаза 1 (импорт v1) и фаза 4 (cutover) в Дорожной карте.
1. Источники данных
| Источник | Что даёт | Состояние |
|---|---|---|
| PostgreSQL-зеркало Elcoin (18) | 64 таблицы: клиенты, счета, GL, проводки, платежи, холды, справочники | Есть, синхронизируется. Часть таблиц упёрлась в лимит API pageSize=1000 |
Elcoin API (getData + пагинация) |
Дозагрузка неполных таблиц, ежедневная дельта до cutover | Доступ есть (OAuth2 client_credentials + HMAC) |
| Выписки кор-банков (camt.053 / MT940 / CSV) | Независимый источник истины по деньгам на ностро; текущие операции | Clear Junction и др. |
2. Подготовительный этап (сделать ДО уведомления Elcoin об уходе)
- Допагинировать неполные таблицы зеркала. По описанию экспортированных
таблиц в лимит 1000 строк упёрлись:
additionalInformation,clientCounterparty,clientExternalRegistration,transactionEntryHeader,transactionViewHeader,transactionViewLine,remoteCounterparty,remoteWebhook,sepaParticipants,clientMessage. Критичны для миграции: transactionViewHeader / transactionViewLine (это вся бухгалтерская история) и clientCounterparty. Написать пагинирующий экспортёр и выгрузить полностью. - Зафиксировать контрольные суммы: по каждой таблице — количество строк,
MAX(id),MAX(updated_at); по каждому счёту —ledger_balanceиavailable_amountизremoteAccount; сумма дебетов/кредитов по GL. - Собрать полный архив выписок кор-банков за всю историю (независимая верификация ностро-балансов).
- Реверс тарифов: выписать действующие комиссии по каждому тарифному плану
(из прод-платежей:
fee_amount_numпо типам/каналам) — Elcoin не отдаёт правила, только результаты.
3. Порядок импорта (идемпотентный пайплайн tools/import-elcoin)
Каждый шаг — отдельная команда, безопасная для повторного запуска (upsert по бизнес-ключам). Прогон полностью автоматический: «снести тенант-базу → импорт → отчёт сверки» должен выполняться одной командой, потому что прогонов будет много.
Ключевые решения шага 5 (проводки)
- Каждая пара
transactionViewHeader+ строкиtransactionViewLine→ одинjournal_entry(entryType = MIGRATION,migrationSourceId = header_id) с postings по строкам:debet_num > 0→ DEBIT,credit_num > 0→ CREDIT. - Строки на GL-счета линкуются к
gl_accounts, строки на клиентские счета — кaccounts. - Несбалансированные заголовки (если найдутся) — в карантинную таблицу
migration_quarantineи ручной разбор; НЕ вставляем силой. - Opening balance fallback: если история проводок в Elcoin неполна (глубина экспорта меньше возраста счёта), балансы не сойдутся из проводок. Тогда стратегия: импортируем проводки за доступную глубину + одна opening-balance проводка на счёт на дату начала истории (Dr/Cr против GL «миграционный мост», который в итоге обязан схлопнуться в ноль по каждой валюте). Решение принимаем после первого прогона на реальных данных.
Шаг 9 — сверка (обязательный отчёт каждого прогона)
| Проверка | Критерий |
|---|---|
| Баланс каждого счёта | наш SUM(postings) = remoteAccount.ledger_balance |
| Баланс каждого GL | наш = сумме по transactionViewLine Elcoin |
| Глобальный инвариант | SUM(DEBIT) = SUM(CREDIT) по каждой валюте |
| Ностро | наш GL-баланс ностро = closing balance последней выписки кор-банка |
| Количества | клиенты, счета, контрагенты, проводки — счётчики совпали с зеркалом |
| Холды | сумма активных холдов по счёту = Elcoin |
Отчёт сохраняется как артефакт; cutover разрешён только при полном PASS.
4. Параллельный прогон (shadow ledger, 2–4 недели)
- Elcoin остаётся мастером; клиенты ничего не замечают.
- Ежедневный инкрементальный импорт дельты (новые проводки, платежи, клиенты)
по
id/updated_atводяным знакам. - Ежедневная сверка балансов (тот же отчёт из шага 9). Любое расхождение — баг импортера или пропуск данных: чиним до cutover.
- Shadow-валидация платежей: сервер Client Bank дублирует
prepare/validateв нашу АБС (fire-and-forget, ответ клиенту — только от Elcoin) и логирует различия (комиссия, канал, ошибки валидации). Так мы проверяем платёжную логику на реальном трафике без риска.
5. Cutover (выходные, план по часам)
| Шаг | Действие | Откат |
|---|---|---|
| C1 | Пятница вечер: заморозка исходящих платежей в Client Bank (баннер клиентам) | Снять заморозку |
| C2 | Финальный инкремент-импорт дельты из Elcoin | — |
| C3 | Полный отчёт сверки. GO/NO-GO: только при PASS | NO-GO → работаем на Elcoin дальше |
| C4 | Переключение конфига Client Bank на своё API; смоук-тест командой (логин, счета, история, internal-платёж) | Вернуть конфиг на Elcoin (изменений в данных Elcoin не было — откат чистый) |
| C5 | Реквизиты кор-банков: входящие продолжают попадать на те же ностро — они наши, менять ничего не нужно; выписки с этого момента процессит наша АБС | — |
| C6 | Elcoin переводится в read-only (доступ сохраняем на срок договора для истории) | — |
| C7 | Понедельник: гиперзабота — дежурство, ежедневная реконсиляция, приоритетные фиксы 2 недели | — |
Точка невозврата — первый платёж, проведённый в нашей АБС после C4. До неё откат — это смена одной переменной конфигурации.
6. После cutover
- Elcoin-зеркало и архивные таблицы остаются в схеме
elcoin_archiveтенант-базы (историческая справка, разборы споров). - Ежедневная реконсиляция ностро становится штатным процессом (модуль
recon). - Через 1–2 месяца стабильной работы — расторжение договора с Elcoin.
7. Чек-лист готовности к миграции
- Допагинированы transactionViewHeader/Line, clientCounterparty, sepaParticipants
- Контрольные суммы зафиксированы
- Архив выписок кор-банков собран, ностро сверен независимо
- Тарифы реверс-инжинирены и заведены в
fee_rules - Импортер прогоняется одной командой, отчёт сверки PASS на стейджинге
- Shadow-прогон ≥ 2 недель с нулевыми расхождениями
- Смоук-чек-лист Client Bank написан и отрепетирован
- План cutover согласован с операционной командой, дата назначена