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

PUT /api/developer/crm/booking-programs/{programId} — Редактировать программу бронирования

Менеджер застройщика правит условия программы бронирования: платность, сумму, длительности резерва и брони, описание и порядок возврата. Ключевое правило — инвариант INV-BPR-4 (booking-program.md): правка не влияет на уже созданные брони, потому что каждая бронь снимает иммутабельный snapshot BookingProgramTerms при создании (booking.md, VO BookingProgramTerms). При редактировании переприменяются те же проверки, что при создании: согласованность платности/цены (INV-BPR-1) и уникальность имени (INV-BPR-3). Дом полей, инвариантов и индексов — booking-program.md.

Назначение

Обновляет набор условий существующей программы бронирования в TenantId = DeveloperCompanyId. Вызывает менеджер застройщика; на выходе — карточка обновлённой программы. Флаг IsDefault этим методом не меняется — установка программы по умолчанию идёт отдельной командой SetDefault (INV-BPR-2, взаимное снятие флага в том же изменении); архивация — отдельной командой (INV-BPR-5).

Metadata

ПолеЗначение
Сервис/контекстcrm
API areadeveloper
Feature groupbooking
Статусdraft
Документdocs/06-services/crm/api/developer/update-booking-program.md

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

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

Правит программу менеджер застройщика — владелец программы по tenant (BookingProgramEntity.TenantId == actor.DeveloperCompanyId), либо Менеджер ПИН (platform-scope, тот же permission crm.booking-programs.manage). Чужой tenant → CRM_BOOKING_PROGRAM_FORBIDDEN.

HTTP-контракт

ПолеЗначение
MethodPUT
Route/api/developer/crm/booking-programs/{programId}
Success status200 (условия обновлены; тело — карточка программы BookingProgramModel)
Idempotency headerне применяется — PUT семантически идемпотентен (полная замена набора условий); защита от потерянного обновления — RowVersion (тело)
Correlationсквозной trace через заголовок traceparent

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

Важно

IsDefault во входе отсутствует намеренно: смена флага «по умолчанию» — отдельная команда SetDefault (INV-BPR-2). Price — денежная величина (Money, контракт — deal.md); на входе разложена на PriceAmount + PriceCurrencyId, чтобы не принимать нетипизированный объект.

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
TenantIdidentity claimsGuidДаRequired; = DeveloperCompanyIdTenant isolation
ActorUserIdidentity claimsGuidДаJWT менеджера; record-level + UpdatedByUserId
ProgramIdrouteGuidДа!= Guid.Empty; существующая живая программа в TenantId (шаг 2)
NamebodystringДа1..128, не пустое после trim; уникально по TenantId + lower(Name) среди живых, исключая себя (INV-BPR-3)
Descriptionbodystring?Нет0..2000
IsPaidbodyboolДасогласовано с PriceAmount (INV-BPR-1)
PriceAmountbodydecimal?Нет (обязателен при IsPaid = true)> 0; при IsPaid = false — должен быть null (INV-BPR-1)
PriceCurrencyIdbodyshort?Нетссылка на справочник валют handbook; не задан → валюта RUB (RUB-only, ADR-0048); неизвестная/архивная запись → VALIDATION_FAILED; значим только при IsPaid = true
ReserveDurationMinutesbodyintДа> 0 and <= 10080 (неделя)
BookingDurationHoursbodyintДа> 0 and <= 8760 (год)
RefundPolicybodystring?Нет0..2000 (US-INV-10: порядок возврата)
SortOrderbodyintДа>= 0
RowVersionbodyuintДаожидаемая версия для оптимистической блокировки; несовпадение → 409 (шаг 5)

Модель ответа BookingProgramModel

Проекция агрегата — дом полей booking-program.md; ниже только состав карточки программы, возвращаемой update/get/list. TenantId наружу не отдаётся (внутренняя изоляция).

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаBookingProgramEntity.IdИдентификатор программы.
NamestringДаBookingProgramEntity.NameНазвание программы.
Descriptionstring?НетBookingProgramEntity.DescriptionОписание условий для клиента.
IsPaidboolДаBookingProgramEntity.IsPaidПлатная ли бронь.
PriceAmountdecimal?НетBookingProgramEntity.Price.AmountСумма (только при IsPaid).
PriceCurrencyIdshort?НетBookingProgramEntity.Price.CurrencyIdВалюта суммы (справочник handbook; display-код — lookup справочника).
ReserveDurationMinutesintДаBookingProgramEntity.ReserveDurationMinutesДлительность фазы «Резерв».
BookingDurationHoursintДаBookingProgramEntity.BookingDurationHoursДлительность фазы «Бронь».
RefundPolicystring?НетBookingProgramEntity.RefundPolicyПорядок возврата платы.
IsDefaultboolДаBookingProgramEntity.IsDefaultRead-only здесь (меняется командой SetDefault).
SortOrderintДаBookingProgramEntity.SortOrderПорядок в списке опций.
UpdatedAtDateTimeOffsetДаBookingProgramEntity.UpdatedAtМомент правки.
VersionlongДаBookingProgramEntity.VersionИнкрементирована на 1.
RowVersionuintДаBookingProgramEntity.RowVersionНовый токен оптимистической блокировки.

Алгоритм

  1. Контекст и доступ. Выполняет менеджер застройщика (User, permission crm.booking-programs.manage; менеджер ПИН — platform-scope); tenant — TenantId = DeveloperCompanyId. PUT идемпотентен по природе (полная замена условий), от потерянного обновления защищает RowVersion.
  2. Проверка входа и предусловий.
    • ProgramId != Guid.Empty; Name не пусто после trim (1..128); Description/RefundPolicy 0..2000; ReserveDurationMinutes ∈ (0, 10080]; BookingDurationHours ∈ (0, 8760]; SortOrder >= 0; согласованность платности (INV-BPR-1): IsPaid=true ⇒ PriceAmount != null && > 0, IsPaid=false ⇒ PriceAmount == null — иначе CRM_BOOKING_PROGRAM_PRICE_INVALID; PriceCurrencyId (если задан) резолвится в справочнике валют handbook (resolve-references.md); прочие нарушения формата → VALIDATION_FAILED;
    • программа существует среди живых в TenantId (архивные исключены soft-delete-фильтром) — иначе CRM_BOOKING_PROGRAM_NOT_FOUND (архивную программу не редактируют);
    • scope: program.TenantId == TenantId — иначе CRM_BOOKING_PROGRAM_FORBIDDEN (застройщик правит только свои программы);
    • уникальность имени (pre-check, INV-BPR-3): если Name (ci) отличается от текущего и такое имя уже занято живой программой того же tenant (кроме себя) → CRM_BOOKING_PROGRAM_NAME_DUPLICATE.
  3. Что читаем. Только саму программу (single-aggregate update). Существующие брони не читаются и не трогаются — INV-BPR-4: их условия зафиксированы snapshot-ом BookingProgramTerms при создании брони и от текущей программы не зависят.
  4. Бизнес-правила и переходы. Обновление условий закрепляет INV-BPR-1 (согласованность IsPaid/Price). Маппинг поле-в-поле: Name = trim(Name), Description, IsPaid; Price при IsPaid собирается из PriceAmount + PriceCurrencyId (не задан → валюта RUB справочника), при IsPaid=false — пусто; ReserveDurationMinutes, BookingDurationHours, RefundPolicy, SortOrder, UpdatedByUserId = ActorUserId, Version += 1. Id/TenantId/IsDefault/CreatedAt/ CreatedByUserId не меняются. Фиксируется доменный факт BookingProgramChanged.
  5. Что меняется. Обновляются условия программы (имя, описание, платность/цена, длительности, порядок возврата, SortOrder, audit/Version). Оптимистическая блокировка по RowVersion: если параллельный редактор уже закоммитил правку — конфликт CRM_BOOKING_PROGRAM_VERSION_CONFLICT (перечитать и повторить). Гонку с параллельным rename/create того же имени ловит ux_booking_programs_tenant_nameCRM_BOOKING_PROGRAM_NAME_DUPLICATE; check ck_booking_programs_price подстраховывает INV-BPR-1.
  6. События. Интеграционных Kafka-событий нет: программы читаются синхронно (API список опций), а условия распространяются snapshot-ом внутри booking-событий (booking-created.md, ProgramTerms); топика pin.crm.booking-program не существует. Внутренний доменный факт BookingProgramChanged инвалидирует кэш списка опций бронирования. Правка не рассылается ни существующим броням, ни chessboard — INV-BPR-4 (снятые снимки условий и статусы объектов от текущей программы не зависят).
  7. Согласованность. Обновление программы фиксируется в одном согласованном изменении (атомарно). Конфликт RowVersion или нарушение unique-индекса откатывают изменение → соответствующий код (шаг 5).
  8. Результат. Карточка BookingProgramModel обновлённой программы (все условия + новые Version/RowVersion) → HTTP 200.

Диаграмма

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

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

Ошибки и failure modes

Ошибки
КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400формат/длина/диапазоны полей (Name, длительности, SortOrder)«Проверьте корректность полей программы»no retry
CRM_BOOKING_PROGRAM_PRICE_INVALID422рассогласование IsPaid/PriceAmount (INV-BPR-1; validator + ck_booking_programs_price)«Укажите стоимость для платной брони»no retry
CRM_BOOKING_PROGRAM_NOT_FOUND404программа не найдена / архивна в TenantId«Программа бронирования не найдена»no retry
CRM_BOOKING_PROGRAM_FORBIDDEN403чужой tenant / нет crm.booking-programs.manage«Нет доступа к программе бронирования»no retry
CRM_BOOKING_PROGRAM_NAME_DUPLICATE409живая программа с таким Name (ci) уже есть в TenantId (pre-check либо ux_booking_programs_tenant_name)«Программа с таким названием уже существует»no retry (сменить имя)
CRM_BOOKING_PROGRAM_VERSION_CONFLICT409RowVersion не совпал (параллельная правка)«Программа изменена другим пользователем, обновите данные»retry (перечитать GetById и повторить)
Не относится к этому методу

Инварианты INV-BPR-2 (CRM_BOOKING_PROGRAM_DEFAULT_CONFLICT) и INV-BPR-5 (CRM_BOOKING_PROGRAM_LAST) к правке условий не относятся — смена IsDefault идёт командой SetDefault, архивация — командой архивирования (см. booking-program.md).

Acceptance Criteria

  • Given живая программа P в TenantId, When менеджер шлёт валидные условия с актуальным RowVersion, Then условия обновлены, Version += 1, возвращён свежий RowVersion; Id/TenantId/IsDefault/CreatedAt/CreatedByUserId неизменны; ответ 200 с BookingProgramModel.
  • Given IsPaid = true без PriceAmount (или IsPaid = false с непустым PriceAmount), When update, Then CRM_BOOKING_PROGRAM_PRICE_INVALID (422) по INV-BPR-1, изменение не применено.
  • Given другая живая программа TenantId уже носит это Name (ci), When update, Then CRM_BOOKING_PROGRAM_NAME_DUPLICATE (409) по INV-BPR-3 (pre-check или ux_booking_programs_tenant_name).
  • Given параллельный редактор уже закоммитил правку и RowVersion устарел, When update, Then CRM_BOOKING_PROGRAM_VERSION_CONFLICT (409); клиент перечитывает и повторяет.
  • Given программа архивна либо не существует в TenantId, When update, Then CRM_BOOKING_PROGRAM_NOT_FOUND (404); чужой tenant → CRM_BOOKING_PROGRAM_FORBIDDEN (403).
  • Given длительности вне диапазонов (ReserveDurationMinutes ∉ (0,10080] / BookingDurationHours ∉ (0,8760]), пустое после trim Name или SortOrder < 0, When update, Then VALIDATION_FAILED (400).
  • Given у P есть ранее созданные брони со снапшотом BookingProgramTerms, When условия P изменены, Then снимки в бронях и статусы объектов в chessboard не меняются (INV-BPR-4), Kafka-события не публикуются, доменный BookingProgramChanged лишь инвалидирует кэш списка опций.

Совместимость и наблюдаемость

  • Route/тело стабильны; добавление optional-полей в BookingProgramModel — additive (без breaking); удаление/переименование/смена типа — breaking (migration + rollout). PriceCurrencyId/RefundPolicy — additive optional. Правка условий совместима с существующими бронями по построению (INV-BPR-4: они хранят собственный snapshot).
  • Logs: booking-program update requested/succeeded/conflict/duplicate (TenantId, ProgramId, IsPaid; PII нет); metrics: crm_booking_program_update_total{result}; traces — сквозной trace через traceparent; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/ (будущий путь по конвенции).

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

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

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

  • Сущность и инварианты: booking-program.md (INV-BPR-1 цена/платность, INV-BPR-3 уникальность Name(ci), INV-BPR-4 иммутабельный snapshot, индекс ux_booking_programs_tenant_name, RowVersion); потребитель условий: booking.md (owned BookingProgramTerms); область developer и карта событий — ../../README.md.
  • Канал распространения условий наружу: booking-created.md (ProgramTerms snapshot в payload — единственный путь, которым внешние контексты видят условия программы).
  • ADR: 0052 tenancy, 0056 simplicity-first.