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

UC-IDN-009. Приглашение агента по телефону и его принятие

Назначение

Добор агента «со стороны» (ADR-0061): руководитель агентства приглашает по номеру телефона — создаётся CompanyInvitation (Pending); приглашённый (инвестор или агент другой компании) сам подтверждает вступление, после чего открывается активное AgentMembership, выдаётся роль agent в scope Organization, а инвестор повышается до UserType = Agent. В отличие от UC-IDN-006 (invite сразу создаёт Invited-учётку для внутренних сотрудников) здесь адресат подтверждает вступление сам. Связан с контекстом identity.

Metadata

ПолеЗначение
IDUC-IDN-009
Контекстidentity (см. docs/06-services/identity/)
Типосновной
Статусdraft

Актор

ПолеЗначение
РольАгентство (руководитель, agency-owner) — приглашает; Инвестор/Агент — принимает
Permissions / scopeприглашение — identity.users.manage в scope Organization(CompanyId); принятие — self-действие адресата (совпадение телефона, INV-CI2)
Tenantприглашение — TenantId = CompanyId приглашающего; принимающий действует вне tenant (ADR-0052)
Record-levelприглашение видит адресат по совпадению PhoneNumber

Предусловие

  • Компания приглашающего Status = Active, Type ∈ {Agency, Broker, Partner}.
  • У адресата нет активного AgentMembership (иначе сначала выход — UC-IDN-010, A3).
  • Нет активного Pending на (компания, телефон, роль) — INV-CI3.

Триггер

ПолеЗначение
ИсточникUI button
Триггерящий элементЛК компании → «Команда» → «Пригласить агента по телефону»

Основной Поток

  1. Руководитель вводит телефон агента и (опц.) целевую роль (по умолчанию системная agent).
  2. Система принимает запрос приглашения (контракт — invite-agent-by-phone.md).
  3. Проверить доступ и вход: у актора есть identity.users.manage; компания Status = Active и Type ∈ {Agency, Broker, Partner} (INV-CI6); целевая роль допустима (INV-CI7); нет активного дубля Pending (INV-CI3, иначе IDENTITY_INVITATION_DUPLICATE). Создать CompanyInvitation (Status = Pending, ExpiresAt = now + 14 дней); опубликовать CompanyInvitationChangedKafkaEvent (Pending). Приглашение и публикация — в одном согласованном изменении. Ответ { invitationId, Pending }.
  4. notifications по событию pin.identity.company-invitation-changed.v1 шлёт адресату СМС «Вас пригласили в <Название>» со ссылкой (identity сам СМС не шлёт — границы).
  5. Адресат открывает ссылку, входит по своему номеру (UC-IDN-001, если ещё не аутентифицирован) и видит приглашение (список приглашений на его PhoneNumber в статусе Pending).
  6. Адресат нажимает «Принять» → POST /api/v1/me/agent-invitations/{invitationId}/accept (accept-invitation.md).
  7. Проверить: телефон адресата совпал с приглашением (INV-CI2, иначе IDENTITY_INVITATION_PHONE_MISMATCH); приглашение Pending и не истекло (INV-CI1/CI4, иначе IDENTITY_INVITATION_EXPIRED); у адресата нет активного членства (INV-AM1, иначе IDENTITY_AGENT_ALREADY_IN_AGENCY). В одном согласованном изменении: принять приглашение (Pending → Accepted) → открыть AgentMembership (Active) → повысить пользователя (Investor → Agent, зеркало CompanyId = агентство) → создать RoleAssignment (agent, Organization(CompanyId)); опубликовать company-invitation-changed (Accepted) + agent-membership-changed (Active) + user-upserted + user-access-changed.
  8. Руководитель видит агента в ростере (Active); агент получает доступ к клиентской базе/лидам агентства в границах scope.

Переходы статуса в этом сценарии. CompanyInvitation: Pending → Accepted (адресат принимает, INV-CI1), Pending → Declined (адресат отклоняет, A1), Pending → Revoked (приглашающий отзывает, A2, INV-CI5), Pending → Expired (job по ExpiresAt, A3). Все переходы из Pending терминальны; из терминального статуса переходов нет — повторное действие возвращает текущий статус или отказ. AgentMembership: ∅ → Active при accept; дальнейшее закрытие членства — UC-IDN-010 (Active → Left). UserEntity: Investor → Agent при accept (для инвестора-адресата), тип не понижается автоматически при decline/revoke.

Альтернативные Потоки

A1. Адресат отклоняет приглашение

  1. Шаг 6 → «Отклонить» (decline-invitation.md): Pending → Declined, событие company-invitation-changed (Declined). Членство/роли не создаются. Поток завершается.

A2. Руководитель отзывает приглашение

  1. До решения адресата — операция отзыва (revoke-invitation.md): Pending → Revoked (INV-CI5).

A3. Срок приглашения истёк

  1. Job expire-invitations при ExpiresAt <= now переводит Pending → Expired; accept/decline возвращают IDENTITY_INVITATION_EXPIRED (409). Требуется новое приглашение.

A4. Адресат уже состоит в другом агентстве

  1. Шаг 7: INV-AM1 → IDENTITY_AGENT_ALREADY_IN_AGENCY (409). Адресат сначала выходит из текущего (UC-IDN-010), затем повторяет accept.

A5. Телефон принимающего не совпадает с приглашением

  1. Шаг 7: INV-CI2 → IDENTITY_INVITATION_PHONE_MISMATCH (403) — приглашение адресовано другому номеру.

A6. Несколько приглашений на один номер и гонка accept

  1. Разные компании могут одновременно держать по одному Pending на один номер (INV-CI3 уникален в пределах (компания, телефон, роль), не глобально). Адресат видит список всех своих Pending.
  2. После accept одного из них INV-AM1 (одно активное членство) блокирует accept остальных IDENTITY_AGENT_ALREADY_IN_AGENCY; прочие приглашения остаются Pending до decline/revoke/expire.
  3. Гонка «accept против revoke/expire того же приглашения» разрешается оптимистичной блокировкой: побеждает первый перевод из Pending, проигравший видит уже неактуальный статус → IDENTITY_INVITATION_EXPIRED (для истёкшего/отозванного/принятого — соответствующий отказ).

Постусловие

  • CompanyInvitation.Status = Accepted, AcceptedByUserId заполнен.
  • Создан AgentMembership (Active, JoinedAt); UserEntity.Type = Agent, зеркало CompanyId = агентство.
  • Создано RoleAssignment (agent, Organization(CompanyId), Active).
  • Опубликованы company-invitation-changed, agent-membership-changed, user-upserted, user-access-changed.

Возможные Ошибки

КодHTTPУсловиеПоведение клиента
403 FORBIDDEN403Нет identity.users.manage (invite)Скрыть кнопку приглашения
IDENTITY_INVITATION_DUPLICATE409Уже есть Pending (INV-CI3)«Приглашение уже отправлено»
IDENTITY_INVITATION_PHONE_MISMATCH403Телефон адресата не совпал (INV-CI2, A5)«Приглашение для другого номера»
IDENTITY_INVITATION_EXPIRED409Истёк срок (A3)«Срок приглашения истёк»
IDENTITY_AGENT_ALREADY_IN_AGENCY409Есть активное членство (INV-AM1, A4)«Сначала выйдите из текущего агентства»

Доменные События

СобытиеКогда поднимаетсяSubscribers
CompanyInvitationCreatedEventСоздание Pendingin-proc
CompanyInvitationResolvedEventAccept/Decline/Revoke/Expirein-proc
AgentJoinedAgencyEventОткрытие Active membershipin-proc
RoleAssignmentGrantedEventВыдача роли agent в Organizationin-proc аудит

Kafka Интеграционные События

СобытиеTopicКогда публикуетсяConsumers
CompanyInvitationChangedKafkaEvent.v1pin.identity.company-invitation-changed.v1Pending / Accepted / Declined / Revoked / Expirednotifications, crm, rewards
AgentMembershipChangedKafkaEvent.v1pin.identity.agent-membership-changed.v1Accept → Activecrm, rewards, notifications
UserUpsertedKafkaEvent.v1pin.identity.users.v1Investor → Agentcrm, profile, messenger, notifications
UserAccessChangedKafkaEvent.v1pin.identity.access.v1Новое назначение ролиcrm, catalog, chessboard, gateway

Гарантии доставки и согласованность. События user/access/membership публикуются с ключом партиционирования UserId и применяются потребителями идемпотентно; порядок изменений на пользователя сохраняется. После accept роль, членство и повышение типа существуют в identity сразу (одно согласованное изменение), но доступ к клиентской базе/лидам агентства агент получает после инвалидации кэшей прав по pin.identity.access.v1 и обновления ростера в crm по agent-membership-changed — окно eventual-consistency ≤ TTL (UC-IDN-008). До этого адресат уже Agent, но списки агентства могут подтянуться с задержкой. CompanyInvitationChangedKafkaEvent — компактифицируемый снапшот статуса приглашения: повторная доставка любого статуса безопасна.

Acceptance Criteria

#КритерийПроверка
AC1Приглашение по телефону создаёт Pending без учёткитест: учётка не появляется до accept
AC2Дубль Pending (компания, телефон, роль) → 409 (INV-CI3)двойной invite
AC3Accept создаёт membership + роль + повышает тип в одном согласованном изменениисбой между шагами → откат
AC4Accept требует совпадения телефона (INV-CI2)негативный тест чужим номером
AC5Accept при активном членстве → 409 (INV-AM1, A4)тест агента другого агентства
AC6Истёкшее приглашение не принимается (A3)тест по ExpiresAt
AC7Decline/Revoke не создают членство/роль (A1/A2)тесты терминальных статусов
AC8Два Pending от разных компаний на один номер; accept одного → второй accept 409 (INV-AM1, A6)конкурентный тест

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

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

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