POST /api/v1/me/become-investor — Агент возвращается к роли инвестора
Агент отказывается от роли agent и возвращается к инвестору (ADR-0061): снимается роль agent,
закрывается активное AgentMembershipEntity (если есть), тип Agent → Investor. UserId сохраняется —
новая учётка не создаётся; исторические сделки/комиссии остаются у прежней компании.
Назначение
Полный обратный переход к инвестору (в отличие от leave-agency.md, который оставляет
независимым агентом). Owning-контекст — identity.
| Поле | Значение |
|---|
| Сервис/контекст | identity |
| API area | agent |
| Feature group | agent-membership |
| Статус | draft |
| Документ | docs/06-services/identity/api/agent/become-investor.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (агент) |
| Auth policy / Permissions | authenticated; self-действие (UserId из sub) |
| Scope (RBAC) | self; снимает роль agent (Organization или Platform), оставляет investor (Platform) |
| Record-level | собственная учётка + собственное активное членство |
| Tenant isolation | не действует (результат — инвестор вне tenant, ADR-0052) |
| Audit | update user (Type = Investor, зеркало CompanyId → null); revoke RoleAssignment (agent); при наличии — AgentMembership Left |
| MFA/approval | не требуется |
HTTP-контракт
| Поле | Значение |
|---|
| Method | POST |
| Route | /api/v1/me/become-investor |
| Success status | 200 |
| Idempotency header | не требуется (повтор для уже инвестора — идемпотентный 200) |
| Correlation | OpenTelemetry trace (traceparent) |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
ActorUserId | identity claims (sub) | Guid | Да | валидный JWT | 401 без токена |
Модель ответа BecomeInvestorCommandResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
UserId | Guid | Да | UserEntity.Id | — | Учётка (не меняется). |
UserType | string (enum UserType) | Да | UserEntity.Type | — | Investor. |
ClosedMembershipId | Guid? | Нет | AgentMembershipEntity.Id | — | Закрытое членство (если было активно); иначе null. |
Алгоритм
- Контекст и доступ. Операцию выполняет сам агент (
ActorUserId из claim sub); без токена — 401.
- Проверка входа и предусловий. Учётка
ActorUserId существует и в статусе Active (иначе
IDENTITY_USER_BLOCKED); тип учётки — Agent, иначе IDENTITY_NOT_AN_AGENT (уже инвестор —
идемпотентный no-op на повтор).
- Что читаем. Учётка агента; её активные назначения роли
agent; активное членство
AgentMembership по AgentUserId (может отсутствовать у независимого агента).
- Бизнес-правила и переходы.
- если активное членство есть — оно закрывается (
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).
- Что меняется. Обновляются учётка, назначение роли
agent и (при наличии) членство. Изменения и
публикация событий — в одном согласованном изменении.
- События. Публикуются
UserUpsertedKafkaEvent → pin.identity.users.v1 (Type = Investor,
CompanyId = null); UserAccessChangedKafkaEvent → pin.identity.access.v1; при закрытии членства —
AgentMembershipChangedKafkaEvent → pin.identity.agent-membership-changed.v1 (Left).
- Результат.
BecomeInvestorCommandResult: UserId (не меняется), UserType = Investor,
ClosedMembershipId (если членство было закрыто, иначе null).
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|
| Integration events | UserUpsertedKafkaEvent (Investor); UserAccessChangedKafkaEvent; при активном членстве — AgentMembershipChangedKafkaEvent (Left) |
| External calls | none |
| Cache / projections / search | permission-кэш инвалидируется событием access; user-centric метрики агента сохраняются (история) |
| Notifications | опционально руководителю при закрытии членства через notifications |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
IDENTITY_NOT_AN_AGENT | 409 | user.Type != Agent (уже инвестор) | «Вы не являетесь агентом» | no retry |
IDENTITY_USER_BLOCKED | 400 | Status != Active | «Учётная запись заблокирована» | no retry |
CONCURRENCY_CONFLICT | 409 | Гонка версий с 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.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы