PUT /api/agent/crm/leads/{leadId}/wishes — Обновить хотелки лида
Редактирование «шапки хотелок» инвестора (LeadWishes, owned VO) в карточке лида агента: бюджет, желаемый город,
срочность, цель покупки, предпочтительный способ оплаты, комментарий. Лид — источник истины по хотелкам; заявка на фиксацию
хранит собственный nullable snapshot FixationWishesOverride и при этом изменении не трогается
(fixation-request.md).
Назначение
Дать агенту-владельцу (или Менеджеру ПИН) обновить типизированную шапку хотелок активного лида после разговора с
инвестором. Полная замена owned-VO LeadWishes в агрегате LeadEntity; статус лида и лента событий не меняются,
интеграционные события не публикуются.
PUT заменяет owned-VO LeadWishes целиком: optional-поле, не переданное в теле, становится null
(сбрасывается, включая пару CityId/CityName) — кроме BudgetCurrencyId, для которого отсутствие значения
означает валюту RUB справочника (ADR-0048).
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | crm |
| API area | agent (JWT агента/руководителя агентства/Менеджера ПИН) |
| Feature group | leads |
| Статус | draft |
| Документ | docs/06-services/crm/api/agent/update-lead-wishes.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Агент-владелец лида) / User (Менеджер ПИН, platform-scope) |
| Auth policy / Permissions | crm.leads.manage |
| Scope (RBAC) | организация (агентство) — record-level по владельцу; Менеджер ПИН — платформа |
| Record-level | INV-LEAD-5: lead.ExecutorAgentUserId == ActorUserId (агент-исполнитель) или Менеджер ПИН; иначе CRM_LEAD_FORBIDDEN |
| Tenant isolation | TenantId = AgencyCompanyId в каждом запросе; lead.TenantId обязан совпасть |
| Audit | update (UpdatedByUserId, Version); лента событий не пишется |
Permission crm.leads.manage действует в пределах своего TenantId (агентство). Внутри tenant record-level
(INV-LEAD-5) сужает круг: агент правит хотелки только у лидов, где он — исполнитель
(ExecutorAgentUserId = actor); Менеджер ПИН — любой лид на платформе.
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | PUT |
| Route | /api/agent/crm/leads/{leadId}/wishes |
| Success status | 200 (обновлено, возвращается карточка лида) |
| Idempotency header | не применяется — PUT семантически идемпотентен (полная замена шапки); конкурентность — RowVersion |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
Поля тела 1:1 отображаются в owned-VO LeadWishes (дом полей и ограничения — lead.md,
раздел «LeadWishes»); enum-ы — строкой. Здесь — только источник и валидация запроса.
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
TenantId | identity claims | Guid | Да | Required; = AgencyCompanyId | Tenant isolation |
ActorUserId | identity claims | Guid | Да | Required | Record-level (владелец) |
LeadId | route | Guid | Да | != Guid.Empty; существующий лид | — |
Budget | body | NullableRange<decimal> | Нет | {Min, Max}; каждая граница >= 0; Min <= Max | — |
BudgetCurrencyId | body | short? | Нет | ссылка на справочник валют handbook; не задан → валюта RUB; неизвестная/архивная запись → VALIDATION_FAILED (резолв, шаг 3); RUB-only (ADR-0048) | — |
CityId | body | long? | Нет | ссылка на справочник городов handbook; неизвестный/архивный город → CRM_CITY_UNKNOWN (резолв, шаг 3); display-имя CityName снимается из resolve при записи | — |
Urgency | body | LeadUrgency? (enum-строка) | Нет | известное значение enum, unknown → ValidationError | — |
PurchaseGoal | body | PurchaseGoal? (enum-строка) | Нет | известное значение enum | — |
PreferredPaymentMethod | body | PaymentMethod? (enum-строка) | Нет | известное значение enum (purchase-program.md) | — |
Comment | body | string? | Нет | 0..2000 | PII — в логи не пишется |
Модель ответа LeadCardModel
Read-проекция обновлённого агрегата LeadEntity (дом полей — lead.md); здесь — только
дельта, релевантная операции.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
LeadId | Guid | Да | lead.Id | — | Идентификатор лида. |
Status | LeadStatus (enum-строка) | Да | lead.Status | — | Всегда Active (операция разрешена только в этом статусе). |
Wishes | LeadWishesModel | Да | lead.Wishes (owned VO) | — | Обновлённая шапка хотелок (поля VO — lead.md). |
UpdatedAt | DateTimeOffset | Да | lead.UpdatedAt | — | Момент правки. |
Version | long | Да | lead.Version | — | Для оптимистичной блокировки на клиенте. |
Остальные секции карточки (клиент, лента, сроки жизни) возвращаются без изменений — см. проекцию в lead.md.
Алгоритм
- Контекст и доступ. Операцию выполняет агент-владелец / Менеджер ПИН (permission
crm.leads.manage) в рамкахTenantId. - Проверка входа и предусловий.
- формат:
LeadId != Guid.Empty;Budget.Min/Max >= 0иMin <= Maxпри обоих заданных;Urgency/PurchaseGoal/PreferredPaymentMethod— известные enum-значения;Comment <= 2000— иначе ошибка валидации; - лид существует — иначе
CRM_LEAD_NOT_FOUND; - состояние:
lead.Status == Active(INV-LEAD-2) — иначеCRM_LEAD_NOT_ACTIVE; - record-level (INV-LEAD-5):
lead.TenantId == TenantIdи (lead.ExecutorAgentUserId == actorили Менеджер ПИН) — иначеCRM_LEAD_FORBIDDEN.
- формат:
- Резолв справочников (write-path, строгий). Если заданы
CityIdи/илиBudgetCurrencyId— один batch-резолв в справочникеhandbook(resolve-references.md):- город не найден или архивен →
CRM_CITY_UNKNOWN(422); из resolve снимается display-имя города — оно сохранится вCityName(денормализация при записи); - валюта не найдена или архивна →
VALIDATION_FAILED;BudgetCurrencyIdне задан — используется валютаRUBсправочника (RUB-only, ADR-0048 — типизированная ссылка, не мультивалютность); - справочник недоступен →
CRM_HANDBOOK_UNAVAILABLE(503): валидация строгая, «тихая» запись непроверенных ссылок запрещена; оба id не заданы — резолв не вызывается.
- город не найден или архивен →
- Что читаем. Лид (строка блокируется на время правки от гонки с одновременным редактированием шапки /
продлением). Прочих агрегатов грузить не нужно:
LeadWishes— owned-тип в графе лида. - Бизнес-правила и переходы. Собрать новый VO
LeadWishes { Budget, BudgetCurrencyId, CityId, CityName, Urgency, PurchaseGoal, PreferredPaymentMethod, Comment }(маппинг поле-в-поле из body;CityName— из resolve шага 3, приCityId = null— тожеnull); полностью замещает owned-VO лида, ставитUpdatedByUserId. Повторно проверяются INV-LEAD-2 (Active) и INV-LEAD-5 (владелец/Менеджер ПИН). Переход статуса:Active → Active(без смены). Фиксации лида не изменяются (ихFixationWishesOverride— независимый snapshot). Доменные события не поднимаются. - Что меняется. Обновляются owned-поля хотелок лида +
UpdatedAt/Version— атомарно;RowVersionдаёт оптимистичную блокировку → при конфликтеCRM_LEAD_CONCURRENCY. - События. Ни доменных, ни интеграционных событий нет.
- Результат. Обновлённая карточка лида (шапка хотелок + мета) → HTTP
200.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | none (спецификация: событий нет) |
| Внешние вызовы | batch-резолв справочника handbook (resolve-references.md) — только при заданных CityId/BudgetCurrencyId; недоступность → отказ операции (строгая write-path-валидация) |
| Проекции / поиск | none (хотелки в поиске клиентов/лидов не участвуют) |
| Уведомления | none |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
VALIDATION_FAILED | 400 | Budget.Min > Budget.Max, BudgetCurrencyId не найден/архивен в справочнике, неизвестный enum, Comment > 2000 | «Проверьте корректность полей хотелок» | no retry |
CRM_LEAD_NOT_FOUND | 404 | лид не найден по LeadId в tenant | «Лид не найден» | no retry |
CRM_CITY_UNKNOWN | 422 | CityId не найден или архивен в справочнике городов handbook (резолв, шаг 3) | «Город не найден в справочнике» | no retry (выбрать город из lookup) |
CRM_LEAD_NOT_ACTIVE | 409 | Status != Active (INV-LEAD-2) | «Редактировать хотелки можно только у активного лида» | no retry |
CRM_LEAD_FORBIDDEN | 403 | actor не владелец и не Менеджер ПИН (INV-LEAD-5) | «Недостаточно прав на редактирование лида» | no retry |
CRM_LEAD_CONCURRENCY | 409 | конфликт RowVersion (параллельная правка/продление) | «Лид изменён, обновите и повторите» | retry (перечитать) |
CRM_HANDBOOK_UNAVAILABLE | 503 | справочник handbook недоступен при заданных CityId/BudgetCurrencyId | «Справочник временно недоступен, повторите позже» | retry (backoff) |
Предусловия и постусловия
Предусловия (истинны до сценария):
- актор авторизован, имеет permission
crm.leads.manage; для чужого лида — является Менеджером ПИН; - лид существует в
TenantIdактора и находится в статусеActive(INV-LEAD-2); - у лида может как быть, так и не быть активных заявок на фиксацию — это на операцию не влияет.
Постусловия (гарантированы после успеха):
- owned-VO
LeadWishesполностью заменён значениями запроса; это семантика полной замены, а не патча: optional-поле, не переданное в теле, становитсяnull(сбрасывается, включая паруCityId/CityName), кромеBudgetCurrencyId(default — валютаRUBсправочника); - при заданном
CityIdзаписан денормализованныйCityName— display-имя города из resolve на момент правки (последующие переименования города в справочнике его не «догоняют» до следующей перезаписи хотелок); UpdatedByUserId= актор,UpdatedAtобновлён,Versionинкрементирован;- статус лида остаётся
Active(перехода нет), лента событий лида не пополняется; - заявки на фиксацию этого лида и их независимый snapshot
FixationWishesOverrideне изменены; - доменных и интеграционных событий не опубликовано; смежные контексты (
rewards,dev-stats) не затронуты.
Acceptance Criteria
- Happy path. Дано: активный лид, которым владеет актор-агент. Когда: агент отправляет корректную шапку
хотелок. Тогда:
200,LeadWishesзаменён целиком,Version+1, статус остаётсяActive, заявки на фиксацию и их override не тронуты, событий нет. - Полная замена (сброс поля). Дано: у лида задан
Urgency. Когда: PUT-запрос без поляUrgency. Тогда:200,Urgencyв VO становитсяnull(замена всей шапки, а не частичный патч). - Город из справочника. Дано: живой город справочника
handbook. Когда: PUT с егоCityId. Тогда:200, в хотелках сохраненыCityIdи снятый из resolveCityName. - Неизвестный/архивный город. Дано:
CityId, отсутствующий в справочнике или архивный. Когда: отправка. Тогда:422 CRM_CITY_UNKNOWN, изменений нет. - Справочник недоступен. Дано: задан
CityId/BudgetCurrencyId,handbookне отвечает. Когда: отправка. Тогда:503 CRM_HANDBOOK_UNAVAILABLE, изменений нет; без справочных id операция resolve не вызывает и проходит штатно. - Лид не активен. Дано: лид в статусе, отличном от
Active(например, конвертирован/архивирован). Когда: правка хотелок. Тогда:409 CRM_LEAD_NOT_ACTIVE, изменений нет. - Чужой лид. Дано: лид принадлежит другому агенту, актор не Менеджер ПИН. Когда: правка. Тогда:
403 CRM_LEAD_FORBIDDEN. - Некорректный бюджет/валюта. Дано:
Budget.Min > Budget.MaxилиBudgetCurrencyIdне найден/архивен в справочнике. Когда: отправка. Тогда:400 VALIDATION_FAILED, изменений нет. - Конкурентная правка. Дано: лид параллельно изменён (правка шапки/продление срока жизни). Когда: отправка с
устаревшей версией. Тогда:
409 CRM_LEAD_CONCURRENCY; клиент перечитывает карточку и повторяет.
Совместимость и наблюдаемость
- Все поля тела — optional-хотелки (
BudgetCurrencyIdне задан → валютаRUBсправочника); добавление новых optional-полей VO — additive; удаление/переименование/смена типа — breaking (migration + rollout). Unknown enum → отказ валидации (без «тихого» приведения). Справочные ссылки (CityId long,BudgetCurrencyId short) используют типы id источникаhandbook. - Logs:
lead-wishes updated(LeadId, tenant, actor;Comment— исключается из логов); metrics:crm_lead_wishes_update_total{result}, latency p95; traces — сквозной trace черезtraceparent; dashboardpin-crm; runbook —docs/06-services/crm/runbooks/.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: LeadEntity
- API (1): POST /api/agent/crm/leads — Создать лид вручную
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность-дом: lead.md (
LeadWishes, INV-LEAD-2/5, методUpdateWishes); enumPaymentMethod— purchase-program.md; snapshot-override — fixation-request.md. - Справочник: batch-резолв — resolve-references.md; города/валюты — city.md, currency.md; snapshot-модели — snapshots.md.
- Обзор области agent и карта методов лидов — README.