POST /api/v1/me/agent-invitations/{invitationId}/accept — Принятие приглашения в агентство
Адресат приглашения подтверждает вступление: приглашение переходит Pending → Accepted, открывается
активное членство в агентстве (AgentMembershipEntity), пользователю выдаётся роль agent в scope
Organization, а инвестор повышается до UserType = Agent (ADR-0061).
Назначение
Точка, где приглашённый (инвестор или агент другой компании) сам соглашается вступить в агентство.
Вызывается адресатом приглашения из личного кабинета; на выходе — принятое приглашение, созданное членство,
назначенная роль и повышенный тип пользователя. Авторизация — по совпадению телефона (INV-CI2), а не по
tenant: приглашение существует в пространстве приглашающей компании, а принимающий находится вне него.
Owning-контекст — identity.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | identity |
| API area | investor |
| Feature group | agent-onboarding |
| Статус | approved |
| Документ | docs/06-services/identity/api/investor/accept-invitation.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (адресат приглашения: инвестор или агент) |
| Auth policy / Permissions | authenticated; self-действие — по совпадению телефона (INV-CI2) |
| Scope (RBAC) | self; принимающий действует над приглашением вне своего tenant (record-level по телефону) |
| Record-level | приглашение доступно только пользователю, чей PhoneNumber == invitation.PhoneNumber |
| Tenant isolation | на входе не применяется (принимающий вне tenant компании); членство и роль создаются в scope Organization(invitation.CompanyId) |
| Audit | update invitation (AcceptedByUserId); create membership + assignment; update user (Type, зеркало CompanyId) |
| MFA/approval | не требуется |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/v1/me/agent-invitations/{invitationId}/accept |
| Success status | 200 |
| Idempotency header | Idempotency-Key (Guid) — повтор возвращает первый результат |
| Correlation | сквозной trace через traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
ActorUserId | identity claims (sub) | Guid | Да | валидный JWT | 401 без токена |
InvitationId | route | Guid | Да | существующее приглашение | — |
IdempotencyKey | header Idempotency-Key | Guid | Да | Required | — |
Модель ответа AcceptInvitationCommandResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
InvitationId | Guid | Да | CompanyInvitationEntity.Id | — | Принятое приглашение. |
MembershipId | Guid | Да | AgentMembershipEntity.Id | — | Созданное активное членство. |
CompanyId | Guid | Да | AgentMembershipEntity.CompanyId | — | Агентство. |
RoleAssignmentId | Guid | Да | RoleAssignment.Id | — | Назначение роли agent в scope Organization. |
UserType | string (enum UserType) | Да | UserEntity.Type | — | Agent. |
Алгоритм
- Контекст и доступ. Операцию выполняет аутентифицированный пользователь — адресат приглашения
(
ActorUserIdиз токена). Повтор с тем жеIdempotency-Keyвозвращает результат первого вызова. - Проверка входа. Идентификатор приглашения корректен — иначе
ValidationError; актор аутентифицирован — иначе401. - Проверки сущностей. Получить приглашение по
InvitationId(не найдено →IDENTITY_INVITATION_NOT_FOUND), пользователя-актора и компанию приглашения. Условия:- телефон (INV-CI2): телефон актора совпадает с телефоном приглашения — иначе
IDENTITY_INVITATION_PHONE_MISMATCH(принимающий не адресат); - статус/срок (INV-CI1/CI4): приглашение
Pendingи не истекло — иначеIDENTITY_INVITATION_INVALID_STATUS/IDENTITY_INVITATION_EXPIRED; - компания (INV-CI6/AM5): компания
Active, тип ∈ {Agency, Broker, Partner} — иначеIDENTITY_COMPANY_SUSPENDED; - единственность членства (INV-AM1): у актора нет активного членства — иначе
IDENTITY_AGENT_ALREADY_IN_AGENCY(сначала выйти из текущего агентства, leave-agency.md); - роль приглашения активна и назначима в scope
Organization.
- телефон (INV-CI2): телефон актора совпадает с телефоном приглашения — иначе
- Переходы и маппинг.
- приглашение:
Status = Accepted,AcceptedAt = now,AcceptedByUserId = актор(INV-CI1); - создаётся членство:
Status = Active,JoinedAt = now(INV-AM1/AM4/AM5); - пользователь: при
Type = Investor→Agent, зеркалоCompanyId = компания(INV-2 user.md, INV-AM6); независимый агент, вступающий в агентство, ужеAgent— обновляется только зеркало; - роль: создаётся назначение
agentв scopeOrganization(companyId)(INV-R2/R3/R4/R6); если у пользователя было платформенное назначениеagent(независимый) — оно отзывается (роль переезжаетPlatform → Organization).
- приглашение:
- Что меняется. В одном согласованном изменении: приглашение обновляется, пользователь обновляется,
создаётся членство, создаётся назначение роли (и отзывается прежнее платформенное назначение).
Конкурентный второй accept/revoke/leave →
CONCURRENCY_CONFLICT; гонка двух membership не создаёт второе активное членство (гарантия уникальности активного членства на пользователя). - События. В шину, в том же изменении:
CompanyInvitationChangedKafkaEvent→pin.identity.company-invitation-changed.v1(Accepted);AgentMembershipChangedKafkaEvent→pin.identity.agent-membership-changed.v1(Active);UserUpsertedKafkaEvent→pin.identity.users.v1(снапшотType = Agent, зеркалоCompanyId);UserAccessChangedKafkaEvent→pin.identity.access.v1(новое назначение). - Результат. Идентификаторы приглашения, членства, компании, назначения роли и
UserType = Agent; HTTP 200.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | CompanyInvitationChangedKafkaEvent (Accepted); AgentMembershipChangedKafkaEvent (Active); UserUpsertedKafkaEvent; UserAccessChangedKafkaEvent |
| Внешние вызовы | none — СМС руководителю «приглашение принято» шлёт notifications по событию |
| Проекции / поиск | permission-кэш агента прогреется на следующей проверке; ростер агентства обновляется в crm по membership-событию |
| Уведомления | руководителю (принято) через notifications |
Собственная транзакция принявшего фиксируется атомарно, поэтому пользователь сразу видит себя агентом
(UserType = Agent, доступен агентский функционал), а его новое назначение роли agent@Organization
действует со следующей проверки прав (кэш perm:{userId} прогревается при первом check-permission).
Наблюдатели-реплики отстают на секунды: руководитель видит нового агента в ростере агентства (crm) и в
статистике после доставки AgentMembershipChangedKafkaEvent; rewards/news подхватывают членство по тому
же событию. До доставки событий у зависимых сервисов действует прежняя картина (агент ещё «не в агентстве»),
что не мешает самому агенту работать.
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
ValidationError | 400 | Битый InvitationId | Список ошибок | no retry |
IDENTITY_INVITATION_NOT_FOUND | 404 | Приглашение не существует | «Приглашение не найдено» | no retry |
IDENTITY_INVITATION_PHONE_MISMATCH | 403 | Телефон актора ≠ телефон приглашения (INV-CI2) | «Приглашение адресовано другому номеру» | no retry |
IDENTITY_INVITATION_INVALID_STATUS | 409 | Не Pending (INV-CI1) | «Приглашение уже разрешено» | no retry |
IDENTITY_INVITATION_EXPIRED | 409 | Истёк ExpiresAt (INV-CI4) | «Срок приглашения истёк» | no retry |
IDENTITY_AGENT_ALREADY_IN_AGENCY | 409 | У актора уже есть активное членство (INV-AM1) | «Сначала выйдите из текущего агентства» | no retry |
IDENTITY_COMPANY_SUSPENDED | 409 | Компания не Active | «Компания недоступна» | no retry |
CONCURRENCY_CONFLICT | 409 | Гонка accept/revoke/leave (оптимистическая блокировка) | «Данные изменены, повторите» | retry после перечитки |
Acceptance Criteria
- Given
Pending-приглашение на телефон актора, актор без активного членства, компанияActive, When актор вызывает accept, Then приглашение →Accepted(AcceptedByUserId= актор), создаётся активное членство, назначается рольagent@Organization,UserType→Agent, и уходят событияCompanyInvitationChangedKafkaEvent(Accepted),AgentMembershipChangedKafkaEvent(Active),UserUpsertedKafkaEvent,UserAccessChangedKafkaEvent. - Given приглашение с телефоном, отличным от телефона актора, When accept, Then
IDENTITY_INVITATION_PHONE_MISMATCH(403) и ни одна сущность не изменена. - Given у актора уже есть активное членство в другом агентстве, When accept, Then
IDENTITY_AGENT_ALREADY_IN_AGENCY(409) — сначала требуется выход (leave-agency.md). - Given приглашение с истёкшим
ExpiresAt, When accept, ThenIDENTITY_INVITATION_EXPIRED(409); Given приглашение ужеAccepted/Declined/Revoked, When accept, ThenIDENTITY_INVITATION_INVALID_STATUS(409). - Given независимый агент с назначением
agent@Platform, When accept, Then платформенное назначение отзывается, создаётсяagent@Organization(роль «переезжает»Platform → Organization), членство активно. - Given тот же
Idempotency-Key, что и у ранее принятого приглашения, When повторный accept, Then возвращается результат первого вызова, вторичных событий и второго членства нет. - Given два параллельных accept одного приглашения, When оба доходят до записи, Then один
побеждает, второй получает
CONCURRENCY_CONFLICT(409); активное членство ровно одно.
Совместимость и наблюдаемость
- Новый эндпоинт, additive. Контракты событий — версия контракта 1.
- Logs:
ActorUserId,InvitationId,CompanyId,MembershipId; metrics:identity_invitation_accept_total{result}; traces: OTel.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (2): Entity / Aggregate: AgentMembershipEntity, Entity / Aggregate: CompanyInvitationEntity
- API (6): Identity API — карта областей и реестр investor-области, POST /api/v1/companies/my/agent-invitations — Приглашение агента по номеру телефона, POST /api/v1/companies/my/agent-invitations/{invitationId}/revoke — Отзыв приглашения агента, POST /api/v1/me/agency-membership/leave — Выход агента из агентства, POST /api/v1/me/agent-invitations/{invitationId}/decline — Отклонение приглашения в агентство, POST /api/v1/me/become-agent — Инвестор становится агентом
- Use Cases (1): UC-IDN-009. Приглашение агента по телефону и его принятие
- События (2): Event Contract: pin.identity.agent-membership-changed (AgentMembershipChangedKafkaEvent), Event Contract: pin.identity.company-invitation-changed (CompanyInvitationChangedKafkaEvent)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности: company-invitation.md (INV-CI1/CI2/CI4), agent-membership.md (INV-AM1/AM4/AM6), user.md (INV-2), role.md (INV-R2/R3).
- Отклонение: decline-invitation.md; отзыв: revoke-invitation.md; выход: leave-agency.md.
- События: company-invitation-changed.md, agent-membership-changed.md.
- Use case: UC-IDN-009-invite-agent-by-phone-and-accept.md.