POST /api/developer/crm/booking-programs/{programId}/set-default — Программа бронирования по умолчанию
Менеджер застройщика назначает живую программу бронирования предлагаемой по умолчанию при бронировании
с витрины (точка входа инвестора «Забронировать» без явного выбора условий; см.
create-booking-request.md). Реализует инвариант INV-BPR-2
(booking-program.md): не более одной живой default-программы на
застройщика — установка новой снимает флаг с прежней в том же изменении, а гарантия БД — unique partial
ux_booking_programs_tenant_default. Дом полей и инвариантов —
booking-program.md.
Назначение
Помечает одну программу бронирования застройщика (TenantId) флагом IsDefault = true, атомарно сняв его с
прежней default-программы. Вызывает менеджер застройщика; тела ответа нет.
Отдельного ключа идемпотентности нет — операция естественно идемпотентна: если целевая программа уже
default, повторный вызов — no-op (мутаций и факта BookingProgramChanged нет), ответ 204.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | crm |
| API area | developer |
| Feature group | booking-programs |
| Статус | draft |
| Документ | docs/06-services/crm/api/developer/set-default-booking-program.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Менеджер застройщика в рамках своего TenantId = DeveloperCompanyId) |
| Auth policy / Permissions | crm.booking-programs.manage (управление справочником программ застройщика) |
| Scope (RBAC) | организация (застройщик): менеджер оперирует программами своего TenantId |
| Record-level | BookingProgramEntity.TenantId == actor.DeveloperCompanyId (программа принадлежит застройщику) |
| Tenant isolation | TenantId = DeveloperCompanyId в каждом запросе (ADR-0052) |
| Audit | update (UpdatedByUserId = actor на обеих затронутых программах, инкремент Version) |
Tenant isolation обязателен — TenantId = DeveloperCompanyId в каждом запросе (ADR-0052). Менеджер
оперирует только программами своего застройщика; программа чужого tenant → 403 CRM_BOOKING_PROGRAM_FORBIDDEN.
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/developer/crm/booking-programs/{programId}/set-default |
| Success status | 204 (флаг установлен; тело пустое) / 204 на идемпотентный повтор (программа уже default, шаг 4) |
| Idempotency header | не применяется — операция естественно идемпотентна (повторная установка той же default — no-op) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
TenantId | identity claims | Guid | Да | Required; = DeveloperCompanyId | Tenant isolation |
ProgramId | route | Guid | Да | != Guid.Empty; существующая живая программа в TenantId (шаг 2) | — |
ActorUserId | identity claims | Guid | Да | JWT менеджера; атрибуция UpdatedByUserId на обеих программах | — |
Тело запроса пустое: действие детерминировано (IsDefault = true на ProgramId).
Модель ответа
Тело отсутствует — по успеху HTTP 204 No Content. Актуальный список программ (с новой отметкой default)
клиент перечитывает отдельным list-методом; дом полей программы —
booking-program.md.
Алгоритм
- Контекст и доступ. Выполняет менеджер застройщика (
User, permissioncrm.booking-programs.manage); tenant —TenantId = DeveloperCompanyId. Операция естественно идемпотентна — повторная установка той же default обрабатывается как no-op (шаг 4). - Проверка входа и предусловий.
ProgramId != Guid.Empty— иначеVALIDATION_FAILED;- программа существует среди живых в
TenantId(архивные скрыты soft-delete-фильтром) — иначеCRM_BOOKING_PROGRAM_NOT_FOUND(default нельзя назначить неживой программе, INV-BPR-2 «живая default»); - scope:
program.TenantId == TenantId(застройщик оперирует своими программами) — иначеCRM_BOOKING_PROGRAM_FORBIDDEN.
- Что читаем. Целевую программу под блокировкой строки. Затем текущую default того же tenant (partial-index
ux_booking_programs_tenant_defaultгарантирует ≤ 1 такой строки) — тоже под блокировкой, чтобы сериализовать параллельные set-default одного застройщика. Порядок: сперва target, затем прежняя default. - Бизнес-правила и переходы. Ветвление по
target.IsDefault:- уже
true→ идемпотентный no-op: мутаций и событий нет, сразу результат (шаг 8); false→ снять флаг с прежней default (если она есть и это не target):IsDefault = false,UpdatedByUserId = ActorUserId, фактBookingProgramChanged; затем назначить целевую:IsDefault = true,UpdatedByUserId = ActorUserId, фактBookingProgramChanged(INV-BPR-2). Порядок «снять → назначить» исключает промежуточное состояние с двумяis_default = true.
- уже
- Что меняется. Прежняя default (если была) теряет флаг, целевая получает
IsDefault = true. Конкурентный доступ — по версии строки; параллельный set-default, пропустивший заблокированную строку, упирается в unique partialux_booking_programs_tenant_defaultпри попытке второгоis_default = true→CRM_BOOKING_PROGRAM_DEFAULT_CONFLICT. - События. Интеграционных Kafka-событий нет: программы читаются синхронно (список опций бронирования —
API CRM), условия распространяются snapshot-ом внутри booking-событий; топика
pin.crm.booking-program-*не существует. Внутренние доменные фактыBookingProgramChanged(на target и, если менялась, на прежнюю default) инвалидируют кэш списка опций. - Согласованность. Снятие прежней default и назначение целевой фиксируются в одном согласованном изменении
(атомарно, обе строки одного
TenantId). - Результат. Тела ответа нет → HTTP
204 No Content; идемпотентный повтор (target уже default) — тоже204.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | none (программы читаются синхронно; kafka-топика программ нет) |
| External calls | none |
| Cache / projections / search | новая default попадает в список опций бронирования (query-метод программ); прежняя теряет отметку |
| Notifications | none (внутренняя настройка справочника; клиента/агента не уведомляем) |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | ProgramId = Guid.Empty | «Некорректный идентификатор программы» | 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_DEFAULT_CONFLICT | 409 | гонка параллельных set-default (нарушение unique ux_booking_programs_tenant_default) | «Не удалось назначить программу по умолчанию, повторите» | retry (перечитать и повторить) |
Acceptance Criteria
- Given живая программа
PзастройщикаTсIsDefault = falseи текущая defaultP0, When менеджерTвызывает set-default дляP, ThenPполучаетIsDefault = true,P0теряет флаг в том же изменении, ответ204, инвариант INV-BPR-2 соблюдён (ровно одна default уT). - Given у застройщика ещё нет ни одной default-программы, When set-default для живой
P, ThenPстановится default, ответ204, снимать флаг не с кого. - Given
Pуже является default, When повторный set-default дляP, Then no-op — мутаций и фактаBookingProgramChangedнет, ответ204. - Given
ProgramIdне существует либо архивирована вT, When set-default, ThenCRM_BOOKING_PROGRAM_NOT_FOUND(404), default не изменена (неживой программе default не назначается). - Given программа другого застройщика либо нет
crm.booking-programs.manage, When set-default, ThenCRM_BOOKING_PROGRAM_FORBIDDEN(403). - Given два параллельных set-default одного
Tна разные программы, When оба коммитятся, Then один выигрывает, второй нарушает uniqueux_booking_programs_tenant_default→CRM_BOOKING_PROGRAM_DEFAULT_CONFLICT(409); промежуточное состояние с двумяis_default = trueне наблюдается. - Given
ProgramId = Guid.Empty, When set-default, ThenVALIDATION_FAILED(400).
Совместимость и наблюдаемость
- Route/пустое тело стабильны; ответ
204без тела — добавление тела было бы breaking (сейчас клиент перечитывает список). Enum-полей на входе/выходе нет. Удаление/переименование route — breaking (rollout). - Logs:
booking-program set-default requested/succeeded/conflict/noop(ProgramId,TenantId,PreviousDefaultId; PII отсутствует); metrics:crm_booking_program_set_default_total{result}; traces — сквозной trace черезtraceparent; dashboardpin-crm; runbook —docs/06-services/crm/runbooks/(будущий путь по конвенции).
Обратные ссылки
Автоссылки: где используется этот документ.
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность и инварианты: booking-program.md (INV-BPR-2 default-конфликт,
unique
ux_booking_programs_tenant_default, доменноеBookingProgramChanged); потребитель условий: booking.md. - Точка входа витрины, где применяется default: create-booking-request.md; карта областей/событий — ../../README.md.
- ADR: 0052 tenancy, 0056 simplicity-first.