POST /api/v1/me/become-agent — Инвестор становится агентом
Инвестор самостоятельно становится агентом (ADR-0061). Два режима: независимый (по умолчанию) —
роль agent в scope Platform, без агентства; через приглашение (invitationId в теле) — вступление
в агентство, эквивалент accept-invitation.md. Учётка и UserId сохраняются —
новая учётка не создаётся.
Назначение
Точка входа «стать агентом» из личного кабинета инвестора. Вызывается инвестором; на выходе — та же
учётка с типом Agent, назначенной ролью agent и (в режиме через приглашение) членством в агентстве.
Owning-контекст — identity.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | identity |
| API area | investor |
| Feature group | agent-onboarding |
| Статус | approved |
| Документ | docs/06-services/identity/api/investor/become-agent.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (инвестор) |
| Auth policy / Permissions | authenticated; self-действие (UserId из sub) |
| Scope (RBAC) | self; независимый режим выдаёт роль agent в scope Platform (INV-R3) |
| Record-level | только собственная учётка |
| Tenant isolation | не применяется (независимый агент — вне tenant, ADR-0052); режим через приглашение — как accept |
| Audit | update user (Type = Agent); create назначение роли (agent, Platform); при invite-режиме — как accept-invitation.md |
| MFA/approval | не требуется |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/v1/me/become-agent |
| Success status | 200 |
| Idempotency header | Idempotency-Key (Guid) |
| Correlation | сквозной trace через traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
ActorUserId | identity claims (sub) | Guid | Да | валидный JWT | 401 без токена |
InvitationId | body | Guid? | Нет | при заданном — существующее Pending-приглашение на телефон актора | — |
IdempotencyKey | header Idempotency-Key | Guid | Да | Required | — |
Модель ответа BecomeAgentCommandResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
UserId | Guid | Да | UserEntity.Id | — | Учётка (не меняется). |
UserType | string (enum UserType) | Да | UserEntity.Type | — | Agent. |
Mode | string (enum BecomeAgentMode) | Да | computed | — | Independent / JoinedAgency. |
CompanyId | Guid? | Нет | AgentMembership.CompanyId | — | Агентство (при JoinedAgency); null для независимого. |
RoleAssignmentId | Guid | Да | RoleAssignment.Id | — | Назначение роли agent. |
Алгоритм
- Контекст и доступ. Операцию выполняет аутентифицированный инвестор (
ActorUserIdиз токена). Повтор с тем жеIdempotency-Keyвозвращает результат первого вызова. - Проверка входа. Синтаксис корректен — иначе
ValidationError; актор аутентифицирован — иначе401. - Проверки и ветвление. Получить пользователя-актора; он
Active— иначеIDENTITY_USER_BLOCKED.InvitationIdзадан → выполнить сценарий принятия приглашения (см. accept-invitation.md, шаги 3–6: INV-CI2 совпадение телефона, INV-AM1 нет активного членства) →Mode = JoinedAgency;InvitationIdне задан (независимый режим): если актор ужеAgentс активным членством —IDENTITY_AGENT_ALREADY_IN_AGENCY(для независимого повтор — идемпотентный no-op); взять системную рольagent.
- Переходы и маппинг (независимый режим). Пользователь:
Type Investor → Agent, зеркалоCompanyId = null(INV-2 user.md); создаётся назначение ролиagentв scopePlatform— допустимо дляagent(INV-R3). - Что меняется. Независимый режим: в одном согласованном изменении обновляется пользователь и создаётся назначение роли.
- События. Независимый режим:
UserUpsertedKafkaEvent→pin.identity.users.v1(Type = Agent,CompanyId = null);UserAccessChangedKafkaEvent→pin.identity.access.v1. Membership-события в независимом режиме не публикуются (записи членства нет). Режим через приглашение публикует события как accept-invitation.md. - Результат.
UserId(не меняется),UserType = Agent,Mode,CompanyId?, идентификатор назначения роли; HTTP 200.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | UserUpsertedKafkaEvent; UserAccessChangedKafkaEvent; при вступлении в агентство дополнительно AgentMembershipChangedKafkaEvent + CompanyInvitationChangedKafkaEvent |
| Внешние вызовы | none |
| Проекции / поиск | permission-кэш агента прогреется на следующей проверке |
| Уведомления | none (независимый режим); при вступлении — как accept |
После коммита пользователь сразу становится Agent и видит агентский интерфейс; назначение роли agent
действует со следующей проверки прав (прогрев perm:{userId}). Потребители снапшота pin.identity.users.v1
(crm/referral/messenger) увидят Type = Agent через секунды после доставки события; до этого их реплики
держат прежний тип. В независимом режиме членства нет, поэтому ростер агентства не затрагивается.
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
ValidationError | 400 | Битый InvitationId | Список ошибок | no retry |
IDENTITY_USER_BLOCKED | 400 | Status != Active | «Учётная запись заблокирована» | no retry |
IDENTITY_AGENT_ALREADY_IN_AGENCY | 409 | Уже агент с активным членством (INV-AM1) | «Вы уже агент агентства» | no retry |
IDENTITY_INVITATION_* | 403/404/409 | В режиме через приглашение — как accept-invitation.md | см. accept | no retry |
CONCURRENCY_CONFLICT | 409 | Гонка (оптимистическая блокировка) | «Данные изменены, повторите» | retry после перечитки |
Acceptance Criteria
- Given активный инвестор без активного членства и без
invitationId, When become-agent, ThenType→Agent, зеркалоCompanyId = null, создаётся назначениеagent@Platform,Mode = Independent, уходятUserUpsertedKafkaEvent(Type = Agent,CompanyId = null) иUserAccessChangedKafkaEvent; membership-события не публикуются. - Given в теле задан
invitationIdнаPending-приглашение с телефоном актора, When become-agent, Then отрабатывает сценарий принятия приглашения (см. accept-invitation.md),Mode = JoinedAgency,CompanyId= агентство. - Given актор уже
Agentс активным членством,invitationIdне задан, When become-agent, ThenIDENTITY_AGENT_ALREADY_IN_AGENCY(409). - Given независимый режим и повтор с тем же
Idempotency-Key, When become-agent, Then возвращается результат первого вызова (идемпотентный no-op), второго назначения роли нет. - Given
Status != Active(заблокирован/архив), When become-agent, ThenIDENTITY_USER_BLOCKED(400) и учётка не меняется. - Given
invitationIdуказывает на приглашение с чужим телефоном / истёкшее / неPending, When become-agent, Then соответствующийIDENTITY_INVITATION_*(403/404/409), как в accept.
Совместимость и наблюдаемость
- Новый эндпоинт, additive. Контракты событий — версия контракта 1.
- Logs:
ActorUserId,Mode,CompanyId?; metrics:identity_become_agent_total{mode,result}; traces: OTel.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: AgentMembershipEntity
- API (2): Identity API — карта областей и реестр investor-области, POST /api/v1/me/become-investor — Агент возвращается к роли инвестора
- Use Cases (1): UC-IDN-011. Инвестор становится агентом
- События (1): Event Contract: pin.identity.agent-membership-changed (AgentMembershipChangedKafkaEvent)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности: user.md (INV-2,
PromoteToAgent), role.md (INV-R3, рольagentPlatform), agent-membership.md. - Путь вступления: accept-invitation.md; обратный переход: become-investor.md.
- События: user-upserted.md, user-access-changed.md, agent-membership-changed.md.
- Use case: UC-IDN-011-investor-becomes-agent.md.