POST /api/developer/crm/booking-programs — Создать программу бронирования
Менеджер застройщика заводит программу бронирования — набор условий брони (платность, сумма, длительности резерва и брони, правила возврата), из которого клиент/агент выбирает опцию при бронировании (например, «Бесплатная бронь 4 часа», «Платная бронь 10 дней»; таймеры — параметры программы). Дом полей, инвариантов и индексов — booking-program.md.
Назначение
Создаёт новую программу бронирования в TenantId = DeveloperCompanyId: задаёт Name, платность (IsPaid +
Price), длительности фаз (ReserveDurationMinutes/BookingDurationHours), необязательные Description/
RefundPolicy, флаг IsDefault и SortOrder. Вызывает менеджер застройщика; на выходе — карточка созданной
программы. Условия иммутабельны для уже созданных броней — бронь снимает snapshot BookingProgramTerms при
создании (booking.md, INV-BPR-4), поэтому правка/создание программ не затрагивает
историю.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | crm |
| API area | developer |
| Feature group | booking-programs |
| Статус | draft |
| Документ | docs/06-services/crm/api/developer/create-booking-program.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Менеджер застройщика в рамках своего TenantId = DeveloperCompanyId) |
| Auth policy / Permissions | crm.booking-programs.manage (Менеджер ПИН — platform-scope, тот же permission) |
| Scope (RBAC) | организация (застройщик): менеджер настраивает свои программы бронирования |
| Record-level | новый BookingProgramEntity.TenantId = actor.DeveloperCompanyId |
| Tenant isolation | TenantId = DeveloperCompanyId в каждом запросе (ADR-0052) |
| Audit | create (CreatedByUserId = actor, инкремент Version); PII отсутствует |
Создаёт программы менеджер застройщика — только в своём TenantId = DeveloperCompanyId (ADR-0052), либо
Менеджер ПИН (platform-scope, тот же permission crm.booking-programs.manage). Кросс-tenant создание запрещено.
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/developer/crm/booking-programs |
| Success status | 201 (создано; тело — карточка BookingProgramModel) |
| Idempotency header | не применяется — создание не идемпотентно (повтор имени → 409, INV-BPR-3) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
TenantId | identity claims | Guid | Да | Required; = DeveloperCompanyId | Tenant isolation |
ActorUserId | identity claims | Guid | Да | JWT менеджера; record-level + CreatedByUserId | — |
Name | body | string | Да | 1..128, не пустое после trim; уникально по TenantId + lower(Name) среди живых (INV-BPR-3) | — |
Description | body | string? | Нет | 0..2000 (текст условий для клиента) | — |
IsPaid | body | bool | Да | default false; связка с Price (INV-BPR-1) | — |
Price | body | Money? (VO {Amount decimal, CurrencyId short?}) | Нет | обязателен и Amount > 0 при IsPaid=true; строго null при IsPaid=false (INV-BPR-1); контракт Money — deal.md | — |
ReserveDurationMinutes | body | int | Нет | > 0 and <= 10080; default 60 (ТЗ.md:469) | — |
BookingDurationHours | body | int | Да | > 0 and <= 8760 (4 ч бесплатная; 240 ч = 10 дней платная — ТЗ.md:470) | — |
RefundPolicy | body | string? | Нет | 0..2000 (порядок возврата, US-INV-10) | — |
IsDefault | body | bool | Нет | default false; true снимает флаг с прежней default в той же транзакции (INV-BPR-2) | — |
SortOrder | body | int | Нет | >= 0; default 0 (порядок в списке опций) | — |
Price.CurrencyId — типизированная ссылка на справочник валют handbook
(snapshots.md): не задан → валюта RUB; заданный id резолвится в
справочнике (resolve-references.md),
неизвестная/архивная запись → VALIDATION_FAILED; денежные операции RUB-only (ADR-0048). Платформа жёстких
границ суммы не навязывает, требует только > 0 (booking-program.md).
Модель ответа BookingProgramModel
Проекция агрегата — дом полей booking-program.md; ниже только состав
карточки, возвращаемой create/list. TenantId наружу не отдаётся (внутренняя изоляция).
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | BookingProgramEntity.Id | — | Идентификатор созданной программы. |
Name | string | Да | BookingProgramEntity.Name | — | Название программы. |
Description | string? | Нет | BookingProgramEntity.Description | — | Описание условий. |
IsPaid | bool | Да | BookingProgramEntity.IsPaid | — | Платная ли бронь. |
Price | Money? | Нет | BookingProgramEntity.Price | — | Стоимость (VO Amount/CurrencyId); null при IsPaid=false. |
ReserveDurationMinutes | int | Да | BookingProgramEntity.ReserveDurationMinutes | — | Длительность фазы «Резерв». |
BookingDurationHours | int | Да | BookingProgramEntity.BookingDurationHours | — | Длительность фазы «Бронь». |
RefundPolicy | string? | Нет | BookingProgramEntity.RefundPolicy | — | Порядок возврата платы. |
IsDefault | bool | Да | BookingProgramEntity.IsDefault | — | Программа по умолчанию для витрины. |
SortOrder | int | Да | BookingProgramEntity.SortOrder | — | Порядок в списке опций. |
CreatedAt | DateTimeOffset | Да | BookingProgramEntity.CreatedAt | — | Момент создания. |
Version | long | Да | BookingProgramEntity.Version | — | Оптимистическая версия (== 1 после создания). |
Алгоритм
- Контекст и доступ. Выполняет менеджер застройщика (
User, permissioncrm.booking-programs.manage; менеджер ПИН — platform-scope с тем же правом); tenant —TenantId = DeveloperCompanyId. Создание намеренно не идемпотентно — роль дедупа выполняет INV-BPR-3. - Проверка входа и предусловий.
Nameне пусто после trim (1..128);Description/RefundPolicy0..2000;ReserveDurationMinutes1..10080(пусто →60);BookingDurationHours1..8760;SortOrder >= 0; кросс-поле INV-BPR-1:IsPaid=true⇒Price != null && Amount > 0, аPrice.CurrencyId(если задан) резолвится в справочнике валютhandbook;IsPaid=false⇒Price == null— иначеVALIDATION_FAILEDлибоCRM_BOOKING_PROGRAM_PRICE_INVALID;- уникальность имени (pre-check, INV-BPR-3): по (
TenantId,lower(Name), живые) — если такое имя уже есть →CRM_BOOKING_PROGRAM_NAME_DUPLICATE; - отсутствие permission →
FORBIDDEN.
- Что читаем. Родительских агрегатов нет (create). Читаем лишь наличие живой программы с тем же именем
(шаг 2) и, если
IsDefault=true, прежнюю default-программу застройщика (под блокировкой строки) для атомарного снятия флага (INV-BPR-2). Иные программы не нужны — новая ни от чего не зависит. - Бизнес-правила и переходы. Создаётся программа: конструктор закрепляет INV-BPR-1 (
PriceпринудительноnullприIsPaid=false, иначеAmount > 0). Маппинг поле-в-поле:Id— новый идентификатор,TenantId = DeveloperCompanyId, остальные поля — из одноимённых входных,CreatedByUserId = ActorUserId,CreatedAt = now,Version = 1. ПриIsDefault=trueи наличии прежней default — снятие её флага (INV-BPR-2). Фиксируется доменный фактBookingProgramChanged. - Что меняется. Появляется новая программа бронирования; при переносе default прежняя default теряет флаг.
Гонки закрывают ограничения БД: check
ck_booking_programs_price(INV-BPR-1), partial-uniqueux_booking_programs_tenant_name(INV-BPR-3), partial-uniqueux_booking_programs_tenant_default(INV-BPR-2) — их нарушение отображается в соответствующийCRM_*-код; pre-check шага 2 нужен лишь для дружелюбного ответа. - События. Интеграционных Kafka-событий нет: программы читаются синхронно (список опций — API CRM), а
условия распространяются наружу snapshot-ом
BookingProgramTermsвнутри booking-событий (booking-created.md); топикаpin.crm.booking-programне существует. Внутренний доменный фактBookingProgramChangedинвалидирует кэш списка опций бронирования. - Согласованность. Вставка программы, простановка audit/
Versionи опциональное снятие прежнего default фиксируются в одном согласованном изменении (атомарно). Нарушение любого ограничения откатывает изменение →CRM_*-код (шаг 5). - Результат. Карточка
BookingProgramModelсозданной программы (Id,Name,IsPaid,Price?, длительности,IsDefault,SortOrder,Version = 1) → HTTP201.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | none (домен BookingProgramChanged — в памяти агрегата; kafka-топика нет) |
| External calls | none |
| Cache / projections / search | доменное BookingProgramChanged → инвалидация кэша списка опций бронирования застройщика |
| Notifications | none |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | Name пусто / длительности вне диапазона / SortOrder < 0 | «Проверьте корректность полей программы» | no retry |
CRM_BOOKING_PROGRAM_PRICE_INVALID | 400 | INV-BPR-1: IsPaid=true без Price>0 либо IsPaid=false c заданным Price (валидатор либо ck_booking_programs_price) | «Укажите стоимость для платной брони» | no retry |
FORBIDDEN | 403 | нет permission crm.booking-programs.manage | «Недостаточно прав» | no retry |
CRM_BOOKING_PROGRAM_NAME_DUPLICATE | 409 | INV-BPR-3: живая программа с таким Name (ci) уже есть в TenantId (pre-check либо ux_booking_programs_tenant_name) | «Программа с таким названием уже существует» | no retry |
CRM_BOOKING_PROGRAM_DEFAULT_CONFLICT | 409 | INV-BPR-2: гонка при установке IsDefault=true (нарушение ux_booking_programs_tenant_default) | «Программа по умолчанию уже назначена» | retry (перечитка default) |
Предусловия и постусловия
Предусловия (истинны до сценария):
- актор — менеджер застройщика (или Менеджер ПИН, platform-scope) с permission
crm.booking-programs.manage; - у застройщика нет живой программы с тем же именем (ci) в
TenantId(INV-BPR-3); - при
IsDefault=true— у застройщика может быть текущая default-программа (её флаг будет снят в той же транзакции, INV-BPR-2).
Постусловия (гарантированы после успеха):
- создана новая живая программа бронирования (
Version = 1,CreatedByUserId,CreatedAt) вTenantId; - при
IsDefault=trueпрежняя default-программа теряет флаг (единственная default сохраняется, INV-BPR-2); - инвариант платности закреплён (INV-BPR-1):
PriceстрогоnullприIsPaid=false, иначеAmount > 0; - интеграционных Kafka-событий нет; внутренний факт
BookingProgramChangedинвалидирует кэш списка опций — программа появляется в выборе при следующем чтении (синхронный источник, окна eventual-consistency наружу нет); - существующие брони не затронуты (они держат snapshot
BookingProgramTerms, INV-BPR-4).
Acceptance Criteria
- Happy path (бесплатная). Дано: уникальное имя,
IsPaid=false,Price=null. Когда: создание. Тогда:201, карточка сVersion = 1,Priceпуст. - Happy path (платная). Дано:
IsPaid=true,Price.Amount > 0,Price.CurrencyId— живая валюта справочника (или не задан →RUB). Когда: создание. Тогда:201, платная программа создана. - Некорректная платность. Дано:
IsPaid=trueбезPrice>0илиIsPaid=falseс заданнымPrice. Когда: создание. Тогда:400 CRM_BOOKING_PROGRAM_PRICE_INVALID(INV-BPR-1). - Дубль имени. Дано: живая программа с таким именем (ci) уже есть. Когда: создание. Тогда:
409 CRM_BOOKING_PROGRAM_NAME_DUPLICATE(INV-BPR-3). - Назначение default. Дано:
IsDefault=trueи уже есть default-программа. Когда: создание. Тогда:201, у прежней default флаг снят атомарно. - Гонка default. Дано: два параллельных создания с
IsDefault=true. Когда: оба сохраняются. Тогда: один проигрывает поux_booking_programs_tenant_default→409 CRM_BOOKING_PROGRAM_DEFAULT_CONFLICT, повтор после перечитки default. - Нет прав. Дано: у актора нет
crm.booking-programs.manage. Когда: создание. Тогда:403 FORBIDDEN.
Совместимость и наблюдаемость
- Route/тело стабильны; добавление optional-полей в
BookingProgramModel— additive (без breaking); удаление/переименование/смена типа — breaking (migration + rollout).Money.CurrencyId— типизированная ссылка на справочник валютhandbook(short); неизвестное/архивное значение →VALIDATION_FAILED. - Logs:
booking-program create requested/created/duplicate(TenantId,ProgramId,IsPaid,IsDefault; PII нет); metrics:crm_booking_program_create_total{result}, latency p95; traces — сквозной trace черезtraceparent; dashboardpin-crm; runbook —docs/06-services/crm/runbooks/(будущий путь по конвенции).
Обратные ссылки
Автоссылки: где используется этот документ.
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность и инварианты: booking-program.md (INV-BPR-1 платность/
Price, INV-BPR-2 единственная default, INV-BPR-3 уникальностьName(ci), индексыux_booking_programs_tenant_name/ux_booking_programs_tenant_default, checkck_booking_programs_price, событиеBookingProgramChanged); контрактMoney— deal.md; потребитель условий (snapshotBookingProgramTerms) — booking.md. - Как условия программы уходят наружу: booking-created.md (snapshot внутри booking-события; сами программы не публикуются); карта событий и область developer — ../../README.md.
- ADR: 0052 tenancy, 0056 simplicity-first.