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

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 areadeveloper
Feature groupbooking-programs
Статусdraft
Документdocs/06-services/crm/api/developer/create-booking-program.md

Актор и доступ

ПроверкаЗначение
Actor typeUser (Менеджер застройщика в рамках своего TenantId = DeveloperCompanyId)
Auth policy / Permissionscrm.booking-programs.manage (Менеджер ПИН — platform-scope, тот же permission)
Scope (RBAC)организация (застройщик): менеджер настраивает свои программы бронирования
Record-levelновый BookingProgramEntity.TenantId = actor.DeveloperCompanyId
Tenant isolationTenantId = DeveloperCompanyId в каждом запросе (ADR-0052)
Auditcreate (CreatedByUserId = actor, инкремент Version); PII отсутствует
Доступ

Создаёт программы менеджер застройщика — только в своём TenantId = DeveloperCompanyId (ADR-0052), либо Менеджер ПИН (platform-scope, тот же permission crm.booking-programs.manage). Кросс-tenant создание запрещено.

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/developer/crm/booking-programs
Success status201 (создано; тело — карточка BookingProgramModel)
Idempotency headerне применяется — создание не идемпотентно (повтор имени → 409, INV-BPR-3)
Correlationсквозной trace через заголовок traceparent

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
TenantIdidentity claimsGuidДаRequired; = DeveloperCompanyIdTenant isolation
ActorUserIdidentity claimsGuidДаJWT менеджера; record-level + CreatedByUserId
NamebodystringДа1..128, не пустое после trim; уникально по TenantId + lower(Name) среди живых (INV-BPR-3)
Descriptionbodystring?Нет0..2000 (текст условий для клиента)
IsPaidbodyboolДаdefault false; связка с Price (INV-BPR-1)
PricebodyMoney? (VO {Amount decimal, CurrencyId short?})Нетобязателен и Amount > 0 при IsPaid=true; строго null при IsPaid=false (INV-BPR-1); контракт Moneydeal.md
ReserveDurationMinutesbodyintНет> 0 and <= 10080; default 60 (ТЗ.md:469)
BookingDurationHoursbodyintДа> 0 and <= 8760 (4 ч бесплатная; 240 ч = 10 дней платная — ТЗ.md:470)
RefundPolicybodystring?Нет0..2000 (порядок возврата, US-INV-10)
IsDefaultbodyboolНетdefault false; true снимает флаг с прежней default в той же транзакции (INV-BPR-2)
SortOrderbodyintНет>= 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Описание
IdGuidДаBookingProgramEntity.IdИдентификатор созданной программы.
NamestringДаBookingProgramEntity.NameНазвание программы.
Descriptionstring?НетBookingProgramEntity.DescriptionОписание условий.
IsPaidboolДаBookingProgramEntity.IsPaidПлатная ли бронь.
PriceMoney?НетBookingProgramEntity.PriceСтоимость (VO Amount/CurrencyId); null при IsPaid=false.
ReserveDurationMinutesintДаBookingProgramEntity.ReserveDurationMinutesДлительность фазы «Резерв».
BookingDurationHoursintДаBookingProgramEntity.BookingDurationHoursДлительность фазы «Бронь».
RefundPolicystring?НетBookingProgramEntity.RefundPolicyПорядок возврата платы.
IsDefaultboolДаBookingProgramEntity.IsDefaultПрограмма по умолчанию для витрины.
SortOrderintДаBookingProgramEntity.SortOrderПорядок в списке опций.
CreatedAtDateTimeOffsetДаBookingProgramEntity.CreatedAtМомент создания.
VersionlongДаBookingProgramEntity.VersionОптимистическая версия (== 1 после создания).

Алгоритм

  1. Контекст и доступ. Выполняет менеджер застройщика (User, permission crm.booking-programs.manage; менеджер ПИН — platform-scope с тем же правом); tenant — TenantId = DeveloperCompanyId. Создание намеренно не идемпотентно — роль дедупа выполняет INV-BPR-3.
  2. Проверка входа и предусловий.
    • Name не пусто после trim (1..128); Description/RefundPolicy 0..2000; ReserveDurationMinutes 1..10080 (пусто → 60); BookingDurationHours 1..8760; SortOrder >= 0; кросс-поле INV-BPR-1: IsPaid=truePrice != null && Amount > 0, а Price.CurrencyId (если задан) резолвится в справочнике валют handbook; IsPaid=falsePrice == null — иначе VALIDATION_FAILED либо CRM_BOOKING_PROGRAM_PRICE_INVALID;
    • уникальность имени (pre-check, INV-BPR-3): по (TenantId, lower(Name), живые) — если такое имя уже есть → CRM_BOOKING_PROGRAM_NAME_DUPLICATE;
    • отсутствие permission → FORBIDDEN.
  3. Что читаем. Родительских агрегатов нет (create). Читаем лишь наличие живой программы с тем же именем (шаг 2) и, если IsDefault=true, прежнюю default-программу застройщика (под блокировкой строки) для атомарного снятия флага (INV-BPR-2). Иные программы не нужны — новая ни от чего не зависит.
  4. Бизнес-правила и переходы. Создаётся программа: конструктор закрепляет 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.
  5. Что меняется. Появляется новая программа бронирования; при переносе default прежняя default теряет флаг. Гонки закрывают ограничения БД: check ck_booking_programs_price (INV-BPR-1), partial-unique ux_booking_programs_tenant_name (INV-BPR-3), partial-unique ux_booking_programs_tenant_default (INV-BPR-2) — их нарушение отображается в соответствующий CRM_*-код; pre-check шага 2 нужен лишь для дружелюбного ответа.
  6. События. Интеграционных Kafka-событий нет: программы читаются синхронно (список опций — API CRM), а условия распространяются наружу snapshot-ом BookingProgramTerms внутри booking-событий (booking-created.md); топика pin.crm.booking-program не существует. Внутренний доменный факт BookingProgramChanged инвалидирует кэш списка опций бронирования.
  7. Согласованность. Вставка программы, простановка audit/Version и опциональное снятие прежнего default фиксируются в одном согласованном изменении (атомарно). Нарушение любого ограничения откатывает изменение → CRM_*-код (шаг 5).
  8. Результат. Карточка BookingProgramModel созданной программы (Id, Name, IsPaid, Price?, длительности, IsDefault, SortOrder, Version = 1) → HTTP 201.

Диаграмма

Побочные эффекты

ТипДетали
Integration eventsnone (домен BookingProgramChanged — в памяти агрегата; kafka-топика нет)
External callsnone
Cache / projections / searchдоменное BookingProgramChanged → инвалидация кэша списка опций бронирования застройщика
Notificationsnone

Ошибки и failure modes

Ошибки
КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400Name пусто / длительности вне диапазона / SortOrder < 0«Проверьте корректность полей программы»no retry
CRM_BOOKING_PROGRAM_PRICE_INVALID400INV-BPR-1: IsPaid=true без Price>0 либо IsPaid=false c заданным Price (валидатор либо ck_booking_programs_price)«Укажите стоимость для платной брони»no retry
FORBIDDEN403нет permission crm.booking-programs.manage«Недостаточно прав»no retry
CRM_BOOKING_PROGRAM_NAME_DUPLICATE409INV-BPR-3: живая программа с таким Name (ci) уже есть в TenantId (pre-check либо ux_booking_programs_tenant_name)«Программа с таким названием уже существует»no retry
CRM_BOOKING_PROGRAM_DEFAULT_CONFLICT409INV-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_default409 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; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/ (будущий путь по конвенции).

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные документы

  • Сущность и инварианты: 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, check ck_booking_programs_price, событие BookingProgramChanged); контракт Moneydeal.md; потребитель условий (snapshot BookingProgramTerms) — booking.md.
  • Как условия программы уходят наружу: booking-created.md (snapshot внутри booking-события; сами программы не публикуются); карта событий и область developer — ../../README.md.
  • ADR: 0052 tenancy, 0056 simplicity-first.