Проектирование БД MVP¶
Статус: согласованный проект; foundation-срез реализован в Prisma schema и первой миграции.
Целевая СУБД — PostgreSQL 18, доступ из приложения — Prisma 7. Модель покрывает только backend MVP из утверждённого scope. Админ-панель, admin role, чат, Telegram, календарь и видеосвязь в схему не включены.
1. Главные решения¶
- Один PostgreSQL database и schema
publicдля модульного монолита. Владение таблицами разделено по бизнес-модулям, а не физическими PostgreSQL schemas. - Основные идентификаторы — UUID. Внешние токены, OTP и refresh tokens никогда не хранятся открытым текстом: сохраняется только digest/HMAC.
- Все даты событий —
TIMESTAMPTZв UTC. Локальная дата/время рождения хранится отдельно вместе с IANA timezone и фактически применённым UTC offset. - Деньги —
BIGINTв минимальных единицах валюты. Для RUB это копейки.FLOAT/DOUBLE PRECISIONзапрещены. - Успешные расчёты, AI-результаты, финансовые факты, согласия и audit events неизменяемы. Исправление создаёт новую версию или компенсирующую запись.
- JSONB используется только для версионированных payload/result snapshots. Поля, по которым нужны связи, права, уникальность, состояния или поиск, остаются реляционными колонками.
- Временные методики
v0не влияют на структуру: версия engine, schema, resource package и hash сохраняются с каждым артефактом. - Конкретная AI-модель не является справочником домена. В генерации сохраняется строковый
provider_key + model_snapshot. - Каталог допускает несколько продуктов и цен. Ограничение «один активный тариф MVP» задаётся seed-данными, а не ограничением схемы.
- Удаление пользователя — управляемая анонимизация. Финансовые и обязательные audit-записи не удаляются каскадом.
2. Конвенции PostgreSQL и Prisma¶
| Область | Решение |
|---|---|
| Имена БД | snake_case во множественном числе |
| Имена Prisma | PascalCase; физическое имя задаётся через @@map/@map |
| Primary key | id UUID DEFAULT gen_random_uuid(); Prisma использует dbgenerated |
| Время | TIMESTAMPTZ(3); локальное время — DATE + TIME(0) |
| Hash | CHAR(64) для SHA-256 hex либо BYTEA для HMAC/digest |
| Деньги | amount_minor BIGINT + currency CHAR(3); AI cost telemetry — cost_micros BIGINT |
| Координаты | NUMERIC(9,6) latitude и NUMERIC(9,6) longitude |
| Оптимистическая блокировка | state_version INTEGER NOT NULL DEFAULT 0 на изменяемых state machine records |
| JSON | JSONB + обязательные schema_version/payload_version |
| Soft delete | только там, где нужен жизненный цикл; универсального deleted_at у каждой таблицы нет |
| Enum | PostgreSQL/Prisma enum для закрытых состояний; новое значение добавляется миграцией |
Общие created_at и updated_at ниже не повторяются в каждом описании. updated_at есть только у изменяемых
агрегатов; immutable-таблицам достаточно created_at.
В MVP используются отдельные database roles: migration owner с DDL, runtime role только с нужным DML и backup/monitoring role без прикладных write-прав. PostgreSQL RLS на первом этапе не включается: object-level authorization выполняют service use cases, а integration tests проверяют доступ каждого актора. Перед включением RLS нужно отдельно проверить его взаимодействие с Prisma connection pooling и background workers.
3. Владение таблицами¶
| Модуль | Таблицы |
|---|---|
auth |
users, user_roles, otp_challenges, auth_rate_buckets, sessions |
users |
legal_document_versions, consent_acceptances, onboarding_drafts, place_snapshots, birth_profiles |
calculations |
calculation_engine_releases, calculation_sets, calculation_set_items, calculation_artifacts, evidence_facts |
interpretations |
content_releases, forecast_inputs, interpretation_requests, interpretation_sources, ai_generation_attempts, interpretation_artifacts |
billing |
products, product_features, prices, orders, order_items, subscriptions, subscription_invoices, payment_method_references, payment_attempts, provider_events, entitlements, refund_requests, refunds, ledger_transactions, ledger_entries, practitioner_settlements |
goals |
goals, goal_check_ins |
practitioners |
practitioner_profiles, specialties, practitioner_specialties, practitioner_services, practitioner_documents, practitioner_cases |
consultations |
consultations, consultation_time_options, consultation_transitions, consultation_orders |
notifications |
notification_preferences, notification_deliveries, notification_attempts |
analytics |
anonymous_visitors, attribution_touches, analytics_events |
operations |
idempotency_records, outbox_events, jobs, domain_audit_events, privacy_requests |
4. Identity, onboarding и данные рождения¶
erDiagram
USERS ||--o{ USER_ROLES : has
USERS ||--o{ SESSIONS : owns
USERS ||--o{ BIRTH_PROFILES : owns
USERS ||--o{ CONSENT_ACCEPTANCES : accepts
ONBOARDING_DRAFTS ||--o| BIRTH_PROFILES : creates
ONBOARDING_DRAFTS ||--o{ CONSENT_ACCEPTANCES : records
ONBOARDING_DRAFTS }o--o| USERS : claimed_by
PLACE_SNAPSHOTS ||--o{ BIRTH_PROFILES : used_by
LEGAL_DOCUMENT_VERSIONS ||--o{ CONSENT_ACCEPTANCES : accepted_as
users¶
| Поле | Тип/правило |
|---|---|
id |
UUID PK |
email_normalized |
VARCHAR(320) NOT NULL UNIQUE; lower/trim нормализация в application service |
status |
ACTIVE \| BLOCKED \| ANONYMIZED |
locale |
VARCHAR(10) NOT NULL DEFAULT 'ru' |
anonymized_at |
nullable; обязателен при ANONYMIZED |
E-mail меняется только отдельным подтверждённым flow, которого пока нет в публичном API. При анонимизации поле
заменяется уникальным техническим адресом вида deleted+<uuid>@invalid.local.
user_roles¶
Поля: user_id FK, role CLIENT|PRACTITIONER, granted_at. Primary key — (user_id, role). Admin-значения нет.
Роль практика выдаёт закрытый seed/операционный процесс после создания practitioner_profiles.
otp_challenges¶
Поля: id, purpose LOGIN, email_normalized, code_digest BYTEA, expires_at, resend_available_at,
attempt_count, max_attempts DEFAULT 5, consumed_at, blocked_at, request_ip_hash, user_agent_hash.
Ограничения и индексы:
expires_at > created_at,attempt_count BETWEEN 0 AND max_attempts;- индекс
(email_normalized, created_at DESC)для cooldown и поиска активного challenge; - частичный индекс по
expires_atдля очистки неиспользованных записей; - в БД нет исходного кода OTP и полного IP.
auth_rate_buckets¶
PostgreSQL-реализация anti-abuse без Redis: scope, subject_hash, window_started_at, counter,
blocked_until. Primary key (scope, subject_hash, window_started_at). Увеличение выполняется атомарным UPSERT.
Scope: OTP_EMAIL, OTP_IP, OTP_DEVICE, LOGIN_EMAIL.
sessions¶
Поля: id, user_id, token_digest BYTEA UNIQUE, family_id UUID, rotated_from_id self FK, expires_at,
last_seen_at, revoked_at, revoke_reason, reuse_detected_at, ip_hash, user_agent_hash.
Refresh rotation создаёт новую строку и отзывает старую в одной транзакции. Повтор уже использованного токена отзывает
всю family_id.
legal_document_versions¶
Поля: id, document_type TERMS|PRIVACY|PERSONAL_DATA|RECURRING_PAYMENT|DISCLAIMER, version, locale,
content_hash, public_url, effective_from, retired_at. Unique:
(document_type, version, locale).
consent_acceptances¶
Immutable-поля: id, document_version_id, user_id nullable, onboarding_draft_id nullable, accepted_at,
ip_hash, user_agent_hash, source.
SQL CHECK требует хотя бы одного владельца. До регистрации заполнен draft; при claim добавляется user_id, но
accepted_at и версия документа не меняются. Unique отдельно для (user_id, document_version_id) и
(onboarding_draft_id, document_version_id).
onboarding_drafts¶
Поля: id, public_token_digest BYTEA UNIQUE, status OPEN|CLAIMED|EXPIRED, expires_at, claimed_by_user_id,
claimed_at, attribution_visitor_id, request_ip_hash.
Публичный token показывается один раз. Claim возможен только для OPEN и неистёкшей записи под row lock. В draft не
дублируются дата рождения и результаты — они находятся в его versioned birth_profiles и calculation sets.
place_snapshots¶
Immutable-поля: id, input_label, resolved_label, country_code, latitude, longitude, timezone_id,
provider_key, provider_place_id, provider_version, tzdata_version, resolved_at.
CHECK: latitude [-90,90], longitude [-180,180], непустой IANA timezone_id. Обновление ответа провайдера создаёт
новый snapshot.
birth_profiles¶
| Поле | Назначение |
|---|---|
id |
UUID PK |
user_id |
nullable для гостевого draft, FK users |
source_draft_id |
nullable FK onboarding_drafts |
revision |
положительный integer |
previous_profile_id |
nullable self FK |
is_current |
текущая версия профиля пользователя |
birth_name |
исходная строка имени для нумерологии |
local_birth_date |
DATE NOT NULL |
local_birth_time |
nullable TIME(0) |
time_accuracy |
EXACT \| UNKNOWN |
place_snapshot_id |
FK place_snapshots |
resolved_utc_instant |
nullable TIMESTAMPTZ |
resolved_utc_offset_seconds |
nullable integer |
normalization_version |
версия time/place normalization |
CHECK:
- при
EXACTзаполнены local time, UTC instant и offset; - при
UNKNOWNэти три поля пусты; - заполнен хотя бы один owner:
user_idилиsource_draft_id; - partial unique
(user_id) WHERE is_current = true AND user_id IS NOT NULL; - partial unique
(source_draft_id) WHERE is_current = true AND source_draft_id IS NOT NULL; - unique
(user_id, revision)и(source_draft_id, revision).
Редактирование даты, времени, имени или места создаёт новую строку и переключает is_current в одной транзакции —
как для draft, так и после claim.
5. Детерминированные расчёты¶
erDiagram
BIRTH_PROFILES ||--o{ CALCULATION_SETS : calculated_as
CALCULATION_SETS ||--|{ CALCULATION_SET_ITEMS : contains
CALCULATION_ENGINE_RELEASES ||--o{ CALCULATION_SET_ITEMS : requested_with
CALCULATION_ENGINE_RELEASES ||--o{ CALCULATION_ARTIFACTS : produced_by
CALCULATION_SET_ITEMS }o--o| CALCULATION_ARTIFACTS : resolves_to
CALCULATION_ARTIFACTS ||--o{ EVIDENCE_FACTS : exposes
calculation_engine_releases¶
Справочник доступных реализаций: id, system ASTROLOGY|NUMEROLOGY|HUMAN_DESIGN|DESTINY_MATRIX,
engine_key, methodology_version, output_schema_version, code_version, status PROVISIONAL|APPROVED|RETIRED,
resource_manifest JSONB, resource_hash, activated_at, retired_at.
Unique: (system, methodology_version, output_schema_version, resource_hash). Для natal-v1 resource manifest
содержит Swiss version, ephemeris dataset и flags; для v0 — hash таблиц букв/gates/arcanes.
calculation_sets¶
Поля: id, birth_profile_id, orchestration_version, normalized_input_hash, status,
completed_at, state_version.
Статусы: QUEUED|RUNNING|SUCCEEDED|PARTIALLY_SUCCEEDED|FAILED. Unique:
(birth_profile_id, orchestration_version, normalized_input_hash). Aggregate status вычисляется из items, но
материализуется для дешёвого API-чтения.
calculation_set_items¶
Поля: id, calculation_set_id, system, engine_release_id,
status QUEUED|RUNNING|SUCCEEDED|FAILED|BLOCKED, artifact_id nullable,
blocked_reason, last_error_code, last_error_safe_message, started_at, completed_at, state_version.
Unique (calculation_set_id, system). Для неизвестного времени astrology и HD создаются в BLOCKED с причиной
EXACT_TIME_REQUIRED; нумерология и Матрица выполняются.
calculation_artifacts¶
Только успешный immutable-результат: id, engine_release_id, normalized_input_hash, input_schema_version,
result_json JSONB, result_hash, computed_at, duration_ms.
Unique (engine_release_id, normalized_input_hash) реализует content-addressed cache. Artifact не имеет прямого
user_id: доступ проверяется только через calculation_set_items → calculation_sets → birth_profiles. При удалении
последней ссылки artifact может быть очищен retention job.
evidence_facts¶
Поля: id, artifact_id, fact_id, fact_type, extractor_version, value_json JSONB, value_hash.
Unique (artifact_id, fact_id, extractor_version). Оси, веса и совместимость сюда не записываются: они принадлежат
versioned evidence mapping в content_releases.
6. AI-интерпретации и прогнозы¶
erDiagram
USERS ||--o{ INTERPRETATION_REQUESTS : requests
CALCULATION_SETS ||--o{ INTERPRETATION_REQUESTS : interprets
FORECAST_INPUTS }o--|| BIRTH_PROFILES : for
INTERPRETATION_REQUESTS ||--o{ INTERPRETATION_SOURCES : freezes
CALCULATION_ARTIFACTS ||--o{ INTERPRETATION_SOURCES : source
INTERPRETATION_REQUESTS ||--o{ AI_GENERATION_ATTEMPTS : attempts
INTERPRETATION_REQUESTS }o--o| INTERPRETATION_ARTIFACTS : publishes
CONTENT_RELEASES ||--o{ INTERPRETATION_REQUESTS : configures
content_releases¶
Versioned seeded content без admin CRUD: id, kind EVIDENCE_MAPPING|PROMPT|SAFETY_POLICY|DISCLAIMER|EMAIL_TEMPLATE,
key, version, locale, status DRAFT|ACTIVE|RETIRED, body_json JSONB, content_hash, activated_at,
retired_at. Unique (kind, key, version, locale).
Partial unique (kind, key, locale) WHERE status = ACTIVE запрещает две одновременно активные редакции одного
контента.
Одновременно активная версия не означает перезапись старой: interpretation/notification всегда ссылаются на точный release ID.
forecast_inputs¶
Immutable детерминированный слой: id, user_id, birth_profile_id, period_kind DAY|WEEK|MONTH,
period_start, period_end, algorithm_version, input_hash, result_json, result_hash.
Unique (birth_profile_id, period_kind, period_start, algorithm_version, input_hash).
interpretation_requests¶
Поля: id, user_id, calculation_set_id, forecast_input_id nullable,
kind CORE_READING|ROUTE|FORECAST, period_start/end nullable, locale,
status QUEUED|RUNNING|READY|FAILED|REJECTED, provider_key_snapshot,
model_snapshot, context_schema_version, context_snapshot_json, context_hash, generation_key,
evidence_mapping_release_id, prompt_release_id, safety_policy_release_id, artifact_id nullable,
last_error_code, completed_at, state_version.
CHECK: период и forecast_input_id обязательны только для FORECAST. Unique (user_id, generation_key) делает
запрос пользователя идемпотентным, а глобально уникальный artifact cache остаётся в interpretation_artifacts.
Generation key включает hashes calculation/forecast sources, context_hash, kind/period, releases,
provider/model snapshot и locale.
Versioned context snapshot фиксирует только разрешённый контекст целей пользователя, помеченный как untrusted data;
изменение goal после запуска не меняет уже созданную интерпретацию.
interpretation_sources¶
Join-table с полями interpretation_request_id, calculation_artifact_id, source_role, artifact_hash_snapshot.
Primary key (interpretation_request_id, calculation_artifact_id, source_role). Она фиксирует точные входы и не даёт
подменить их новым текущим calculation set.
ai_generation_attempts¶
Поля: id, interpretation_request_id, attempt_no, provider_key, model_snapshot,
provider_request_id, status STARTED|SUCCEEDED|FAILED|REJECTED, input_tokens, output_tokens,
cost_micros nullable, cost_currency nullable, latency_ms, finish_reason, error_code,
moderation_result JSONB, started_at, finished_at.
Unique (interpretation_request_id, attempt_no) и частичный unique (provider_key, provider_request_id). Полный
prompt и незаредактированный provider response не дублируются в этой таблице.
interpretation_artifacts¶
Успешный immutable output: id, generation_key UNIQUE, schema_version, input_hash, output_json JSONB,
output_hash, provider_key, model_snapshot, provider_request_id, IDs mapping/prompt/policy releases,
usage_json, moderation_snapshot, published_at.
API выдаёт artifact только через принадлежащий пользователю interpretation_requests. Смена provider/model создаёт
новый generation key и artifact.
7. Каталог, подписка и платежи¶
erDiagram
PRODUCTS ||--o{ PRODUCT_FEATURES : grants
PRODUCTS ||--o{ PRICES : priced_as
USERS ||--o{ ORDERS : places
ORDERS ||--|{ ORDER_ITEMS : contains
USERS ||--o{ SUBSCRIPTIONS : owns
PRICES ||--o{ SUBSCRIPTIONS : selected_price
SUBSCRIPTIONS ||--o{ SUBSCRIPTION_INVOICES : billed_by
ORDERS ||--o| SUBSCRIPTION_INVOICES : represents
ORDERS ||--o{ PAYMENT_ATTEMPTS : paid_by
PAYMENT_ATTEMPTS ||--o{ REFUNDS : refunded_by
USERS ||--o{ ENTITLEMENTS : receives
ORDERS ||--o{ REFUND_REQUESTS : may_have
Каталог¶
products: id, code UNIQUE, kind SUBSCRIPTION|CONSULTATION, name, status DRAFT|ACTIVE|RETIRED,
metadata_version.
product_features: product_id, feature_code, limits_json. Primary key (product_id, feature_code).
Примеры features: CORE_READING, ROUTE, FORECAST_DAY, FORECAST_WEEK, FORECAST_MONTH, GOALS.
prices: id, product_id, version, amount_minor, currency, billing_type ONE_TIME|RECURRING,
interval_unit DAY|WEEK|MONTH|YEAR nullable, interval_count nullable, active_from, active_to,
status DRAFT|ACTIVE|RETIRED, receipt_item_snapshot JSONB.
Unique (product_id, version); CHECK amount > 0 и трёхбуквенная валюта.
Для ONE_TIME interval-поля пусты; для RECURRING они обязательны и interval_count > 0.
Partial unique (product_id, currency) WHERE status = ACTIVE оставляет одну продаваемую цену продукта; разные
будущие тарифы являются разными products и могут быть активны одновременно.
В MVP seed активирует один subscription product с одной месячной RUB-ценой. Другие тарифы добавляются новыми
products/product_features/prices.
Заказы¶
orders: id, user_id, status CREATED|PAYMENT_PENDING|PAID|CANCELED|EXPIRED, currency,
subtotal_minor, total_minor, policy_version, idempotency_key, paid_at, expires_at, state_version.
Unique (user_id, idempotency_key).
order_items: id, order_id, product_id, price_id nullable, practitioner_service_id nullable,
quantity, unit_amount_minor, total_amount_minor, name_snapshot, receipt_snapshot JSONB.
SQL CHECK требует ровно один источник цены: catalog price_id либо practitioner_service_id. После перехода заказа
в PAYMENT_PENDING snapshots и суммы immutable. Promo/discount в MVP нет, поэтому order.total = sum(items.total).
Подписки и entitlement¶
subscriptions: id, user_id, product_id, price_id, status PENDING|ACTIVE|PAST_DUE|CANCELED|EXPIRED,
current_period_start/end, cancel_at_period_end, cancel_requested_at, ended_at,
payment_method_reference_id, grace_ends_at, state_version.
Partial unique запрещает две одновременно действующие подписки одного user + product в
PENDING|ACTIVE|PAST_DUE.
subscription_invoices: id, subscription_id, order_id UNIQUE, period_start, period_end,
sequence_no, status OPEN|PAID|VOID|UNCOLLECTIBLE. Unique (subscription_id, period_start) и
(subscription_id, sequence_no) гарантирует один invoice за период.
entitlements: id, user_id, feature_code, grant_kind PAID|GRACE|ONE_TIME,
source_invoice_id nullable, source_order_item_id nullable, source_subscription_id nullable, starts_at,
ends_at, revoked_at, revoke_reason, limits_snapshot JSONB.
CHECK требует ровно один source. PAID ссылается на оплаченный invoice, ONE_TIME — на order item, GRACE — на
subscription и её versioned policy. Unique по source + feature + интервалу запрещает повторную выдачу. Доступ
проверяется по времени и revoked_at IS NULL, а не по одному subscription.status.
YooKassa¶
payment_method_references: id, user_id, provider YOOKASSA, provider_method_id, status ACTIVE|DISABLED,
reusable, title_masked, saved_at, disabled_at. Unique (provider, provider_method_id). Реквизитов карты нет.
payment_attempts: id, order_id, provider, provider_payment_id nullable, idempotency_key UNIQUE,
status CREATED|PENDING|WAITING_FOR_CAPTURE|SUCCEEDED|CANCELED, amount_minor, currency,
confirmation_url nullable, payment_method_reference_id nullable, provider_snapshot JSONB,
failure_code, succeeded_at, state_version.
Provider payment ID уникален, когда заполнен. Сумма/currency должны совпадать с order. confirmation_url очищается
после истечения операционной необходимости.
provider_events: id, provider, event_type, provider_object_type, provider_object_id, payload_hash,
payload_redacted JSONB, status RECEIVED|VERIFYING|PROCESSED|IGNORED|FAILED, received_at, processed_at,
last_error_code. Unique (provider, event_type, provider_object_id, payload_hash).
Возвраты и ledger¶
refund_requests: id, order_id, requested_by_user_id, amount_minor nullable, reason_code,
comment, status REQUESTED|ACCEPTED|REJECTED|WITHDRAWN, policy_version, resolved_at.
refunds: id, payment_attempt_id, refund_request_id nullable, provider_refund_id,
idempotency_key UNIQUE, amount_minor, currency, status CREATED|PENDING|SUCCEEDED|CANCELED,
reason_code, provider_snapshot JSONB, succeeded_at. Provider refund ID unique, когда заполнен.
Payment/order не меняются на искусственный статус «refunded»: возвращённая сумма агрегируется из успешных refunds.
ledger_transactions: immutable header id, operation_type, payment_attempt_id nullable, refund_id nullable,
settlement_id nullable, idempotency_key UNIQUE, occurred_at, description_code. SQL CHECK требует ровно один
FK-источник и не допускает финансовую ссылку только через пару source_type/source_id.
ledger_entries: id, transaction_id, account_code, direction DEBIT|CREDIT, amount_minor,
currency, user_id nullable, practitioner_id nullable. Для каждого transaction/currency сумма debit равна credit;
проверка выполняется deferred constraint trigger и тестами.
practitioner_settlements: id, practitioner_id, period_start/end, gross_minor, platform_fee_minor,
provider_fee_minor, refund_minor, payable_minor, currency, status DRAFT|READY|PAID|CANCELED,
provider_transfer_id nullable, paid_at. До pre-pilot используется только ручной settlement через allowlisted
операционную команду с audit event; admin endpoint отсутствует.
8. Цели, практики и консультации¶
Цели¶
goals: id, user_id, kind GOAL|REQUEST, area, text, target_at,
status ACTIVE|COMPLETED|ARCHIVED|CANCELED, completed_at, state_version.
goal_check_ins: immutable id, goal_id, result ACHIEVED|PARTIAL|NOT_ACHIEVED|SKIPPED, note,
occurred_at. Индекс (goal_id, occurred_at DESC). Check-in не переписывает историю; текущая статистика строится
агрегацией.
Практики¶
practitioner_profiles: id, user_id nullable UNIQUE, public_slug UNIQUE, accepting_requests,
status DRAFT|APPROVED|SUSPENDED|RETIRED, display_name, bio, experience_years, verification_note,
approved_at. В MVP строки создаются seed/controlled script.
Профиль без user допустим как заготовка каталога, но accepting_requests = true требует связанного user с ролью
PRACTITIONER и статусом профиля APPROVED.
specialties: id, code UNIQUE, name, active.
practitioner_specialties: primary key (practitioner_id, specialty_id).
practitioner_services: id, practitioner_id, code, name, description, duration_minutes,
amount_minor, currency, status ACTIVE|RETIRED, version. Unique (practitioner_id, code, version).
Изменение цены создаёт новую version.
practitioner_documents: id, practitioner_id, document_type, storage_key, content_hash,
review_status PENDING|VERIFIED|REJECTED, reviewed_at. Содержимое хранится не в PostgreSQL, а в закрытом object
storage. Публичного download endpoint нет.
practitioner_cases: id, practitioner_id, title, body, status DRAFT|PUBLISHED|RETIRED,
published_at. В MVP publication выполняется seed/controlled content process.
Консультации¶
erDiagram
USERS ||--o{ CONSULTATIONS : client
PRACTITIONER_PROFILES ||--o{ CONSULTATIONS : practitioner
PRACTITIONER_SERVICES ||--o{ CONSULTATIONS : selected_service
BIRTH_PROFILES ||--o{ CONSULTATIONS : shared_profile
CONSULTATIONS ||--o{ CONSULTATION_TIME_OPTIONS : proposes
CONSULTATIONS ||--o{ CONSULTATION_TRANSITIONS : records
CONSULTATIONS ||--o{ CONSULTATION_ORDERS : paid_by
ORDERS ||--o| CONSULTATION_ORDERS : links
consultations: id, client_user_id, practitioner_id, service_id, birth_profile_id nullable,
request_text,
status REQUESTED|ACCEPTED|DECLINED|SCHEDULING|AWAITING_PAYMENT|CONFIRMED|COMPLETED|NO_SHOW|CANCELED_BY_CLIENT|CANCELED_BY_PRACTITIONER|DISPUTED|CLOSED,
policy_version, scheduled_start_at nullable, scheduled_end_at nullable,
client_timezone_id, completed_at, state_version.
consultation_time_options: id, consultation_id, proposed_by_user_id, starts_at, ends_at,
timezone_id, status PROPOSED|ACCEPTED|WITHDRAWN. Один вариант может быть accepted; принятие атомарно заполняет
scheduled interval консультации. Это структурные поля согласования, не календарная интеграция и не чат.
Partial unique (consultation_id) WHERE status = ACCEPTED обеспечивает один принятый интервал.
consultation_transitions: immutable id, consultation_id, from_status, to_status, actor_user_id nullable,
actor_type USER|SYSTEM, reason_code, policy_version, request_id, occurred_at.
consultation_orders: id, consultation_id, order_id UNIQUE, is_current, linked_at. Partial unique оставляет
один current order на консультацию, но после canceled/expired order позволяет создать новый. Оплата консультации не
создаёт subscription entitlement.
Отдельной таблицы messages или chat в MVP нет. Доступ практика к birth_profile разрешён service-слоем только
для допустимых активных состояний и фиксируется domain_audit_events.
9. Уведомления, аналитика и операции¶
E-mail¶
notification_preferences: user_id, topic, enabled, timezone_id, quiet_hours_start/end nullable.
Primary key (user_id, topic). Transactional security/payment e-mail не отключаются marketing preference.
notification_deliveries: id, user_id nullable, otp_challenge_id nullable, topic,
template_release_id, destination_hash, payload_version, payload_redacted JSONB,
status QUEUED|SENDING|SENT|DELIVERED|BOUNCED|COMPLAINED|FAILED|SUPPRESSED, next_attempt_at, sent_at,
delivered_at. OTP code не попадает в persisted payload.
notification_attempts: id, delivery_id, attempt_no, provider RESEND, provider_message_id,
status STARTED|ACCEPTED|FAILED, error_code, started_at, finished_at. Unique (delivery_id, attempt_no) и
provider message ID.
OTP-письмо — особый синхронный flow: challenge с digest фиксируется транзакцией, после commit исходный код из памяти передаётся Resend и сохраняется только результат попытки. Неуспешная отправка не ретраится из persisted payload: следующий request создаёт новый challenge и новый код. Обычные транзакционные письма отправляются асинхронно.
Attribution и продуктовые события¶
anonymous_visitors: id, public_id_digest UNIQUE, first_seen_at, claimed_by_user_id nullable,
claimed_at, expires_at.
attribution_touches: id, visitor_id, user_id nullable, touch_type FIRST|LAST|OTHER, utm_source,
utm_medium, utm_campaign, utm_content, utm_term, referrer_host, landing_path, captured_at.
analytics_events: id, event_name, event_version, visitor_id nullable, user_id nullable,
occurred_at, request_id, properties JSONB. Event name и properties валидируются allowlist/schema registry в
коде. Birth data, e-mail, goal/consultation text и AI output запрещены.
Надёжные операции¶
idempotency_records: id, scope, actor_key, idempotency_key, request_hash, status IN_PROGRESS|COMPLETED|FAILED,
response_status nullable, response_json_redacted nullable, locked_until, expires_at. Unique
(scope, actor_key, idempotency_key). Повтор с другим request hash получает conflict.
outbox_events: id, event_type, event_version, aggregate_type, aggregate_id, payload JSONB,
idempotency_key UNIQUE, status PENDING|PUBLISHED|FAILED, available_at, published_at, attempt_count.
jobs: id, job_type, payload_version, payload JSONB, idempotency_key UNIQUE,
status QUEUED|RUNNING|RETRY_WAIT|SUCCEEDED|DEAD|CANCELED, priority, attempt_count, max_attempts,
next_run_at, lease_owner, lease_expires_at, last_error_code, finished_at.
Worker забирает jobs индексом (status, next_run_at, priority) через FOR UPDATE SKIP LOCKED; отдельный индекс
(status, lease_expires_at) возвращает зависшие leases. Payload содержит ID, а не копии чувствительных объектов.
domain_audit_events: immutable id, actor_type USER|SYSTEM|OPERATOR_SCRIPT, actor_user_id nullable,
action, target_type, target_id, request_id, reason_code, metadata_redacted JSONB, occurred_at.
Таблица фиксирует security/financial/access transitions, но не является admin audit log.
privacy_requests: id, user_id, kind EXPORT|DELETE, status REQUESTED|PROCESSING|COMPLETED|REJECTED,
requested_at, completed_at, result_storage_key nullable, expires_at, failure_code.
10. State machines¶
- Calculation item:
QUEUED → RUNNING → SUCCEEDED/FAILED;FAILED → QUEUEDтолько безопасным retry;BLOCKEDтерминален для текущего input. - Interpretation request:
QUEUED → RUNNING → READY/FAILED/REJECTED; retry создаёт новый attempt, не стирая старый. - Order:
CREATED → PAYMENT_PENDING → PAID; изCREATED/PAYMENT_PENDINGдопустимыCANCELEDилиEXPIRED. - Payment attempt:
CREATED → PENDING/WAITING_FOR_CAPTURE → SUCCEEDED/CANCELED; terminal status не откатывается. - Subscription:
PENDING → ACTIVE → PAST_DUE → ACTIVE/EXPIRED;ACTIVE → CANCELEDпроисходит только на границе оплаченного периода. - Refund:
CREATED → PENDING → SUCCEEDED/CANCELED. - Consultation:
REQUESTED → ACCEPTED/DECLINED/CANCELED_BY_CLIENT; затемACCEPTED → SCHEDULING → AWAITING_PAYMENT → CONFIRMED → COMPLETED/NO_SHOW. Отмена практика/клиента, dispute и close разрешаются только versioned policy. - Job:
QUEUED → RUNNING → SUCCEEDED/RETRY_WAIT/DEAD; истёкший lease возвращает работу в retry.
Каждый transition:
- выполняется use case-ом под row lock или с условием
state_version = expected; - проверяет текущий статус, роль и policy version;
- увеличивает
state_version; - в той же транзакции создаёт domain audit/outbox event;
- не запускает сетевой вызов внутри открытой DB-транзакции.
11. Критические транзакции¶
Claim onboarding draft после OTP¶
- Lock draft и проверить
OPEN + expires_at. - Найти/создать
usersпо e-mail, выдатьCLIENT. - Привязать birth profile и consent acceptances к user, назначить revision/current profile.
- Пометить draft
CLAIMEDи visitorclaimed_by_user_id. - Commit. Повтор возвращает тот же результат либо conflict при другом user.
Завершение расчёта¶
- Worker получает item lease через
jobs. - Вне транзакции запускает engine.
- В транзакции делает insert-or-select artifact по unique cache key, связывает item, создаёт evidence facts.
- Пересчитывает aggregate status calculation set и создаёт outbox event.
Подтверждение платежа¶
- Webhook durable-сохраняется в
provider_eventsи быстро отвечает после commit. - Worker делает authenticated GET в YooKassa вне транзакции.
- В транзакции lock-ает payment/order, сверяет shop/object/amount/currency/metadata и выполняет monotonic transition.
- Один раз создаёт/обновляет subscription invoice, entitlement, ledger transaction и notification outbox.
- Помечает provider event processed. Unique constraints превращают повтор webhook в no-op.
Продление¶
- Unique
(subscription_id, period_start)резервирует invoice. - Payment attempt создаётся со стабильным idempotency key до сетевого запроса.
- При успехе период сдвигается от старой границы, а не от времени callback.
- При отказе обновляются
PAST_DUE/graceи следующий job; двойной invoice невозможен.
AI generation¶
- Interpretation request фиксирует sources и releases.
- Unique generation key возвращает существующий artifact либо создаёт job.
- Provider вызывается вне транзакции.
- После schema/evidence/safety validation одной транзакцией сохраняются attempt, artifact, request
READYи outbox.
12. Foreign keys и удаление¶
CASCADEдопустим только для чистых дочерних записей, не имеющих самостоятельного юридического/аудитного смысла: user roles, specialties join, calculation set items до появления artifact.RESTRICTприменяется к prices, orders, payments, invoices, refunds, ledger, consent/document versions, engine/content releases и опубликованным artifacts.SET NULLдопустим для actor-ссылок audit/transition после анонимизации, но target ID и redacted metadata остаются.- User API не выполняет физический
DELETE users. Workflow очищает/псевдонимизирует профильные поля, отзывает sessions, удаляет неподлежащие хранению тексты и ставит tombstone. - Общие cached artifacts удаляются только когда на них нет source/item links и истёк retention period.
- Practitioner documents удаляются из object storage отдельной подтверждаемой задачей; строка хранит tombstone/hash согласно утверждённой policy.
13. Индексы¶
Обязательные индексы первой миграции:
users(email_normalized) UNIQUE;otp_challenges(email_normalized, created_at DESC)иotp_challenges(expires_at);sessions(token_digest) UNIQUE,sessions(user_id, expires_at DESC),sessions(family_id);- partial unique current
birth_profiles(user_id); - partial unique current
birth_profiles(source_draft_id); calculation_sets(birth_profile_id, created_at DESC);calculation_set_items(status, created_at)для worker/recovery;- unique cache
calculation_artifacts(engine_release_id, normalized_input_hash); - partial unique active
content_releases(kind, key, locale); interpretation_requests(user_id, created_at DESC)и unique(user_id, generation_key);- global unique
interpretation_artifacts(generation_key); orders(user_id, created_at DESC);- partial unique active
prices(product_id, currency); - unique provider IDs/idempotency keys payment/refund;
subscriptions(user_id, status)иsubscriptions(current_period_end);entitlements(user_id, feature_code, starts_at, ends_at) WHERE revoked_at IS NULL;consultations(client_user_id, created_at DESC)и(practitioner_id, status, scheduled_start_at);- partial unique accepted
consultation_time_options(consultation_id); - partial unique current
consultation_orders(consultation_id); jobs(status, next_run_at, priority)иjobs(status, lease_expires_at);outbox_events(status, available_at);notification_deliveries(status, next_attempt_at);- BRIN
analytics_events(occurred_at),domain_audit_events(occurred_at),provider_events(received_at).
GIN на JSONB не создаётся «на всякий случай». Он добавляется только под подтверждённый query plan. На объёме пилота партиционирование не нужно; первыми кандидатами после роста являются analytics, audit и provider events по месяцу.
14. Временные правила хранения¶
До утверждения pre-pilot privacy gate используются конфигурируемые internal defaults:
| Данные | Internal default |
|---|---|
| OTP challenges | удалять через 24 часа после expiry/consumption |
| Rate buckets | 7 дней |
| Unclaimed onboarding drafts | 30 дней либо раньше по expires_at |
| Revoked/expired sessions | 30 дней |
| Успешные job/outbox payload | 30 дней; DEAD — 90 дней |
| Notification payload/attempt metadata | 90 дней, без OTP и полного e-mail |
| Raw analytics events | 90 дней для internal |
| User calculations/interpretations/goals | пока существует аккаунт или до privacy workflow |
| Финансы, consent, audit | не удалять общим cleanup; срок определяет утверждённая legal policy |
Значения задаются конфигурацией cleanup jobs, а не hardcode в таблицах. Временные defaults нельзя автоматически переносить на внешний пилот.
15. Что намеренно отсутствует¶
admins, admin roles, admin sessions, permissions matrix и/adminaudit;- messages/chat, video rooms, external calendar events и Telegram identities;
- promo codes, referrals, loyalty, tips, goods/courses;
- универсальные polymorphic links без FK для финансовых источников;
- plaintext OTP/refresh/payment credentials;
- возможность вручную изменить calculation/AI artifact или поставить payment
SUCCEEDED; - hardcoded ID единственного тарифа.
Будущий admin module проектируется отдельной миграцией поверх доменных use cases после MVP.
16. Порядок реализации Prisma schema¶
- Базовые enums,
users/auth, anonymous visitors/attribution, legal documents и onboarding/birth profiles. - Engine releases, calculation sets/items/artifacts и evidence facts.
- Content releases, forecast/interpretation records и AI attempts.
- Goals, practitioner catalog/services/documents и subscription product/price/features.
- Order/payment/subscription/entitlement/refund/ledger, затем consultations и structured time options.
- Notifications, analytics events, idempotency, jobs/outbox, audit и privacy requests.
- Raw SQL migration для CHECK, partial unique, BRIN и deferred ledger constraints, которые Prisma schema не выражает.
- Seed: legal/content versions, четыре engine releases, specialties, один subscription product/price/features и тестовые practitioners.
Для каждой миграции нужны forward migration, проверка на пустой и наполненной БД, rollback/runbook и prisma validate.
17. Критерии согласования перед кодом¶
- каждая таблица имеет владельца-модуль и понятный lifecycle;
- все API из чернового контракта имеют источник данных;
- неизвестное время не допускает скрытого astrology/HD artifact;
- смена методики, content mapping или AI provider не переписывает историю;
- один webhook/retry не создаёт второй платёж, invoice, entitlement или ledger transaction;
- добавление второго тарифа не требует изменения схемы;
- консультация работает без chat/calendar/video и имеет отдельный payment flow;
- удаление пользователя не разрушает финансовую целостность;
- ни одна MVP-таблица не требует admin-панели для штатной работы.