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

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 areainvestor
Feature groupagent-onboarding
Статусapproved
Документdocs/06-services/identity/api/investor/accept-invitation.md

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

ПроверкаЗначение
Actor typeUser (адресат приглашения: инвестор или агент)
Auth policy / Permissionsauthenticated; self-действие — по совпадению телефона (INV-CI2)
Scope (RBAC)self; принимающий действует над приглашением вне своего tenant (record-level по телефону)
Record-levelприглашение доступно только пользователю, чей PhoneNumber == invitation.PhoneNumber
Tenant isolationна входе не применяется (принимающий вне tenant компании); членство и роль создаются в scope Organization(invitation.CompanyId)
Auditupdate invitation (AcceptedByUserId); create membership + assignment; update user (Type, зеркало CompanyId)
MFA/approvalне требуется

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/me/agent-invitations/{invitationId}/accept
Success status200
Idempotency headerIdempotency-Key (Guid) — повтор возвращает первый результат
Correlationсквозной trace через traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorUserIdidentity claims (sub)GuidДавалидный JWT401 без токена
InvitationIdrouteGuidДасуществующее приглашение
IdempotencyKeyheader Idempotency-KeyGuidДаRequired

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

ПолеТипОбязательностьИсточникMaskingОписание
InvitationIdGuidДаCompanyInvitationEntity.IdПринятое приглашение.
MembershipIdGuidДаAgentMembershipEntity.IdСозданное активное членство.
CompanyIdGuidДаAgentMembershipEntity.CompanyIdАгентство.
RoleAssignmentIdGuidДаRoleAssignment.IdНазначение роли agent в scope Organization.
UserTypestring (enum UserType)ДаUserEntity.TypeAgent.

Алгоритм

  1. Контекст и доступ. Операцию выполняет аутентифицированный пользователь — адресат приглашения (ActorUserId из токена). Повтор с тем же Idempotency-Key возвращает результат первого вызова.
  2. Проверка входа. Идентификатор приглашения корректен — иначе ValidationError; актор аутентифицирован — иначе 401.
  3. Проверки сущностей. Получить приглашение по 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.
  4. Переходы и маппинг.
    • приглашение: Status = Accepted, AcceptedAt = now, AcceptedByUserId = актор (INV-CI1);
    • создаётся членство: Status = Active, JoinedAt = now (INV-AM1/AM4/AM5);
    • пользователь: при Type = InvestorAgent, зеркало CompanyId = компания (INV-2 user.md, INV-AM6); независимый агент, вступающий в агентство, уже Agent — обновляется только зеркало;
    • роль: создаётся назначение agent в scope Organization(companyId) (INV-R2/R3/R4/R6); если у пользователя было платформенное назначение agent (независимый) — оно отзывается (роль переезжает Platform → Organization).
  5. Что меняется. В одном согласованном изменении: приглашение обновляется, пользователь обновляется, создаётся членство, создаётся назначение роли (и отзывается прежнее платформенное назначение). Конкурентный второй accept/revoke/leave → CONCURRENCY_CONFLICT; гонка двух membership не создаёт второе активное членство (гарантия уникальности активного членства на пользователя).
  6. События. В шину, в том же изменении: CompanyInvitationChangedKafkaEventpin.identity.company-invitation-changed.v1 (Accepted); AgentMembershipChangedKafkaEventpin.identity.agent-membership-changed.v1 (Active); UserUpsertedKafkaEventpin.identity.users.v1 (снапшот Type = Agent, зеркало CompanyId); UserAccessChangedKafkaEventpin.identity.access.v1 (новое назначение).
  7. Результат. Идентификаторы приглашения, членства, компании, назначения роли и UserType = Agent; HTTP 200.

Диаграмма

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

ТипДетали
Интеграционные событияCompanyInvitationChangedKafkaEvent (Accepted); AgentMembershipChangedKafkaEvent (Active); UserUpsertedKafkaEvent; UserAccessChangedKafkaEvent
Внешние вызовыnone — СМС руководителю «приглашение принято» шлёт notifications по событию
Проекции / поискpermission-кэш агента прогреется на следующей проверке; ростер агентства обновляется в crm по membership-событию
Уведомленияруководителю (принято) через notifications
Окно eventual-consistency

Собственная транзакция принявшего фиксируется атомарно, поэтому пользователь сразу видит себя агентом (UserType = Agent, доступен агентский функционал), а его новое назначение роли agent@Organization действует со следующей проверки прав (кэш perm:{userId} прогревается при первом check-permission). Наблюдатели-реплики отстают на секунды: руководитель видит нового агента в ростере агентства (crm) и в статистике после доставки AgentMembershipChangedKafkaEvent; rewards/news подхватывают членство по тому же событию. До доставки событий у зависимых сервисов действует прежняя картина (агент ещё «не в агентстве»), что не мешает самому агенту работать.

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400Битый InvitationIdСписок ошибокno retry
IDENTITY_INVITATION_NOT_FOUND404Приглашение не существует«Приглашение не найдено»no retry
IDENTITY_INVITATION_PHONE_MISMATCH403Телефон актора ≠ телефон приглашения (INV-CI2)«Приглашение адресовано другому номеру»no retry
IDENTITY_INVITATION_INVALID_STATUS409Не Pending (INV-CI1)«Приглашение уже разрешено»no retry
IDENTITY_INVITATION_EXPIRED409Истёк ExpiresAt (INV-CI4)«Срок приглашения истёк»no retry
IDENTITY_AGENT_ALREADY_IN_AGENCY409У актора уже есть активное членство (INV-AM1)«Сначала выйдите из текущего агентства»no retry
IDENTITY_COMPANY_SUSPENDED409Компания не Active«Компания недоступна»no retry
CONCURRENCY_CONFLICT409Гонка accept/revoke/leave (оптимистическая блокировка)«Данные изменены, повторите»retry после перечитки

Acceptance Criteria

  • Given Pending-приглашение на телефон актора, актор без активного членства, компания Active, When актор вызывает accept, Then приглашение → Accepted (AcceptedByUserId = актор), создаётся активное членство, назначается роль agent@Organization, UserTypeAgent, и уходят события 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, Then IDENTITY_INVITATION_EXPIRED (409); Given приглашение уже Accepted/Declined/Revoked, When accept, Then IDENTITY_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.

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

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

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