UC-CRM-014. Агент выбирает активное агентство перед созданием лида/сделки
Назначение
Агент, состоящий одновременно в нескольких агентствах, перед работой с CRM выбирает, в контексте какого
агентства он сейчас действует. Выбор устанавливает tenant его сессии (tenant_id = ActiveCompanyId), от
которого зависит принадлежность создаваемых лидов и сделок (TenantId = AgencyCompanyId = Origin-агентство,
иммутабельный снимок). Пока активное агентство не выбрано, создание CRM-ресурсов невозможно —
tenant неоднозначен. Операция выбора живёт в identity (set-active-agency.md);
здесь описано её влияние на создание CRM-процессов.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-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-AM1SingleActivePerCompany, ADR-0070). - Активное агентство ещё не выбрано либо выбранное перестало быть активным (например, после увольнения из него,
UC-CRM-013) — сессия несёт
nextAction = SelectActiveAgency; либо агент явно переключает агентство из личного кабинета. - Целевое агентство существует и в статусе
Active.
Триггер
| Поле | Значение |
|---|---|
| Источник | UI-действие → вызов API (после логина/refresh с nextAction = SelectActiveAgency или явное переключение) |
| Триггерящий элемент | Переключатель агентств в ЛК агента → POST /api/v1/me/active-agency |
Основной Поток
- Контекст. Агент после входа получает
nextAction = SelectActiveAgency(у него > 1 активного членства и активное агентство не выбрано) или вручную открывает переключатель агентств. Список — активные членства агента с именами агентств. - Выбор. Агент выбирает агентство (
CompanyId) →POST /api/v1/me/active-agencyс телом{ CompanyId }. - Проверка входа и предусловий.
CompanyId— среди активных членств актора (иначеIDENTITY_MEMBERSHIP_NOT_ACTIVE); учёткаActive(иначеIDENTITY_USER_BLOCKED); тип —Agent(иначеIDENTITY_NOT_AN_AGENT); агентство целевого членстваActive(иначеIDENTITY_COMPANY_SUSPENDED). - Установка активного агентства.
RefreshSession.ActiveCompanyId = CompanyId(durable — переживает refresh); зеркалоUserEntity.CompanyId = CompanyId; переиздаётся access-JWT с claimtenant_id = ActiveCompanyId; снимаетсяnextAction. Уже созданные CRM-ресурсы не пересчитываются (ихAgencyCompanyId/OriginAgencyCompanyIdиммутабельны). - Результат выбора. Клиент получает
ActiveCompanyId, имя агентства, роль в нём, полный список членств (флагIsActive) и новый access-JWT. Смена агентства действует немедленно. - Создание CRM-процесса. Агент создаёт лид (create-lead.md, UC-CRM-001)
или сделку (create-deal.md, UC-CRM-002):
TenantId = AgencyCompanyIdберётся из claimtenant_id= выбранное агентство. Лид получаетOriginAgentUserId = actor,OriginAgencyCompanyId = ActiveCompanyId(иммутабельный снимок); агент становится активнымExecutor. - Постусловие. Созданные процессы принадлежат выбранному агентству навсегда, даже если агент позже переключит активное агентство или уйдёт из этого — атрибуция и воронка агентства сохраняются.
Альтернативные Потоки
- Агент переключает активное агентство из ЛК (не по
nextAction) — тот же вызов с другимCompanyId. - JWT переиздаётся с новым
tenant_id; последующие create-* уходят в новое агентство. Уже созданные лиды/ сделки прежнего агентства не пересчитываются (иммутабельные снимки) — агент продолжает видеть их как участник, но новые процессы принадлежат новому агентству.
Сходится с основным потоком на шаге 6.
CompanyIdуже равенActiveCompanyId→ идемпотентный200, выдаётся свежий access-JWT, состояние не меняется.
- Агента уволили из выбранного агентства (UC-CRM-013) — зеркало
сброшено, при следующем refresh снова приходит
nextAction = SelectActiveAgency. - Агент выбирает другое активное агентство; если активных членств не осталось — он независимый (роль
agentв scopePlatform), CRM create-* агентства недоступны.
- Активное агентство не выбрано (
nextAction = SelectActiveAgencyактивен) — claimtenant_idне пригоден для CRM create-*; создание процесса не допускается до выбора агентства (клиент направляется на шаг 2).
- Агент не член / уволен / членство закрыто →
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 | Условие | Поведение клиента |
|---|---|---|---|
ValidationError | 400 | Пустой/некорректный CompanyId | Показать ошибку формы |
IDENTITY_USER_BLOCKED | 400 | Учётка не Active | «Учётная запись заблокирована» |
IDENTITY_NOT_AN_AGENT | 409 | Актор — инвестор (нет членств) | «Вы не являетесь агентом» |
IDENTITY_MEMBERSHIP_NOT_ACTIVE | 404 | CompanyId не среди активных членств актора (A5) | «Активное членство не найдено» |
IDENTITY_COMPANY_SUSPENDED | 409 | Агентство целевого членства не Active | «Агентство заблокировано» |
CONCURRENCY_CONFLICT | 409 | Гонка версий сессии/учётки (параллельный refresh/leave) | «Повторите попытку» |
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|---|---|
UserUpsertedEvent (зеркало) | Фактическая смена UserEntity.CompanyId = ActiveCompanyId | in-proc identity; проекции |
Отдельного доменного события «сменил активное агентство» нет — выбор является состоянием сессии.
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
UserUpsertedKafkaEvent.v1 | pin.identity.users.v1 | При смене зеркала UserEntity.CompanyId | crm, 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) | Двойной вызов |
| AC7 | Durable-выбор переживает 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-снимков гарантирует, что
переключение агентства не искажает историческую принадлежность и воронку ранее созданных процессов.
Обратные ссылки
Автоссылки: где используется этот документ.
- Use Cases (1): UC-CRM-013. Увольнение агента: каскад отвязки от процессов CRM
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- Выбор активного агентства (identity): set-active-agency.md.
- Создание процессов (следствие): create-lead.md (UC-CRM-001), create-deal.md (UC-CRM-002).
- Domain: agent-membership.md (INV-AM1
SingleActivePerCompany, зеркало), user-session.md (RefreshSession.ActiveCompanyId), ../domain/lead.md / ../domain/deal.md (иммутабельный Origin-снимок). - Событие: agent-membership-changed.md (мульти-членство).
- Смежные сценарии: UC-CRM-013 (увольнение сбрасывает выбор), UC-IDN-010.
- ADR: 0070 multi-agency, 0052 tenancy, 0061 independent-agent.