POST /api/developer/crm/fixations/{fixationId}/reject — Отклонить фиксацию
Менеджер застройщика отклоняет поданную заявку на фиксацию клиента. Переход Pending → Rejected (INV-FIX-3),
причина отклонения обязательна (INV-FIX-5). Отклонение не даёт агенту авторизацию вести клиента по объектам
застройщика и права на комиссию не возникает (в отличие от одобрения, которое такую авторизацию выдаёт, но
сделку тоже не создаёт — INV-FIX-6); событие fixation-rejected
уведомляет агента-инициатора. Дом полей заявки, статусы и инварианты — в
fixation-request.md.
Назначение
Позволяет стороне застройщика (владелец решения, ADR-0052) отклонить заявку на фиксацию с обязательной
причиной: клиент уже в базе на других условиях, некорректные данные, конфликт закрепления и т.п. Отклонение
возможно только из Pending и только менеджером компании-адресата (TenantId = DeveloperCompanyId) или
менеджером ПИН. На выходе — обновлённая карточка фиксации. После отклонения освобождается unique-тройка
ux_fixation_active_triple — по той же паре агент↔клиент↔застройщик можно подать новую заявку.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | crm |
| API area | developer (JWT менеджера застройщика) |
| Feature group | fixation |
| Статус | draft |
| Документ | docs/06-services/crm/api/developer/reject-fixation.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Менеджер застройщика); Operator (Менеджер ПИН — platform-scope, арбитраж) |
| Auth policy / Permissions | crm.fixations.decide (сторона-адресат решения) |
| Scope (RBAC) | организация (застройщик): TenantId = DeveloperCompanyId |
| Record-level | заявка читается только в своём tenant (fixation.TenantId == actor.TenantId); Менеджер ПИН — обход tenant-scope |
| Tenant isolation | TenantId = DeveloperCompanyId из claims — застройщик решает по своим заявкам (ADR-0052); чужой tenant не раскрывается = 404 |
| Audit | update: переход Pending → Rejected, поля DecidedByUserId/DecidedAt/RejectReason, инкремент Version; RejectReason — free-text, в логи не пишется |
Решение принимает менеджер застройщика — владелец заявки по tenant (TenantId = DeveloperCompanyId, permission
crm.fixations.decide), либо Менеджер ПИН в режиме арбитража (platform-scope). Заявка чужого tenant не
раскрывается: обращение к ней даёт 404 CRM_FIXATION_NOT_FOUND, а не 403; отсутствие permission у
менеджера своего tenant — 403 (ADR-0052).
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/developer/crm/fixations/{fixationId}/reject |
| Success status | 200 (возвращается обновлённая карточка FixationCardModel) |
| Idempotency header | не применяется — повтор ловится проверкой состояния (шаг 4): вторая попытка над уже Rejected заявкой → CRM_FIXATION_INVALID_STATE |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
TenantId | identity claims | Guid | Да | Required; = DeveloperCompanyId актора | Tenant isolation |
FixationId | route | Guid | Да | != Guid.Empty; существующая заявка Pending в tenant застройщика (шаги 2–4) | — |
RejectReason | body | string | Да | 1..1000, не пусто после trim (INV-FIX-5, ck_fixation_reject_reason); иначе CRM_FIXATION_REASON_REQUIRED | free-text менеджера — в логи не пишется (может содержать PII) |
ActorUserId | identity claims | Guid | Да | JWT менеджера застройщика; пишется в DecidedByUserId | — |
Тело — named-модель RejectFixationCommand (единственное поле RejectReason); вложенных сущностей и
коллекций нет.
Модель ответа FixationCardModel
Дом проекции — get-fixation.md (та же карточка фиксации); поля агрегата —
fixation-request.md. Наружу отдаётся обновлённое состояние; дельта после
отклонения: Status = Rejected (enum строкой), RejectReason заполнено, DecidedByUserId/DecidedAt
проставлены. Ссылки на сделку у фиксации нет (сделка не создаётся ни при отклонении, ни при одобрении).
Остальные поля — 1:1 из агрегата.
Предусловия и постусловия
Предусловия (должны быть истинны до вызова):
- заявка на фиксацию существует в
TenantIdактора и не soft-deleted; - заявка в статусе
Pending(решение ещё не принято, INV-FIX-3); - актор — менеджер компании-адресата с
crm.fixations.decide(или менеджер ПИН, platform-scope); - причина отклонения будет передана непустой (INV-FIX-5).
Постусловия при успехе:
- переход
Pending → Rejected; проставленыRejectReason/DecidedByUserId/DecidedAt; версия инкрементирована; авторизация не выдаётся, сделка не создаётся, право на комиссию не возникает; - освобождена активная unique-тройка
ux_fixation_active_triple— по той же паре агент↔клиент↔застройщик можно подать новую заявку; - опубликовано
FixationRejectedEvent→pin.crm.fixation-rejected.v1; поисковая проекция заявки переведена вIsActive = false(выпадает из активных выборок и дубль-детекта новых заявок).
Постусловия при отказе: заявка не изменилась (статус, причина, версия — прежние), событие не публиковалось, тройка не освобождалась.
Алгоритм
- Контекст и доступ. Выполняет менеджер застройщика (
User, permissioncrm.fixations.decide) либо менеджер ПИН (platform-scope); tenant —TenantId = DeveloperCompanyId. Идемпотентность не требуется — повтор ловится проверкой состояния (шаг 4). - Проверка входа и предусловий.
FixationId != Guid.Empty;RejectReasonнеnull, после trim не пусто, длина1..1000— иначеVALIDATION_FAILED; отдельно пустая причина →CRM_FIXATION_REASON_REQUIRED(INV-FIX-5,ck_fixation_reject_reason);- заявка существует в tenant застройщика и не soft-deleted — иначе
CRM_FIXATION_NOT_FOUND(чужие заявки не раскрываются: 404); - состояние допускает отклонение:
Status == Pending(INV-FIX-3) — иначеCRM_FIXATION_INVALID_STATE(уже решена/отменена); - scope: заявка своего tenant (застройщик — сторона-владелец решения; отдельная record-level проверка не нужна).
- Что читаем. Заявку в tenant застройщика (одиночная строка; гонку разрешает версия строки на шаге 5). Для
проекции ответа: если у заявки нет override пожеланий (
WishesOverride == null) — читаем лид для актуальныхLead.Wishes; иначе лид не нужен. Клиент/сделка не читаются (отклонение их не затрагивает). - Бизнес-правила и переходы. Отклонение (INV-FIX-3/5): guard
Status == Pending(иначеCRM_FIXATION_INVALID_STATE),reasonнепуст (иначеCRM_FIXATION_REASON_REQUIRED); переходPending → Rejected; маппинг:Status = Rejected,RejectReason = trim(RejectReason),DecidedByUserId = ActorUserId,DecidedAt = now,UpdatedByUserId = ActorUserId,UpdatedAt = now,Version += 1. Поля дубль-детекта (Uniqueness/DuplicateOfFixationId) не меняются; сделка не создаётся. Фиксируется доменный фактFixationDecided (Status = Rejected). Проекция ответаFixationCardModel: эффективныеWishes = WishesOverride ?? Lead.Wishes, остальное 1:1. - Что меняется. Заявка переходит в
Rejectedс причиной; освобождается unique-тройкаux_fixation_active_triple— по той же паре агент↔клиент↔застройщик можно подать новую заявку. Конкурентный доступ — по версии строки: если агент параллельно отозвал заявку (Pending → Cancelled) между чтением и записью →CRM_FIXATION_CONFLICT, менеджер перечитывает заявку. Лид/клиент не мутируются. - События. Интеграционное
FixationRejectedEvent→ топикpin.crm.fixation-rejected.v1(ContractVersion = 1); payload из агрегата:FixationRequestId,TenantId,OriginAgencyCompanyId,OriginAgentUserId,ExecutorAgentUserId,InvestorUserId,RejectedByUserId,Reason,RejectedAt(дом — fixation-rejected.md). При отклоненииfixation-approvedне публикуется (авторизация не выдаётся); сделка не создаётся ни при отклонении, ни при одобрении. - Согласованность. Переход статуса, простановка
DecidedBy/At/RejectReasonи запись события фиксируются в одном согласованном изменении (атомарно). - Результат. Карточка
FixationCardModel(Status = Rejected,RejectReason,DecidedAt) → HTTP200; ошибки —400/404/409(таблица ниже).
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | FixationRejectedEvent → pin.crm.fixation-rejected.v1 (одно событие), outbox |
| External calls | none (сделка не создаётся; chessboard/rewards синхронно не вызываются) |
| Cache / projections / search | проекция фиксаций (FixationRequestSearchDocument) обновляется (Status = Rejected → IsActive = false) — заявка выпадает из активных выборок и дубль-детекта новых заявок; ФИО/телефон snapshot иммутабельны; освобождается ux_fixation_active_triple |
| Notifications | опосредованно: notifications — консюмер fixation-rejected (агенту «Заявка на фиксацию отклонена» с причиной) |
Интеграционная последовательность и окно согласованности
Менеджеру ответ 200 приходит сразу после атомарного перехода в Rejected, но уведомление агента-инициатора —
асинхронное, после доставки fixation-rejected.v1 потребителю notifications (окно = задержка outbox →
консюмер).
- Синхронно (в одном изменении): заявка переведена в
Rejected, событие положено в outbox — ответ200менеджеру. В экране «Фиксации» застройщика строка сразу видна какRejected. - Асинхронно:
fixation-rejected.v1доставляется потребителюnotifications, который уведомляет агента-инициатора «Заявка отклонена» с причиной. Порядок по заявке сохраняется ключом партиционированияFixationRequestId; повторная доставка обрабатывается идемпотентно. - Окно eventual-consistency = задержка outbox → консюмер: до её завершения агент в своей проекции ещё видит
заявку как
Pending; после обработки события — какRejected.rewards/chessboardсинхронно не вызываются (сделка не создаётся; при одобренииrewardsлишь фиксирует право/eligibility).
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | FixationId = Guid.Empty / формат тела | «Проверьте корректность запроса» | no retry |
CRM_FIXATION_REASON_REQUIRED | 400 | RejectReason пуст/пробелы (INV-FIX-5, ck_fixation_reject_reason) | «Укажите причину отклонения» | no retry |
CRM_FIXATION_NOT_FOUND | 404 | заявки нет в tenant застройщика (или soft-deleted) | «Заявка на фиксацию не найдена» | no retry |
CRM_FIXATION_INVALID_STATE | 409 | Status != Pending (уже решена/отменена, INV-FIX-3) | «Заявку нельзя отклонить в текущем статусе» | no retry |
CRM_FIXATION_CONFLICT | 409 | RowVersion-гонка: агент отозвал заявку параллельно (шаг 7) | «Заявка изменена, обновите данные» | retry (перечитать состояние) |
Совместимость и наблюдаемость
- Контракт минимальный (route +
RejectReason→FixationCardModel); новые optional поля карточки — additive; смена маршрута/семантики статуса или удаление/переименование поля — breaking (migration + rollout). EnumStatusсериализуется строкой — клиент обязан толерантно обрабатывать неизвестное значение. Контракт события — additive-only (fixation-rejected.md). - Logs:
fixation-reject requested/rejected/conflict(FixationId,TenantId,OriginAgentUserId,ActorUserId;RejectReasonи ФИО/телефон в логи не пишутся); metrics:crm_fixation_reject_total{result}, latency p95; traces — сквозной trace черезtraceparent; dashboardpin-crm; runbook —docs/06-services/crm/runbooks/(будущий путь по конвенции).
Acceptance criteria
- Given заявка в
Pendingи непустая причина, When менеджер застройщика отклоняет её, Then200сFixationCardModel(Status = Rejected,RejectReason,DecidedAt), опубликованоfixation-rejected.v1, тройкаux_fixation_active_tripleосвобождена. - Given
RejectReasonпустой/из пробелов, When отклонение, Then400 CRM_FIXATION_REASON_REQUIRED(INV-FIX-5); состояние заявки не изменилось. - Given заявка не в
Pending(уже решена/отменена), When отклонение, Then409 CRM_FIXATION_INVALID_STATE(в т.ч. как отлов повторного вызова). - Given агент параллельно отозвал заявку (
Pending → Cancelled) между чтением и записью, When отклонение коммитит, Then409 CRM_FIXATION_CONFLICT(перечитать состояние). - Given заявка чужого
TenantId/несуществующая/soft-deleted, When отклонение, Then404 CRM_FIXATION_NOT_FOUND(чужой tenant не раскрывается). - Given заявка отклонена, When по той же паре агент↔клиент↔застройщик подаётся новая заявка, Then она допускается (активная тройка свободна).
- Given актор без
crm.fixations.decide, When отклонение, Then403(нет прав); без JWT —401.
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (1): UC-FIX-003. Застройщик отклоняет фиксацию
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность (дом полей, статусы, INV-FIX-3/5, метод
Reject): fixation-request.md; лид-основание — lead.md. - Модель ответа
FixationCardModel(дом проекции): get-fixation.md. - Контраст (одобрение → авторизация + kafka-событие, без сделки): fixation-approved.md; отзыв агентом (без kafka-события): cancel-fixation.md.
- Событие: fixation-rejected.md; карта области — README.
- ADR: 0052 tenancy, 0056 simplicity-first.