Core Banking System

Архитектурный план — доступ ограничен

Core Banking System

Архитектурный план собственной АБС · MD + AB · июль 2026

АБС «на минималках» — Видение и рамки проекта

Документ 0 из 6. Остальные разделы: Архитектура, Техстек, Модель данных, Дорожная карта, Миграция с Elcoin. Дата: 2026-07-19. Статус: утверждён план, идёт проектирование.


1. Зачем мы строим свою АБС

Сегодня проект MD работает поверх Elcoin ABS (SaaS):

Триггер изменений: получена банковская лицензия в одной из стран Африки. Под этой эгидой создаётся АБС для двух проектов (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. Что такое «АБС на минималках»

Ядро из пяти неразделимых вещей, без которых банк не банк:

  1. Леджер — неизменяемый журнал двойной записи (проводки: дебет = кредит, всегда).
  2. План счетов (GL) — иерархия бухгалтерских счетов (245 GL импортируем из Elcoin).
  3. Клиентские счета — привязка клиент → счёт → валюта → GL, балансы как производные от проводок.
  4. Клиенты — карточка, статусы, группы, контакты, контрагенты (бенефициары).
  5. Операционный контур — холды, комиссии, валюты и курсы, статусная машина платежей.

Всё остальное — модули, подключаемые поэтапно (см. Дорожную карту).

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 месяцев)

5. Что НЕ входит в MVP (отложено, фаза 5+)

Отложено Как закрываем потребность до тех пор
Карточный процессинг Текущая схема Intercash + card-abs-sync переносится на новое ядро на фазе 5
Кредиты, депозиты как продукты Не нужны на старте ни одному из проектов
Собственный AML/sanctions-скрининг Интеграция с внешним провайдером (SumSub / ComplyAdvantage), в MVP — blocklist + ручная авторизация
Прямое членство в SEPA/SWIFT Работаем через кор-банки, как сейчас
Отчётность регулятору (авто) Ручная выгрузка из леджера SQL-отчётами; автоматизация — фаза 5 по требованиям конкретного регулятора
Мобильные приложения Web-first

6. Принципы (не подлежат пересмотру в ходе реализации)

  1. Леджер неизменяем. Проводка не редактируется и не удаляется — только сторнируется новой проводкой. Баланс никогда не хранится как «истина» — он выводим из проводок (кэшируется, но всегда пересчитываем).
  2. Дебет = кредит в каждой транзакции. Проверяется на уровне БД (constraint/триггер), а не только в коде.
  3. Деньги — не float. NUMERIC в БД, строки/decimal в коде и API.
  4. Идемпотентность всех записывающих операций. Повторный запрос с тем же idempotency key не создаёт дубль.
  5. Аудит всего. Каждое действие оператора и API-клиента — в append-only аудит-лог.
  6. Maker-checker. Операции выше порога требуют второй авторизации оператора.
  7. Совместимость важнее красоты. 3-step flow и знакомые поля (debtor_account, creditor_account_iban, fee_amount_num…) сохраняем, чтобы фронт и накопленные знания команды переиспользовались.

7. Критерии успеха проекта

  1. Client Bank MD работает на своей АБС, Elcoin отключён; клиенты не заметили миграции.
  2. Балансы всех счетов после миграции сходятся с Elcoin и с выписками кор-банков до цента.
  3. Африканский банк запущен как второй тенант без изменений в ядре.
  4. Ежемесячные расходы на инфраструктуру существенно ниже подписки Elcoin.
  5. Новый платёжный канал (новый кор-банк / рейл) подключается силами команды без вендора.

8. Ключевые риски (детально — в Дорожной карте)

АБС — Архитектура системы

Документ 1 из 6. Контекст и цели — в разделе Видение и рамки.


1. Стиль архитектуры: модульный монолит

Микросервисы для команды из 1–3 разработчиков — самоубийство: сетевые границы, распределённые транзакции, зоопарк деплоев. Мы строим модульный монолит на NestJS:

2. Контекстная диаграмма

Диаграмма architecture-1

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. Ядро: леджер двойной записи

Самая важная часть системы. Три таблицы (детали схемы — в разделе Модель данных):

Диаграмма architecture-2

Правила:

  1. journal_entry — атомарная транзакция: 2+ postings, SUM(debit) = SUM(credit) по каждой валюте. Проверяется constraint-триггером в PostgreSQL при COMMIT.
  2. Postings append-only: ни UPDATE, ни DELETE (запрещены грантами БД). Исправление — сторнирующая проводка со ссылкой на оригинал.
  3. Каждый posting ссылается либо на клиентский счёт, либо на GL-счёт. Клиентский счёт всегда привязан к GL (пассивы банка), поэтому баланс банка сходится на любом уровне.
  4. Баланс = SUM(credits) - SUM(debits) (для пассивных счетов). Кэш в account_balances обновляется в той же транзакции; ночной job пересчитывает и сверяет кэш с сырыми postings.
  5. Available balance = ledger balance − активные холды.
  6. 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)

где modelinternal | outward | conversion (+ inward создаётся импортом выписок).

Статусная машина (совпадает с текущей, к которой привык фронт и операторы):

Диаграмма architecture-3

Проводки по этапам (пример 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:

  1. Бизнес-транзакция пишет событие (payment.accepted, statement.line.matched…) в таблицу outbox_events в той же транзакции БД.
  2. Диспетчер читает outbox и кладёт задачи в BullMQ (Redis): отправка в кор-банк, вебхуки, email, пересчёт кэшей.
  3. Обработчики идемпотентны; ретраи с backoff; DLQ для разбора вручную.

Так мы не теряем события при падении процесса и не тащим Kafka в проект на 3 человека.

7. Мульти-тенантность

Database-per-tenant. Один инстанс приложения (или по инстансу на регион), у каждого банка своя PostgreSQL-база:

Диаграмма architecture-4

Почему не schema-per-tenant и не row-level:

Цена — прогон Prisma-миграций по N баз; при N=2 это не цена.

8. Безопасность

9. Наблюдаемость и эксплуатация

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.tsabs-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 Деньги и точность

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-диаграмма ядра

Диаграмма datamodel-1

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-триггеры БД):

Аналог 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. Диаграмма Ганта

Диаграмма roadmap-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. Порядок работ и зависимости

Диаграмма roadmap-2

Импорт данных начинается внутри фазы 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 об уходе)

  1. Допагинировать неполные таблицы зеркала. По описанию экспортированных таблиц в лимит 1000 строк упёрлись: additionalInformation, clientCounterparty, clientExternalRegistration, transactionEntryHeader, transactionViewHeader, transactionViewLine, remoteCounterparty, remoteWebhook, sepaParticipants, clientMessage. Критичны для миграции: transactionViewHeader / transactionViewLine (это вся бухгалтерская история) и clientCounterparty. Написать пагинирующий экспортёр и выгрузить полностью.
  2. Зафиксировать контрольные суммы: по каждой таблице — количество строк, MAX(id), MAX(updated_at); по каждому счёту — ledger_balance и available_amount из remoteAccount; сумма дебетов/кредитов по GL.
  3. Собрать полный архив выписок кор-банков за всю историю (независимая верификация ностро-балансов).
  4. Реверс тарифов: выписать действующие комиссии по каждому тарифному плану (из прод-платежей: fee_amount_num по типам/каналам) — Elcoin не отдаёт правила, только результаты.

3. Порядок импорта (идемпотентный пайплайн tools/import-elcoin)

Каждый шаг — отдельная команда, безопасная для повторного запуска (upsert по бизнес-ключам). Прогон полностью автоматический: «снести тенант-базу → импорт → отчёт сверки» должен выполняться одной командой, потому что прогонов будет много.

Диаграмма migration-1

Ключевые решения шага 5 (проводки)

Шаг 9 — сверка (обязательный отчёт каждого прогона)

Проверка Критерий
Баланс каждого счёта наш SUM(postings) = remoteAccount.ledger_balance
Баланс каждого GL наш = сумме по transactionViewLine Elcoin
Глобальный инвариант SUM(DEBIT) = SUM(CREDIT) по каждой валюте
Ностро наш GL-баланс ностро = closing balance последней выписки кор-банка
Количества клиенты, счета, контрагенты, проводки — счётчики совпали с зеркалом
Холды сумма активных холдов по счёту = Elcoin

Отчёт сохраняется как артефакт; cutover разрешён только при полном PASS.

4. Параллельный прогон (shadow ledger, 2–4 недели)

Диаграмма migration-2
  1. Elcoin остаётся мастером; клиенты ничего не замечают.
  2. Ежедневный инкрементальный импорт дельты (новые проводки, платежи, клиенты) по id/updated_at водяным знакам.
  3. Ежедневная сверка балансов (тот же отчёт из шага 9). Любое расхождение — баг импортера или пропуск данных: чиним до cutover.
  4. 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

7. Чек-лист готовности к миграции