-- 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);