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

POST /api/developer/crm/fixations/{fixationId}/approve — Одобрить фиксацию (уникальная / не уникальная)

Менеджер застройщика по кнопке «Одобрить» карточки фиксации закрепляет клиента за агентом как уникального (FixedUnique) или не уникального (FixedNonUnique) клиента застройщика. Решение принимается только из Pending (INV-FIX-3). Одобрение — это авторизация, а не сделка: оно подтверждает право агента вести клиента по объектам застройщика и сделку не создаёт (INV-FIX-6). Дом полей заявки — fixation-request.md. Отклонение — отдельная операция (reject).

Назначение

Одобряет заявку на фиксацию: переводит её Pending → FixedUnique или Pending → FixedNonUnique с фиксацией «кто и когда решил». Вызывает менеджер застройщика. Одобрение авторизует агента создавать сделки по объектам этого застройщика для данного клиента (пара «клиент × застройщик») — сколько угодно, связь фиксация → сделки 1:N ведётся со стороны сделки (deal.md). Сама операция сделку не создаёт. На выходе — обновлённая карточка фиксации; смежный контекст rewards получает повод зафиксировать право (eligibility) агента (комиссия материализуется позже — при завершении сделки), notifications — уведомить агента.

Сделки создаются отдельно и позже — агентом, по конкретному объекту (create-deal.md): фиксация — уровня клиента, не объекта, поэтому PropertyObjectId/ProjectId/PaymentMethod задаются при создании сделки, а не здесь.

Metadata

ПолеЗначение
Сервис/контекстcrm
API areadeveloper (JWT менеджера застройщика)
Feature groupfixation
Статусdraft
Документdocs/06-services/crm/api/developer/approve-fixation.md

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

ПроверкаЗначение
Actor typeUser (Менеджер застройщика); Operator (Менеджер ПИН — platform-scope, арбитраж)
Auth policy / Permissionscrm.fixations.decide (сторона застройщика)
Scope (RBAC)организация (застройщик) — TenantId = DeveloperCompanyId
Record-levelзаявка принадлежит tenant актора; решает менеджер компании TenantId (INV-FIX-3)
Tenant isolationTenantId = DeveloperCompanyId из claims; заявка видна только в своём tenant (чужой tenant не раскрывается = 404, ADR-0052)
Auditupdate-decide (DecidedByUserId = actor.UserId, DecidedAt); forceUnique = true фиксируется в audit (INV-FIX-5); ФИО/телефон клиента в логи не пишутся
Доступ

Решение принимает менеджер застройщика — владелец заявки по tenant (TenantId = DeveloperCompanyId), либо Менеджер ПИН в режиме арбитража (platform-scope). Заявка чужого tenant не раскрывается: обращение к ней даёт 404 CRM_FIXATION_NOT_FOUND, а не 403 (ADR-0052).

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/developer/crm/fixations/{fixationId}/approve
Success status200 (одобрено, тело — карточка фиксации в статусе FixedUnique/FixedNonUnique)
Idempotency headerне применяется (защита от повтора — переход только из Pending, INV-FIX-3 → повторный вызов 409)
Correlationсквозной trace через заголовок traceparent

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

Важно

ForceUnique учитывается только при Kind = FixedUnique и обнаруженных дублях (Uniqueness = HasDuplicates, INV-FIX-5) — в остальных случаях поле игнорируется.

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
TenantIdidentity claimsGuidДаRequired; = DeveloperCompanyId актораTenant isolation
FixationIdrouteGuidДа!= Guid.Empty; существующая заявка в tenant (шаг 2)
Kindbodystring (enum FixationApprovalKind)Даодно из FixedUnique/FixedNonUnique строкой; иное → ValidationError
ForceUniquebodybool?Нетучитывается только при Kind = FixedUnique c дублями (INV-FIX-5); default falseaudit
ActorUserIdidentity claimsGuidДаJWT менеджера застройщика; пишется в DecidedByUserId

Kind — публичный вид решения об одобрении; enum FixationApprovalKind { FixedUnique, FixedNonUnique } маппится на статусы FixationStatus (дом — fixation-request.md). Тело плоское, вложенных коллекций нет.

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

Проекция заявки после решения; дом полей — get-fixation.md (агрегат — fixation-request.md). Ниже — только дельта, изменяемая одобрением.

ПолеТипОбязательностьИсточникMaskingОписание
FixationIdGuidДаIdИдентификатор заявки.
Statusstring (enum FixationStatus)ДаStatusПосле операции — FixedUnique / FixedNonUnique.
Uniquenessstring (enum FixationUniqueness)ДаUniquenessРезультат авто-дубль-детекта (NoDuplicates/HasDuplicates); влияет на INV-FIX-5.
DecidedByUserIdGuidДаDecidedByUserIdМенеджер застройщика, принявший решение.
DecidedAtDateTimeOffsetДаDecidedAtМомент одобрения.

Сделка при одобрении не создаётся, поэтому в карточке нет ссылки на сделку. Остальные поля карточки (ClientName/ClientPhoneNumber snapshot, LeadId, ClientId, эффективные Wishes, DuplicateOfFixationId/DuplicateMatchKind) — без изменений, дом — get-fixation.md.

Алгоритм

  1. Контекст и доступ. Операцию выполняет менеджер застройщика (User) с permission crm.fixations.decide; арбитраж возможен менеджером ПИН (platform-scope). Tenant — TenantId = DeveloperCompanyId. Отдельного ключа идемпотентности нет: защита от повторного одобрения обеспечивается переходом только из Pending (шаг 4).

  2. Проверка входа и предусловий.

    • FixationId задан (!= Guid.Empty); Kind задан и принадлежит enum FixationApprovalKind (FixedUnique/FixedNonUnique) — иначе VALIDATION_FAILED;
    • заявка существует в tenant застройщика — иначе CRM_FIXATION_NOT_FOUND (чужой tenant не раскрывается: 404);
    • состояние допускает решение: Status == Pending (INV-FIX-3) — иначе CRM_FIXATION_INVALID_STATE (уже решена/отменена);
    • отсутствие permission .decide403.
  3. Что читаем. Заявку берём под блокировкой строки (гонка «агент отменяет / застройщик одобряет»). Дополнительных сущностей (сделка, канбан, назначение менеджеров ПИН) операция не читает — сделка не создаётся.

  4. Бизнес-правила и переходы. Одобрение = авторизация (INV-FIX-3/5/6):

    • guard уникальности (INV-FIX-5): Kind = FixedUnique допустим только при Uniqueness == NoDuplicates или при ForceUnique = true (последнее фиксируется в audit) — иначе CRM_FIXATION_DUPLICATE_CONFLICT;
    • переход Pending → FixedUnique/FixedNonUnique; маппинг: Status = Kind, DecidedByUserId = ActorUserId, DecidedAt = now, UpdatedByUserId = ActorUserId, UpdatedAt = now;
    • фиксируется доменный факт FixationDecided.

    Сделка при одобрении не создаётся (INV-FIX-6): фиксация авторизует агента вести клиента по объектам застройщика (пара «клиент × застройщик»), но объект здесь не выбирается и участники сделки не собираются. Сделки создаются отдельно и позже — по конкретному объекту (create-deal.md); одобренная фиксация авторизует их сколько угодно (1:N со стороны сделки).

  5. Что меняется. Заявка переходит в решённый статус (FixedUnique/FixedNonUnique) с DecidedBy/At. Конкурентный доступ — по версии строки: если агент параллельно отменил заявку, одобрение не должно затирать отмену → CRM_FIXATION_INVALID_STATE.

  6. События. Публикуется одно интеграционное событие FixationApprovedEvent → топик pin.crm.fixation-approved.v1 (ContractVersion = 1; payload — данные заявки + Uniqueness, без DealId — событие авторизации; потребитель rewards — право/eligibility, notifications — уведомление агенту; дом — fixation-approved.md). deal-created.v1 не публикуется (сделка не создаётся).

  7. Согласованность. Переход статуса заявки, простановка DecidedBy/At и публикация события фиксируются в одном согласованном изменении (атомарно).

  8. Результат. Карточка фиксации FixationCardModel с новым Status, DecidedByUserId, DecidedAt → HTTP 200.

Диаграмма

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

ТипДетали
Integration eventsFixationApprovedEventpin.crm.fixation-approved.v1 (rewards: право/eligibility исполнителя), outbox. Сделка не создаётся — deal-created.v1 не публикуется
External callsnone (сделка не создаётся; объект в этой операции не выбирается)
Cache / projections / searchпоисковая проекция фиксаций (FixationRequestSearchDocument) обновляется (Status = FixedUnique/FixedNonUnique); ix_fixation_requests_status
Notificationsопосредованно: notifications — консюмер fixation-approved (агенту «фиксация одобрена»)

Ошибки и failure modes

Ошибки
КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400формат FixationId / Kind не из enum«Проверьте корректность запроса»no retry
CRM_FIXATION_NOT_FOUND404заявка не найдена в tenant застройщика«Заявка на фиксацию не найдена»no retry
CRM_FIXATION_INVALID_STATE409Status != Pending (INV-FIX-3; в т.ч. гонка отмены агентом по RowVersion)«Заявка уже обработана»no retry (перечитать состояние)
CRM_FIXATION_DUPLICATE_CONFLICT409Kind = FixedUnique при Uniqueness = HasDuplicates без forceUnique (INV-FIX-5)«Найден дубль — выберите «не уникальный» или подтвердите уникальность»no retry

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

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

  • актор — менеджер застройщика (или Менеджер ПИН, арбитраж) с permission crm.fixations.decide;
  • заявка существует в TenantId = DeveloperCompanyId актора и находится в статусе Pending (INV-FIX-3);
  • результат авто-дубль-детекта (Uniqueness) уже проставлен при создании заявки — операция его не пересчитывает.

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

  • заявка переведена Pending → FixedUnique/FixedNonUnique, проставлены DecidedByUserId/DecidedAt; при forceUnique = true факт зафиксирован в audit (INV-FIX-5);
  • сделка не создаётся (INV-FIX-6): одобрение авторизует агента создавать сделки по объектам застройщика для данного клиента отдельно и позже (create-deal.md); связь фиксация → сделки 1:N ведётся со стороны сделки;
  • в outbox — одно событие fixation-approved.v1 (событие авторизации, без DealId).

Интеграционная последовательность (после коммита)

Eventual consistency

Актор в ответе 200 уже видит одобренную заявку, но публикация события и её обработка потребителями — асинхронные, после атомарного коммита (окно eventual-consistency — секунды).

  1. FixationApprovedEventpin.crm.fixation-approved.v1 (данные заявки + Uniqueness, без DealId):
    • rewards — фиксирует право/eligibility исполнителя по одобренной фиксации (комиссия материализуется позже — при завершении сделки, pin.crm.deal-completed.v1);
    • notifications — уведомляет агента «фиксация одобрена».

Сделка не создаётся, поэтому deal-created.v1 не публикуется и чат сделки на этом шаге не открывается.

Что предоставляет: событие — additive-only контракт; консюмеры идемпотентны по своему ключу-эффекту.

Acceptance Criteria

  • Happy path (уникальная). Дано: заявка Pending, Uniqueness = NoDuplicates, Kind = FixedUnique. Когда: менеджер одобряет. Тогда: 200, статус FixedUnique, сделка не создаётся, опубликовано ровно одно fixation-approved.v1 (без DealId); deal-created.v1 не публикуется.
  • Happy path (не уникальная). Дано: заявка Pending, Kind = FixedNonUnique (независимо от дублей). Когда: одобрение. Тогда: 200, статус FixedNonUnique, сделка не создаётся, опубликовано fixation-approved.v1.
  • Конфликт дублей. Дано: Kind = FixedUnique, Uniqueness = HasDuplicates, forceUnique = false. Когда: одобрение. Тогда: 409 CRM_FIXATION_DUPLICATE_CONFLICT, статус не меняется, события нет.
  • Форс-уникальность. Дано: те же дубли, но forceUnique = true. Когда: одобрение. Тогда: 200, факт форсирования в audit (INV-FIX-5).
  • Уже решена / гонка отмены. Дано: заявка не Pending (решена ранее либо агент параллельно отменил по RowVersion). Когда: одобрение. Тогда: 409 CRM_FIXATION_INVALID_STATE, события нет.
  • Повторное одобрение. Дано: заявка уже FixedUnique. Когда: повтор. Тогда: 409 CRM_FIXATION_INVALID_STATE; событие fixation-approved.v1 опубликовано ровно один раз (при первом одобрении).

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

  • ForceUnique — additive optional; Kind — enum строкой, unknown → ValidationError; удаление/переименование поля или смена Success status — breaking (migration + rollout). Контракт события — additive-only (fixation-approved.md).
  • Logs: fixation approved (FixationId, TenantId, Kind, ForceUnique; ФИО/телефон — не пишутся); metrics: crm_fixation_decisions_total{decision="approve",kind}, latency p95 < 500 мс; traces — сквозной trace через traceparent; dashboard pin-crm; runbook — docs/06-services/crm/runbooks/ (будущий путь).

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

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

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