Entity / Aggregate: BookingProgramEntity
Дом полей сущности «Программа бронирования застройщика» контекста crm (глоссарий «Программа
бронирования»; решение 7 ТЗ: «таймеры — параметры программы бронирования»). Snapshot условий в брони
(booking.md, VO BookingProgramTerms) ссылается сюда.
Назначение
BookingProgramEntity — настройки условий бронирования конкретного застройщика: платность и сумма,
длительности резерва и брони, правила возврата. Примеры: «бесплатная бронь на 4 часа», «платная
бронь 10 т.р./50 т.р. на 10 дней». Клиент/агент выбирает программу из списка опций при бронировании; бронь
снимает иммутабельный snapshot условий, поэтому правка программы не влияет на уже созданные брони.
Классификация
| Поле | Значение |
|---|---|
| Контекст | crm |
| Kind | AggregateRoot (настраиваемый справочник) |
| Source of truth | crm |
| Таблица | crm.booking_programs |
| Tenant isolation | TenantId = DeveloperCompanyId |
Поля
| Поле | Тип | Обязательность | Ограничения (PK/FK/index/constraint) | Источник | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | PK | app | Идентификатор программы. |
TenantId | Guid | Да | index ix_booking_programs_tenant_id | identity (actor застройщика) | Застройщик-владелец. |
Name | string | Да | varchar(128), не пустое; unique ux_booking_programs_tenant_name on (TenantId, lower(Name)) where deleted_at is null | app | Название («Бесплатная бронь 4 часа», «Платная бронь 10 дней»). |
Description | string? | Нет | varchar(2000) | app | Описание условий для клиента (показывается при выборе опции). |
IsPaid | bool | Да | default false | app | Платная ли бронь. |
Price | Money? (owned) | Нет (обязателен при IsPaid = true, check ck_booking_programs_price) | контракт VO — deal.md (Amount + CurrencyId); Amount > 0; колонки price_amount/price_currency_id | app | Стоимость бронирования (10 000 – 50 000 ₽ в примерах ТЗ; жёстких границ платформа не навязывает — только > 0). |
ReserveDurationMinutes | int | Да | > 0 and <= 10080 (неделя), default 60 | app | Длительность фазы «Резерв» (по умолчанию 1 час; параметр программы по решению 7). |
BookingDurationHours | int | Да | > 0 and <= 8760 (год) | app | Длительность фазы «Бронь» после подтверждения (4 часа бесплатная; 240 часов = 10 дней платная). |
RefundPolicy | string? | Нет | varchar(2000) | app | Порядок возврата платы за бронь (US-INV-10: «срок блокировки, порядок возврата»). |
IsDefault | bool | Да | default false; unique partial ux_booking_programs_tenant_default on (TenantId) where is_default and deleted_at is null | app | Программа, предлагаемая по умолчанию при бронировании с витрины (точка входа инвестора, решение 7). |
SortOrder | int | Да | >= 0, default 0 | app | Порядок в списке опций бронирования. |
CreatedAt | DateTimeOffset | Да | — | app | Создание. |
CreatedByUserId | Guid? | Нет | — | app | Менеджер застройщика. |
UpdatedAt | DateTimeOffset | Да | — | app | Дата последнего изменения. |
UpdatedByUserId | Guid? | Нет | — | app | Последний изменивший. |
DeletedAt | DateTimeOffset? | Нет | — | app | Архив программы. |
DeletedByUserId | Guid? | Нет | — | app | Кто заархивировал. |
Value objects
Money (owned Price) — контракт в deal.md.
Модели JSONB (versioned)
JSONB-модели не используются.
Связи
| Связь | Тип | Ключ | Правило загрузки | Delete behavior |
|---|---|---|---|---|
BookingEntity | reference (children) | bookings.program_id | загрузка при создании брони (условия — snapshot) | restrict (архив вместо удаления при наличии броней) |
Инварианты и переходы состояний
| Инвариант | Условие | Когда проверяется | Ошибка |
|---|---|---|---|
| INV-BPR-1 | IsPaid = true ⇒ Price задан и > 0; IsPaid = false ⇒ Price = null | при создании/изменении программы (check ck_booking_programs_price) | CRM_BOOKING_PROGRAM_PRICE_INVALID |
| INV-BPR-2 | Не более одной живой default-программы на застройщика; установка default снимает флаг с прежней в одном согласованном изменении | при установке программы по умолчанию (unique partial ux_booking_programs_tenant_default) | CRM_BOOKING_PROGRAM_DEFAULT_CONFLICT |
| INV-BPR-3 | Имя уникально в рамках TenantId среди живых | unique ux_booking_programs_tenant_name | CRM_BOOKING_PROGRAM_NAME_DUPLICATE |
| INV-BPR-4 | Правка условий не затрагивает существующие брони (иммутабельный snapshot BookingProgramTerms в booking.md) | правило snapshot при создании брони | — |
| INV-BPR-5 | У застройщика всегда есть хотя бы одна живая программа (SEED при онбординге: «Бесплатная бронь 4 часа», default) | при архивировании (должна остаться ≥ 1 живая программа) | CRM_BOOKING_PROGRAM_LAST |
Диаграмма состояний неприменима — статусной машины нет (живая ↔ архив через soft-delete).
Доменные и интеграционные события
| Событие | Тип | Когда | Snapshot/поля |
|---|---|---|---|
BookingProgramChanged | доменное | create/update/archive/set-default | Id, TenantId |
Интеграционные события не публикуются: программы читаются синхронно (список опций бронирования — API CRM), условия распространяются snapshot-ом внутри booking-событий.
Индексы, хранение, безопасность
| Аспект | Значение |
|---|---|
| Индексы | PK; ix_booking_programs_tenant_id; ux_booking_programs_tenant_name unique partial; ux_booking_programs_tenant_default unique partial; btree (tenant_id,sort_order) |
| Партиционирование / retention | none (единицы строк на застройщика) |
| Concurrency | оптимистическая блокировка (правка не должна затирать чужую) |
| Permissions / PII | crm.booking-programs.view (участники бронирования — чтение списка опций), crm.booking-programs.manage (менеджеры застройщика). PII отсутствует. |
| Audit | отметки создания/изменения/удаления (кто и когда), soft-delete |
Целостность, конкуренция и snapshot-семантика
- Snapshot защищает существующие брони. Правка/архивирование программы не влияет на уже созданные брони —
каждая держит иммутабельный snapshot условий
BookingProgramTerms(INV-BPR-4). Downstream-события брони разносят именно snapshot, а не текущее состояние программы, поэтому изменение цены/длительности не «догоняет» активные резервы. - Единственный default. Установка программы по умолчанию снимает флаг с прежней в одном согласованном
изменении (INV-BPR-2, unique partial
ux_booking_programs_tenant_default); гонка двух одновременных «сделать default» разрешается оптимистической блокировкой — побеждает первый коммит, второй перечитывает и получаетCRM_BOOKING_PROGRAM_DEFAULT_CONFLICT. - Уникальность имени. Живые программы уникальны по имени (case-insensitive) в рамках застройщика
(INV-BPR-3) →
CRM_BOOKING_PROGRAM_NAME_DUPLICATE. - Минимум одна живая программа. При онбординге застройщика SEED заводит «Бесплатная бронь 4 часа»
(default); архивирование последней живой программы запрещено (INV-BPR-5) →
CRM_BOOKING_PROGRAM_LAST. - Список опций читается синхронно. API CRM отдаёт список живых программ для формы бронирования; при точке
входа инвестора с витрины предвыбирается
IsDefault. Интеграционных событий программа не публикует.
Acceptance-критерии
- Платная без цены. Дано:
IsPaid = true,Priceне задан. Когда программа сохраняется. Тогда отказCRM_BOOKING_PROGRAM_PRICE_INVALID; приIsPaid = falseзаданнаяPrice— тоже нарушение INV-BPR-1. - Смена default. Дано: default-программа A. Когда застройщик делает default программу B. Тогда флаг у A снят, у B установлен — в одном изменении; ровно одна default.
- Дубль имени. Дано: живая программа «Платная бронь 10 дней». Когда создаётся вторая с тем же именем.
Тогда отказ
CRM_BOOKING_PROGRAM_NAME_DUPLICATE. - Архив последней. Дано: у застройщика одна живая программа. Когда её архивируют. Тогда отказ
CRM_BOOKING_PROGRAM_LAST. - Snapshot неизменен. Дано: бронь создана по программе. Когда программу правят/архивируют. Тогда условия
этой брони (
BookingProgramTerms) не меняются.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: BookingEntity
- API (9): GET /api/developer/crm/booking-programs — Программы бронирования, POST /api/agent/crm/bookings/{bookingId}/confirm — Подтвердить бронь (бесплатная программа), POST /api/agent/crm/deals/{dealId}/bookings — Создать бронь (Резерв) от имени клиента, POST /api/developer/crm/booking-programs — Создать программу бронирования, POST /api/developer/crm/booking-programs/{programId}/archive — Архивировать программу бронирования, POST /api/developer/crm/booking-programs/{programId}/set-default — Программа бронирования по умолчанию, POST /api/developer/crm/bookings/{bookingId}/extend — Продлить бронь, POST /api/investor/crm/deals/{dealId}/acknowledge-terms — Отметить «Ознакомлен», PUT /api/developer/crm/booking-programs/{programId} — Редактировать программу бронирования
- Use Cases (2): UC-BKG-002. Бронирование Резерв → Бронь, US-реестр блока «Сделка» (US-INV / US-AGT / US-DEV / US-MGR)
- События (3): Event Contract: pin.crm.booking-confirmed (BookingConfirmedEvent), Event Contract: pin.crm.booking-created (BookingCreatedEvent), Event Contract: pin.crm.booking-expired (BookingExpiredEvent)
- Фоновые задачи (1): Background Job: BookingExpirationJob
- Саги (1): Saga «Booking» (Резерв → Бронь → передача в продажу)
- index.md (1): CRM Service
- registries (2): Реестр инвариантов PIN, Реестр сущностей PIN
Связанные документы
- booking.md — потребитель условий (snapshot
BookingProgramTerms). - Решение 7 ТЗ (расхождение «1 час vs 72 часа vs 4 часа/10 дней» закрыто параметрами программы) — technical-assignment.md.