POST /api/v1/me/agency-membership/leave — Выход агента из агентства
Агент выходит из агентства, оставаясь независимым агентом (ADR-0061): активное
AgentMembershipEntity Active → Left, роль agent перевыдаётся Organization → Platform, UserId и
все метрики/достижения сохраняются. Тип пользователя остаётся Agent (для полного возврата к инвестору —
become-investor.md).
Назначение
Развязка членства «агент ↔ агентство» без потери наработок. История членства сохраняется (запись не
удаляется, INV-AM2). Owning-контекст — identity.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | identity |
| API area | agent |
| Feature group | agent-membership |
| Статус | draft |
| Документ | docs/06-services/identity/api/agent/leave-agency.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (агент) |
| Auth policy / Permissions | authenticated; self-действие (UserId из sub); руководитель может исключить агента через identity.users.manage (отдельный вызов, тот же домен-метод) |
| Scope (RBAC) | self; переводит назначение роли agent из Organization в Platform |
| Record-level | собственное активное членство (по AgentUserId) |
| Tenant isolation | членство читается по AgentUserId (агент видит своё сквозь компании); мутация в scope прежнего Organization |
| Audit | update membership (Left, LeftAt); update user (зеркало CompanyId → null); revoke+grant RoleAssignment |
| MFA/approval | не требуется |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/v1/me/agency-membership/leave |
| Success status | 200 |
| Idempotency header | не требуется (повтор без активного членства — идемпотентный 200) |
| Correlation | OpenTelemetry trace (traceparent) |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
ActorUserId | identity claims (sub) | Guid | Да | валидный JWT | 401 без токена |
Модель ответа LeaveAgencyCommandResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
MembershipId | Guid | Да | AgentMembershipEntity.Id | — | Закрытое членство. |
CompanyId | Guid | Да | AgentMembershipEntity.CompanyId | — | Прежнее агентство. |
LeftAt | DateTimeOffset | Да | AgentMembershipEntity.LeftAt | — | Момент выхода. |
UserType | string (enum UserType) | Да | UserEntity.Type | — | Agent (независимый). |
Алгоритм
- Контекст и доступ. Операцию выполняет сам агент (
ActorUserIdиз claimsub); без токена —401. - Проверка входа и предусловий. Тело пустое. Учётка
ActorUserIdсуществует и в статусеActive(иначеIDENTITY_USER_BLOCKED). - Что читаем. Учётка агента; активное членство
AgentMembershipпоAgentUserId— если его нет,IDENTITY_MEMBERSHIP_NOT_ACTIVE(агент уже независимый или не агент); активное назначение ролиagentв scopeOrganization(membership.CompanyId). - Бизнес-правила и переходы (INV-AM6):
- членство закрывается:
Status = Left,LeftAt = now(INV-AM2/AM3); - у учётки обнуляется зеркало
CompanyId → null, тип остаётсяAgent; - назначение роли
agentв scopeOrganizationотзывается и перевыпускается в scopePlatform(INV-R3) — метрики и достижения агента сохраняются.
- членство закрывается:
- Что меняется. Обновляются членство, учётка и назначение роли
agent(старое — вRevoked, новое в scopePlatform); история членства не удаляется. Изменения и публикация событий — в одном согласованном изменении. - События. Публикуются
AgentMembershipChangedKafkaEvent→pin.identity.agent-membership-changed.v1(Status = Left);UserUpsertedKafkaEvent→pin.identity.users.v1(CompanyId = null);UserAccessChangedKafkaEvent→pin.identity.access.v1. - Результат.
LeaveAgencyCommandResult:MembershipId,CompanyId(прежнее агентство),LeftAt,UserType = Agent(независимый).
Историческая привязка не пересчитывается
Историческая привязка сделок/лидов/фиксаций не пересчитывается (снапшоты crm иммутабельны, back-fill запрещён, ADR-0061 п.4).
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Integration events | AgentMembershipChangedKafkaEvent (Left); UserUpsertedKafkaEvent; UserAccessChangedKafkaEvent |
| External calls | none — СМС агенту/руководителю шлёт notifications по membership-событию |
| Cache / projections / search | ростер агентства обновляется в crm; user-centric метрики агента остаются (агрегация по AgentUserId) |
| Notifications | агенту и руководителю через notifications |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
IDENTITY_MEMBERSHIP_NOT_ACTIVE | 404 | Нет активного членства (уже независимый / не агент) | «Активное членство не найдено» | no retry |
IDENTITY_USER_BLOCKED | 400 | user.Status != Active | «Учётная запись заблокирована» | no retry |
CONCURRENCY_CONFLICT | 409 | Гонка версий (leave/accept) | «Данные изменены, повторите» | retry после перечитки |
Acceptance Criteria
- Given агент с активным членством, When leave, Then членство
Active → Left(LeftAt), рольagentперевыпущенаOrganization → Platform(INV-R3), зеркалоCompanyId → null, тип остаётсяAgent; ответ содержитMembershipId/CompanyId/LeftAt; публикуютсяAgentMembershipChangedKafkaEvent(Left),UserUpsertedKafkaEvent(CompanyId = null),UserAccessChangedKafkaEvent. - Given независимый агент без активного членства, When leave, Then
IDENTITY_MEMBERSHIP_NOT_ACTIVE(404) — повтор безопасен (идемпотентный исход). - Given учётка
Blocked, When leave, ThenIDENTITY_USER_BLOCKED(400). - Given руководитель c
identity.users.manageисключает агента, When тот же домен-метод с целевымAgentUserId, Then те же переходы; событие/audit несут инициатора-руководителя (actor-stamping). - Given параллельные leave и accept нового приглашения, When конфликт версий, Then
CONCURRENCY_CONFLICT(409); retry после перечитки. - Given выход выполнен, Then метрики/достижения агента (агрегация по
AgentUserId) и историческая привязка сделок/лидов/фиксаций сохранены (снапшоты crm иммутабельны, back-fill запрещён); crm обновляет ростер агентства по membership-событию (асинхронно, секунды — до применения агент краткий промежуток ещё виден в прежнем ростере).
Совместимость и наблюдаемость
- Новый эндпоинт, additive. Контракты событий —
ContractVersion = 1. - Logs:
ActorUserId,MembershipId,CompanyId; metrics:identity_leave_agency_total{result}; traces: OTel.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: AgentMembershipEntity
- API (5): Identity Agent API — реестр методов и общие правила области, POST /api/v1/companies/{companyId}/agents/{agentUserId}/terminate — Исключение агента из агентства, POST /api/v1/me/active-agency — Выбор активного агентства, POST /api/v1/me/agent-invitations/{invitationId}/accept — Принятие приглашения в агентство, POST /api/v1/me/become-investor — Агент возвращается к роли инвестора
- Use Cases (3): UC-CRM-013. Увольнение агента: каскад отвязки от процессов CRM, UC-IDN-010. Агент выходит из агентства, сохраняя метрики, US-реестр «Участники и владение процессами» (US-CRM-PART / US-AG-ADMIN / US-AGT-MULTI / US-INV-CO)
- События (1): Event Contract: pin.identity.agent-membership-changed (AgentMembershipChangedKafkaEvent)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности: agent-membership.md (INV-AM2/AM3/AM6), user.md (зеркало
CompanyId), role.md (рольagentOrganization → Platform). - Полный возврат к инвестору: become-investor.md; вступление: accept-invitation.md.
- Событие: agent-membership-changed.md; историческая привязка (reference only):
docs/06-services/crm/domain/deal.md,crm/api/agent/get-statistics.md. - Use case: UC-IDN-010-agent-leaves-agency-keeps-metrics.md.