Files
Bridge-and-Join-s/migrations/fansy-store/002__working.sql
T
fontvielle 93bcbca12c 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>
2026-05-14 00:45:37 +03:00

232 lines
14 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- 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);