Перейти к основному содержимому

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, индексов, партиционирования и миграций.

Опорные документы

Этот документ следует читать вместе со следующими материалами:

Что Исправляет Новый Подход

Предыдущая версия схемы страдала от нескольких системных проблем:

  • слишком сильная property-centric модель;
  • недостаточно выраженная offer-level модель;
  • смешение stable и volatile data;
  • ранняя фиксация конкретной SQL-формы там, где домен ещё не был стабилизирован;
  • размазывание identity, commercial и governance-смысла по нескольким таблицам без жёсткого центра;
  • недостаточная различимость между supplier representation, canonical entity, quote-time state и booking-time state.

Новая версия схемы исходит из другой логики:

  1. сначала определить уровни истины;
  2. затем закрепить канонические сущности;
  3. затем разделить persistent и volatile контуры;
  4. затем разложить модель по устойчивым схемам;
  5. только после этого уточнять конкретные SQL implementation details.

Что Второй Несущий Круг Требует От Persistent Model

После появления документов второго круга database-schema.md уже нельзя читать как просто общий storage baseline. Теперь он обязан явно удерживать следующие смысловые требования:

Практический вывод из этого простой:

если 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.suppliers
  • support.regions
  • support.currencies
  • support.exchange_rates
  • support.commercial_policy_sets
  • support.classifiers
  • support.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.properties
  • core.property_content
  • core.property_classifications
  • core.products
  • core.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.properties
  • supplier.products
  • supplier.rates
  • supplier.content_payloads
  • supplier.sync_runs
  • supplier.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_mappings
  • governance.product_mappings
  • governance.mapping_decisions

Почему не просто FK

Потому что платформа должна помнить:

  • кто и почему посчитал соответствие корректным;
  • было ли решение автоматическим или ручным;
  • какая была confidence;
  • какие поля вызвали конфликт;
  • какой source precedence был применён.

Контур offer

Это один из самых важных persistent-operational контуров платформы.

Основные таблицы

  • offer.offers
  • offer.availability_snapshots
  • offer.price_snapshots
  • offer.quote_sessions
  • offer.quotes
  • offer.quote_repricing_events
  • offer.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.bookings
  • booking.booking_items
  • booking.booking_events
  • booking.booking_attempts
  • booking.payments
  • booking.refunds
  • booking.amendments
  • booking.settlement_events
  • booking.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.users
  • identity.organizations
  • identity.tenants
  • identity.workspaces
  • identity.organization_memberships
  • identity.role_assignments
  • identity.capability_grants
  • identity.api_clients
  • identity.api_credentials
  • identity.access_policies
  • identity.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_drafts
  • tour.tour_draft_items
  • tour.tour_draft_alternatives
  • tour.tour_proposals
  • tour.proposal_versions
  • tour.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_mappings
  • governance.product_mappings
  • governance.field_lineage
  • governance.merge_decisions
  • governance.review_cases
  • governance.manual_locks
  • governance.anomalies
  • governance.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 нужно будет перепроверить:

Роль Старых Черновиков

Предыдущая версия этого документа сохранена в:

Её не следует считать актуальной 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 нужен);
  • расширение tenant table полями 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:

  1. Phased migration — не all-at-once, по доменам с приоритетом Tier 1 (payment + booking + audit) первыми;
  2. Forward-safe migrations — каждая миграция совместима с running code (no breaking changes within deployment window);
  3. Rollback considered — для каждой миграции явный rollback plan;
  4. Idempotency — повторное применение safe;
  5. Audit trail — каждая миграция логирована;
  6. Performance review — migrations на больших таблицах (booking, audit_log) с явным planning (online schema changes через pg_repack или equivalent).

Связь с tenant isolation

Каждая новая table обязана:

  • иметь tenant_id column для 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_log immutable (WORM-grade storage, append-only API);
  • доступы через RBAC + ABAC, проверяются на каждом query;
  • backup encrypted at-rest;
  • доступ к production database только через bastion с MFA + audit logging.

Открытые вопросы для DDL фазы стадии 1

  1. Search engine choice — Elasticsearch (full-featured) vs Meilisearch (simpler, faster) vs Typesense vs PostgreSQL native FTS. Влияет на DDL schema для search_projection.
  2. DWH choice — ClickHouse (open-source columnar) vs BigQuery (managed) vs Redshift vs DuckDB (для analytics). Влияет на DDL для tracking_event и analytics.
  3. Feature store — Feast (open-source) vs Hopsworks vs собственное. Влияет на DDL для feature_value.
  4. Event store — собственное в PostgreSQL vs EventStore vs Kafka log vs managed (Confluent). Влияет на DDL для domain_event.
  5. Per-tenant encryption key strategy — KMS managed vs Vault transit secrets engine vs application-managed. Влияет на DDL для encrypted columns.
  6. 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

Реальная схема в apivitianadbDDL датаКаноничная группа сущностейСостояние
hotels schema2026-04-16Canonical Master + Supplier-derived (смешано)Первая итерация; требует разделения
usr_* (18 таблиц)первая итерацияIdentity / Tenancy / AccessНе покрывает full multi-tenant модель
events_themes2026-04-24Domain events + analytical events (смешано)Долг 8 — требует разделения
locations + translations (ru/uk) + location_supplier_mapпервая итерацияGeo (часть Property context)Хорошее покрытие
Amenity справочник (145 концептов + 224 supplier_map)первая итерациячасть Property feature catalogСоответствует канону

Production реальность:

  • БД: apivitianadb PostgreSQL 18.1, хост vitianaapipg.psql.tools:10167;
  • 127 304 отелей загружены, 4.6M фотографий;
  • 182 страны синхронизированы;
  • production endpoint https://api.vitrip.store/stubaStuba adapter, не Partner API Surface.

Технические долги схемы (источник — relation-to-implementation-baseline.md)

ДолгОписаниеЦелевая каноничная сущность
Долг 1Single-tenant apivitianadb без tenant_idтребуется добавление tenant_id во все таблицы + RLS policies
Долг 2hotels 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)
Долг 4PHP backend для core flows (включая платежи)требуется миграция критических path в TypeScript/Go (см. proposal-stack-roadmap-by-product-tier.md)
Долг 5Frontend Preact + HTM (ограниченный для rich UI)требуется Next.js для B2C + Vite SPA для admin/Tour Builder
Долг 6Отсутствует formal observability stackтребуется Prometheus + Grafana + Loki + Tempo (Phase 1)
Долг 7Manual / partial CI/CDтребуется formal CI/CD на стадии 1
Долг 8events_themes смешивает domain + analytical eventsтребуется разделение через data-platform-and-events-tracking.md

Каноничный путь конвергенции

Согласно overview/relation-to-implementation-baseline.md, каноничный путь:

  1. Strangler Fig pattern — новые services пишутся вокруг existing system, постепенно заменяя legacy paths;
  2. Canonical model first, then implementation — перед coding каждого нового domain — фиксируется каноничная schema (этот документ);
  3. Tenant isolation от первого commit — любой новый код multi-tenant aware;
  4. No hardcoded supplier logic — adapter pattern, captive Stuba-specific logic в общем коде запрещена (правило 00000);
  5. Security by design — каждый новый code path проходит security review;
  6. 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.