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 area | developer |
| Feature group | booking |
| Статус | draft |
| Документ | docs/06-services/crm/api/developer/update-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 | update (UpdatedByUserId = actor, инкремент Version); PII отсутствует |
Правит программу менеджер застройщика — владелец программы по tenant (BookingProgramEntity.TenantId == actor.DeveloperCompanyId), либо Менеджер ПИН (platform-scope, тот же permission
crm.booking-programs.manage). Чужой tenant → CRM_BOOKING_PROGRAM_FORBIDDEN.
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | PUT |
| Route | /api/developer/crm/booking-programs/{programId} |
| Success status | 200 (условия обновлены; тело — карточка программы BookingProgramModel) |
| Idempotency header | не применяется — PUT семантически идемпотентен (полная замена набора условий); защита от потерянного обновления — RowVersion (тело) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
IsDefault во входе отсутствует намеренно: смена флага «по умолчанию» — отдельная команда SetDefault
(INV-BPR-2). Price — денежная величина (Money, контракт — deal.md); на входе
разложена на PriceAmount + PriceCurrencyId, чтобы не принимать нетипизированный объект.
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
TenantId | identity claims | Guid | Да | Required; = DeveloperCompanyId | Tenant isolation |
ActorUserId | identity claims | Guid | Да | JWT менеджера; record-level + UpdatedByUserId | — |
ProgramId | route | Guid | Да | != Guid.Empty; существующая живая программа в TenantId (шаг 2) | — |
Name | body | string | Да | 1..128, не пустое после trim; уникально по TenantId + lower(Name) среди живых, исключая себя (INV-BPR-3) | — |
Description | body | string? | Нет | 0..2000 | — |
IsPaid | body | bool | Да | согласовано с PriceAmount (INV-BPR-1) | — |
PriceAmount | body | decimal? | Нет (обязателен при IsPaid = true) | > 0; при IsPaid = false — должен быть null (INV-BPR-1) | — |
PriceCurrencyId | body | short? | Нет | ссылка на справочник валют handbook; не задан → валюта RUB (RUB-only, ADR-0048); неизвестная/архивная запись → VALIDATION_FAILED; значим только при IsPaid = true | — |
ReserveDurationMinutes | body | int | Да | > 0 and <= 10080 (неделя) | — |
BookingDurationHours | body | int | Да | > 0 and <= 8760 (год) | — |
RefundPolicy | body | string? | Нет | 0..2000 (US-INV-10: порядок возврата) | — |
SortOrder | body | int | Да | >= 0 | — |
RowVersion | body | uint | Да | ожидаемая версия для оптимистической блокировки; несовпадение → 409 (шаг 5) | — |
Модель ответа BookingProgramModel
Проекция агрегата — дом полей booking-program.md; ниже только состав
карточки программы, возвращаемой update/get/list. TenantId наружу не отдаётся (внутренняя изоляция).
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
Id | Guid | Да | BookingProgramEntity.Id | — | Идентификатор программы. |
Name | string | Да | BookingProgramEntity.Name | — | Название программы. |
Description | string? | Нет | BookingProgramEntity.Description | — | Описание условий для клиента. |
IsPaid | bool | Да | BookingProgramEntity.IsPaid | — | Платная ли бронь. |
PriceAmount | decimal? | Нет | BookingProgramEntity.Price.Amount | — | Сумма (только при IsPaid). |
PriceCurrencyId | short? | Нет | BookingProgramEntity.Price.CurrencyId | — | Валюта суммы (справочник handbook; display-код — lookup справочника). |
ReserveDurationMinutes | int | Да | BookingProgramEntity.ReserveDurationMinutes | — | Длительность фазы «Резерв». |
BookingDurationHours | int | Да | BookingProgramEntity.BookingDurationHours | — | Длительность фазы «Бронь». |
RefundPolicy | string? | Нет | BookingProgramEntity.RefundPolicy | — | Порядок возврата платы. |
IsDefault | bool | Да | BookingProgramEntity.IsDefault | — | Read-only здесь (меняется командой SetDefault). |
SortOrder | int | Да | BookingProgramEntity.SortOrder | — | Порядок в списке опций. |
UpdatedAt | DateTimeOffset | Да | BookingProgramEntity.UpdatedAt | — | Момент правки. |
Version | long | Да | BookingProgramEntity.Version | — | Инкрементирована на 1. |
RowVersion | uint | Да | BookingProgramEntity.RowVersion | — | Новый токен оптимистической блокировки. |
Алгоритм
- Контекст и доступ. Выполняет менеджер застройщика (
User, permissioncrm.booking-programs.manage; менеджер ПИН — platform-scope); tenant —TenantId = DeveloperCompanyId. PUT идемпотентен по природе (полная замена условий), от потерянного обновления защищаетRowVersion. - Проверка входа и предусловий.
ProgramId != Guid.Empty;Nameне пусто после trim (1..128);Description/RefundPolicy0..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.
- Что читаем. Только саму программу (single-aggregate update). Существующие брони не читаются и не
трогаются — INV-BPR-4: их условия зафиксированы snapshot-ом
BookingProgramTermsпри создании брони и от текущей программы не зависят. - Бизнес-правила и переходы. Обновление условий закрепляет 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. - Что меняется. Обновляются условия программы (имя, описание, платность/цена, длительности, порядок
возврата,
SortOrder, audit/Version). Оптимистическая блокировка поRowVersion: если параллельный редактор уже закоммитил правку — конфликтCRM_BOOKING_PROGRAM_VERSION_CONFLICT(перечитать и повторить). Гонку с параллельным rename/create того же имени ловитux_booking_programs_tenant_name→CRM_BOOKING_PROGRAM_NAME_DUPLICATE; checkck_booking_programs_priceподстраховывает INV-BPR-1. - События. Интеграционных Kafka-событий нет: программы читаются синхронно (API список опций), а
условия распространяются snapshot-ом внутри booking-событий (booking-created.md,
ProgramTerms); топикаpin.crm.booking-programне существует. Внутренний доменный фактBookingProgramChangedинвалидирует кэш списка опций бронирования. Правка не рассылается ни существующим броням, ниchessboard— INV-BPR-4 (снятые снимки условий и статусы объектов от текущей программы не зависят). - Согласованность. Обновление программы фиксируется в одном согласованном изменении (атомарно). Конфликт
RowVersionили нарушение unique-индекса откатывают изменение → соответствующий код (шаг 5). - Результат. Карточка
BookingProgramModelобновлённой программы (все условия + новыеVersion/RowVersion) → HTTP200.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | none (программы — внутренний справочник; kafka-топика нет; условия идут snapshot-ом в booking-событиях) |
| External calls | none |
| Cache / projections / search | доменное BookingProgramChanged → инвалидация кэша списка опций бронирования застройщика |
| Notifications | none (существующие брони не затрагиваются — INV-BPR-4) |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | формат/длина/диапазоны полей (Name, длительности, SortOrder) | «Проверьте корректность полей программы» | no retry |
CRM_BOOKING_PROGRAM_PRICE_INVALID | 422 | рассогласование IsPaid/PriceAmount (INV-BPR-1; validator + ck_booking_programs_price) | «Укажите стоимость для платной брони» | no retry |
CRM_BOOKING_PROGRAM_NOT_FOUND | 404 | программа не найдена / архивна в TenantId | «Программа бронирования не найдена» | no retry |
CRM_BOOKING_PROGRAM_FORBIDDEN | 403 | чужой tenant / нет crm.booking-programs.manage | «Нет доступа к программе бронирования» | no retry |
CRM_BOOKING_PROGRAM_NAME_DUPLICATE | 409 | живая программа с таким Name (ci) уже есть в TenantId (pre-check либо ux_booking_programs_tenant_name) | «Программа с таким названием уже существует» | no retry (сменить имя) |
CRM_BOOKING_PROGRAM_VERSION_CONFLICT | 409 | RowVersion не совпал (параллельная правка) | «Программа изменена другим пользователем, обновите данные» | 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, ThenCRM_BOOKING_PROGRAM_PRICE_INVALID(422) по INV-BPR-1, изменение не применено. - Given другая живая программа
TenantIdуже носит этоName(ci), When update, ThenCRM_BOOKING_PROGRAM_NAME_DUPLICATE(409) по INV-BPR-3 (pre-check илиux_booking_programs_tenant_name). - Given параллельный редактор уже закоммитил правку и
RowVersionустарел, When update, ThenCRM_BOOKING_PROGRAM_VERSION_CONFLICT(409); клиент перечитывает и повторяет. - Given программа архивна либо не существует в
TenantId, When update, ThenCRM_BOOKING_PROGRAM_NOT_FOUND(404); чужой tenant →CRM_BOOKING_PROGRAM_FORBIDDEN(403). - Given длительности вне диапазонов (
ReserveDurationMinutes ∉ (0,10080]/BookingDurationHours ∉ (0,8760]), пустое после trimNameилиSortOrder < 0, When update, ThenVALIDATION_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; dashboardpin-crm; runbook —docs/06-services/crm/runbooks/(будущий путь по конвенции).
Обратные ссылки
Автоссылки: где используется этот документ.
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность и инварианты: booking-program.md (INV-BPR-1 цена/платность,
INV-BPR-3 уникальность
Name(ci), INV-BPR-4 иммутабельный snapshot, индексux_booking_programs_tenant_name,RowVersion); потребитель условий: booking.md (ownedBookingProgramTerms); область developer и карта событий — ../../README.md. - Канал распространения условий наружу: booking-created.md (
ProgramTermssnapshot в payload — единственный путь, которым внешние контексты видят условия программы). - ADR: 0052 tenancy, 0056 simplicity-first.