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 area | developer (JWT менеджера застройщика) |
| Feature group | fixation |
| Статус | draft |
| Документ | docs/06-services/crm/api/developer/approve-fixation.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Менеджер застройщика); Operator (Менеджер ПИН — platform-scope, арбитраж) |
| Auth policy / Permissions | crm.fixations.decide (сторона застройщика) |
| Scope (RBAC) | организация (застройщик) — TenantId = DeveloperCompanyId |
| Record-level | заявка принадлежит tenant актора; решает менеджер компании TenantId (INV-FIX-3) |
| Tenant isolation | TenantId = DeveloperCompanyId из claims; заявка видна только в своём tenant (чужой tenant не раскрывается = 404, ADR-0052) |
| Audit | update-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-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/developer/crm/fixations/{fixationId}/approve |
| Success status | 200 (одобрено, тело — карточка фиксации в статусе FixedUnique/FixedNonUnique) |
| Idempotency header | не применяется (защита от повтора — переход только из Pending, INV-FIX-3 → повторный вызов 409) |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
ForceUnique учитывается только при Kind = FixedUnique и обнаруженных дублях (Uniqueness = HasDuplicates,
INV-FIX-5) — в остальных случаях поле игнорируется.
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
TenantId | identity claims | Guid | Да | Required; = DeveloperCompanyId актора | Tenant isolation |
FixationId | route | Guid | Да | != Guid.Empty; существующая заявка в tenant (шаг 2) | — |
Kind | body | string (enum FixationApprovalKind) | Да | одно из FixedUnique/FixedNonUnique строкой; иное → ValidationError | — |
ForceUnique | body | bool? | Нет | учитывается только при Kind = FixedUnique c дублями (INV-FIX-5); default false | audit |
ActorUserId | identity claims | Guid | Да | JWT менеджера застройщика; пишется в DecidedByUserId | — |
Kind — публичный вид решения об одобрении; enum FixationApprovalKind { FixedUnique, FixedNonUnique }
маппится на статусы FixationStatus (дом — fixation-request.md). Тело
плоское, вложенных коллекций нет.
Модель ответа FixationCardModel
Проекция заявки после решения; дом полей — get-fixation.md (агрегат — fixation-request.md). Ниже — только дельта, изменяемая одобрением.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
FixationId | Guid | Да | Id | — | Идентификатор заявки. |
Status | string (enum FixationStatus) | Да | Status | — | После операции — FixedUnique / FixedNonUnique. |
Uniqueness | string (enum FixationUniqueness) | Да | Uniqueness | — | Результат авто-дубль-детекта (NoDuplicates/HasDuplicates); влияет на INV-FIX-5. |
DecidedByUserId | Guid | Да | DecidedByUserId | — | Менеджер застройщика, принявший решение. |
DecidedAt | DateTimeOffset | Да | DecidedAt | — | Момент одобрения. |
Сделка при одобрении не создаётся, поэтому в карточке нет ссылки на сделку. Остальные поля карточки
(ClientName/ClientPhoneNumber snapshot, LeadId, ClientId, эффективные Wishes,
DuplicateOfFixationId/DuplicateMatchKind) — без изменений, дом — get-fixation.md.
Алгоритм
-
Контекст и доступ. Операцию выполняет менеджер застройщика (
User) с permissioncrm.fixations.decide; арбитраж возможен менеджером ПИН (platform-scope). Tenant —TenantId = DeveloperCompanyId. Отдельного ключа идемпотентности нет: защита от повторного одобрения обеспечивается переходом только изPending(шаг 4). -
Проверка входа и предусловий.
FixationIdзадан (!= Guid.Empty);Kindзадан и принадлежит enumFixationApprovalKind(FixedUnique/FixedNonUnique) — иначеVALIDATION_FAILED;- заявка существует в tenant застройщика — иначе
CRM_FIXATION_NOT_FOUND(чужой tenant не раскрывается: 404); - состояние допускает решение:
Status == Pending(INV-FIX-3) — иначеCRM_FIXATION_INVALID_STATE(уже решена/отменена); - отсутствие permission
.decide→403.
-
Что читаем. Заявку берём под блокировкой строки (гонка «агент отменяет / застройщик одобряет»). Дополнительных сущностей (сделка, канбан, назначение менеджеров ПИН) операция не читает — сделка не создаётся.
-
Бизнес-правила и переходы. Одобрение = авторизация (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 со стороны сделки).
- guard уникальности (INV-FIX-5):
-
Что меняется. Заявка переходит в решённый статус (
FixedUnique/FixedNonUnique) сDecidedBy/At. Конкурентный доступ — по версии строки: если агент параллельно отменил заявку, одобрение не должно затирать отмену →CRM_FIXATION_INVALID_STATE. -
События. Публикуется одно интеграционное событие
FixationApprovedEvent→ топикpin.crm.fixation-approved.v1(ContractVersion = 1; payload — данные заявки +Uniqueness, безDealId— событие авторизации; потребительrewards— право/eligibility,notifications— уведомление агенту; дом — fixation-approved.md).deal-created.v1не публикуется (сделка не создаётся). -
Согласованность. Переход статуса заявки, простановка
DecidedBy/Atи публикация события фиксируются в одном согласованном изменении (атомарно). -
Результат. Карточка фиксации
FixationCardModelс новымStatus,DecidedByUserId,DecidedAt→ HTTP200.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | FixationApprovedEvent → pin.crm.fixation-approved.v1 (rewards: право/eligibility исполнителя), outbox. Сделка не создаётся — deal-created.v1 не публикуется |
| External calls | none (сделка не создаётся; объект в этой операции не выбирается) |
| Cache / projections / search | поисковая проекция фиксаций (FixationRequestSearchDocument) обновляется (Status = FixedUnique/FixedNonUnique); ix_fixation_requests_status |
| Notifications | опосредованно: notifications — консюмер fixation-approved (агенту «фиксация одобрена») |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | формат FixationId / Kind не из enum | «Проверьте корректность запроса» | no retry |
CRM_FIXATION_NOT_FOUND | 404 | заявка не найдена в tenant застройщика | «Заявка на фиксацию не найдена» | no retry |
CRM_FIXATION_INVALID_STATE | 409 | Status != Pending (INV-FIX-3; в т.ч. гонка отмены агентом по RowVersion) | «Заявка уже обработана» | no retry (перечитать состояние) |
CRM_FIXATION_DUPLICATE_CONFLICT | 409 | Kind = 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).
Интеграционная последовательность (после коммита)
Актор в ответе 200 уже видит одобренную заявку, но публикация события и её обработка потребителями —
асинхронные, после атомарного коммита (окно eventual-consistency — секунды).
FixationApprovedEvent→pin.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; dashboardpin-crm; runbook —docs/06-services/crm/runbooks/(будущий путь).
Обратные ссылки
Автоссылки: где используется этот документ.
- Поиск (1): FixationRequestSearchDocument
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности: fixation-request.md (агрегат, INV-FIX-3/5/6,
Approve, enum-ы), deal.md (сделки, авторизованные фиксацией; связь 1:NFixationRequestId), lead.md (лид-основание). - Событие: fixation-approved.md (
pin.crm.fixation-approved.v1, событие авторизации). - Смежные операции: create-fixation.md (подача заявки),
get-fixation.md (дом
FixationCardModel), create-deal.md (ручной путь сделки + выбор объекта); карта области — README. - ADR: 0052 tenancy, 0055 мини-мессенджер, 0056 simplicity-first.