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

POST /api/v1/me/become-investor — Агент возвращается к роли инвестора

Агент отказывается от роли agent и возвращается к инвестору (ADR-0061): снимается роль agent, закрывается активное AgentMembershipEntity (если есть), тип Agent → Investor. UserId сохраняется — новая учётка не создаётся; исторические сделки/комиссии остаются у прежней компании.

Назначение

Полный обратный переход к инвестору (в отличие от leave-agency.md, который оставляет независимым агентом). Owning-контекст — identity.

Metadata

ПолеЗначение
Сервис/контекстidentity
API areaagent
Feature groupagent-membership
Статусdraft
Документdocs/06-services/identity/api/agent/become-investor.md

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

ПроверкаЗначение
Actor typeUser (агент)
Auth policy / Permissionsauthenticated; self-действие (UserId из sub)
Scope (RBAC)self; снимает роль agent (Organization или Platform), оставляет investor (Platform)
Record-levelсобственная учётка + собственное активное членство
Tenant isolationне действует (результат — инвестор вне tenant, ADR-0052)
Auditupdate user (Type = Investor, зеркало CompanyId → null); revoke RoleAssignment (agent); при наличии — AgentMembership Left
MFA/approvalне требуется

HTTP-контракт

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

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

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

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

ПолеТипОбязательностьИсточникMaskingОписание
UserIdGuidДаUserEntity.IdУчётка (не меняется).
UserTypestring (enum UserType)ДаUserEntity.TypeInvestor.
ClosedMembershipIdGuid?НетAgentMembershipEntity.IdЗакрытое членство (если было активно); иначе null.

Алгоритм

  1. Контекст и доступ. Операцию выполняет сам агент (ActorUserId из claim sub); без токена — 401.
  2. Проверка входа и предусловий. Учётка ActorUserId существует и в статусе Active (иначе IDENTITY_USER_BLOCKED); тип учётки — Agent, иначе IDENTITY_NOT_AN_AGENT (уже инвестор — идемпотентный no-op на повтор).
  3. Что читаем. Учётка агента; её активные назначения роли agent; активное членство AgentMembership по AgentUserId (может отсутствовать у независимого агента).
  4. Бизнес-правила и переходы.
    • если активное членство есть — оно закрывается (Active → Left, проставляется LeftAt, INV-AM2/AM3);
    • тип учётки понижается Agent → Investor, зеркало CompanyId → null (INV-2 user.md);
    • назначение роли agent (в scope Organization или Platform) отзывается; роль investor (scope Platform) остаётся действующей. Историческая привязка сделок/комиссий не пересчитывается (снапшоты crm иммутабельны, ADR-0061 п.4).
  5. Что меняется. Обновляются учётка, назначение роли agent и (при наличии) членство. Изменения и публикация событий — в одном согласованном изменении.
  6. События. Публикуются UserUpsertedKafkaEventpin.identity.users.v1 (Type = Investor, CompanyId = null); UserAccessChangedKafkaEventpin.identity.access.v1; при закрытии членства — AgentMembershipChangedKafkaEventpin.identity.agent-membership-changed.v1 (Left).
  7. Результат. BecomeInvestorCommandResult: UserId (не меняется), UserType = Investor, ClosedMembershipId (если членство было закрыто, иначе null).

Диаграмма

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

ТипДетали
Integration eventsUserUpsertedKafkaEvent (Investor); UserAccessChangedKafkaEvent; при активном членстве — AgentMembershipChangedKafkaEvent (Left)
External callsnone
Cache / projections / searchpermission-кэш инвалидируется событием access; user-centric метрики агента сохраняются (история)
Notificationsопционально руководителю при закрытии членства через notifications

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
IDENTITY_NOT_AN_AGENT409user.Type != Agent (уже инвестор)«Вы не являетесь агентом»no retry
IDENTITY_USER_BLOCKED400Status != Active«Учётная запись заблокирована»no retry
CONCURRENCY_CONFLICT409Гонка версий с leave/accept«Данные изменены, повторите»retry после перечитки

Acceptance Criteria

  • Given агент с активным членством в агентстве, When become-investor, Then тип Agent → Investor, членство Active → Left (LeftAt), назначение роли agent отозвано, investor (scope Platform) остаётся, зеркало CompanyId → null; ответ содержит ClosedMembershipId; публикуются UserUpsertedKafkaEvent (Investor, CompanyId = null), UserAccessChangedKafkaEvent, AgentMembershipChangedKafkaEvent (Left).
  • Given независимый агент без активного членства, When become-investor, Then тип → Investor, роль agent отозвана, ClosedMembershipId = null, событие членства не публикуется.
  • Given пользователь уже Investor, When become-investor (повтор), Then идемпотентный 200 (no-op — тип уже Investor, членство закрыто).
  • Given учётка Blocked, When become-investor, Then IDENTITY_USER_BLOCKED (400).
  • Given параллельные leave/accept, When become-investor конфликтует по версии, Then CONCURRENCY_CONFLICT (409); retry после перечитки.
  • Given переход выполнен, Then исторические сделки/комиссии остаются у прежней компании (снапшоты crm иммутабельны, back-fill запрещён — ADR-0061 п.4); user-centric метрики агента (агрегация по AgentUserId) сохранены; permission-кэш инвалидируется по access-событию (секунды).

Совместимость и наблюдаемость

  • Новый эндпоинт, additive. Контракты событий — ContractVersion = 1.
  • Logs: ActorUserId, ClosedMembershipId?; metrics: identity_become_investor_total{result}; traces: OTel.

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

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

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