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

UC-CRM-014. Агент выбирает активное агентство перед созданием лида/сделки

Назначение

Агент, состоящий одновременно в нескольких агентствах, перед работой с CRM выбирает, в контексте какого агентства он сейчас действует. Выбор устанавливает tenant его сессии (tenant_id = ActiveCompanyId), от которого зависит принадлежность создаваемых лидов и сделок (TenantId = AgencyCompanyId = Origin-агентство, иммутабельный снимок). Пока активное агентство не выбрано, создание CRM-ресурсов невозможно — tenant неоднозначен. Операция выбора живёт в identity (set-active-agency.md); здесь описано её влияние на создание CRM-процессов.

Metadata

ПолеЗначение
IDUC-CRM-014
Контекстcrm (следствие) + identity (выбор агентства) (см. docs/06-services/crm/, docs/06-services/identity/)
Типосновной
Статусdraft

Актор

ПолеЗначение
РольАгент с ≥ 1 активным членством (мульти-агентство — ≥ 2)
Permissions / scopeвыбор агентства — authenticated, self-действие (UserId из sub); создание процессов — crm.leads.manage / crm.deals.manage в scope выбранного Organization
Tenantрезультат меняет claim tenant_id актора = ActiveCompanyId; создаваемый лид/сделка берёт TenantId/Origin из этого claim
Record-levelсобственная сессия (sid) и собственные активные AgentMembership (по AgentUserId)

Предусловие

  • У агента ≥ 2 активных членства (AgentMembership.Status = Active) — не более одного на пару (AgentUserId, CompanyId) (INV-AM1 SingleActivePerCompany, ADR-0070).
  • Активное агентство ещё не выбрано либо выбранное перестало быть активным (например, после увольнения из него, UC-CRM-013) — сессия несёт nextAction = SelectActiveAgency; либо агент явно переключает агентство из личного кабинета.
  • Целевое агентство существует и в статусе Active.

Триггер

ПолеЗначение
ИсточникUI-действие → вызов API (после логина/refresh с nextAction = SelectActiveAgency или явное переключение)
Триггерящий элементПереключатель агентств в ЛК агента → POST /api/v1/me/active-agency

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

  1. Контекст. Агент после входа получает nextAction = SelectActiveAgency (у него > 1 активного членства и активное агентство не выбрано) или вручную открывает переключатель агентств. Список — активные членства агента с именами агентств.
  2. Выбор. Агент выбирает агентство (CompanyId) → POST /api/v1/me/active-agency с телом { CompanyId }.
  3. Проверка входа и предусловий. CompanyId — среди активных членств актора (иначе IDENTITY_MEMBERSHIP_NOT_ACTIVE); учётка Active (иначе IDENTITY_USER_BLOCKED); тип — Agent (иначе IDENTITY_NOT_AN_AGENT); агентство целевого членства Active (иначе IDENTITY_COMPANY_SUSPENDED).
  4. Установка активного агентства. RefreshSession.ActiveCompanyId = CompanyId (durable — переживает refresh); зеркало UserEntity.CompanyId = CompanyId; переиздаётся access-JWT с claim tenant_id = ActiveCompanyId; снимается nextAction. Уже созданные CRM-ресурсы не пересчитываются (их AgencyCompanyId/OriginAgencyCompanyId иммутабельны).
  5. Результат выбора. Клиент получает ActiveCompanyId, имя агентства, роль в нём, полный список членств (флаг IsActive) и новый access-JWT. Смена агентства действует немедленно.
  6. Создание CRM-процесса. Агент создаёт лид (create-lead.md, UC-CRM-001) или сделку (create-deal.md, UC-CRM-002): TenantId = AgencyCompanyId берётся из claim tenant_id = выбранное агентство. Лид получает OriginAgentUserId = actor, OriginAgencyCompanyId = ActiveCompanyId (иммутабельный снимок); агент становится активным Executor.
  7. Постусловие. Созданные процессы принадлежат выбранному агентству навсегда, даже если агент позже переключит активное агентство или уйдёт из этого — атрибуция и воронка агентства сохраняются.

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

A1. Явное переключение агентства во время работы
  1. Агент переключает активное агентство из ЛК (не по nextAction) — тот же вызов с другим CompanyId.
  2. JWT переиздаётся с новым tenant_id; последующие create-* уходят в новое агентство. Уже созданные лиды/ сделки прежнего агентства не пересчитываются (иммутабельные снимки) — агент продолжает видеть их как участник, но новые процессы принадлежат новому агентству.

Сходится с основным потоком на шаге 6.

A2. Повторный выбор того же агентства (идемпотентность)
  1. CompanyId уже равен ActiveCompanyId → идемпотентный 200, выдаётся свежий access-JWT, состояние не меняется.
A3. Выбранное агентство перестало быть активным
  1. Агента уволили из выбранного агентства (UC-CRM-013) — зеркало сброшено, при следующем refresh снова приходит nextAction = SelectActiveAgency.
  2. Агент выбирает другое активное агентство; если активных членств не осталось — он независимый (роль agent в scope Platform), CRM create-* агентства недоступны.
A4. Попытка создать лид/сделку без выбранного активного агентства
  1. Активное агентство не выбрано (nextAction = SelectActiveAgency активен) — claim tenant_id не пригоден для CRM create-*; создание процесса не допускается до выбора агентства (клиент направляется на шаг 2).
A5. Целевое агентство не среди активных членств
  1. Агент не член / уволен / членство закрыто → IDENTITY_MEMBERSHIP_NOT_ACTIVE (404); выбор недоступен.

Постусловие

  • RefreshSession.ActiveCompanyId = CompanyId (durable); зеркало UserEntity.CompanyId = CompanyId; access-JWT несёт tenant_id = ActiveCompanyId; nextAction = SelectActiveAgency снят.
  • Последующие CRM create-* (лиды/сделки) используют выбранное агентство как TenantId/Origin.
  • Ранее созданные CRM-ресурсы неизменны (снапшоты AgencyCompanyId/OriginAgencyCompanyId иммутабельны).
  • При фактической смене зеркала опубликован user-upserted (CompanyId = ActiveCompanyId).

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

КодHTTPУсловиеПоведение клиента
ValidationError400Пустой/некорректный CompanyIdПоказать ошибку формы
IDENTITY_USER_BLOCKED400Учётка не Active«Учётная запись заблокирована»
IDENTITY_NOT_AN_AGENT409Актор — инвестор (нет членств)«Вы не являетесь агентом»
IDENTITY_MEMBERSHIP_NOT_ACTIVE404CompanyId не среди активных членств актора (A5)«Активное членство не найдено»
IDENTITY_COMPANY_SUSPENDED409Агентство целевого членства не Active«Агентство заблокировано»
CONCURRENCY_CONFLICT409Гонка версий сессии/учётки (параллельный refresh/leave)«Повторите попытку»

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

СобытиеКогда поднимаетсяSubscribers
UserUpsertedEvent (зеркало)Фактическая смена UserEntity.CompanyId = ActiveCompanyIdin-proc identity; проекции

Отдельного доменного события «сменил активное агентство» нет — выбор является состоянием сессии.

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

СобытиеTopicКогда публикуетсяConsumers
UserUpsertedKafkaEvent.v1pin.identity.users.v1При смене зеркала UserEntity.CompanyIdcrm, messenger, profile (текущее агентство агента)

Refresh-токен не ротируется (та же сессия) — событие входа не публикуется; создание лида/сделки публикует свои события (pin.crm.lead-created.v1 / pin.crm.deal-created.v1) в рамках соответствующих UC.

Acceptance Criteria

#КритерийПроверка
AC1Агент с ≥ 2 активными членствами выбирает агентство → ActiveCompanyId/зеркало установлены, JWT несёт tenant_id = CompanyId, nextAction снятИнтеграционный тест POST /me/active-agency
AC2После выбора создание лида/сделки берёт TenantId/Origin = выбранное агентствоТест create-lead/create-deal → TenantId = ActiveCompanyId
AC3Ранее созданные процессы не пересчитываются при переключении (A1)Тест: снапшоты OriginAgencyCompanyId неизменны
AC4Выбор агентства не из активных членств → IDENTITY_MEMBERSHIP_NOT_ACTIVE (A5)Негативный тест
AC5Приостановленное агентство → IDENTITY_COMPANY_SUSPENDEDНегативный тест
AC6Повторный выбор того же агентства → идемпотентный 200 (A2)Двойной вызов
AC7Durable-выбор переживает refresh (tenant_id продолжает выводиться из ActiveCompanyId)Тест refresh после выбора

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

Выбор активного агентства — серверная операция identity: заголовочный tenant отвергается, tenant валидируется против активных членств. Результат (новый tenant_id в JWT) определяет tenant последующих CRM create-*; распространение зеркала и ростера у потребителей — eventually consistent по pin.identity.users.v1 (порядок per-user по ключу UserId). Гонка «выбор против параллельного refresh/leave» разрешается оптимистической блокировкой сессии (CONCURRENCY_CONFLICT). Иммутабельность Origin-снимков гарантирует, что переключение агентства не искажает историческую принадлежность и воронку ранее созданных процессов.

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

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

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