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

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 areaagent
Feature groupagent-membership
Статусdraft
Документdocs/06-services/identity/api/agent/leave-agency.md

Актор и доступ

ПроверкаЗначение
Actor typeUser (агент)
Auth policy / Permissionsauthenticated; self-действие (UserId из sub); руководитель может исключить агента через identity.users.manage (отдельный вызов, тот же домен-метод)
Scope (RBAC)self; переводит назначение роли agent из Organization в Platform
Record-levelсобственное активное членство (по AgentUserId)
Tenant isolationчленство читается по AgentUserId (агент видит своё сквозь компании); мутация в scope прежнего Organization
Auditupdate membership (Left, LeftAt); update user (зеркало CompanyId → null); revoke+grant RoleAssignment
MFA/approvalне требуется

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/me/agency-membership/leave
Success status200
Idempotency headerне требуется (повтор без активного членства — идемпотентный 200)
CorrelationOpenTelemetry trace (traceparent)

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorUserIdidentity claims (sub)GuidДавалидный JWT401 без токена

Модель ответа LeaveAgencyCommandResult

ПолеТипОбязательностьИсточникMaskingОписание
MembershipIdGuidДаAgentMembershipEntity.IdЗакрытое членство.
CompanyIdGuidДаAgentMembershipEntity.CompanyIdПрежнее агентство.
LeftAtDateTimeOffsetДаAgentMembershipEntity.LeftAtМомент выхода.
UserTypestring (enum UserType)ДаUserEntity.TypeAgent (независимый).

Алгоритм

  1. Контекст и доступ. Операцию выполняет сам агент (ActorUserId из claim sub); без токена — 401.
  2. Проверка входа и предусловий. Тело пустое. Учётка ActorUserId существует и в статусе Active (иначе IDENTITY_USER_BLOCKED).
  3. Что читаем. Учётка агента; активное членство AgentMembership по AgentUserId — если его нет, IDENTITY_MEMBERSHIP_NOT_ACTIVE (агент уже независимый или не агент); активное назначение роли agent в scope Organization(membership.CompanyId).
  4. Бизнес-правила и переходы (INV-AM6):
    • членство закрывается: Status = Left, LeftAt = now (INV-AM2/AM3);
    • у учётки обнуляется зеркало CompanyId → null, тип остаётся Agent;
    • назначение роли agent в scope Organization отзывается и перевыпускается в scope Platform (INV-R3) — метрики и достижения агента сохраняются.
  5. Что меняется. Обновляются членство, учётка и назначение роли agent (старое — в Revoked, новое в scope Platform); история членства не удаляется. Изменения и публикация событий — в одном согласованном изменении.
  6. События. Публикуются AgentMembershipChangedKafkaEventpin.identity.agent-membership-changed.v1 (Status = Left); UserUpsertedKafkaEventpin.identity.users.v1 (CompanyId = null); UserAccessChangedKafkaEventpin.identity.access.v1.
  7. Результат. LeaveAgencyCommandResult: MembershipId, CompanyId (прежнее агентство), LeftAt, UserType = Agent (независимый).
Историческая привязка не пересчитывается

Историческая привязка сделок/лидов/фиксаций не пересчитывается (снапшоты crm иммутабельны, back-fill запрещён, ADR-0061 п.4).

Диаграмма

Побочные эффекты

ТипДетали
Integration eventsAgentMembershipChangedKafkaEvent (Left); UserUpsertedKafkaEvent; UserAccessChangedKafkaEvent
External callsnone — СМС агенту/руководителю шлёт notifications по membership-событию
Cache / projections / searchростер агентства обновляется в crm; user-centric метрики агента остаются (агрегация по AgentUserId)
Notificationsагенту и руководителю через notifications

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
IDENTITY_MEMBERSHIP_NOT_ACTIVE404Нет активного членства (уже независимый / не агент)«Активное членство не найдено»no retry
IDENTITY_USER_BLOCKED400user.Status != Active«Учётная запись заблокирована»no retry
CONCURRENCY_CONFLICT409Гонка версий (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, Then IDENTITY_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.

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

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

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