UC-IDN-009. Приглашение агента по телефону и его принятие
Назначение
Добор агента «со стороны» (ADR-0061): руководитель агентства приглашает по номеру телефона — создаётся
CompanyInvitation (Pending); приглашённый (инвестор или агент другой компании) сам подтверждает
вступление, после чего открывается активное AgentMembership, выдаётся роль agent в scope Organization,
а инвестор повышается до UserType = Agent. В отличие от UC-IDN-006 (invite сразу создаёт Invited-учётку
для внутренних сотрудников) здесь адресат подтверждает вступление сам. Связан с контекстом identity.
| Поле | Значение |
|---|
| ID | UC-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 |
| Триггерящий элемент | ЛК компании → «Команда» → «Пригласить агента по телефону» |
Основной Поток
- Руководитель вводит телефон агента и (опц.) целевую роль (по умолчанию системная
agent).
- Система принимает запрос приглашения (контракт —
invite-agent-by-phone.md).
- Проверить доступ и вход: у актора есть
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 }.
notifications по событию pin.identity.company-invitation-changed.v1 шлёт адресату СМС «Вас
пригласили в <Название>» со ссылкой (identity сам СМС не шлёт — границы).
- Адресат открывает ссылку, входит по своему номеру (UC-IDN-001, если ещё не аутентифицирован) и видит
приглашение (список приглашений на его
PhoneNumber в статусе Pending).
- Адресат нажимает «Принять» →
POST /api/v1/me/agent-invitations/{invitationId}/accept
(accept-invitation.md).
- Проверить: телефон адресата совпал с приглашением (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.
- Руководитель видит агента в ростере (
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. Адресат отклоняет приглашение
- Шаг 6 → «Отклонить» (decline-invitation.md):
Pending → Declined, событие company-invitation-changed (Declined). Членство/роли не создаются. Поток завершается.
A2. Руководитель отзывает приглашение
- До решения адресата — операция отзыва (revoke-invitation.md):
Pending → Revoked (INV-CI5).
A3. Срок приглашения истёк
- Job
expire-invitations при ExpiresAt <= now переводит Pending → Expired; accept/decline
возвращают IDENTITY_INVITATION_EXPIRED (409). Требуется новое приглашение.
A4. Адресат уже состоит в другом агентстве
- Шаг 7: INV-AM1 →
IDENTITY_AGENT_ALREADY_IN_AGENCY (409). Адресат сначала выходит из текущего
(UC-IDN-010), затем повторяет accept.
A5. Телефон принимающего не совпадает с приглашением
- Шаг 7: INV-CI2 →
IDENTITY_INVITATION_PHONE_MISMATCH (403) — приглашение адресовано другому номеру.
A6. Несколько приглашений на один номер и гонка accept
- Разные компании могут одновременно держать по одному
Pending на один номер (INV-CI3 уникален в пределах
(компания, телефон, роль), не глобально). Адресат видит список всех своих Pending.
- После accept одного из них INV-AM1 (одно активное членство) блокирует accept остальных
IDENTITY_AGENT_ALREADY_IN_AGENCY; прочие приглашения остаются Pending до decline/revoke/expire.
- Гонка «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 FORBIDDEN | 403 | Нет identity.users.manage (invite) | Скрыть кнопку приглашения |
IDENTITY_INVITATION_DUPLICATE | 409 | Уже есть Pending (INV-CI3) | «Приглашение уже отправлено» |
IDENTITY_INVITATION_PHONE_MISMATCH | 403 | Телефон адресата не совпал (INV-CI2, A5) | «Приглашение для другого номера» |
IDENTITY_INVITATION_EXPIRED | 409 | Истёк срок (A3) | «Срок приглашения истёк» |
IDENTITY_AGENT_ALREADY_IN_AGENCY | 409 | Есть активное членство (INV-AM1, A4) | «Сначала выйдите из текущего агентства» |
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|
CompanyInvitationCreatedEvent | Создание Pending | in-proc |
CompanyInvitationResolvedEvent | Accept/Decline/Revoke/Expire | in-proc |
AgentJoinedAgencyEvent | Открытие Active membership | in-proc |
RoleAssignmentGrantedEvent | Выдача роли agent в Organization | in-proc аудит |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|
CompanyInvitationChangedKafkaEvent.v1 | pin.identity.company-invitation-changed.v1 | Pending / Accepted / Declined / Revoked / Expired | notifications, crm, rewards |
AgentMembershipChangedKafkaEvent.v1 | pin.identity.agent-membership-changed.v1 | Accept → Active | crm, rewards, notifications |
UserUpsertedKafkaEvent.v1 | pin.identity.users.v1 | Investor → Agent | crm, profile, messenger, notifications |
UserAccessChangedKafkaEvent.v1 | pin.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 |
| AC3 | Accept создаёт membership + роль + повышает тип в одном согласованном изменении | сбой между шагами → откат |
| AC4 | Accept требует совпадения телефона (INV-CI2) | негативный тест чужим номером |
| AC5 | Accept при активном членстве → 409 (INV-AM1, A4) | тест агента другого агентства |
| AC6 | Истёкшее приглашение не принимается (A3) | тест по ExpiresAt |
| AC7 | Decline/Revoke не создают членство/роль (A1/A2) | тесты терминальных статусов |
| AC8 | Два Pending от разных компаний на один номер; accept одного → второй accept 409 (INV-AM1, A6) | конкурентный тест |
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные Документы