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

UC-FIX-003. Застройщик отклоняет фиксацию

Назначение

Менеджер застройщика отклоняет поданную заявку на фиксацию клиента с обязательной причиной. Операция и её контракт — reject-fixation.md; дом полей заявки, статусов и инвариантов — FixationRequestEntity. Контраст с одобрением — UC-FIX-002; отзыв агентом — поток A1.

Metadata

ПолеЗначение
IDUC-FIX-003
Контекстcrm (см. docs/06-services/crm/), модуль фиксации
Типосновной (отклонение — успешный терминальный исход решения; отзыв агентом — альтернативный поток A1)
Статусdraft

Актор

ПолеЗначение
РольДевелопер (Менеджер застройщика); арбитраж — Менеджер ПИН
Permissions / scopecrm.fixations.decide; scope: организация (застройщик) — заявки своего TenantId; Менеджер ПИН — platform-scope (обход tenant)
TenantTenantId = DeveloperCompanyId (владелец решения по заявке — застройщик, ADR-0052)
Record-levelзаявка читается только в своём tenant (fixation.TenantId == actor.TenantId); Менеджер ПИН обходит tenant-scope

Предусловие

  • Заявка на фиксацию существует в tenant застройщика (TenantId = DeveloperCompanyId), DeletedAt is null.
  • Status = Pending (решение ещё не принято, DecidedByUserId = null, DealId = null).
  • Актор — менеджер компании-адресата с crm.fixations.decide (или Менеджер ПИН, platform-scope).
  • Агент ещё не отозвал заявку (иначе — альтернативный поток A1/A2).

Триггер

ПолеЗначение
ИсточникUI-действие → вызов API
Триггерящий элементкарточка заявки в ЛК застройщика (список list-fixations.md), кнопка «Отклонить» → POST /api/developer/crm/fixations/{fixationId}/reject

Основной Поток

Детальный алгоритм — reject-fixation.md; здесь — сценарная дельта. Модель запроса RejectFixationCommand { RejectReason }.

  1. Менеджер открывает карточку заявки Pending в списке фиксаций застройщика: видит snapshot ClientName/ ClientPhoneNumber и подсветку дублей (FixationUniqueness, поля DuplicateOfFixationId/DuplicateMatchKind) и решает отклонить (клиент уже в базе на других условиях, некорректные данные, конфликт закрепления).
  2. Менеджер заполняет обязательное поле «Причина отклонения» и подтверждает.
  3. Проверка входа. RejectReason не пуст после trim, длина 1..1000 (INV-FIX-5, соответствует check ck_fixation_reject_reason); пустая причина → CRM_FIXATION_REASON_REQUIRED. Заявка существует в своём tenant (чужой tenant — как отсутствие); null/DeletedAt != nullCRM_FIXATION_NOT_FOUND (404 не раскрывает чужие заявки).
  4. Домен-решение и маппинг. Guard INV-FIX-3 (Status == Pending, иначе CRM_FIXATION_INVALID_STATE) + INV-FIX-5; переход Pending → Rejected. Маппинг поле-в-поле: Status ← Rejected; RejectReason ← введённая причина (trim); DecidedByUserId ← actor; DecidedAt ← now; UpdatedByUserId/ UpdatedAt ← actor/now. DealId остаётся null (сделка не создаётся), поля дубль-детекта не меняются. Событие FixationDecided (Status = Rejected, in-process).
  5. Что меняется и какие события. Заявка обновляется; в outbox пишется FixationRejectedEvent (топик pin.crm.fixation-rejected.v1, ContractVersion = 1, AggregateId = fixation.Id, AggregateType = "FixationRequest"; payload из агрегата — fixation-rejected.md).
  6. Согласованность. Переход статуса + audit/version stamping + запись outbox фиксируются атомарно в одном согласованном изменении. Правка не должна затирать чужую — конкурентный доступ разрешается оптимистичной блокировкой; при параллельном отзыве агентом — конфликт (поток A2).
  7. Результат. Возвращается обновлённая карточка (Status = Rejected, RejectReason, DecidedAt, DealId = null). notifications-консюмер события шлёт агенту-инициатору «Заявка на фиксацию отклонена» с причиной.

Диаграмма

Альтернативные Потоки

A1. Агент отзывает заявку до решения застройщика
  1. Агент-инициатор (или руководитель агентства / Менеджер ПИН) вызывает cancel-fixation.md: отзыв (guard INV-FIX-4 — Status == Pending), переход Pending → Cancelled, CancelledAt проставляется.
  2. Kafka-события нет (в отличие от решения застройщика): сделка не создавалась, право на комиссию не возникало, инвестор фиксаций не видит. Публикуется только доменное FixationDecided.

Заявка терминальна (Cancelled); последующая попытка отклонения не из Pending завершается A3.

A2. Гонка «агент отзывает / застройщик отклоняет»
  1. Обе команды прочитали заявку в Pending.
  2. Первая коммитит — вторая проигрывает оптимистичную блокировку → CRM_FIXATION_CONFLICT (409).

Проигравший перечитывает актуальное состояние заявки (Rejected либо Cancelled) и не повторяет решение. Завершается.

A3. Заявка уже решена или отменена
  1. Отклонение заявки не из Pending (Rejected/FixedUnique/FixedNonUnique/Cancelled) — guard INV-FIX-3 отклоняет: CRM_FIXATION_INVALID_STATE (409).

Завершается без мутаций и событий.

Постусловие

  • Заявка Rejected; RejectReason/DecidedByUserId/DecidedAt заполнены; DealId = nullсделка не создаётся (в отличие от одобрения, UC-FIX-002).
  • Освобождается активная тройка ux_fixation_active_triple — по паре агент↔клиент↔застройщик можно подать новую заявку.
  • Опубликован FixationRejectedEventpin.crm.fixation-rejected.v1 (один раз, переход терминальный); агент-инициатор уведомлён notifications. Право на комиссию не возникает.
  • Проекция фиксаций (FixationRequestSearchDocument) обновляется (Status = RejectedIsActive = false); ФИО/телефон snapshot иммутабельны; заявка выпадает из активных выборок и дубль-детекта новых заявок.

Возможные Ошибки

КодHTTPУсловиеПоведение клиента
VALIDATION_FAILED400FixationId пуст / некорректное телопоказать «Проверьте корректность запроса»; no retry
CRM_FIXATION_REASON_REQUIRED400RejectReason пуст/пробелы (INV-FIX-5, ck_fixation_reject_reason)подсветить обязательное поле «Причина»; no retry
CRM_FIXATION_NOT_FOUND404заявки нет в tenant застройщика (или soft-deleted)«Заявка на фиксацию не найдена»; обновить список
CRM_FIXATION_INVALID_STATE409Status != Pending — уже решена/отменена (A3, INV-FIX-3)«Заявку нельзя отклонить в текущем статусе»; перечитать карточку
CRM_FIXATION_CONFLICT409Гонка конкурентной правки: агент отозвал параллельно (A2)«Заявка изменена, обновите данные»; перечитать состояние (retry)

Доменные События

СобытиеКогда поднимаетсяSubscribers
FixationDecidedпереход Pending → Rejectedвнутренняя аудит-лента/метрики crm; интеграции наружу не порождает

Kafka Интеграционные События

СобытиеTopicКогда публикуетсяConsumers
FixationRejectedEvent (ContractVersion = 1)pin.crm.fixation-rejected.v1Pending → Rejected в том же коммите (outbox, AggregateId = FixationRequestId)notifications — уведомление агенту «Заявка на фиксацию отклонена» с Reason (дом контракта — fixation-rejected.md)

Одобрение публикует отдельное pin.crm.fixation-approved.v1 (fixation-approved.md); отзыв агентом (Cancel) интеграционного события не порождает.

Acceptance Criteria

#КритерийПроверка
AC1Отклонение без причины отклоняется до мутациипустой RejectReasonCRM_FIXATION_REASON_REQUIRED (400); статус заявки не меняется
AC2Успешное отклонение переводит Pending → Rejected и заполняет решениеStatus = Rejected, RejectReason/DecidedByUserId/DecidedAt проставлены, DealId = null
AC3Сделка не создаётся, право на комиссию не возникаетнет строки DealEntity; rewards события fixation-approved не получает
AC4Публикуется ровно одно fixation-rejected атомарно с переходомв outbox одна запись pin.crm.fixation-rejected.v1; при откате транзакции события нет
AC5Повторное/не-из-Pending отклонение отклоняетсязаявка Rejected/Cancelled/Fixed*CRM_FIXATION_INVALID_STATE (409)
AC6Гонка отзыв/отклонение разрешается оптимистичной блокировкойпараллельные Reject/Cancel — проигравший получает CRM_FIXATION_CONFLICT (409), данные не затираются
AC7Освобождается активная тройкапосле Rejected возможна новая заявка по той же паре агент↔клиент↔застройщик (ux_fixation_active_triple)

Кросс-сервисные ожидания и гарантии

Отклонение — синхронная HTTP-команда; единственный внешний эффект — уведомление агента (асинхронно). В отличие от одобрения (UC-FIX-002) сделка не создаётся и право на комиссию не возникает.

Что crm предоставляет (исходящие гарантии):

КонтрактКлюч / версияPayload (ключевое)Кому
pin.crm.fixation-rejected.v1key = FixationRequestId, ContractVersion = 1FixationRequestId, TenantId, OriginAgentUserId, ExecutorAgentUserId, RejectReason, DecidedAtnotifications (агенту-инициатору «отклонена» + причина)

Событие публикуется через outbox атомарно с переходом Pending → Rejected (один раз на переход, AC4). Отзыв агентом (Cancel, поток A1) интеграционного события не порождает — публикуется только доменное FixationDecided; downstream об отмене не узнаёт (сделки не было, право на комиссию не возникало).

Что операция ожидает от соседей:

  • notifications — на fixation-rejected шлёт агенту-инициатору push/СМС; ФИО/контакты резолвит по собственной реплике identity (raw-PII в событии нет, RejectReason — не PII).
  • Поисковая проекция FixationRequestSearchDocumenteventually consistent: после Rejected переиндексируется IsActive = false, заявка выпадает из активных выборок и из дубль-детекта новых заявок с задержкой индексации (не в том же коммите).

Промежуточное состояние: застройщик сразу видит карточку Rejected; освобождение активной тройки ux_fixation_active_triple — синхронно (в том же коммите), поэтому новую заявку по паре агент↔клиент↔застройщик можно подать немедленно; уведомление агенту и обновление поисковой проекции — с задержкой.

Идемпотентность, конкурентность и восстановление

  • Идемпотентность. Ключа идемпотентности нет; защита от повтора — guard Status = Pending (INV-FIX-3): повторное/не-из-Pending отклонение → CRM_FIXATION_INVALID_STATE (A3), событие не дублируется.
  • Конкурентность (A2). Гонка «застройщик отклоняет / агент отзывает»: обе команды прочитали Pending, первая коммитит, вторая проигрывает оптимистичную блокировку → CRM_FIXATION_CONFLICT (409); проигравший перечитывает актуальное состояние (Rejected либо Cancelled) и решение не повторяет.
  • Доставка downstream (retry/DLQ). fixation-rejected — at-least-once через outbox; notifications идемпотентен по MessageId. Транзиентная ошибка — ретрай; при исчерпании — DLQ (агент не получит уведомление до ручного разбора). Отклонение в crm при этом не откатывается.
  • Восстановление. Мутаций при отказных ветках (A2/A3) нет; переход статуса, audit/version-stamping и запись outbox — атомарны (частичной записи нет).

Наблюдаемость и НФТ

  • Логи (без PII): FixationRequestId, TenantId, actor, исход (успех / код ошибки); RejectReason логируется как бизнес-факт решения (не PII); ФИО/телефон клиента в логи не пишутся (только Id).
  • Метрики: число отклонений; распределение по типовой причине (агрегировано); доля CRM_FIXATION_REASON_REQUIRED/CRM_FIXATION_INVALID_STATE/CRM_FIXATION_CONFLICT; лаг доставки уведомления агенту; лаг переиндексации IsActive = false.
  • Латентность/нагрузка: интерактивная команда — P95 в пределах сотен мс; низкообъёмная сущность (ADR-0056); окно уведомления — минуты.
  • Доступ: crm.fixations.decide в scope застройщика (заявки своего TenantId); Менеджер ПИН — platform-scope; чужой tenant неотличим от отсутствия (CRM_FIXATION_NOT_FOUND, 404).

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

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

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