feat(fansy-store): DDL принимающей БД + контракт данных для команды Fansy

- docs/fansy-contract/v1/ddl/000__roles.sql: роли fansy_etl, bj_reader, bj_migrator
- docs/fansy-contract/v1/ddl/001__schemas.sql: схемы fansy_staging и fansy с грантами
- docs/fansy-contract/v1/ddl/002__working.sql: рабочая схема (participants, securities, clients, client_documents, iia_contracts, settlement_requisites, depo_accounts, portfolios, etl_errors)
- docs/fansy-contract/v1/ddl/003__staging.sql: staging-зеркало с loaded_at и сниженными ограничениями
- docs/fansy-contract/v1/ddl/004__seed_participants.sql: предзаполнение справочника (НРД, БКС 5406121446, Ренессанс 7709258228, Альфа-Банк 7728168971)
- docs/fansy-contract/v1/data-dictionary.md: семантика каждого поля
- docs/fansy-contract/v1/etl-requirements.md: требования к ETL (UPSERT в staging, SLA свежести по таблицам, обработка ошибок)
- docs/fansy-contract/v1/examples/example-claim.md: SQL-запросы для формирования M2MTransferRequest
- docs/fansy-contract/v1/examples/seed-data.sql: 5 тестовых клиентов + портфели + договоры
- migrations/fansy-store/: рабочие копии миграций

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
fontvielle
2026-05-14 00:45:37 +03:00
parent 1d6ab86a57
commit 93bcbca12c
16 changed files with 1357 additions and 26 deletions
+48 -25
View File
@@ -1,33 +1,56 @@
# docs/fansy-contract/v1 — контракт данных с командой Fansy # docs/fansy-contract/v1 — контракт данных с командой Fansy
ETL Fansy → принимающая БД (`fansy-store`) реализует **другая команда ETL Fansy → принимающая БД (`fansy-store`) реализует **другая команда
разработки**. С нашей стороны: разработки**. С нашей стороны зафиксирован контракт: схема таблиц,
индексы, миграции, требования к выгрузке и тестовые данные.
1. Спроектировать таблицы по требованиям документации НРД к данным M2M. ## Состав каталога
2. Передать команде Fansy DDL и контракт данных.
3. Согласовать тип load (UPSERT в staging), окна обновления, SLA на
свежесть данных.
4. Не давать ETL-роли DDL-прав в принимающей схеме.
Состав каталога (создаём в M1, отправляем в начале M2): - **`ddl/`** — SQL-миграции PostgreSQL:
- `000__roles.sql` — роли `fansy_etl` (ETL Fansy), `bj_reader`
(наши сервисы), `bj_migrator` (миграции).
- `001__schemas.sql` — две схемы: `fansy_staging` (куда пишет ETL) и
`fansy` (рабочая, для нашего чтения). Гранты по ролям.
- `002__working.sql` — рабочие таблицы: `participants`, `securities`,
`clients`, `client_documents`, `iia_contracts`,
`settlement_requisites`, `depo_accounts`, `portfolios`,
`etl_errors`.
- `003__staging.sql` — staging-зеркало рабочих таблиц с полем
`loaded_at` и сниженными ограничениями.
- `004__seed_participants.sql` — предзаполнение справочника
участников: НРД, БКС (5406121446), Ренессанс (7709258228),
Альфа-Банк (7728168971).
- **`data-dictionary.md`** — семантика каждого поля.
- **`etl-requirements.md`** — требования к процессу выгрузки от
команды Fansy: подключение, тип load (UPSERT в staging),
SLA свежести по таблицам, обработка ошибок, окна простоя, ПДн.
- **`examples/`**:
- `example-claim.md` — какие данные `m2m-core` тянет из БД для
одной типовой M2M-заявки (с конкретными SQL).
- `seed-data.sql` — 5 тестовых клиентов, портфели, договоры —
основа для приёмочного теста.
- `ddl/``*.sql` миграции PostgreSQL для всех таблиц. ## Рабочие копии миграций
- `data-dictionary.md` — семантика каждого поля (источник в Fansy,
nullable, единицы, примеры).
- `etl-requirements.md` — требования к процессу выгрузки: тип load,
расписание, способ записи, окна простоя, обработка ошибок,
конфиденциальность.
- `examples/` — пример заявки M2M «end-to-end», 5–10 тестовых клиентов
и заявок для совместного приёмочного теста.
Минимальный набор таблиц (см. план): Те же файлы лежат в `migrations/fansy-store/` — оттуда они
применяются при инициализации БД сервиса.
- Депоненты / клиенты. ## Порядок согласования
- Документы инвестора (`IdentityDocumentCodeEnum`).
- ИИС-договоры (`IIAContractTypeEnum ∈ {T12, T03}`). 1. Передать команде Fansy ссылку на эту папку (тег `fansy-contract-v1`).
- Депо-счета и разделы (`AccountId`, `SectionId`, `DeponentCode`). 2. Обсудить с ними SLA, окна простоя, тип load.
- Реквизиты расчётов (ИНН депозитария). 3. По согласовании — дать им учётку с ролью `fansy_etl` и подсеть для
- Портфели и остатки (Whole / Fractional, `IsolationStatus = SGDN`). доступа.
- Справочник ЦБ (`SecurityCode`, `ISIN`, `Classification`, `Category`). 4. Запустить совместный приёмочный тест на `seed-data.sql`.
- Контрагенты-участники сервиса MOST (Справочник пользователей). 5. Изменения контракта — через новую папку `v2/` с changelog'ом, без
- Audit / staging-таблицы для каждой основной. правки `v1/`.
## Принципы
- Имена таблиц/колонок — `snake_case` английский.
- Комментарии к таблицам и важным колонкам — на русском
через `COMMENT ON ... IS '...'`.
- Все timestamp — `timestamptz` в UTC.
- DDL-права только у `bj_migrator`, у `fansy_etl` нет.
- ETL пишет ТОЛЬКО в `fansy_staging.*`. Перелив в `fansy.*` — на нашей
стороне после валидации.
+123
View File
@@ -0,0 +1,123 @@
# Data Dictionary — fansy-store v1
Семантика полей рабочей схемы `fansy`. Структура staging-схемы
`fansy_staging` повторяет её один-к-одному, плюс поле `loaded_at` и
отсутствие части ограничений (валидация — при переливе).
Обозначения: `?` — nullable; `!` — обязательное.
## participants — справочник контрагентов M2M
| Поле | Тип | Обяз. | Описание | Источник Fansy | Пример |
|---|---|---|---|---|---|
| inn | varchar(10) | ! | ИНН юрлица, PK | `client_master.inn` | `7702165310` |
| ogrn | varchar(15) | ? | ОГРН | `client_master.ogrn` | `1027739132563` |
| full_name_rus | text | ! | Полное наименование на русском | `client_master.full_name` | `НКО АО НРД` |
| short_name_rus | text | ? | Короткое наименование | `client_master.short_name` | `НРД` |
| display_name_rus | text | ! | Отображаемое имя для UI | `client_master.display_name` | `НРД` |
| full_name_eng | text | ? | Полное наименование на английском | `client_master.full_name_en` | `National Settlement Depository` |
| short_name_eng | text | ? | Короткое английское | `client_master.short_name_en` | `NSD` |
| display_name_eng | text | ? | Английское display | `client_master.display_name_en` | `NSD` |
| depository_participant_code | varchar(12) | ? | Код участника M2M (депозитарий) | `m2m_codes.dep_code` | `MC0010300000` |
| broker_participant_code | varchar(12) | ? | Код участника M2M (брокер) | `m2m_codes.brk_code` | `MC0079200001` |
| is_available_for_m2m | boolean | ! | Готовность к приёму M2M | `m2m_codes.is_active` | `true` |
| comment | text | ? | Свободный комментарий | — | — |
| created_at, updated_at | timestamptz | ! | Авто | — | — |
## securities — справочник ЦБ
| Поле | Тип | Обяз. | Описание | Источник Fansy | Пример |
|---|---|---|---|---|---|
| security_code | char(12) | ! | Идентификатор ЦБ в системе НРД, PK | `security_master.nsd_code` | `MM0766162534` |
| isin | char(12) | ? | ISIN | `security_master.isin` | `RU0007661625` |
| classification | varchar(4) | ? | `BOND` (облигация), `SHAR` (акция), `MFUN` (ПИФ) | `security_master.type_code` | `SHAR` |
| category | varchar(4) | ? | `ORDN`/`PREF`/`UKWN` | `security_master.category` | `ORDN` |
| security_type | varchar(256) | ? | Текстовое описание типа | `security_master.type_text` | `Акция обыкновенная` |
| security_series | text | ? | Серия выпуска (для облигаций) | `security_master.series` | `01` |
| reg_number | varchar(256) | ? | Регистрационный номер выпуска / правил ДУ ПИФ | `security_master.reg_number` | `1-01-00010-A` |
| fund_class | varchar(120) | ? | Класс паёв ПИФ | `security_master.fund_class` | `A` |
| display_name | text | ! | Отображаемое имя для UI | `security_master.display` | `Сбербанк ао` |
## clients — депоненты-физлица
| Поле | Тип | Обяз. | Описание | Источник Fansy | Пример |
|---|---|---|---|---|---|
| id | uuid | ! | PK, генерируется БД | `customer.uuid` | — |
| inn | varchar(12) | ? | ИНН (10 цифр юрлицо, 12 цифр физлицо) | `customer.inn` | `771234567890` |
| last_name | varchar(50) | ! | Фамилия | `customer.last_name` | `Иванов` |
| first_name | varchar(50) | ! | Имя | `customer.first_name` | `Иван` |
| middle_name | varchar(50) | ? | Отчество | `customer.middle_name` | `Иванович` |
| birth_date | date | ? | Дата рождения | `customer.birth_date` | `1980-01-15` |
## client_documents — документы инвестора
| Поле | Тип | Обяз. | Описание | Источник Fansy | Пример |
|---|---|---|---|---|---|
| id | uuid | ! | PK | — | — |
| client_id | uuid | ! | FK на `clients.id` | `customer_doc.customer_uuid` | — |
| document_type | varchar(2) | ! | Код документа по справочнику НРД (01..91) | `customer_doc.type_code` | `21` |
| series | text | ? | Серия (без пробелов) | `customer_doc.series` | `4512` |
| number | text | ! | Номер (без пробелов) | `customer_doc.number` | `654321` |
| issued_at | date | ? | Дата выдачи | `customer_doc.issued_at` | `2010-05-12` |
| issuer | text | ? | Кем выдан | `customer_doc.issuer` | `ОУФМС России` |
## iia_contracts — договоры ИИС
| Поле | Тип | Обяз. | Описание | Источник Fansy | Пример |
|---|---|---|---|---|---|
| id | uuid | ! | PK | — | — |
| client_id | uuid | ! | FK на `clients.id` | — | — |
| agreement_type | varchar(3) | ! | `T12` (ИИС-1/ИИС-2) или `T03` (ИИС-3) | `iia.type` | `T03` |
| agreement_number | varchar(128) | ! | Номер договора | `iia.number` | `ИИС78/2024` |
| agreement_date | date | ! | Дата заключения | `iia.signed_at` | `2026-01-15` |
| broker_inn | varchar(10) | ! | ИНН брокера, ведущего ИИС | `iia.broker_inn` | `0707083893` |
## settlement_requisites — реквизиты депозитариев
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
| id | uuid | ! | PK |
| inn | varchar(10) | ! | ИНН депозитария, UNIQUE |
| display_name | text | ! | Отображаемое имя |
## depo_accounts — счета депо
| Поле | Тип | Обяз. | Описание | Источник Fansy | Пример |
|---|---|---|---|---|---|
| id | uuid | ! | PK | — | — |
| client_id | uuid | ! | FK на `clients.id` | — | — |
| deponent_code | varchar(50) | ! | Код депонента у депозитария | `depo.deponent_code` | `DP789456` |
| account_id | varchar(50) | ! | Номер счёта депо | `depo.account_id` | `31MC0021900000F01` |
| section_id | varchar(50) | ! | Номер раздела счёта | `depo.section_id` | `P001` |
| depository_inn | varchar(10) | ! | ИНН депозитария | `depo.depository_inn` | `7702070139` |
| is_active | boolean | ! | Активен ли счёт | `depo.is_active` | `true` |
| is_trading | boolean | ! | Торговый раздел | `depo.is_trading` | `true` |
Уникальность по тройке `(deponent_code, account_id, section_id)`.
## portfolios — портфели и остатки ЦБ
| Поле | Тип | Обяз. | Описание | Источник Fansy | Пример |
|---|---|---|---|---|---|
| id | uuid | ! | PK | — | — |
| client_id | uuid | ! | FK на `clients.id` | — | — |
| depo_account_id | uuid | ! | FK на `depo_accounts.id` | — | — |
| security_code | char(12) | ! | FK на `securities.security_code` | — | `MM0766162534` |
| isin | char(12) | ? | Кэш ISIN из securities | — | `RU0007661625` |
| quantity_whole | numeric(38,0) | ? | Целое количество (для акций/облигаций) | `position.qty_whole` | `1500` |
| quantity_fractional | numeric(38,16) | ? | Дробное (для паёв) | `position.qty_fract` | `2500.7500000000000000` |
| isolation_status | varchar(4) | ! | Всегда `SGDN` | — | `SGDN` |
| valued_at | timestamptz | ! | На какой момент актуально | `position.valued_at` | `2026-03-02T11:30:00Z` |
Должно быть заполнено ровно одно из (`quantity_whole`, `quantity_fractional`).
## etl_errors — журнал ошибок ETL
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
| id | bigserial | ! | PK |
| source_table | text | ! | Таблица в Fansy |
| source_pk | text | ? | PK записи в Fansy |
| payload | jsonb | ? | Сама запись для ретрая |
| error_message | text | ! | Сообщение об ошибке |
| created_at | timestamptz | ! | Когда зафиксирована |
+26
View File
@@ -0,0 +1,26 @@
-- 000__roles.sql
-- Роли для принимающей БД fansy-store.
-- Запускать первым, отдельно от структурных миграций.
-- Пароли проставляются администратором БД через ALTER ROLE.
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'fansy_etl') THEN
CREATE ROLE fansy_etl LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT;
COMMENT ON ROLE fansy_etl IS
'Роль команды Fansy для ETL: INSERT/UPDATE/SELECT в схему fansy_staging. DDL-прав нет.';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'bj_reader') THEN
CREATE ROLE bj_reader LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT;
COMMENT ON ROLE bj_reader IS
'Роль сервисов Bridge-and-Join-s (m2m-core, lk-gateway) для чтения схемы fansy.';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'bj_migrator') THEN
CREATE ROLE bj_migrator LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT;
COMMENT ON ROLE bj_migrator IS
'Роль с DDL-правами для миграций. Только эта роль может CREATE/ALTER/DROP.';
END IF;
END
$$;
@@ -0,0 +1,23 @@
-- 001__schemas.sql
-- Две схемы: fansy_staging (куда пишет ETL Fansy) и fansy (рабочая,
-- куда переливаются данные после валидации).
CREATE SCHEMA IF NOT EXISTS fansy_staging AUTHORIZATION bj_migrator;
COMMENT ON SCHEMA fansy_staging IS
'Staging-схема. ETL Fansy делает UPSERT в эти таблицы. Сюда же пишутся ошибки выгрузки.';
CREATE SCHEMA IF NOT EXISTS fansy AUTHORIZATION bj_migrator;
COMMENT ON SCHEMA fansy IS
'Рабочая схема. Сюда переливаются актуальные данные триггерами или процедурами после валидации staging.';
-- Права по ролям. DDL-права остаются только у владельца bj_migrator.
GRANT USAGE ON SCHEMA fansy_staging TO fansy_etl;
GRANT USAGE ON SCHEMA fansy TO bj_reader;
ALTER DEFAULT PRIVILEGES IN SCHEMA fansy_staging
GRANT SELECT, INSERT, UPDATE ON TABLES TO fansy_etl;
ALTER DEFAULT PRIVILEGES IN SCHEMA fansy_staging
GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO fansy_etl;
ALTER DEFAULT PRIVILEGES IN SCHEMA fansy
GRANT SELECT ON TABLES TO bj_reader;
+231
View File
@@ -0,0 +1,231 @@
-- 002__working.sql
-- Рабочая схема fansy. Данные сюда переливаются из fansy_staging после
-- валидации. Сервисы Bridge-and-Join-s читают только эту схему.
SET search_path TO fansy, public;
-- ---------------------------------------------------------------------
-- participants — справочник участников сервиса MOST (контрагенты M2M)
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS participants (
inn varchar(10) PRIMARY KEY,
ogrn varchar(15),
full_name_rus text NOT NULL,
short_name_rus text,
display_name_rus text NOT NULL,
full_name_eng text,
short_name_eng text,
display_name_eng text,
depository_participant_code varchar(12),
broker_participant_code varchar(12),
is_available_for_m2m boolean NOT NULL DEFAULT false,
comment text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CHECK (inn ~ '^[0-9]{10}$'),
CHECK (depository_participant_code IS NULL OR depository_participant_code ~ '^[A-Z0-9]+$'),
CHECK (broker_participant_code IS NULL OR broker_participant_code ~ '^[A-Z0-9]+$')
);
COMMENT ON TABLE participants IS 'Справочник участников сервиса MOST: депозитарии и брокеры, между которыми идут M2M-переводы.';
COMMENT ON COLUMN participants.inn IS 'ИНН юрлица (10 цифр), первичный ключ.';
COMMENT ON COLUMN participants.depository_participant_code IS 'Код участника M2M на стороне депозитария (для DepositoryPlace в M2MTransferHandbook).';
COMMENT ON COLUMN participants.broker_participant_code IS 'Код участника M2M на стороне брокера (для BrokerPlace).';
COMMENT ON COLUMN participants.is_available_for_m2m IS 'Готовность участника принимать/отправлять M2M-сообщения (включается после подписания НРД-договора).';
CREATE INDEX IF NOT EXISTS idx_participants_dep_code ON participants(depository_participant_code) WHERE depository_participant_code IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_participants_brk_code ON participants(broker_participant_code) WHERE broker_participant_code IS NOT NULL;
-- ---------------------------------------------------------------------
-- securities — справочник ценных бумаг
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS securities (
security_code char(12) PRIMARY KEY,
isin char(12),
classification varchar(4),
category varchar(4),
security_type varchar(256),
security_series text,
reg_number varchar(256),
fund_class varchar(120),
display_name text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CHECK (security_code ~ '^[0-9A-Z_/-]+$'),
CHECK (isin IS NULL OR isin ~ '^[A-Z]{2}[A-Z0-9]{9}[0-9]$'),
CHECK (classification IS NULL OR classification IN ('BOND', 'SHAR', 'MFUN')),
CHECK (category IS NULL OR category IN ('ORDN', 'PREF', 'UKWN'))
);
COMMENT ON TABLE securities IS 'Справочник ценных бумаг с их идентификаторами и классификацией.';
COMMENT ON COLUMN securities.security_code IS 'Идентификатор ценной бумаги в системе НРД (XSD SecurityCodeType).';
COMMENT ON COLUMN securities.classification IS 'Тип ценной бумаги: BOND (облигация), SHAR (акция), MFUN (ПИФ).';
COMMENT ON COLUMN securities.category IS 'Категория акций: ORDN (обыкновенные), PREF (привилегированные), UKWN (неизвестно).';
COMMENT ON COLUMN securities.reg_number IS 'Регистрационный номер выпуска (для акций и облигаций) или регномер правил доверительного управления ПИФ.';
COMMENT ON COLUMN securities.fund_class IS 'Класс паёв ПИФа (если применимо).';
CREATE INDEX IF NOT EXISTS idx_securities_isin ON securities(isin) WHERE isin IS NOT NULL;
-- ---------------------------------------------------------------------
-- clients — депоненты / инвесторы
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS clients (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
inn varchar(12),
last_name varchar(50) NOT NULL,
first_name varchar(50) NOT NULL,
middle_name varchar(50),
birth_date date,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CHECK (inn IS NULL OR inn ~ '^[0-9]{10,12}$')
);
COMMENT ON TABLE clients IS 'Депоненты-физлица. Привязка к документам и счетам — через FK из дочерних таблиц.';
COMMENT ON COLUMN clients.inn IS 'ИНН физлица (12 цифр) или организации (10 цифр), опционально.';
COMMENT ON COLUMN clients.last_name IS 'Фамилия (XSD String50, обязательно).';
COMMENT ON COLUMN clients.first_name IS 'Имя (XSD String50, обязательно).';
COMMENT ON COLUMN clients.middle_name IS 'Отчество (XSD String50, опционально).';
CREATE INDEX IF NOT EXISTS idx_clients_inn ON clients(inn) WHERE inn IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_clients_lastname ON clients(last_name, first_name);
-- ---------------------------------------------------------------------
-- client_documents — документы инвестора
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS client_documents (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
client_id uuid NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
document_type varchar(2) NOT NULL,
series text,
number text NOT NULL,
issued_at date,
issuer text,
created_at timestamptz NOT NULL DEFAULT now(),
CHECK (document_type IN (
'01','02','03','04','05','06','07','09','10','11','12','13','14',
'21','22','23','26','27','91'
)),
CHECK (series IS NULL OR series ~ '^\S+$'),
CHECK (number ~ '^\S+$')
);
COMMENT ON TABLE client_documents IS 'Документы, удостоверяющие личность инвестора. Коды по справочнику НРД (XSD IdentityDocumentCodeEnum).';
COMMENT ON COLUMN client_documents.document_type IS 'Код вида документа (01..91, см. XSD НРД).';
COMMENT ON COLUMN client_documents.series IS 'Серия документа (без пробелов).';
COMMENT ON COLUMN client_documents.number IS 'Номер документа (без пробелов, обязательно).';
CREATE INDEX IF NOT EXISTS idx_client_documents_client ON client_documents(client_id);
-- ---------------------------------------------------------------------
-- iia_contracts — договоры ИИС
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS iia_contracts (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
client_id uuid NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
agreement_type varchar(3) NOT NULL,
agreement_number varchar(128) NOT NULL,
agreement_date date NOT NULL,
broker_inn varchar(10) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
CHECK (agreement_type IN ('T12', 'T03')),
CHECK (broker_inn ~ '^[0-9]{10}$')
);
COMMENT ON TABLE iia_contracts IS 'Договоры на ведение ИИС инвестора.';
COMMENT ON COLUMN iia_contracts.agreement_type IS 'Тип договора: T12 — ИИС-1/ИИС-2 (старый формат); T03 — ИИС-3 (новый).';
COMMENT ON COLUMN iia_contracts.broker_inn IS 'ИНН брокера, с которым заключён договор ИИС.';
CREATE INDEX IF NOT EXISTS idx_iia_contracts_client ON iia_contracts(client_id);
CREATE INDEX IF NOT EXISTS idx_iia_contracts_broker ON iia_contracts(broker_inn);
-- ---------------------------------------------------------------------
-- settlement_requisites — реквизиты расчётов (депозитарии)
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS settlement_requisites (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
inn varchar(10) NOT NULL UNIQUE,
display_name text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
CHECK (inn ~ '^[0-9]{10}$')
);
COMMENT ON TABLE settlement_requisites IS 'Реквизиты передающего и принимающего депозитариев (XSD SettlementRequisitesType — содержит только ИНН).';
-- ---------------------------------------------------------------------
-- depo_accounts — депо-счета и разделы
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS depo_accounts (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
client_id uuid NOT NULL REFERENCES clients(id) ON DELETE RESTRICT,
deponent_code varchar(50) NOT NULL,
account_id varchar(50) NOT NULL,
section_id varchar(50) NOT NULL,
depository_inn varchar(10) NOT NULL,
is_active boolean NOT NULL DEFAULT true,
is_trading boolean NOT NULL DEFAULT false,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CHECK (depository_inn ~ '^[0-9]{10}$'),
UNIQUE (deponent_code, account_id, section_id)
);
COMMENT ON TABLE depo_accounts IS 'Счета депо инвестора и их разделы у различных депозитариев.';
COMMENT ON COLUMN depo_accounts.deponent_code IS 'Код депонента у конкретного депозитария (XSD SettlementDepositoryLocationType.DeponentCode).';
COMMENT ON COLUMN depo_accounts.account_id IS 'Номер счёта депо (XSD AccountIdType).';
COMMENT ON COLUMN depo_accounts.section_id IS 'Номер раздела счёта депо.';
COMMENT ON COLUMN depo_accounts.is_trading IS 'Признак торгового раздела (для отделения от изолированных).';
CREATE INDEX IF NOT EXISTS idx_depo_accounts_client ON depo_accounts(client_id);
CREATE INDEX IF NOT EXISTS idx_depo_accounts_deponent ON depo_accounts(deponent_code);
-- ---------------------------------------------------------------------
-- portfolios — портфели и остатки ЦБ
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS portfolios (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
client_id uuid NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
depo_account_id uuid NOT NULL REFERENCES depo_accounts(id) ON DELETE CASCADE,
security_code char(12) NOT NULL REFERENCES securities(security_code),
isin char(12),
quantity_whole numeric(38, 0),
quantity_fractional numeric(38, 16),
isolation_status varchar(4) NOT NULL DEFAULT 'SGDN',
valued_at timestamptz NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CHECK (isolation_status IN ('SGDN')),
CHECK ((quantity_whole IS NOT NULL) OR (quantity_fractional IS NOT NULL)),
CHECK (isin IS NULL OR isin ~ '^[A-Z]{2}[A-Z0-9]{9}[0-9]$')
);
COMMENT ON TABLE portfolios IS 'Остатки ценных бумаг на счетах депо. Whole/Fractional — choice по XSD QuantityType (заполняется ровно одно).';
COMMENT ON COLUMN portfolios.quantity_whole IS 'Целое количество (для акций, облигаций).';
COMMENT ON COLUMN portfolios.quantity_fractional IS 'Дробное количество (для паёв ПИФ, до 16 знаков после точки).';
COMMENT ON COLUMN portfolios.isolation_status IS 'Статус обособления по XSD НРД, всегда SGDN.';
COMMENT ON COLUMN portfolios.valued_at IS 'Дата/время оценки (на какой момент актуален остаток).';
CREATE INDEX IF NOT EXISTS idx_portfolios_client ON portfolios(client_id);
CREATE INDEX IF NOT EXISTS idx_portfolios_depo ON portfolios(depo_account_id);
CREATE INDEX IF NOT EXISTS idx_portfolios_security ON portfolios(security_code);
CREATE INDEX IF NOT EXISTS idx_portfolios_valued_at ON portfolios(valued_at DESC);
-- ---------------------------------------------------------------------
-- etl_errors — ошибки выгрузки Fansy
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS etl_errors (
id bigserial PRIMARY KEY,
source_table text NOT NULL,
source_pk text,
payload jsonb,
error_message text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
COMMENT ON TABLE etl_errors IS 'Журнал ошибок выгрузки Fansy: что не смогли записать в staging и почему.';
COMMENT ON COLUMN etl_errors.source_table IS 'Название таблицы в источнике (Fansy).';
COMMENT ON COLUMN etl_errors.source_pk IS 'Первичный ключ записи в источнике (для повторной попытки).';
COMMENT ON COLUMN etl_errors.payload IS 'Сама запись, которую не удалось загрузить (для диагностики).';
CREATE INDEX IF NOT EXISTS idx_etl_errors_created ON etl_errors(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_etl_errors_table ON etl_errors(source_table);
+109
View File
@@ -0,0 +1,109 @@
-- 003__staging.sql
-- Staging-схема. Структура повторяет fansy.*, плюс loaded_at и
-- допущения на промежуточные NULL'ы (валидация будет в процессе
-- перелива в fansy.*).
SET search_path TO fansy_staging, public;
CREATE TABLE IF NOT EXISTS participants (
inn varchar(10) PRIMARY KEY,
ogrn varchar(15),
full_name_rus text,
short_name_rus text,
display_name_rus text,
full_name_eng text,
short_name_eng text,
display_name_eng text,
depository_participant_code varchar(12),
broker_participant_code varchar(12),
is_available_for_m2m boolean,
comment text,
loaded_at timestamptz NOT NULL DEFAULT now()
);
COMMENT ON TABLE participants IS 'Staging для справочника участников. Перезаливка целиком, не чаще раза в сутки.';
CREATE TABLE IF NOT EXISTS securities (
security_code char(12) PRIMARY KEY,
isin char(12),
classification varchar(4),
category varchar(4),
security_type varchar(256),
security_series text,
reg_number varchar(256),
fund_class varchar(120),
display_name text,
loaded_at timestamptz NOT NULL DEFAULT now()
);
COMMENT ON TABLE securities IS 'Staging для справочника ЦБ. Перезаливка целиком.';
CREATE TABLE IF NOT EXISTS clients (
id uuid PRIMARY KEY,
inn varchar(12),
last_name varchar(50),
first_name varchar(50),
middle_name varchar(50),
birth_date date,
loaded_at timestamptz NOT NULL DEFAULT now()
);
COMMENT ON TABLE clients IS 'Staging для клиентов. Инкрементный UPSERT по id.';
CREATE TABLE IF NOT EXISTS client_documents (
id uuid PRIMARY KEY,
client_id uuid NOT NULL,
document_type varchar(2),
series text,
number text,
issued_at date,
issuer text,
loaded_at timestamptz NOT NULL DEFAULT now()
);
COMMENT ON TABLE client_documents IS 'Staging для документов клиента. UPSERT по id.';
CREATE TABLE IF NOT EXISTS iia_contracts (
id uuid PRIMARY KEY,
client_id uuid NOT NULL,
agreement_type varchar(3),
agreement_number varchar(128),
agreement_date date,
broker_inn varchar(10),
loaded_at timestamptz NOT NULL DEFAULT now()
);
COMMENT ON TABLE iia_contracts IS 'Staging для договоров ИИС.';
CREATE TABLE IF NOT EXISTS settlement_requisites (
id uuid PRIMARY KEY,
inn varchar(10) NOT NULL,
display_name text,
loaded_at timestamptz NOT NULL DEFAULT now()
);
COMMENT ON TABLE settlement_requisites IS 'Staging для реквизитов расчётов.';
CREATE TABLE IF NOT EXISTS depo_accounts (
id uuid PRIMARY KEY,
client_id uuid NOT NULL,
deponent_code varchar(50),
account_id varchar(50),
section_id varchar(50),
depository_inn varchar(10),
is_active boolean,
is_trading boolean,
loaded_at timestamptz NOT NULL DEFAULT now()
);
COMMENT ON TABLE depo_accounts IS 'Staging для депо-счетов.';
CREATE TABLE IF NOT EXISTS portfolios (
id uuid PRIMARY KEY,
client_id uuid NOT NULL,
depo_account_id uuid NOT NULL,
security_code char(12) NOT NULL,
isin char(12),
quantity_whole numeric(38, 0),
quantity_fractional numeric(38, 16),
isolation_status varchar(4),
valued_at timestamptz,
loaded_at timestamptz NOT NULL DEFAULT now()
);
COMMENT ON TABLE portfolios IS 'Staging для портфелей. UPSERT по id; SLA свежести — 1 мин.';
CREATE INDEX IF NOT EXISTS idx_stg_portfolios_loaded ON portfolios(loaded_at DESC);
CREATE INDEX IF NOT EXISTS idx_stg_clients_loaded ON clients(loaded_at DESC);
@@ -0,0 +1,56 @@
-- 004__seed_participants.sql
-- Предзаполнение справочника участников по DOC/Справочник пользователей.pdf
-- НРД и тестовые контрагенты Регламента M2M.
SET search_path TO fansy, public;
INSERT INTO participants (
inn, ogrn, full_name_rus, short_name_rus, display_name_rus,
full_name_eng, short_name_eng, display_name_eng,
depository_participant_code, broker_participant_code,
is_available_for_m2m, comment
) VALUES
(
'7702165310', '1027739132563',
'Небанковская кредитная организация акционерное общество "Национальный расчётный депозитарий"',
'НКО АО НРД', 'НРД',
'National Settlement Depository', 'NSD', 'NSD',
'MC0010300000', NULL, true,
'Центральный депозитарий, держатель реестра M2M-сделок.'
),
(
'5406121446', '1025402459334',
'Общество с ограниченной ответственностью "Компания БКС"',
'ООО "Компания БКС"', 'БКС',
'BCS Company Ltd', 'BCS', 'BCS',
NULL, 'MC0079200001', true,
'Брокер БКС, контрагент M2M.'
),
(
'7709258228', '1027739675260',
'Общество с ограниченной ответственностью "Ренессанс Брокер"',
'ООО "Ренессанс Брокер"', 'Ренессанс Брокер',
'Renaissance Broker Ltd', 'Renaissance', 'Renaissance',
NULL, 'MC0010300032', true,
'Брокер Ренессанс, контрагент M2M.'
),
(
'7728168971', '1027700067328',
'Акционерное общество "Альфа-Банк"',
'АО "Альфа-Банк"', 'Альфа-Банк',
'Alfa-Bank JSC', 'Alfa-Bank', 'Alfa-Bank',
NULL, 'MC0079200033', true,
'Брокер Альфа-Банк, контрагент M2M.'
)
ON CONFLICT (inn) DO UPDATE SET
full_name_rus = EXCLUDED.full_name_rus,
short_name_rus = EXCLUDED.short_name_rus,
display_name_rus = EXCLUDED.display_name_rus,
full_name_eng = EXCLUDED.full_name_eng,
short_name_eng = EXCLUDED.short_name_eng,
display_name_eng = EXCLUDED.display_name_eng,
depository_participant_code = EXCLUDED.depository_participant_code,
broker_participant_code = EXCLUDED.broker_participant_code,
is_available_for_m2m = EXCLUDED.is_available_for_m2m,
comment = EXCLUDED.comment,
updated_at = now();
@@ -0,0 +1,87 @@
# Требования к ETL Fansy → fansy-store v1
## Подключение
- СУБД: PostgreSQL 16 / PostgreSQL Pro Certified (по согласованию).
- Хост, порт, имя БД, IP-allowlist — выдаются администратором ВМ
Bridge-and-Join-s отдельно.
- Учётная запись: роль **`fansy_etl`** (создаётся миграцией
`000__roles.sql`). Пароль выдаётся через защищённый канал, не в
репозиторий.
- TLS: обязательно (`sslmode=verify-full` со стороны клиента ETL).
## Куда писать
- Только в схему `fansy_staging`. Прав на DDL нет, на схему `fansy`
тоже нет. INSERT/UPDATE/SELECT на таблицы staging.
- Запись в `fansy.*` происходит на нашей стороне после валидации.
## Тип load
- **Инкрементный UPSERT** в staging по PK (`id`):
```sql
INSERT INTO fansy_staging.clients (id, ...) VALUES (...)
ON CONFLICT (id) DO UPDATE SET ..., loaded_at = now();
```
- Справочники с относительно небольшим размером и редкой сменой
(`securities`, `participants`) разрешена **полная перезаливка** не
чаще одного раза в сутки. Полная перезаливка реализуется через
транзакцию: `TRUNCATE` + `COPY` + `COMMIT`.
## SLA на свежесть данных
| Таблица | SLA свежести |
|---|---|
| `portfolios` | ≤ 1 минута после фактического изменения в Fansy |
| `clients`, `depo_accounts`, `client_documents`, `iia_contracts` | ≤ 5 минут |
| `securities`, `participants`, `settlement_requisites` | ≤ 24 часа (по событию или по расписанию) |
## Форматы и кодировки
- Все timestamp — `timestamptz` в **UTC** (явная зона `+00`).
- Все строковые поля — UTF-8.
- ИНН, коды депонентов, ISIN, SecurityCode — в верхнем регистре.
- Числа с дробной частью (`numeric(38,16)`) — точка как разделитель,
без разделителей тысяч.
## Обработка ошибок
При нарушении CHECK-ограничений, FK или типов команда Fansy:
1. Пишет запись в `fansy_staging.etl_errors`:
```sql
INSERT INTO fansy_staging.etl_errors (source_table, source_pk, payload, error_message)
VALUES ('fansy.position', '<pk>', '<json>', '<text>');
```
2. Логирует у себя и продолжает работу.
3. Не блокирует загрузку остальных записей.
Мы (Bridge-and-Join-s) еженедельно просматриваем `etl_errors`,
поднимаем инциденты с командой Fansy.
## Окна и расписание
- Регламентное окно простоя — **с 23:00 до 23:30 МСК**, по средам.
В это время ETL может приостанавливаться для обновлений.
- Внеплановые работы — анонсируются за 2 часа в общем чате.
## Конфиденциальность
- ПДн (ФИО, документ, дата рождения) — только по нужным таблицам.
- Журналирование SQL-запросов ETL **не должно** включать значения ПДн.
- Соединения только с IP-allowlist'а.
## Контроль и наблюдаемость
Мы предоставим команде Fansy `read-only` доступ к двум представлениям:
- `fansy_staging.v_load_lag` — задержка свежести по таблицам.
- `fansy_staging.v_load_stats` — счётчики INSERT/UPDATE за сутки.
(Создаются в более позднем PR — `M3`.)
## Точка контакта
- Технический контакт со стороны Bridge-and-Join-s — указан в
`docs/architecture/plan.md`, раздел «Контакты».
- Эскалация — в общий канал интеграции, тред «fansy-store ETL».
@@ -0,0 +1,118 @@
# Пример заявки M2M end-to-end
Типовой сценарий: инвестор Иванов И.И. подаёт через ЛК заявку на
перевод 3 ценных бумаг с депо-счёта у БКС в депо-счёт у Ренессанс
Брокера. Один из переводов — паи ПИФ с дробным количеством. ИИС
тип T03.
## Какие данные нужны m2m-core для формирования M2MTransferRequest
Сервис `m2m-core` достаёт следующее из `fansy-store` (рабочая схема
`fansy`) по идентификатору клиента и набору ЦБ:
### 1. Анкета клиента (для `InvestorInformation`)
```sql
SELECT
c.last_name,
c.first_name,
c.middle_name,
d.document_type,
d.series AS document_series,
d.number AS document_number
FROM fansy.clients c
JOIN fansy.client_documents d ON d.client_id = c.id
WHERE c.id = :client_id
ORDER BY d.created_at DESC
LIMIT 1;
```
### 2. ИИС-договор (для `IIAAgreementDetails`)
```sql
SELECT agreement_type, agreement_number, agreement_date, broker_inn
FROM fansy.iia_contracts
WHERE client_id = :client_id
ORDER BY agreement_date DESC
LIMIT 1;
```
### 3. Реквизиты передающего/принимающего депозитариев
```sql
SELECT inn
FROM fansy.settlement_requisites
WHERE inn IN (:transferring_inn, :receiving_inn);
```
### 4. Депо-счета и разделы инвестора (для `SettlementAccount`)
```sql
SELECT
da.deponent_code,
da.account_id,
da.section_id,
da.depository_inn
FROM fansy.depo_accounts da
WHERE da.client_id = :client_id
AND da.depository_inn = :depository_inn
AND da.is_active = true;
```
### 5. Информация о ценных бумагах и их остатках
```sql
SELECT
p.security_code,
s.isin,
s.classification,
s.category,
s.security_type,
s.reg_number,
s.fund_class,
p.quantity_whole,
p.quantity_fractional,
p.isolation_status
FROM fansy.portfolios p
JOIN fansy.securities s USING (security_code)
WHERE p.client_id = :client_id
AND p.security_code = ANY(:requested_codes)
AND p.valued_at >= now() - interval '5 minutes';
```
### 6. Проверка достаточности остатков
```sql
SELECT
p.security_code,
COALESCE(p.quantity_whole, 0) + COALESCE(p.quantity_fractional, 0) AS available
FROM fansy.portfolios p
WHERE p.client_id = :client_id
AND p.security_code = ANY(:requested_codes);
```
Сравниваем `available` с запрошенным количеством. Если меньше — отказ
от формирования M2MTransferRequest, ошибка в ЛК.
## Какие данные команда Fansy обязана положить в staging
Из примера выше:
- `clients`: запись на инвестора Иванова И.И.
- `client_documents`: документ с DocumentType `21`.
- `iia_contracts`: договор T03 с брокером (БКС, ИНН 5406121446).
- `depo_accounts`: счёт у БКС с разделом для перевода и счёт у
Ренессанс Брокера.
- `securities`: 3 записи (SHAR/ORDN, SHAR/PREF, MFUN/UKWN с
fund_class='A').
- `portfolios`: остатки по этим 3 ЦБ на 1500 / 300 / 2500.75
соответственно.
- `participants`: НРД, БКС (5406121446), Ренессанс (7709258228) — из
начального seed.
## Результат
`m2m-core` собирает данные → формирует `M2MTransferRequest`
валидирует → подписывает (через `crypto-service`) → отправляет в НРД
через `nsd-adapter`. Получает `M2MTransferDecision` от принимающей
стороны, обновляет статус сделки и шлёт callback в ЛК.
@@ -0,0 +1,90 @@
-- seed-data.sql
-- Тестовые данные для совместного приёмочного тестирования
-- Bridge-and-Join-s ↔ команда Fansy. Запускать поверх 002__working.sql.
SET search_path TO fansy, public;
BEGIN;
-- ---------------------------------------------------------------------
-- Реквизиты депозитариев
-- ---------------------------------------------------------------------
INSERT INTO settlement_requisites (id, inn, display_name) VALUES
('00000000-0000-0000-0000-000000000001', '7702070139', 'Депозитарий Сбербанк'),
('00000000-0000-0000-0000-000000000002', '7802031669', 'Депозитарий СПб Банк'),
('00000000-0000-0000-0000-000000000003', '0702345678', 'Депозитарий БКС'),
('00000000-0000-0000-0000-000000000004', '0710987654', 'Депозитарий Ренессанс')
ON CONFLICT (inn) DO NOTHING;
-- ---------------------------------------------------------------------
-- Справочник ЦБ (минимальный)
-- ---------------------------------------------------------------------
INSERT INTO securities (security_code, isin, classification, category, security_type, reg_number, display_name) VALUES
('MM0766162534', 'RU0007661625', 'SHAR', 'ORDN', 'Акция обыкновенная', '1-01-00077-A', 'Газпром ао'),
('MM0907654321', 'RU0009029540', 'SHAR', 'PREF', 'Акция привилегированная', '2-02-00009-A', 'Сбербанк ап'),
('MM2300100100', NULL, 'MFUN', 'UKWN', 'Пай ПИФ', '23-001', 'ПИФ Альфа Капитал')
ON CONFLICT (security_code) DO NOTHING;
UPDATE securities SET fund_class = 'A' WHERE security_code = 'MM2300100100';
-- ---------------------------------------------------------------------
-- 5 тестовых клиентов
-- ---------------------------------------------------------------------
INSERT INTO clients (id, last_name, first_name, middle_name, birth_date) VALUES
('11111111-1111-1111-1111-111111111111', 'Иванов', 'Иван', 'Иванович', '1980-01-15'),
('22222222-2222-2222-2222-222222222222', 'Петров', 'Пётр', 'Петрович', '1985-06-20'),
('33333333-3333-3333-3333-333333333333', 'Сидоров', 'Сидор', 'Сидорович', '1990-11-30'),
('44444444-4444-4444-4444-444444444444', 'Кузнецов','Сергей','Михайлович','1975-03-10'),
('55555555-5555-5555-5555-555555555555', 'Соколова','Анна', 'Викторовна','1988-09-25')
ON CONFLICT (id) DO NOTHING;
-- ---------------------------------------------------------------------
-- Документы клиентов
-- ---------------------------------------------------------------------
INSERT INTO client_documents (id, client_id, document_type, series, number, issued_at, issuer) VALUES
('a0000000-0000-0000-0000-000000000001', '11111111-1111-1111-1111-111111111111', '21', '4512', '654321', '2010-05-12', 'ОУФМС России по Москве'),
('a0000000-0000-0000-0000-000000000002', '22222222-2222-2222-2222-222222222222', '21', '4513', '654322', '2011-06-13', 'ОУФМС России по Москве'),
('a0000000-0000-0000-0000-000000000003', '33333333-3333-3333-3333-333333333333', '21', '4514', '654323', '2012-07-14', 'ОУФМС России по СПб'),
('a0000000-0000-0000-0000-000000000004', '44444444-4444-4444-4444-444444444444', '03', '111', '222333', '1995-08-15', 'Свидетельство о рождении'),
('a0000000-0000-0000-0000-000000000005', '55555555-5555-5555-5555-555555555555', '21', '4516', '654325', '2014-09-16', 'ОУФМС России по СПб')
ON CONFLICT (id) DO NOTHING;
-- ---------------------------------------------------------------------
-- ИИС-договоры (для 3 клиентов)
-- ---------------------------------------------------------------------
INSERT INTO iia_contracts (id, client_id, agreement_type, agreement_number, agreement_date, broker_inn) VALUES
('b0000000-0000-0000-0000-000000000001', '11111111-1111-1111-1111-111111111111', 'T03', 'ИИС78/2024', '2026-01-15', '5406121446'),
('b0000000-0000-0000-0000-000000000002', '22222222-2222-2222-2222-222222222222', 'T12', 'ИИС79/2023', '2025-12-01', '7709258228'),
('b0000000-0000-0000-0000-000000000003', '55555555-5555-5555-5555-555555555555', 'T03', 'ИИС80/2024', '2026-02-10', '7728168971')
ON CONFLICT (id) DO NOTHING;
-- ---------------------------------------------------------------------
-- Депо-счета
-- ---------------------------------------------------------------------
INSERT INTO depo_accounts (id, client_id, deponent_code, account_id, section_id, depository_inn, is_active, is_trading) VALUES
('c0000000-0000-0000-0000-000000000001', '11111111-1111-1111-1111-111111111111', 'DP789456', '31MC0021900000F01', 'P001', '7702070139', true, true),
('c0000000-0000-0000-0000-000000000002', '11111111-1111-1111-1111-111111111111', 'AA789451', '33MC0021900000F02', 'F002', '7802031669', true, true),
('c0000000-0000-0000-0000-000000000003', '22222222-2222-2222-2222-222222222222', 'DP100200', '31MC0010000000A01', 'A001', '7702070139', true, true),
('c0000000-0000-0000-0000-000000000004', '33333333-3333-3333-3333-333333333333', 'DP300400', '31MC0030000000B01', 'B001', '0702345678', true, true),
('c0000000-0000-0000-0000-000000000005', '55555555-5555-5555-5555-555555555555', 'DP500600', '31MC0050000000C01', 'C001', '0710987654', true, true)
ON CONFLICT (deponent_code, account_id, section_id) DO NOTHING;
-- ---------------------------------------------------------------------
-- Портфели (остатки ЦБ)
-- ---------------------------------------------------------------------
INSERT INTO portfolios (id, client_id, depo_account_id, security_code, isin, quantity_whole, quantity_fractional, valued_at) VALUES
('d0000000-0000-0000-0000-000000000001', '11111111-1111-1111-1111-111111111111', 'c0000000-0000-0000-0000-000000000001', 'MM0766162534', 'RU0007661625', 1500, NULL, now()),
('d0000000-0000-0000-0000-000000000002', '11111111-1111-1111-1111-111111111111', 'c0000000-0000-0000-0000-000000000001', 'MM0907654321', 'RU0009029540', 300, NULL, now()),
('d0000000-0000-0000-0000-000000000003', '11111111-1111-1111-1111-111111111111', 'c0000000-0000-0000-0000-000000000001', 'MM2300100100', NULL, NULL, 2500.75, now()),
('d0000000-0000-0000-0000-000000000004', '22222222-2222-2222-2222-222222222222', 'c0000000-0000-0000-0000-000000000003', 'MM0766162534', 'RU0007661625', 5000, NULL, now()),
('d0000000-0000-0000-0000-000000000005', '55555555-5555-5555-5555-555555555555', 'c0000000-0000-0000-0000-000000000005', 'MM2300100100', NULL, NULL, 100.00, now())
ON CONFLICT (id) DO NOTHING;
COMMIT;
+1 -1
View File
@@ -12,7 +12,7 @@ PR-1 → PR-N. Каждая задача — самостоятельный ос
| PR | Файл | Статус | Зависит от | | PR | Файл | Статус | Зависит от |
|----|------|--------|-----------| |----|------|--------|-----------|
| PR-1 | `PR-1-go-models-m2m.md` | выполнено | — | | PR-1 | `PR-1-go-models-m2m.md` | выполнено | — |
| PR-2 | `PR-2-fansy-ddl.md` | готово к запуску | — (параллельно с PR-1) | | PR-2 | `PR-2-fansy-ddl.md` | выполнено | — (параллельно с PR-1) |
| PR-3 | `PR-3-lk-openapi.md` | готово к запуску | — (параллельно с PR-1) | | PR-3 | `PR-3-lk-openapi.md` | готово к запуску | — (параллельно с PR-1) |
| PR-4 | `PR-4-m2m-core-skeleton.md` | готово к запуску | PR-1 | | PR-4 | `PR-4-m2m-core-skeleton.md` | готово к запуску | PR-1 |
| PR-5 | `PR-5-nsd-adapter-skeleton.md` | ждёт ИШ НРД и сертификаты | PR-1, PR-4 | | PR-5 | `PR-5-nsd-adapter-skeleton.md` | ждёт ИШ НРД и сертификаты | PR-1, PR-4 |
+26
View File
@@ -0,0 +1,26 @@
-- 000__roles.sql
-- Роли для принимающей БД fansy-store.
-- Запускать первым, отдельно от структурных миграций.
-- Пароли проставляются администратором БД через ALTER ROLE.
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'fansy_etl') THEN
CREATE ROLE fansy_etl LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT;
COMMENT ON ROLE fansy_etl IS
'Роль команды Fansy для ETL: INSERT/UPDATE/SELECT в схему fansy_staging. DDL-прав нет.';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'bj_reader') THEN
CREATE ROLE bj_reader LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT;
COMMENT ON ROLE bj_reader IS
'Роль сервисов Bridge-and-Join-s (m2m-core, lk-gateway) для чтения схемы fansy.';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'bj_migrator') THEN
CREATE ROLE bj_migrator LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT;
COMMENT ON ROLE bj_migrator IS
'Роль с DDL-правами для миграций. Только эта роль может CREATE/ALTER/DROP.';
END IF;
END
$$;
+23
View File
@@ -0,0 +1,23 @@
-- 001__schemas.sql
-- Две схемы: fansy_staging (куда пишет ETL Fansy) и fansy (рабочая,
-- куда переливаются данные после валидации).
CREATE SCHEMA IF NOT EXISTS fansy_staging AUTHORIZATION bj_migrator;
COMMENT ON SCHEMA fansy_staging IS
'Staging-схема. ETL Fansy делает UPSERT в эти таблицы. Сюда же пишутся ошибки выгрузки.';
CREATE SCHEMA IF NOT EXISTS fansy AUTHORIZATION bj_migrator;
COMMENT ON SCHEMA fansy IS
'Рабочая схема. Сюда переливаются актуальные данные триггерами или процедурами после валидации staging.';
-- Права по ролям. DDL-права остаются только у владельца bj_migrator.
GRANT USAGE ON SCHEMA fansy_staging TO fansy_etl;
GRANT USAGE ON SCHEMA fansy TO bj_reader;
ALTER DEFAULT PRIVILEGES IN SCHEMA fansy_staging
GRANT SELECT, INSERT, UPDATE ON TABLES TO fansy_etl;
ALTER DEFAULT PRIVILEGES IN SCHEMA fansy_staging
GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO fansy_etl;
ALTER DEFAULT PRIVILEGES IN SCHEMA fansy
GRANT SELECT ON TABLES TO bj_reader;
+231
View File
@@ -0,0 +1,231 @@
-- 002__working.sql
-- Рабочая схема fansy. Данные сюда переливаются из fansy_staging после
-- валидации. Сервисы Bridge-and-Join-s читают только эту схему.
SET search_path TO fansy, public;
-- ---------------------------------------------------------------------
-- participants — справочник участников сервиса MOST (контрагенты M2M)
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS participants (
inn varchar(10) PRIMARY KEY,
ogrn varchar(15),
full_name_rus text NOT NULL,
short_name_rus text,
display_name_rus text NOT NULL,
full_name_eng text,
short_name_eng text,
display_name_eng text,
depository_participant_code varchar(12),
broker_participant_code varchar(12),
is_available_for_m2m boolean NOT NULL DEFAULT false,
comment text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CHECK (inn ~ '^[0-9]{10}$'),
CHECK (depository_participant_code IS NULL OR depository_participant_code ~ '^[A-Z0-9]+$'),
CHECK (broker_participant_code IS NULL OR broker_participant_code ~ '^[A-Z0-9]+$')
);
COMMENT ON TABLE participants IS 'Справочник участников сервиса MOST: депозитарии и брокеры, между которыми идут M2M-переводы.';
COMMENT ON COLUMN participants.inn IS 'ИНН юрлица (10 цифр), первичный ключ.';
COMMENT ON COLUMN participants.depository_participant_code IS 'Код участника M2M на стороне депозитария (для DepositoryPlace в M2MTransferHandbook).';
COMMENT ON COLUMN participants.broker_participant_code IS 'Код участника M2M на стороне брокера (для BrokerPlace).';
COMMENT ON COLUMN participants.is_available_for_m2m IS 'Готовность участника принимать/отправлять M2M-сообщения (включается после подписания НРД-договора).';
CREATE INDEX IF NOT EXISTS idx_participants_dep_code ON participants(depository_participant_code) WHERE depository_participant_code IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_participants_brk_code ON participants(broker_participant_code) WHERE broker_participant_code IS NOT NULL;
-- ---------------------------------------------------------------------
-- securities — справочник ценных бумаг
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS securities (
security_code char(12) PRIMARY KEY,
isin char(12),
classification varchar(4),
category varchar(4),
security_type varchar(256),
security_series text,
reg_number varchar(256),
fund_class varchar(120),
display_name text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CHECK (security_code ~ '^[0-9A-Z_/-]+$'),
CHECK (isin IS NULL OR isin ~ '^[A-Z]{2}[A-Z0-9]{9}[0-9]$'),
CHECK (classification IS NULL OR classification IN ('BOND', 'SHAR', 'MFUN')),
CHECK (category IS NULL OR category IN ('ORDN', 'PREF', 'UKWN'))
);
COMMENT ON TABLE securities IS 'Справочник ценных бумаг с их идентификаторами и классификацией.';
COMMENT ON COLUMN securities.security_code IS 'Идентификатор ценной бумаги в системе НРД (XSD SecurityCodeType).';
COMMENT ON COLUMN securities.classification IS 'Тип ценной бумаги: BOND (облигация), SHAR (акция), MFUN (ПИФ).';
COMMENT ON COLUMN securities.category IS 'Категория акций: ORDN (обыкновенные), PREF (привилегированные), UKWN (неизвестно).';
COMMENT ON COLUMN securities.reg_number IS 'Регистрационный номер выпуска (для акций и облигаций) или регномер правил доверительного управления ПИФ.';
COMMENT ON COLUMN securities.fund_class IS 'Класс паёв ПИФа (если применимо).';
CREATE INDEX IF NOT EXISTS idx_securities_isin ON securities(isin) WHERE isin IS NOT NULL;
-- ---------------------------------------------------------------------
-- clients — депоненты / инвесторы
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS clients (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
inn varchar(12),
last_name varchar(50) NOT NULL,
first_name varchar(50) NOT NULL,
middle_name varchar(50),
birth_date date,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CHECK (inn IS NULL OR inn ~ '^[0-9]{10,12}$')
);
COMMENT ON TABLE clients IS 'Депоненты-физлица. Привязка к документам и счетам — через FK из дочерних таблиц.';
COMMENT ON COLUMN clients.inn IS 'ИНН физлица (12 цифр) или организации (10 цифр), опционально.';
COMMENT ON COLUMN clients.last_name IS 'Фамилия (XSD String50, обязательно).';
COMMENT ON COLUMN clients.first_name IS 'Имя (XSD String50, обязательно).';
COMMENT ON COLUMN clients.middle_name IS 'Отчество (XSD String50, опционально).';
CREATE INDEX IF NOT EXISTS idx_clients_inn ON clients(inn) WHERE inn IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_clients_lastname ON clients(last_name, first_name);
-- ---------------------------------------------------------------------
-- client_documents — документы инвестора
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS client_documents (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
client_id uuid NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
document_type varchar(2) NOT NULL,
series text,
number text NOT NULL,
issued_at date,
issuer text,
created_at timestamptz NOT NULL DEFAULT now(),
CHECK (document_type IN (
'01','02','03','04','05','06','07','09','10','11','12','13','14',
'21','22','23','26','27','91'
)),
CHECK (series IS NULL OR series ~ '^\S+$'),
CHECK (number ~ '^\S+$')
);
COMMENT ON TABLE client_documents IS 'Документы, удостоверяющие личность инвестора. Коды по справочнику НРД (XSD IdentityDocumentCodeEnum).';
COMMENT ON COLUMN client_documents.document_type IS 'Код вида документа (01..91, см. XSD НРД).';
COMMENT ON COLUMN client_documents.series IS 'Серия документа (без пробелов).';
COMMENT ON COLUMN client_documents.number IS 'Номер документа (без пробелов, обязательно).';
CREATE INDEX IF NOT EXISTS idx_client_documents_client ON client_documents(client_id);
-- ---------------------------------------------------------------------
-- iia_contracts — договоры ИИС
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS iia_contracts (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
client_id uuid NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
agreement_type varchar(3) NOT NULL,
agreement_number varchar(128) NOT NULL,
agreement_date date NOT NULL,
broker_inn varchar(10) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
CHECK (agreement_type IN ('T12', 'T03')),
CHECK (broker_inn ~ '^[0-9]{10}$')
);
COMMENT ON TABLE iia_contracts IS 'Договоры на ведение ИИС инвестора.';
COMMENT ON COLUMN iia_contracts.agreement_type IS 'Тип договора: T12 — ИИС-1/ИИС-2 (старый формат); T03 — ИИС-3 (новый).';
COMMENT ON COLUMN iia_contracts.broker_inn IS 'ИНН брокера, с которым заключён договор ИИС.';
CREATE INDEX IF NOT EXISTS idx_iia_contracts_client ON iia_contracts(client_id);
CREATE INDEX IF NOT EXISTS idx_iia_contracts_broker ON iia_contracts(broker_inn);
-- ---------------------------------------------------------------------
-- settlement_requisites — реквизиты расчётов (депозитарии)
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS settlement_requisites (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
inn varchar(10) NOT NULL UNIQUE,
display_name text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
CHECK (inn ~ '^[0-9]{10}$')
);
COMMENT ON TABLE settlement_requisites IS 'Реквизиты передающего и принимающего депозитариев (XSD SettlementRequisitesType — содержит только ИНН).';
-- ---------------------------------------------------------------------
-- depo_accounts — депо-счета и разделы
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS depo_accounts (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
client_id uuid NOT NULL REFERENCES clients(id) ON DELETE RESTRICT,
deponent_code varchar(50) NOT NULL,
account_id varchar(50) NOT NULL,
section_id varchar(50) NOT NULL,
depository_inn varchar(10) NOT NULL,
is_active boolean NOT NULL DEFAULT true,
is_trading boolean NOT NULL DEFAULT false,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CHECK (depository_inn ~ '^[0-9]{10}$'),
UNIQUE (deponent_code, account_id, section_id)
);
COMMENT ON TABLE depo_accounts IS 'Счета депо инвестора и их разделы у различных депозитариев.';
COMMENT ON COLUMN depo_accounts.deponent_code IS 'Код депонента у конкретного депозитария (XSD SettlementDepositoryLocationType.DeponentCode).';
COMMENT ON COLUMN depo_accounts.account_id IS 'Номер счёта депо (XSD AccountIdType).';
COMMENT ON COLUMN depo_accounts.section_id IS 'Номер раздела счёта депо.';
COMMENT ON COLUMN depo_accounts.is_trading IS 'Признак торгового раздела (для отделения от изолированных).';
CREATE INDEX IF NOT EXISTS idx_depo_accounts_client ON depo_accounts(client_id);
CREATE INDEX IF NOT EXISTS idx_depo_accounts_deponent ON depo_accounts(deponent_code);
-- ---------------------------------------------------------------------
-- portfolios — портфели и остатки ЦБ
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS portfolios (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
client_id uuid NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
depo_account_id uuid NOT NULL REFERENCES depo_accounts(id) ON DELETE CASCADE,
security_code char(12) NOT NULL REFERENCES securities(security_code),
isin char(12),
quantity_whole numeric(38, 0),
quantity_fractional numeric(38, 16),
isolation_status varchar(4) NOT NULL DEFAULT 'SGDN',
valued_at timestamptz NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CHECK (isolation_status IN ('SGDN')),
CHECK ((quantity_whole IS NOT NULL) OR (quantity_fractional IS NOT NULL)),
CHECK (isin IS NULL OR isin ~ '^[A-Z]{2}[A-Z0-9]{9}[0-9]$')
);
COMMENT ON TABLE portfolios IS 'Остатки ценных бумаг на счетах депо. Whole/Fractional — choice по XSD QuantityType (заполняется ровно одно).';
COMMENT ON COLUMN portfolios.quantity_whole IS 'Целое количество (для акций, облигаций).';
COMMENT ON COLUMN portfolios.quantity_fractional IS 'Дробное количество (для паёв ПИФ, до 16 знаков после точки).';
COMMENT ON COLUMN portfolios.isolation_status IS 'Статус обособления по XSD НРД, всегда SGDN.';
COMMENT ON COLUMN portfolios.valued_at IS 'Дата/время оценки (на какой момент актуален остаток).';
CREATE INDEX IF NOT EXISTS idx_portfolios_client ON portfolios(client_id);
CREATE INDEX IF NOT EXISTS idx_portfolios_depo ON portfolios(depo_account_id);
CREATE INDEX IF NOT EXISTS idx_portfolios_security ON portfolios(security_code);
CREATE INDEX IF NOT EXISTS idx_portfolios_valued_at ON portfolios(valued_at DESC);
-- ---------------------------------------------------------------------
-- etl_errors — ошибки выгрузки Fansy
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS etl_errors (
id bigserial PRIMARY KEY,
source_table text NOT NULL,
source_pk text,
payload jsonb,
error_message text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
COMMENT ON TABLE etl_errors IS 'Журнал ошибок выгрузки Fansy: что не смогли записать в staging и почему.';
COMMENT ON COLUMN etl_errors.source_table IS 'Название таблицы в источнике (Fansy).';
COMMENT ON COLUMN etl_errors.source_pk IS 'Первичный ключ записи в источнике (для повторной попытки).';
COMMENT ON COLUMN etl_errors.payload IS 'Сама запись, которую не удалось загрузить (для диагностики).';
CREATE INDEX IF NOT EXISTS idx_etl_errors_created ON etl_errors(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_etl_errors_table ON etl_errors(source_table);
+109
View File
@@ -0,0 +1,109 @@
-- 003__staging.sql
-- Staging-схема. Структура повторяет fansy.*, плюс loaded_at и
-- допущения на промежуточные NULL'ы (валидация будет в процессе
-- перелива в fansy.*).
SET search_path TO fansy_staging, public;
CREATE TABLE IF NOT EXISTS participants (
inn varchar(10) PRIMARY KEY,
ogrn varchar(15),
full_name_rus text,
short_name_rus text,
display_name_rus text,
full_name_eng text,
short_name_eng text,
display_name_eng text,
depository_participant_code varchar(12),
broker_participant_code varchar(12),
is_available_for_m2m boolean,
comment text,
loaded_at timestamptz NOT NULL DEFAULT now()
);
COMMENT ON TABLE participants IS 'Staging для справочника участников. Перезаливка целиком, не чаще раза в сутки.';
CREATE TABLE IF NOT EXISTS securities (
security_code char(12) PRIMARY KEY,
isin char(12),
classification varchar(4),
category varchar(4),
security_type varchar(256),
security_series text,
reg_number varchar(256),
fund_class varchar(120),
display_name text,
loaded_at timestamptz NOT NULL DEFAULT now()
);
COMMENT ON TABLE securities IS 'Staging для справочника ЦБ. Перезаливка целиком.';
CREATE TABLE IF NOT EXISTS clients (
id uuid PRIMARY KEY,
inn varchar(12),
last_name varchar(50),
first_name varchar(50),
middle_name varchar(50),
birth_date date,
loaded_at timestamptz NOT NULL DEFAULT now()
);
COMMENT ON TABLE clients IS 'Staging для клиентов. Инкрементный UPSERT по id.';
CREATE TABLE IF NOT EXISTS client_documents (
id uuid PRIMARY KEY,
client_id uuid NOT NULL,
document_type varchar(2),
series text,
number text,
issued_at date,
issuer text,
loaded_at timestamptz NOT NULL DEFAULT now()
);
COMMENT ON TABLE client_documents IS 'Staging для документов клиента. UPSERT по id.';
CREATE TABLE IF NOT EXISTS iia_contracts (
id uuid PRIMARY KEY,
client_id uuid NOT NULL,
agreement_type varchar(3),
agreement_number varchar(128),
agreement_date date,
broker_inn varchar(10),
loaded_at timestamptz NOT NULL DEFAULT now()
);
COMMENT ON TABLE iia_contracts IS 'Staging для договоров ИИС.';
CREATE TABLE IF NOT EXISTS settlement_requisites (
id uuid PRIMARY KEY,
inn varchar(10) NOT NULL,
display_name text,
loaded_at timestamptz NOT NULL DEFAULT now()
);
COMMENT ON TABLE settlement_requisites IS 'Staging для реквизитов расчётов.';
CREATE TABLE IF NOT EXISTS depo_accounts (
id uuid PRIMARY KEY,
client_id uuid NOT NULL,
deponent_code varchar(50),
account_id varchar(50),
section_id varchar(50),
depository_inn varchar(10),
is_active boolean,
is_trading boolean,
loaded_at timestamptz NOT NULL DEFAULT now()
);
COMMENT ON TABLE depo_accounts IS 'Staging для депо-счетов.';
CREATE TABLE IF NOT EXISTS portfolios (
id uuid PRIMARY KEY,
client_id uuid NOT NULL,
depo_account_id uuid NOT NULL,
security_code char(12) NOT NULL,
isin char(12),
quantity_whole numeric(38, 0),
quantity_fractional numeric(38, 16),
isolation_status varchar(4),
valued_at timestamptz,
loaded_at timestamptz NOT NULL DEFAULT now()
);
COMMENT ON TABLE portfolios IS 'Staging для портфелей. UPSERT по id; SLA свежести — 1 мин.';
CREATE INDEX IF NOT EXISTS idx_stg_portfolios_loaded ON portfolios(loaded_at DESC);
CREATE INDEX IF NOT EXISTS idx_stg_clients_loaded ON clients(loaded_at DESC);
@@ -0,0 +1,56 @@
-- 004__seed_participants.sql
-- Предзаполнение справочника участников по DOC/Справочник пользователей.pdf
-- НРД и тестовые контрагенты Регламента M2M.
SET search_path TO fansy, public;
INSERT INTO participants (
inn, ogrn, full_name_rus, short_name_rus, display_name_rus,
full_name_eng, short_name_eng, display_name_eng,
depository_participant_code, broker_participant_code,
is_available_for_m2m, comment
) VALUES
(
'7702165310', '1027739132563',
'Небанковская кредитная организация акционерное общество "Национальный расчётный депозитарий"',
'НКО АО НРД', 'НРД',
'National Settlement Depository', 'NSD', 'NSD',
'MC0010300000', NULL, true,
'Центральный депозитарий, держатель реестра M2M-сделок.'
),
(
'5406121446', '1025402459334',
'Общество с ограниченной ответственностью "Компания БКС"',
'ООО "Компания БКС"', 'БКС',
'BCS Company Ltd', 'BCS', 'BCS',
NULL, 'MC0079200001', true,
'Брокер БКС, контрагент M2M.'
),
(
'7709258228', '1027739675260',
'Общество с ограниченной ответственностью "Ренессанс Брокер"',
'ООО "Ренессанс Брокер"', 'Ренессанс Брокер',
'Renaissance Broker Ltd', 'Renaissance', 'Renaissance',
NULL, 'MC0010300032', true,
'Брокер Ренессанс, контрагент M2M.'
),
(
'7728168971', '1027700067328',
'Акционерное общество "Альфа-Банк"',
'АО "Альфа-Банк"', 'Альфа-Банк',
'Alfa-Bank JSC', 'Alfa-Bank', 'Alfa-Bank',
NULL, 'MC0079200033', true,
'Брокер Альфа-Банк, контрагент M2M.'
)
ON CONFLICT (inn) DO UPDATE SET
full_name_rus = EXCLUDED.full_name_rus,
short_name_rus = EXCLUDED.short_name_rus,
display_name_rus = EXCLUDED.display_name_rus,
full_name_eng = EXCLUDED.full_name_eng,
short_name_eng = EXCLUDED.short_name_eng,
display_name_eng = EXCLUDED.display_name_eng,
depository_participant_code = EXCLUDED.depository_participant_code,
broker_participant_code = EXCLUDED.broker_participant_code,
is_available_for_m2m = EXCLUDED.is_available_for_m2m,
comment = EXCLUDED.comment,
updated_at = now();