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

POST /api/developer/crm/booking-programs/{programId}/archive — Архивировать программу бронирования

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

Ограничения

Архивировать нельзя: последнюю живую программу застройщика (INV-BPR-5 → 409 CRM_BOOKING_PROGRAM_LAST) и программу, назначенную default (INV-BPR-2 → 409 CRM_BOOKING_PROGRAM_IS_DEFAULT; сначала переназначьте default другой программой).

Назначение

Помечает программу бронирования архивной (DeletedAt) в TenantId = DeveloperCompanyId, не затрагивая ни существующие брони (их условия — snapshot), ни ссылку bookings.program_id (связь restrict, архив вместо физического удаления — booking-program.md, раздел «Связи»). Вызывает менеджер застройщика; по успеху программа выпадает из списков опций и управления.

Metadata

ПолеЗначение
Сервис/контекстcrm
API areadeveloper
Feature groupbooking-programs
Статусdraft
Документdocs/06-services/crm/api/developer/archive-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)
Auditdelete (soft-delete: DeletedAt, DeletedByUserId = actor, инкремент Version)
Доступ

Tenant isolation обязателен — TenantId = DeveloperCompanyId в каждом запросе (ADR-0052). Программа чужого tenant неотличима от отсутствия: 403 CRM_BOOKING_PROGRAM_FORBIDDEN.

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/developer/crm/booking-programs/{programId}/archive
Success status204 (программа заархивирована; тело пустое)
Idempotency headerне применяется — повторный архив уже недоступной (архивной) программы → 404 (soft-delete-фильтр исключает её из живых)
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
TenantIdidentity claimsGuidДаRequired; = DeveloperCompanyIdTenant isolation
ProgramIdrouteGuidДа!= Guid.Empty; существующая живая программа в TenantId (шаг 2)
ActorUserIdidentity claimsGuidДаJWT менеджера; используется для record-level и DeletedByUserId

Тело запроса пустое: действие детерминировано (пометить программу архивной). Параметров нет.

Результат операции

Тела ответа нет — по успеху возвращается HTTP 204 No Content. Наружу никакая проекция не отдаётся: клиент по успеху убирает программу из списка опций бронирования и списков управления. Именованной response-модели нет (мутация без данных).

Алгоритм

  1. Контекст и доступ. Выполняет менеджер застройщика (User, permission crm.booking-programs.manage; менеджер ПИН — platform-scope); tenant — TenantId = DeveloperCompanyId. Идемпотентность не требуется — повтор архивной программы отсекается soft-delete-фильтром (шаг 2) как 404.
  2. Проверка входа и предусловий.
    • ProgramId != Guid.Empty — иначе VALIDATION_FAILED;
    • программа существует среди живых в TenantId (архивные исключены soft-delete-фильтром deleted_at is null) — иначе CRM_BOOKING_PROGRAM_NOT_FOUND (повторный архив тоже даёт 404);
    • scope: program.TenantId == TenantId (застройщик оперирует своими программами) — иначе CRM_BOOKING_PROGRAM_FORBIDDEN;
    • default-guard (INV-BPR-2): программа не default (IsDefault == false) — иначе CRM_BOOKING_PROGRAM_IS_DEFAULT (у витрины должен остаться живой default; менеджер сперва переназначает default другой программой командой SetDefault, затем архивирует эту).
  3. Что читаем. Берём транзакционную блокировку на уровне застройщика (по TenantId) — она сериализует конкурентные архив/назначение default одного застройщика и детерминированно закрывает гонку INV-BPR-5 (см. шаг 7) без внешней инфраструктуры (ADR-0056: PostgreSQL-only). Затем целевую программу под блокировкой строки и счётчик живых программ застройщика (deleted_at is null) в том же снапшоте — для INV-BPR-5.
  4. Бизнес-правила и переходы.
    • INV-BPR-5 (последняя живая): живых программ должно остаться больше одной — иначе CRM_BOOKING_PROGRAM_LAST (нельзя оставить застройщика без единственной программы; SEED «Бесплатная бронь 4 часа» — минимум);
    • soft-delete программы. Маппинг: DeletedAt = now, DeletedByUserId = ActorUserId, UpdatedAt = now, UpdatedByUserId = ActorUserId, Version += 1;
    • существующие брони с program_id = ProgramId не трогаются: их условия — иммутабельный snapshot BookingProgramTerms (INV-BPR-4, booking.md); FK остаётся валидным (архив, не физическое удаление — связь restrict);
    • фиксируется доменный факт BookingProgramChanged.
  5. Что меняется. Программа помечается архивной (DeletedAt) и исчезает из живых списков; дочерние брони не удаляются (restrict). Конкурентный доступ — по версии строки: при параллельном архиве/назначении default той же строки запись проигрывает → перечитка и CRM_BOOKING_PROGRAM_NOT_FOUND (уже архивна) либо CRM_BOOKING_PROGRAM_LAST.
  6. События. Интеграционных Kafka-событий нет: топика pin.crm.booking-program не существует; условия распространяются snapshot-ом внутри booking-событий (booking-created.md), уже отправленные booking-события архивом не затрагиваются. Внутренний доменный факт BookingProgramChanged инвалидирует кэш списка опций бронирования.
  7. Согласованность. Блокировка застройщика → счётчик живых (INV-BPR-5) → soft-delete фиксируются в одном согласованном изменении (атомарно). Поэтому конкурентный архив двух последних разных программ одного застройщика не может обойти проверку: второй запрос ждёт блокировку, перечитывает счётчик = 1 и получает CRM_BOOKING_PROGRAM_LAST.
  8. Результат. Тела ответа нет → HTTP 204 No Content. Повторный вызов на той же программе → CRM_BOOKING_PROGRAM_NOT_FOUND (404).

Диаграмма

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

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

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400ProgramId = Guid.Empty«Некорректный идентификатор программы»no retry
CRM_BOOKING_PROGRAM_NOT_FOUND404программа не найдена среди живых в TenantId (или уже архивна)«Программа бронирования не найдена»no retry
CRM_BOOKING_PROGRAM_FORBIDDEN403чужой tenant / нет crm.booking-programs.manage«Нет доступа к программе бронирования»no retry
CRM_BOOKING_PROGRAM_IS_DEFAULT409IsDefault = true (INV-BPR-2: сначала переназначить default)«Нельзя архивировать программу по умолчанию — назначьте другую»no retry (сделать default другую)
CRM_BOOKING_PROGRAM_LAST409Count(живые) <= 1 — последняя живая программа застройщика (INV-BPR-5)«Нельзя архивировать последнюю программу бронирования»no retry (создать другую)

Предусловия и постусловия

Предусловия (истинны до сценария):

  • актор — менеджер застройщика (или Менеджер ПИН, platform-scope) с permission crm.booking-programs.manage;
  • программа существует среди живых (deleted_at is null) в TenantId = DeveloperCompanyId актора;
  • программа не default (IsDefault == false, INV-BPR-2);
  • у застройщика более одной живой программы (INV-BPR-5) — иначе архивировать нельзя.

Постусловия (гарантированы после успеха):

  • программа помечена архивной (DeletedAt, DeletedByUserId, Version +1) и исчезает из живых списков опций и управления; повторный архив той же программы даёт 404;
  • уже созданные брони со ссылкой program_id не изменены: их условия зафиксированы иммутабельным snapshot BookingProgramTerms (INV-BPR-4), FK остаётся валидным (связь restrict, архив ≠ физическое удаление);
  • default-программа застройщика и число живых программ (≥1) сохраняют инварианты;
  • интеграционных Kafka-событий не публикуется (топика pin.crm.booking-program нет); внутренний доменный факт BookingProgramChanged инвалидирует кэш списка опций бронирования, поэтому у витрины/агента программа выпадает из выбора при следующем чтении (синхронный источник, без окна eventual-consistency наружу).

Acceptance Criteria

  • Happy path. Дано: живая не-default программа, у застройщика есть ещё программы. Когда: менеджер архивирует. Тогда: 204, программа архивна, существующие брони сохраняют условия snapshot, событий нет.
  • Последняя программа. Дано: у застройщика ровно одна живая программа. Когда: попытка архива. Тогда: 409 CRM_BOOKING_PROGRAM_LAST, программа остаётся живой.
  • Default-программа. Дано: IsDefault == true. Когда: архив. Тогда: 409 CRM_BOOKING_PROGRAM_IS_DEFAULT (сначала назначить default другую программу).
  • Повторный архив. Дано: программа уже архивна. Когда: повтор. Тогда: 404 CRM_BOOKING_PROGRAM_NOT_FOUND.
  • Чужой застройщик. Дано: программа другого tenant. Когда: архив. Тогда: 403 CRM_BOOKING_PROGRAM_FORBIDDEN (чужой tenant неотличим от отсутствия).
  • Гонка двух последних. Дано: у застройщика две живые программы, два параллельных запроса архивируют разные. Когда: оба выполняются. Тогда: один — 204, второй ждёт блокировку застройщика, перечитывает счётчик = 1 и получает 409 CRM_BOOKING_PROGRAM_LAST (INV-BPR-5 не обойти).

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

  • Route/поведение стабильны; операция терминальна (архив). Восстановление из архива — вне этого контракта (отдельная команда/create). Смена HTTP-статуса или семантики soft-delete — breaking (migration + rollout).
  • Logs: booking-program-archive requested/succeeded/last/forbidden (TenantId, ProgramId, actor; PII отсутствует); metrics: crm_booking_program_archive_total{result}; traces — сквозной trace через traceparent; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/ (будущий путь по конвенции).

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

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

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

  • Сущность и инварианты: booking-program.md (INV-BPR-5 последняя живая, INV-BPR-4 иммутабельность snapshot, INV-BPR-2 default, ISoftDeleteEntity, индексы ux_booking_programs_tenant_default/ux_booking_programs_tenant_name, доменное BookingProgramChanged); потребитель условий: booking.md (BookingProgramTerms); карта областей — ../../README.md.
  • Snapshot условий в событиях брони: booking-created.md (условия программы уезжают snapshot-ом; архив программы их не меняет).
  • ADR: 0052 tenancy, 0056 simplicity-first.