UC-FIX-003. Застройщик отклоняет фиксацию
Назначение
Менеджер застройщика отклоняет поданную заявку на фиксацию клиента с обязательной причиной. Операция и её
контракт — reject-fixation.md; дом полей заявки, статусов и
инвариантов — FixationRequestEntity. Контраст с одобрением —
UC-FIX-002; отзыв агентом — поток A1.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-FIX-003 |
| Контекст | crm (см. docs/06-services/crm/), модуль фиксации |
| Тип | основной (отклонение — успешный терминальный исход решения; отзыв агентом — альтернативный поток A1) |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Девелопер (Менеджер застройщика); арбитраж — Менеджер ПИН |
| Permissions / scope | crm.fixations.decide; scope: организация (застройщик) — заявки своего TenantId; Менеджер ПИН — platform-scope (обход tenant) |
| Tenant | TenantId = 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 }.
- Менеджер открывает карточку заявки
Pendingв списке фиксаций застройщика: видит snapshotClientName/ClientPhoneNumberи подсветку дублей (FixationUniqueness, поляDuplicateOfFixationId/DuplicateMatchKind) и решает отклонить (клиент уже в базе на других условиях, некорректные данные, конфликт закрепления). - Менеджер заполняет обязательное поле «Причина отклонения» и подтверждает.
- Проверка входа.
RejectReasonне пуст после trim, длина1..1000(INV-FIX-5, соответствует checkck_fixation_reject_reason); пустая причина →CRM_FIXATION_REASON_REQUIRED. Заявка существует в своём tenant (чужой tenant — как отсутствие);null/DeletedAt != null→CRM_FIXATION_NOT_FOUND(404 не раскрывает чужие заявки). - Домен-решение и маппинг. 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). - Что меняется и какие события. Заявка обновляется; в outbox пишется
FixationRejectedEvent(топикpin.crm.fixation-rejected.v1,ContractVersion = 1,AggregateId = fixation.Id,AggregateType = "FixationRequest"; payload из агрегата — fixation-rejected.md). - Согласованность. Переход статуса + audit/version stamping + запись outbox фиксируются атомарно в одном согласованном изменении. Правка не должна затирать чужую — конкурентный доступ разрешается оптимистичной блокировкой; при параллельном отзыве агентом — конфликт (поток A2).
- Результат. Возвращается обновлённая карточка (
Status = Rejected,RejectReason,DecidedAt,DealId = null).notifications-консюмер события шлёт агенту-инициатору «Заявка на фиксацию отклонена» с причиной.
Диаграмма
Альтернативные Потоки
- Агент-инициатор (или руководитель агентства / Менеджер ПИН) вызывает
cancel-fixation.md: отзыв (guard INV-FIX-4 —
Status == Pending), переходPending → Cancelled,CancelledAtпроставляется. - Kafka-события нет (в отличие от решения застройщика): сделка не создавалась, право на комиссию не
возникало, инвестор фиксаций не видит. Публикуется только доменное
FixationDecided.
Заявка терминальна (Cancelled); последующая попытка отклонения не из Pending завершается A3.
- Обе команды прочитали заявку в
Pending. - Первая коммитит — вторая проигрывает оптимистичную блокировку →
CRM_FIXATION_CONFLICT(409).
Проигравший перечитывает актуальное состояние заявки (Rejected либо Cancelled) и не повторяет решение.
Завершается.
- Отклонение заявки не из
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— по паре агент↔клиент↔застройщик можно подать новую заявку. - Опубликован
FixationRejectedEvent→pin.crm.fixation-rejected.v1(один раз, переход терминальный); агент-инициатор уведомлёнnotifications. Право на комиссию не возникает. - Проекция фиксаций (
FixationRequestSearchDocument) обновляется (Status = Rejected→IsActive = false); ФИО/телефон snapshot иммутабельны; заявка выпадает из активных выборок и дубль-детекта новых заявок.
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|---|---|---|
VALIDATION_FAILED | 400 | FixationId пуст / некорректное тело | показать «Проверьте корректность запроса»; 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) | «Заявка на фиксацию не найдена»; обновить список |
CRM_FIXATION_INVALID_STATE | 409 | Status != Pending — уже решена/отменена (A3, INV-FIX-3) | «Заявку нельзя отклонить в текущем статусе»; перечитать карточку |
CRM_FIXATION_CONFLICT | 409 | Гонка конкурентной правки: агент отозвал параллельно (A2) | «Заявка изменена, обновите данные»; перечитать состояние (retry) |
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|---|---|
FixationDecided | переход Pending → Rejected | внутренняя аудит-лента/метрики crm; интеграции наружу не порождает |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
FixationRejectedEvent (ContractVersion = 1) | pin.crm.fixation-rejected.v1 | Pending → Rejected в том же коммите (outbox, AggregateId = FixationRequestId) | notifications — уведомление агенту «Заявка на фиксацию отклонена» с Reason (дом контракта — fixation-rejected.md) |
Одобрение публикует отдельное pin.crm.fixation-approved.v1 (fixation-approved.md);
отзыв агентом (Cancel) интеграционного события не порождает.
Acceptance Criteria
| # | Критерий | Проверка |
|---|---|---|
| AC1 | Отклонение без причины отклоняется до мутации | пустой RejectReason → CRM_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.v1 | key = FixationRequestId, ContractVersion = 1 | FixationRequestId, TenantId, OriginAgentUserId, ExecutorAgentUserId, RejectReason, DecidedAt | notifications (агенту-инициатору «отклонена» + причина) |
Событие публикуется через outbox атомарно с переходом Pending → Rejected (один раз на переход, AC4).
Отзыв агентом (Cancel, поток A1) интеграционного события не порождает — публикуется только доменное
FixationDecided; downstream об отмене не узнаёт (сделки не было, право на комиссию не возникало).
Что операция ожидает от соседей:
notifications— наfixation-rejectedшлёт агенту-инициатору push/СМС; ФИО/контакты резолвит по собственной репликеidentity(raw-PII в событии нет,RejectReason— не PII).- Поисковая проекция
FixationRequestSearchDocument— eventually 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).
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (2): UC-FIX-002. Застройщик одобряет фиксацию (авторизация работы с клиентом, без создания сделки), US-реестр блока «Сделка» (US-INV / US-AGT / US-DEV / US-MGR)
- scenarios (2): UC-OVR-004. От реферальной ссылки до объекта у собственника, UC-OVR-005. От лида до сделки (4 фазы)
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API метод (операция + алгоритм): reject-fixation.md; список фиксаций застройщика — list-fixations.md.
- Domain entity (дом полей, статусы, INV-FIX-3/5, отклонение): fixation-request.md.
- Kafka событие: fixation-rejected.md.
- Контраст: UC-FIX-002 / fixation-approved.md (одобрение → сделка + комиссия); отзыв агентом — cancel-fixation.md; подача — UC-FIX-001.
- Реестр US: user-stories-deal.md; карта области — README.
- ADR: 0052 tenancy, 0056 simplicity-first.