Database Schema — Каноническая модель хранения платформы
Версия: 2.0
Дата: 23.04.2026
Статус: Готов к обсуждению
Назначение документа
Этот документ фиксирует не просто набор SQL-таблиц, а каноническую persistent model платформы vitrip.store.
Его задача — определить:
- какие доменные контуры действительно должны быть отражены в хранилище;
- какие данные являются master-data, а какие являются volatile operational state;
- какие сущности должны храниться как canonical source of truth;
- какие связи между inventory, offers, pricing, booking, identity, governance и Tour Builder должны быть закреплены на уровне модели данных;
- как построить промышленную схему хранения без смешения разных уровней истины.
Этот документ не должен читаться как “готовый финальный SQL для немедленного запуска”. Его нужно читать как жёсткий design baseline для persistent model, который будет направлять последующую детализацию DDL, индексов, партиционирования и миграций.
Опорные документы
Этот документ следует читать вместе со следующими материалами:
- Архитектурная основа платформы vitrip.store
- Domain Model — Центральная доменная модель платформы
- Tenancy And Identity — Субъекты платформы, изоляция и модель доступа
- Offer Pricing Booking Semantics — Семантика предложения, цены и бронирования
- Commercial Model — Коммерческая модель, цена, settlement и канальные условия
- Tour Builder Domain — Домен композиции, draft/proposal lifecycle и пакетного продукта
- Data Governance And Matching — Provenance, merge policy и human-in-the-loop контроль
- Business Services — Сервисная декомпозиция платформы
- Главные выводы и проблемные зоны платформы
- Documentation Master Plan — Project 15 Structure Snapshot
- Ingestion Layer — Слой приёма и обработки
- Storage Layer — Слой хранения данных
- API Contracts — Surface Contracts и правила внешнего взаимодействия
Что Исправляет Новый Подход
Предыдущая версия схемы страдала от нескольких системных проблем:
- слишком сильная property-centric модель;
- недостаточно выраженная offer-level модель;
- смешение stable и volatile data;
- ранняя фиксация конкретной SQL-формы там, где домен ещё не был стабилизирован;
- размазывание identity, commercial и governance-смысла по нескольким таблицам без жёсткого центра;
- недостаточная различимость между supplier representation, canonical entity, quote-time state и booking-time state.
Новая версия схемы исходит из другой логики:
- сначала определить уровни истины;
- затем закрепить канонические сущности;
- затем разделить persistent и volatile контуры;
- затем разложить модель по устойчивым схемам;
- только после этого уточнять конкретные SQL implementation details.
Что Второй Несущий Круг Требует От Persistent Model
После появления документов второго круга database-schema.md уже нельзя читать как просто общий storage baseline. Теперь он обязан явно удерживать следующие смысловые требования:
- Domain Model — Центральная доменная модель платформы требует, чтобы persistent model различала canonical, supplier-derived, operational, transactional, composition, governance и identity-сущности не на словах, а в структуре хранения.
- Tenancy And Identity — Субъекты платформы, изоляция и модель доступа требует, чтобы
organization,tenant,workspace,role assignment,capability grant,api clientиapi credentialне были сведены к расплывчатымusersиagencies. - Offer Pricing Booking Semantics — Семантика предложения, цены и бронирования требует, чтобы
Offer,QuoteиBookingбыли тремя разными уровнями истины, а не одним непрозрачным operational blob. - Commercial Model — Коммерческая модель, цена, settlement и канальные условия требует, чтобы
quoted price,commercial rule output,settlement-relevant figures,override originиchannel-aware policy traceбыли различимы не только концептуально, но и в persistent model. - Tour Builder Domain — Домен композиции, draft/proposal lifecycle и пакетного продукта требует, чтобы у Tour Builder был собственный persistent lifecycle: drafts, items, alternatives, proposals, versions, artifacts.
- Data Governance And Matching — Provenance, merge policy и human-in-the-loop контроль требует, чтобы provenance, lineage, review cases, manual locks и merge decisions были first-class persistence concern.
Практический вывод из этого простой:
если persistent model не умеет хранить эти различия отдельно, остальные документы останутся теоретически правильными, но не смогут быть реализованы как промышленная платформа.
Базовые Принципы Persistent Model
1. Platform Core First
Схема должна отражать platform core, а не повторять supplier data model.
2. Canonical Before Supplier-Specific
Канонические сущности платформы должны быть отделены от supplier-specific representations.
3. Offer-Centric Operational Model
Модель данных должна уметь хранить не только property и room, но и полноценный offer как центральную operational единицу.
4. Stable And Volatile Separation
Master-data и volatile operational state должны быть разведены как минимум логически, а при необходимости и физически.
5. Booking As Transactional Truth
Booking не должен быть “ещё одной записью около отеля”. Это отдельный транзакционный домен с собственным lifecycle и audit semantics.
6. Governance Is First-Class
Provenance, source precedence, confidence, merge decisions и manual review не должны быть случайными JSON-полями “на потом”. Это полноценная часть промышленной модели данных.
7. Identity And Commercial Rules Are Core Domains
Identity / tenancy / access и pricing / commercial semantics должны иметь устойчивое место в persistent model.
8. Commercial Trace Must Survive Operational Volatility
Persistent model должна сохранять не только сумму, но и происхождение коммерческого решения:
- какой policy set был применён;
- какие overrides сработали;
- какая currency/exchange basis использовалась;
- какой quoted promise был дан;
- какие settlement-relevant figures были рассчитаны позже.
Уровни Истины В Модели Данных
Одна из главных задач схемы — не смешивать разные уровни истины.
1. Canonical Master Truth
Это стабильный слой платформы:
- property identity;
- canonical product model;
- geography;
- classifications;
- organization and user model;
- long-lived partner and agency settings;
- policy and governance outcomes.
Этот слой живёт в постоянной модели платформы и должен считаться главным persistent truth.
2. Supplier Representation Truth
Это не canonical truth платформы, а truth о том, как конкретный upstream поставщик описывает объект, продукт, тариф или availability.
Этот слой нужен для:
- traceability;
- mapping;
- reconciliation;
- governance;
- replay and diagnostics.
3. Operational Offer Truth
Это краткоживущий, но структурно важный слой:
- offers;
- availability snapshots;
- price snapshots;
- quote readiness;
- revalidation outcomes.
Он должен храниться так, чтобы:
- поддерживать operational decisions;
- не путать его с perpetual canonical truth;
- сохранять историю там, где это критично.
4. Transactional Booking Truth
Это truth о коммерчески значимом событии:
- booking attempt;
- supplier confirmation;
- platform confirmation;
- cancellation;
- compensation;
- payment relations;
- uncertain and partially failed states.
5. Governance Truth
Это truth о том, почему система считает определённые данные каноническими, спорными, подтверждёнными или требующими review.
Схемы Хранения
Для промышленной платформы разумно проектировать модель как набор логических схем.
На текущем этапе предлагается следующая структура:
core— canonical entities и стабильный backbone платформы;supplier— supplier-specific representations и ingestion-derived layer;offer— operational offer, price, availability, quote-time state;booking— bookings, payments, amendments, settlement-relevant events;identity— users, agencies, partners, API clients, permissions, tenancy;governance— provenance, merge decisions, review queues, anomalies;support— справочники, валюты, география, системные настройки;tour— draft/proposal/itinerary composition и связанные persistent structures.
Такое деление не означает, что всё обязано быть в разных физических БД. Оно означает, что платформа должна мыслить эти контуры раздельно.
Контур support
Сюда относятся устойчивые справочники, которые нужны всей платформе.
Основные таблицы
support.supplierssupport.regionssupport.currenciessupport.exchange_ratessupport.commercial_policy_setssupport.classifierssupport.system_settings
Зачем этот контур отдельный
Чтобы базовые reference-данные не смешивались с inventory, booking или governance semantics.
Пример
CREATE TABLE support.suppliers (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(50) UNIQUE NOT NULL,
name VARCHAR(200) NOT NULL,
integration_type VARCHAR(30) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'active',
config JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE support.regions (
id BIGSERIAL PRIMARY KEY,
parent_id BIGINT REFERENCES support.regions(id),
region_type VARCHAR(30) NOT NULL,
code VARCHAR(100),
path LTREE,
name_en VARCHAR(200) NOT NULL,
name_ru VARCHAR(200),
name_uk VARCHAR(200),
timezone_name VARCHAR(64),
currency_code CHAR(3),
centroid GEOGRAPHY(POINT, 4326),
bbox GEOGRAPHY(POLYGON, 4326),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
Важные замечания
- Использовать
Europe/Kyiv, а не устаревшееEurope/Kiev. - Для geo-полей проектировать PostGIS-совместимые типы, а не смешивать их с нативным
POINT, если предполагается активное использованиеST_*функций.
Контур core
Это сердце persistent model платформы.
Центральные сущности
core.propertiescore.property_contentcore.property_classificationscore.productscore.product_capabilities
core.properties
Это canonical master-record объекта размещения.
Должно содержать
- platform-wide identity;
- canonical name;
- geo reference;
- canonical address structure;
- property type;
- star / class semantics;
- lifecycle status;
- content ownership reference.
Не должно содержать
- supplier raw price;
- transient availability;
- booking state;
- partner-specific commercial conditions.
core.products
Это каноническая продуктовая сущность, которая связывает property с тем, что реально может участвовать в offer generation.
Продукт в модели нужен потому, что room-level и stay-level semantics нельзя навсегда оставить только на стороне supplier.
Возможные поля
CREATE TABLE core.properties (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
property_code VARCHAR(100) UNIQUE NOT NULL,
canonical_name VARCHAR(255) NOT NULL,
property_type VARCHAR(50) NOT NULL,
region_id BIGINT REFERENCES support.regions(id),
location GEOGRAPHY(POINT, 4326),
address_json JSONB NOT NULL DEFAULT '{}',
status VARCHAR(30) NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE core.products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
property_id UUID NOT NULL REFERENCES core.properties(id) ON DELETE CASCADE,
product_code VARCHAR(100) NOT NULL,
product_type VARCHAR(50) NOT NULL,
canonical_name VARCHAR(255) NOT NULL,
occupancy_rules JSONB NOT NULL DEFAULT '{}',
stay_capabilities JSONB NOT NULL DEFAULT '{}',
status VARCHAR(30) NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(property_id, product_code)
);
Зачем нужен core.products
Без промежуточной canonical product model всё будет смешиваться между:
- supplier room types;
- tariff plans;
- occupancy rules;
- offer semantics;
- booking targets.
Контур supplier
Этот слой нужен для хранения supplier-specific representations, а не для подмены core.
Основные таблицы
supplier.propertiessupplier.productssupplier.ratessupplier.content_payloadssupplier.sync_runssupplier.sync_events
Основная идея
Здесь хранится то, как внешний поставщик описывает объект и продукт.
Это нужно для:
- replay;
- diagnostics;
- mapping;
- governance;
- auditability;
- conflict resolution.
Пример
CREATE TABLE supplier.properties (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
supplier_id BIGINT NOT NULL REFERENCES support.suppliers(id),
supplier_property_id VARCHAR(150) NOT NULL,
raw_name VARCHAR(255),
raw_address JSONB,
raw_location GEOGRAPHY(POINT, 4326),
raw_payload JSONB NOT NULL,
normalized_payload JSONB,
sync_status VARCHAR(30) NOT NULL DEFAULT 'active',
last_seen_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(supplier_id, supplier_property_id)
);
CREATE TABLE supplier.products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
supplier_id BIGINT NOT NULL REFERENCES support.suppliers(id),
supplier_property_ref UUID NOT NULL REFERENCES supplier.properties(id) ON DELETE CASCADE,
supplier_product_id VARCHAR(150) NOT NULL,
product_kind VARCHAR(50),
occupancy_model JSONB,
raw_payload JSONB NOT NULL,
normalized_payload JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(supplier_id, supplier_product_id)
);
Связь supplier И core
Связь между supplier layer и canonical layer не должна быть “одним FK без истории”.
Нужен отдельный mapping/decision слой.
Основные таблицы
governance.property_mappingsgovernance.product_mappingsgovernance.mapping_decisions
Почему не просто FK
Потому что платформа должна помнить:
- кто и почему посчитал соответствие корректным;
- было ли решение автоматическим или ручным;
- какая была confidence;
- какие поля вызвали конфликт;
- какой source precedence был применён.
Контур offer
Это один из самых важных persistent-operational контуров платформы.
Основные таблицы
offer.offersoffer.availability_snapshotsoffer.price_snapshotsoffer.quote_sessionsoffer.quotesoffer.quote_repricing_eventsoffer.revalidation_events
offer.offers
offer.offers — не просто кеш ответа поиска. Это структурная operational сущность.
Она должна связывать:
- canonical property / product;
- supplier representation;
- stay interval;
- occupancy;
- cancellation and meal semantics;
- availability reference;
- price reference;
- validity window;
- revalidation requirement.
Пример
CREATE TABLE offer.offers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
core_property_id UUID NOT NULL REFERENCES core.properties(id),
core_product_id UUID REFERENCES core.products(id),
supplier_id BIGINT NOT NULL REFERENCES support.suppliers(id),
supplier_product_id UUID REFERENCES supplier.products(id),
stay_from DATE NOT NULL,
stay_to DATE NOT NULL,
occupancy_json JSONB NOT NULL,
meal_plan_code VARCHAR(50),
cancellation_policy_json JSONB,
availability_snapshot_id UUID,
price_snapshot_id UUID,
validity_until TIMESTAMPTZ,
offer_status VARCHAR(30) NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
offer.availability_snapshots
Нужны для фиксации:
- состояния доступности;
- источника;
- freshness;
- confidence;
- TTL;
- связи с upstream response.
offer.price_snapshots
Нужны для фиксации:
- raw price;
- normalized base price;
- commercial rule output basis, если он нужен до уровня quote;
- currency;
- tax semantics;
- snapshot timestamp;
- freshness;
- source.
offer.quote_sessions
Нужны для перехода между поиском, quote и booking.
Именно здесь должна фиксироваться временная quote-semantic реальность, а не только в UI-памяти или случайном Redis ключе.
Но после фиксации Offer Pricing Booking Semantics — Семантика предложения, цены и бронирования уже недостаточно держать только quote_session как технический мост.
Платформе нужен отдельный persistent слой для Quote как actor-aware коммерческой фиксации.
offer.quotes
offer.quotes должны хранить:
- ссылку на
offer basis; - actor context, в котором quote был создан;
- tenant / organization context;
- price view, коммерческие корректировки и currency context;
- ссылку на применённый
commercial policy set; - breakdown monetary components;
- publication scope / readiness;
- validity window quote;
- revalidation basis;
- repricing basis и причину переоценки;
- quote status и причину потери актуальности;
- trace к последующему
booking, если он был создан.
Именно Quote, а не Offer, должен считаться persistent bridge между operational visibility и transactional commitment.
Что Persistent Model Должна Удерживать Для Quote
На уровне схемы платформа должна быть готова хранить:
source_price;normalized_base_price;platform_markup;agency_markup, если применимо;partner_override, если применимо;service_fee;discount;currency_conversion_adjustment;rounding_adjustment;final_visible_price;- currency/exchange reference;
- commercial policy trace.
Это не означает, что все суммы обязаны стать плоскими колонками в первой миграции. Но persistent model обязана сохранять эти различия как queryable и audit-capable реальность.
offer.quote_repricing_events
После фиксации Commercial Model — Коммерческая модель, цена, settlement и канальные условия и Offer Pricing Booking Semantics — Семантика предложения, цены и бронирования persistent model должна быть готова хранить историю repricing как отдельный след.
Минимально нужно уметь фиксировать:
- какой quote был пересчитан;
- что стало причиной repricing;
- изменилась ли только freshness basis или изменилась денежная структура;
- сохранилась ли коммерческая приемлемость для исходного actor/channel context;
- какой новый quote стал заменой.
Контур booking
Booking должен быть самостоятельным транзакционным слоем.
Основные таблицы
booking.bookingsbooking.booking_itemsbooking.booking_eventsbooking.booking_attemptsbooking.paymentsbooking.refundsbooking.amendmentsbooking.settlement_eventsbooking.settlement_figure_snapshots
booking.bookings
Это не просто финальная запись, а агрегат верхнего уровня.
Он должен ссылаться на:
- offer / quote basis;
- actor context, who initiated;
- tenant / workspace / organization context;
- commercial context;
- applied commercial policy trace;
- supplier booking reference;
- current lifecycle state.
Пример
CREATE TABLE booking.bookings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
booking_code VARCHAR(50) UNIQUE NOT NULL,
quote_session_id UUID REFERENCES offer.quote_sessions(id),
quote_id UUID REFERENCES offer.quotes(id),
offer_id UUID REFERENCES offer.offers(id),
initiator_user_id UUID,
actor_context_json JSONB NOT NULL DEFAULT '{}',
tenant_id UUID,
workspace_id UUID,
agency_id UUID,
partner_id UUID,
supplier_id BIGINT REFERENCES support.suppliers(id),
supplier_booking_reference VARCHAR(150),
booking_status VARCHAR(40) NOT NULL,
commercial_snapshot JSONB NOT NULL DEFAULT '{}',
settlement_basis_snapshot JSONB NOT NULL DEFAULT '{}',
traveller_snapshot JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE booking.booking_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
booking_id UUID NOT NULL REFERENCES booking.bookings(id) ON DELETE CASCADE,
event_type VARCHAR(50) NOT NULL,
event_payload JSONB NOT NULL DEFAULT '{}',
event_time TIMESTAMPTZ NOT NULL DEFAULT now()
);
Почему нужен booking.booking_events
Потому что для production-grade платформы недостаточно иметь только текущее состояние.
Нужна история:
- кто инициировал;
- что ответил supplier;
- где произошёл timeout;
- где была попытка rollback;
- когда произошло подтверждение или отмена.
Почему нужен booking.booking_items
После фиксации Tour Builder и составного продуктового контура платформа не должна исходить из того, что booking всегда является одной монолитной строкой.
Даже если первая production-итерация начнёт с single-offer booking, persistent model должна быть готова хранить:
- несколько booking items;
- item-level supplier references;
- item-level amendment / cancellation semantics;
- связь между booking aggregate и downstream package composition.
booking.settlement_events
booking.settlement_events не должны быть просто “финансовыми логами рядом с booking”.
Они нужны, чтобы отделить:
- quoted commercial promise;
- booking transactional state;
- downstream settlement-relevant financial facts.
Именно здесь persistent model должна позволять хранить события уровня:
- supplier payable recognition;
- agency commission recognition;
- partner revenue-share / fee recognition;
- refund/reversal events;
- reconciliation adjustments.
booking.settlement_figure_snapshots
Платформа должна быть готова хранить не только settlement events, но и снимки согласованной settlement-структуры по booking или booking item.
Это нужно для:
- dispute resolution;
- reporting reproducibility;
- reconciliation traceability;
- объяснения различий между quoted price и downstream economics.
Контур identity
Это отдельный backbone, а не набор вспомогательных user-таблиц.
Основные таблицы
identity.usersidentity.organizationsidentity.tenantsidentity.workspacesidentity.organization_membershipsidentity.role_assignmentsidentity.capability_grantsidentity.api_clientsidentity.api_credentialsidentity.access_policiesidentity.actor_context_audit
Основная модель
Вместо жёсткой привязки “есть users и agencies” платформа должна быть готова к более зрелой модели:
- organization как базовая единица субъекта;
- organization type:
internal,agency,partner; - tenant как boundary of isolation and policy application;
- workspace как рабочий контекст внутри tenant или organization contour;
- users принадлежат организациям через memberships;
- roles и capabilities накладываются через assignment/grant layer;
- API clients живут как отдельные субъекты для machine-to-machine access.
Что Здесь Нельзя Потерять
После появления Tenancy And Identity — Субъекты платформы, изоляция и модель доступа identity-контур нельзя больше описывать как “users + organizations + roles”.
Persistent model должна позволять отдельно хранить:
- устойчивую
identity; - организационный субъект;
- tenant boundary;
- рабочий
actor context; - human access;
- machine access;
- capability scope;
- audit след того, в каком контексте был выполнен action.
Пример
CREATE TABLE identity.organizations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_type VARCHAR(30) NOT NULL,
code VARCHAR(100) UNIQUE NOT NULL,
display_name VARCHAR(255) NOT NULL,
status VARCHAR(30) NOT NULL DEFAULT 'active',
settings JSONB NOT NULL DEFAULT '{}',
commercial_profile JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE identity.tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID REFERENCES identity.organizations(id),
tenant_type VARCHAR(30) NOT NULL,
tenant_code VARCHAR(100) UNIQUE NOT NULL,
status VARCHAR(30) NOT NULL DEFAULT 'active',
policy_profile JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE identity.users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
first_name VARCHAR(150),
last_name VARCHAR(150),
language_code CHAR(2),
timezone_name VARCHAR(64) DEFAULT 'Europe/Kyiv',
user_status VARCHAR(30) NOT NULL DEFAULT 'active',
preferences JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE identity.api_clients (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES identity.tenants(id),
organization_id UUID REFERENCES identity.organizations(id),
client_code VARCHAR(100) UNIQUE NOT NULL,
client_type VARCHAR(30) NOT NULL,
status VARCHAR(30) NOT NULL DEFAULT 'active',
capability_profile JSONB NOT NULL DEFAULT '{}',
quota_profile JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
Контур tour
Tour Builder требует собственного persistent слоя.
Основные таблицы
tour.tour_draftstour.tour_draft_itemstour.tour_draft_alternativestour.tour_proposalstour.proposal_versionstour.proposal_artifacts
Почему нельзя ограничиться bookings.tours
Потому что тур — это не просто ещё одна сущность около booking.
Нужно различать:
- draft-состояние;
- proposal-состояние;
- alternative/variant state;
- состав компонентов;
- history/versioning;
- presentation/export;
- связь с downstream bookings.
Пример
CREATE TABLE tour.tour_drafts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
owner_user_id UUID,
owner_organization_id UUID,
workspace_id UUID,
draft_status VARCHAR(30) NOT NULL DEFAULT 'draft',
title VARCHAR(255) NOT NULL,
context_json JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE tour.tour_draft_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
draft_id UUID NOT NULL REFERENCES tour.tour_drafts(id) ON DELETE CASCADE,
item_type VARCHAR(50) NOT NULL,
source_offer_id UUID REFERENCES offer.offers(id),
source_quote_id UUID REFERENCES offer.quotes(id),
component_payload JSONB NOT NULL DEFAULT '{}',
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
Что Должно Появиться Дальше
Чтобы модель реально соответствовала Tour Builder Domain — Домен композиции, draft/proposal lifecycle и пакетного продукта, persistent contour tour в следующей итерации должен быть доуточнён как минимум сущностями:
tour.tour_draft_alternatives;tour.tour_proposals;tour.proposal_versions;tour.proposal_artifacts;- при необходимости
tour.proposal_audience_linksили аналогичным каналом публикации.
Контур governance
Для промышленной платформы governance не может быть декоративным.
Основные таблицы
governance.property_mappingsgovernance.product_mappingsgovernance.field_lineagegovernance.merge_decisionsgovernance.review_casesgovernance.manual_locksgovernance.anomaliesgovernance.source_precedence_rules
Коммерческий След В Persistent Model
После появления Commercial Model — Коммерческая модель, цена, settlement и канальные условия в persistent model уже нельзя ограничиваться commercial_snapshot JSONB как единственным местом, где “что-то хранится про цену”.
Persistent Model Должна Явно Поддерживать
- commercial policy set or profile reference;
- actor-aware quote context;
- monetary breakdown trace;
- repricing history;
- override origin;
- exchange-rate basis;
- settlement figure lineage.
Почему Это Критично
Без этого платформа не сможет надёжно отвечать на вопросы:
- почему данному агенту была показана именно такая цена;
- чем quote для партнёра отличался от B2C-представления;
- когда изменилась цена и было ли это revalidation или repricing;
- почему quoted price не совпал с settlement-relevant figures;
- какая часть суммы является supplier-derived, а какая platform-derived.
Что Commercial Model Требует От Persistent Model
После фиксации коммерческого контура persistent model должна явно допускать:
- хранение
quoted promiseкак audit-capable сущности; - различие между commercial interpretation и settlement reality;
- ссылочную или структурную фиксацию policy/override context;
- историю repricing и quote invalidation;
- downstream settlement trace, связанный с booking lifecycle, но не слитый с ним.
governance.field_lineage
Нужна для ответа на вопросы:
- откуда пришло конкретное значение;
- какой supplier был источником;
- когда поле обновилось;
- было ли значение подтверждено вручную;
- какой precedence rule был применён.
governance.review_cases
Нужны для human-in-the-loop контуров:
- duplicate review;
- merge review;
- confidence review;
- anomaly resolution;
- content disputes.
После появления Data Governance And Matching — Provenance, merge policy и human-in-the-loop контроль governance-контур уже нельзя считать достаточным, если он хранит только “review tasks”.
Платформе нужны как минимум:
review caseкак доменная единица разбирательства;manual lockкак защита canonical state;merge decisionкак policy-driven outcome;field lineageкак доказуемая история происхождения поля;anomalyкак отдельный сигнал риска, а не просто комментарий к mapping.
Пример
CREATE TABLE governance.review_cases (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
case_type VARCHAR(50) NOT NULL,
entity_type VARCHAR(50) NOT NULL,
entity_id UUID,
priority VARCHAR(20) NOT NULL DEFAULT 'normal',
status VARCHAR(20) NOT NULL DEFAULT 'open',
source_context JSONB NOT NULL DEFAULT '{}',
resolution_payload JSONB,
assigned_to UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
resolved_at TIMESTAMPTZ
);
Что Должно Храниться В PostgreSQL, А Что Нет
Важно не пытаться затащить в PostgreSQL всё подряд без разграничения смысла.
В PostgreSQL должно храниться
- canonical entities;
- supplier representations and lineage;
- offers and quote sessions, когда они нужны как audited operational state;
- booking lifecycle and event history;
- tour draft/proposal/history;
- identities, organizations, API clients;
- governance outcomes and review tasks;
- settlement-relevant events.
В PostgreSQL не обязано храниться как первичный runtime-store
- ultra-short-lived cache results;
- ephemeral session caches;
- derived search result sets, которые можно безопасно восстановить;
- технические transient state, не требующие audit persistence.
Это разграничение затем должно быть дополнительно развито в storage.md.
Ключевые Индексные И Партиционные Направления
Этот документ не должен превращаться в exhaustive index dump, но стратегические направления нужно зафиксировать.
Для core
- уникальные business codes;
- geo-индексы;
- search vectors;
- region and status filters.
Для supplier
(supplier_id, supplier_property_id);(supplier_id, supplier_product_id);last_seen_at;- sync status and replay-related keys.
Для offer
- stay interval;
- supplier and property dimensions;
- validity window;
- snapshot recency;
- quote lookup paths.
Для booking
- booking code;
- created_at / state transitions;
- organization context;
- supplier reference;
- payment and settlement lookup paths.
Для governance
- open review tasks;
- anomalies by type/status;
- unresolved mappings;
- lineage by entity and field.
Геоданные И PostGIS
Геоконтур платформы должен проектироваться осознанно.
Базовый принцип
Если предполагается использование PostGIS-функций, нужно последовательно использовать совместимые типы:
GEOGRAPHY(POINT, 4326)для расстояний и реального геопоиска;GEOMETRYтолько там, где это осознанно необходимо;- не смешивать conceptual PostGIS usage с нативным
POINT, если затем используютсяST_DWithin,ST_Distanceи подобные функции.
Что это меняет
Новая схема не должна повторять старую неоднозначность с POINT, если логика платформы relies on PostGIS semantics.
Что Должно Быть Перепроверено После Этого Документа
После стабилизации этой persistent model нужно будет перепроверить:
-
Storage Layer — Слой хранения данных Там нужно сильнее развести persistent и volatile layers.
-
Ingestion Layer — Слой приёма и обработки Там нужно выровнять ingestion and governance semantics с новой persistent model.
-
Tenancy And Identity — Субъекты платформы, изоляция и модель доступа Там при следующем проходе нужно сверить scope model и persistent representation
tenant / workspace / actor context. -
Offer Pricing Booking Semantics — Семантика предложения, цены и бронирования Там при следующем проходе нужно ещё жёстче связать semantics
Offer / Quote / Bookingс будущим DDL-контуром. -
Tour Builder Domain — Домен композиции, draft/proposal lifecycle и пакетного продукта Там следующая синхронизация должна доуточнить, насколько богатой должна быть модель alternatives, proposal versions и artifacts.
-
Data Governance And Matching — Provenance, merge policy и human-in-the-loop контроль Там нужно ещё плотнее свести governance policy и конкретные persistent entities.
-
Business Services — Сервисная декомпозиция платформы Там service responsibilities уже перестроены, но последующие уточнения модели данных могут потребовать ещё одной синхронизации.
-
API Contracts — OpenAPI 3.0 Specifications Контракты не должны опережать стабилизированную persistent and domain model.
Роль Старых Черновиков
Предыдущая версия этого документа сохранена в:
Её не следует считать актуальной database truth. Но её можно использовать как архив старых решений, примеров DDL, технических идей и промежуточных формулировок, которые могут пригодиться при дальнейшей детализации.
Текущий Практический Вывод
Persistent model платформы должна строиться так:
- не вокруг одной сущности
property; - не вокруг случайного supplier mirror;
- не вокруг раннего списка CRUD-таблиц;
- а вокруг устойчивого разделения между: canonical entities, supplier representations, offers, bookings, tour composition, identity, governance, support reference data.
Если удержать именно эту логику, то дальнейшая детализация SQL, миграций, индексов, API и runtime-контуров будет развиваться как инженерная проработка уже понятной платформы, а не как постоянная борьба с несовместимыми черновиками.
Расширение схемы под Фазы 4–6 каноничной архитектуры (27.04.2026)
После Фаз 4–6 опубликованы 16 новых каноничных доменных и операционных документов, вводящих новые сущности, требующие persistent storage. Эта секция перечисляет каждый такой домен и каноничные сущности, для которых требуется DDL расширение текущей схемы.
Документ остаётся на версии 2.0 как working baseline, но рассматривается как incomplete до интеграции расширений ниже. Полная DDL миграция — задача стадии 1 implementation baseline (см. development/roadmap.md).
Новые сущности по доменам Фазы 4
Платёжный домен (см. payment-domain.md)
payment_intent— намерение платежа (создаётся перед оплатой);payment_transaction— фактическая транзакция через PSP;refund— возврат средств;chargeback— возврат средств через банк customer;settlement— расчёт между платформой и поставщиком;payout_batch— партия выплат поставщикам;payment_method— токенизированный способ оплаты (только PSP token, никогда PAN).
Особенности DDL:
- card data никогда не хранится на платформе (только PSP tokens);
- все суммы в
numeric(15,4)для precision; - audit log per state transition (Tier 1 retention 7 лет).
Программный интерфейс как продукт (см. api-as-product.md)
partner— партнёр платформы;partner_application— конкретная интеграция партнёра (может быть несколько per partner);partner_environment— sandbox vs production;partner_certification— статус сертификации;api_key(partner-scoped, hashed);api_key_usage_event(для metering);partner_tier_subscription(Free / Starter / Professional / Enterprise);partner_quota_state(текущее потребление per tier).
Поиск и обнаружение (см. search-and-discovery.md)
search_projection— материализованное представление для поиска (read model);ranking_policy— конфигурация ranking per tenant;search_facet_definition— структура фасетов;search_query_log— для analytics и A/B (truncated после k-anonymization).
Особенности: search projections могут жить в отдельном search engine (Elasticsearch / Meilisearch / Typesense), не обязательно в PostgreSQL.
Платформа данных и трекинг событий (см. data-platform-and-events-tracking.md)
domain_event— общий event store (append-only);tracking_event— analytical events (отдельный поток);event_consumer_offset— для durable subscriptions;event_replay_marker— для replay tooling.
Особенности: domain events — append-only с partitioning по времени; tracking events часто живут в DWH columnar storage (ClickHouse / BigQuery), не в OLTP.
Платформа машинного обучения (см. ml-platform.md)
feature_value(feature store);model_registry_entry;model_serving_deployment;prediction_log(для quality monitoring).
Особенности: feature store часто отдельно от OLTP (Feast / Hopsworks / собственное).
Платформа A/B-тестирования (см. ab-testing-platform.md)
experiment;experiment_variant;experiment_assignment(актор → variant);exposure_event;feature_flag;feature_flag_evaluation.
Уведомления и коммуникации (см. notification-and-communication.md)
notification_template;notification_event;notification_channel_delivery(email/SMS/push/in-app/webhook);consent_log(GDPR-grade);webhook_subscription;webhook_delivery_attempt.
Медиа и контент (см. media-and-content.md)
media_asset;content_bundle;content_translation;media_transform_cache_entry.
Интернационализация и локализация (см. internationalization-and-localization.md)
supported_language;supported_currency;fx_rate_snapshot;translation_string(UI strings).
Аналитика и BI (см. analytics-and-bi.md)
- analytical projections в DWH (отдельный storage class);
analytics_export_job;analytics_query_log(audit для compliance).
Экономическая модель (см. economic-model.md)
revenue_record;cost_category;tenant_economic_profile;pricing_plan.
Новые сущности по доменам Фазы 5
Операционная модель Tour Builder (см. tour-builder-operational-model.md)
composition_rule;tour_composition_draft;tour_booking_transaction(saga state);drift_event(когда композиция расходится с supplier reality);compensation_event(saga compensations).
Статусная машина бронирования (см. booking-state-machine.md)
booking_state_transition(audit log всех переходов);- 14 каноничных states как enum или reference table;
unknown_external_state_recovery— для случаев несинхронности с supplier.
Существующая booking table расширяется полями:
current_state(enum);state_entered_at;unknown_external_state_since(nullable, для recovery tracking).
Сила тенантной изоляции (см. multi-tenant-isolation-strength.md)
isolation_boundary_check_record(continuous automated test);cross_tenant_access_audit(immutable log если cross-tenant access нужен);- расширение
tenanttable полямиisolation_level(logical / dedicated_compute / dedicated_infrastructure).
Новые сущности по доменам Фазы 6 (operations)
Runbooks (см. runbooks-incident-playbooks.md)
incident(canonical incident record);incident_runbook_execution(audit запуска runbook);incident_postmortem.
SLA и дежурства (см. sla-and-on-call-model.md)
sla_class;sla_obligation;sla_measurement(per metric per period);service_credit;sla_breach_incident;oncall_schedule;oncall_handover.
Восстановление после аварий (см. disaster-recovery-and-capacity.md)
recovery_class(enum: Tier 1/2/3/4);backup_policy;recovery_point;dr_drill;capacity_forecast.
Новые сущности из safety / compliance / security доменов
Соответствие требованиям (см. compliance-and-legal.md)
data_field_inventory(lawful basis per поле);retention_policy_execution_log;data_subject_request_log;privacy_impact_assessment;data_processor_agreement;breach_notification_log.
Архитектура безопасности (см. security-architecture.md)
audit_log(immutable, append-only) — single canonical table для всех security audit events;security_incident;vulnerability(vulnerability tracking);secret_rotation_log;mfa_enrollment(per actor);session(active sessions, для revocation).
Каноничные классы хранения (storage tier mapping)
Каждая сущность привязана к recovery class из disaster-recovery-and-capacity.md:
Tier 1 (RTO 60м, RPO 5м):
- payment_intent, payment_transaction, refund, chargeback, settlement, payout_batch;
- booking, booking_state_transition;
- audit_log, security_incident;
- partner_clearing_record;
- user, tenant, service_account, session;
- revenue_record, tax_record;
- consent_log;
- breach_notification_log.
Tier 2 (RTO 4ч, RPO 30м):
- offer, offer_pricing_snapshot, quote;
- search_projection, ranking_policy;
- partner, partner_application, api_key (hashed);
- notification_event, webhook_subscription;
- tour_composition_draft, composition_rule;
- isolation_boundary_check_record;
- sla_obligation, sla_measurement.
Tier 3 (RTO 24ч, RPO 1ч):
- domain_event stream (за rolling window);
- tracking_event;
- analytics_query_log;
- experiment_assignment, exposure_event;
- prediction_log, feature_value;
- media_transform_cache_entry.
Tier 4 (RTO 7д, archived):
- audit_log старше 7 лет;
- analytics_aggregated старше 2 лет;
- archived booking / payment records старше 7 лет.
Принципы DDL миграции
При имплементации стадии 1:
- Phased migration — не all-at-once, по доменам с приоритетом Tier 1 (payment + booking + audit) первыми;
- Forward-safe migrations — каждая миграция совместима с running code (no breaking changes within deployment window);
- Rollback considered — для каждой миграции явный rollback plan;
- Idempotency — повторное применение safe;
- Audit trail — каждая миграция логирована;
- Performance review — migrations на больших таблицах (booking, audit_log) с явным planning (online schema changes через pg_repack или equivalent).
Связь с tenant isolation
Каждая новая table обязана:
- иметь
tenant_idcolumn для logical isolation tenants; - иметь Row-Level Security (RLS) policies для cross-tenant defense in depth;
- быть проверяемой через
IsolationBoundaryCheck(см. multi-tenant-isolation-strength.md).
Для dedicated_compute уровня — отдельные schemas per tenant; для dedicated_infrastructure — отдельные database instances.
Связь с security architecture
Согласно security-architecture.md, каждая table:
- sensitive PII columns шифруются через app-layer encryption (per-tenant key);
audit_logimmutable (WORM-grade storage, append-only API);- доступы через RBAC + ABAC, проверяются на каждом query;
- backup encrypted at-rest;
- доступ к production database только через bastion с MFA + audit logging.
Открытые вопросы для DDL фазы стадии 1
- Search engine choice — Elasticsearch (full-featured) vs Meilisearch (simpler, faster) vs Typesense vs PostgreSQL native FTS. Влияет на DDL schema для search_projection.
- DWH choice — ClickHouse (open-source columnar) vs BigQuery (managed) vs Redshift vs DuckDB (для analytics). Влияет на DDL для tracking_event и analytics.
- Feature store — Feast (open-source) vs Hopsworks vs собственное. Влияет на DDL для feature_value.
- Event store — собственное в PostgreSQL vs EventStore vs Kafka log vs managed (Confluent). Влияет на DDL для domain_event.
- Per-tenant encryption key strategy — KMS managed vs Vault transit secrets engine vs application-managed. Влияет на DDL для encrypted columns.
- Audit log storage — separate immutable storage (S3 Object Lock, ClickHouse, dedicated PostgreSQL replica) или в основной БД с triggers blocking updates. Решение влияет на implementation cost.
Эти решения принимаются в стадии 1 implementation baseline после первого supplier integration prototype.
Уточнение под Ось 6 (28.04.2026) — связь с home-to-go-api первой реализацией
Этот документ описывает каноничную persistent model платформы Vitiana. Каноничная архитектура зафиксирована «от целей платформы» (правило 00000), но платформа строится поверх уже работающего ядра home-to-go-api (= stuba-api в раннем naming), не green-field. Эта секция фиксирует обязательные связи с реальной реализацией.
Главный мост — relation-to-implementation-baseline.md
Полная карта соответствия каноничной модели и реальных таблиц home-to-go-api — в overview/relation-to-implementation-baseline.md. Этот документ — источник истины Оси 6. При расхождении между этим документом (database-schema.md) и реальностью home-to-go-api — мост авторитетен.
Что уже задеплоено в home-to-go-api
Реальная схема в apivitianadb | DDL дата | Каноничная группа сущностей | Состояние |
|---|---|---|---|
hotels schema | 2026-04-16 | Canonical Master + Supplier-derived (смешано) | Первая итерация; требует разделения |
usr_* (18 таблиц) | первая итерация | Identity / Tenancy / Access | Не покрывает full multi-tenant модель |
events_themes | 2026-04-24 | Domain events + analytical events (смешано) | Долг 8 — требует разделения |
locations + translations (ru/uk) + location_supplier_map | первая итерация | Geo (часть Property context) | Хорошее покрытие |
| Amenity справочник (145 концептов + 224 supplier_map) | первая итерация | часть Property feature catalog | Соответствует канону |
Production реальность:
- БД:
apivitianadbPostgreSQL 18.1, хостvitianaapipg.psql.tools:10167; - 127 304 отелей загружены, 4.6M фотографий;
- 182 страны синхронизированы;
- production endpoint
https://api.vitrip.store/stuba— Stuba adapter, не Partner API Surface.
Технические долги схемы (источник — relation-to-implementation-baseline.md)
| Долг | Описание | Целевая каноничная сущность |
|---|---|---|
| Долг 1 | Single-tenant apivitianadb без tenant_id | требуется добавление tenant_id во все таблицы + RLS policies |
| Долг 2 | hotels schema — Stuba-shaped, смешивает canonical Property + SupplierProperty | требуется разделение на canonical Property + отдельный SupplierProperty layer |
| Долг 3 | Нет event-driven архитектуры (синхронная REST) | требуется event store baseline — Redis Streams (Phase 1) → NATS Jetstream (Phase 3) |
| Долг 4 | PHP backend для core flows (включая платежи) | требуется миграция критических path в TypeScript/Go (см. proposal-stack-roadmap-by-product-tier.md) |
| Долг 5 | Frontend Preact + HTM (ограниченный для rich UI) | требуется Next.js для B2C + Vite SPA для admin/Tour Builder |
| Долг 6 | Отсутствует formal observability stack | требуется Prometheus + Grafana + Loki + Tempo (Phase 1) |
| Долг 7 | Manual / partial CI/CD | требуется formal CI/CD на стадии 1 |
| Долг 8 | events_themes смешивает domain + analytical events | требуется разделение через data-platform-and-events-tracking.md |
Каноничный путь конвергенции
Согласно overview/relation-to-implementation-baseline.md, каноничный путь:
- Strangler Fig pattern — новые services пишутся вокруг existing system, постепенно заменяя legacy paths;
- Canonical model first, then implementation — перед coding каждого нового domain — фиксируется каноничная schema (этот документ);
- Tenant isolation от первого commit — любой новый код multi-tenant aware;
- No hardcoded supplier logic — adapter pattern, captive Stuba-specific logic в общем коде запрещена (правило 00000);
- Security by design — каждый новый code path проходит security review;
- Compliance by design — каждая обработка PII проходит privacy review.
Что home-to-go-api даёт стартовый капитал
Несмотря на gaps выше, home-to-go-api — значительный стартовый капитал:
- 127K hotels + 4.6M фото — реальные данные для testing canonical Property model;
- Geo chain (182 страны) — i18n / geo data ready;
- Stuba integration в production — реальный test case для Supplier модели;
- Production endpoint с реальным traffic — operational baseline;
- PostgreSQL 18.1 baseline — current стек для каноничной БД.
Связь с расширением схемы под Фазы 4–6
В этом документе уже опубликована секция «Расширение схемы под Фазы 4–6 каноничной архитектуры (27.04.2026)» — она перечисляет новые сущности доменов фаз 4–6 (Payment, API as Product, Tour Builder operational, Booking state machine, Multi-Tenant Isolation, Compliance, Security и т.д.) и storage tier mapping.
Эта секция дополняет ту: расширение определяет target schema; уточнение Оси 6 определяет path конвергенции от current apivitianadb к target.
Каноничный итог уточнения
Этот документ описывает target canonical schema. Path implementation:
- Карта соответствия target vs current → overview/relation-to-implementation-baseline.md;
- Strangler Fig pattern для миграции → reference там же;
- 8 каноничных tech debts → reference там же;
- DDL phasing для новых доменов → секция «Расширение схемы под Фазы 4–6» в этом документе;
- Implementation slices для первого release unit → стадия 1 implementation baseline (development/roadmap.md).
При имплементации использовать этот документ как target schema, relation-to-implementation-baseline.md как карту current state и path конвергенции.
Уточнение выполнено через no-destruction.