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

POST /api/v1/companies/my/agent-invitations — Приглашение агента по номеру телефона

Руководитель агентства/брокера приглашает агента со стороны по номеру телефона — создаётся CompanyInvitationEntity в статусе Pending (ADR-0061). В отличие от invite-company-user.md (invite = сразу Invited-учётка для внутренних сотрудников), здесь учётка не создаётся: приглашённый (возможно уже инвестор или агент другой компании) должен сам подтвердить вступление (accept-invitation.md).

Назначение

Добор агента в команду: указывается телефон и целевая роль (по умолчанию системная agent). Owning-контекст — identity.

Metadata

ПолеЗначение
Сервис/контекстidentity
API areacompany-user
Feature groupagent-onboarding
Статусdraft
Документdocs/06-services/identity/api/company-user/invite-agent-by-phone.md

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

ПроверкаЗначение
Actor typeUser (руководитель/менеджер агентства)
Auth policy / Permissionsidentity.users.manage (дефолт: agency-owner)
Scope (RBAC)организация: Active RoleAssignment со scope Organization(CompanyId)
Record-levelне применяется
Tenant isolationTenantId = CompanyId из claims: приглашение создаётся в своей компании
Auditcreate: InvitedByUserId = CreatedByUserId = actor, событие CompanyInvitationChangedKafkaEvent
MFA/approvalне требуется

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/companies/my/agent-invitations
Success status201
Idempotency headerIdempotency-Key (Guid) — повтор с тем же ключом возвращает первый результат
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorUserIdidentity claims (sub)GuidДаRequired
CompanyIdidentity claims (tenant_id)GuidДаRequiredTenant isolation
IdempotencyKeyheader Idempotency-KeyGuidДаRequired
PhoneNumberbodystringДаE.164 после нормализации, <= 32PII — в логах маскируется
TargetRoleIdbodyGuid?Нет (default — системная роль agent)Active; системная agent или внутренняя роль этой компании (INV-CI7)
ExpiresInDaysbodyint?Нет (default 14)1..30

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

ПолеТипОбязательностьИсточникMaskingОписание
InvitationIdGuidДаCompanyInvitationEntity.IdId созданного приглашения.
Statusstring (enum CompanyInvitationStatus)ДаCompanyInvitationEntity.StatusВсегда Pending.
ExpiresAtDateTimeOffsetДаCompanyInvitationEntity.ExpiresAtСрок жизни приглашения.

Предусловия и постусловия

Предусловия. Актор аутентифицирован и имеет identity.users.manage в scope Organization(CompanyId); компания актора Active и её тип ∈ {Agency, Broker, Partner} (INV-CI6); целевая роль — Active, системная agent или внутренняя роль этой компании (INV-CI7).

Постусловия при успехе. Создано приглашение CompanyInvitationEntity Status = Pending, Version = 1, со сроком now + ExpiresInDays; опубликовано CompanyInvitationChangedKafkaEvent; учётка адресата не создаётся и не изменяется — вступление подтверждает сам адресат позже. Отметка идемпотентности сохранена.

Постусловия при отказе. Приглашение не создано, событие не публикуется.

Алгоритм

  1. Контекст и доступ. Операцию выполняет руководитель/менеджер агентства; tenant — его компания (CompanyId); требуется право identity.users.manage в scope Organization(CompanyId). Повтор с тем же Idempotency-Key возвращает сохранённый результат.
  2. Проверка входа и предусловий.
    • Синтаксис: телефон нормализуется в E.164, ExpiresInDays в диапазоне 1..30; иначе — ValidationError.
    • Компания актора активна и её тип ∈ {Agency, Broker, Partner} (INV-CI6); иначе — IDENTITY_COMPANY_SUSPENDED / IDENTITY_USER_TYPE_COMPANY_MISMATCH.
    • Целевая роль: при отсутствии TargetRoleId — системная agent; иначе роль должна быть Active и либо системной agent, либо внутренней ролью этой компании (INV-CI7 / INV-R2); платформенные роли ПИН запрещены (INV-R3). Ошибки — IDENTITY_ROLE_NOT_FOUND / IDENTITY_ASSIGNMENT_COMPANY_MISMATCH / IDENTITY_ASSIGNMENT_PLATFORM_SCOPE_FORBIDDEN.
    • Дубль (INV-CI3): уже есть Pending-приглашение на тройку (компания, телефон, роль) → IDENTITY_INVITATION_DUPLICATE.
  3. Что читаем. Компанию актора и целевую роль (для проверок выше). Наличие учётки по телефону не проверяется — адресат может ещё не быть пользователем и подтвердит вступление позже (INV-CI2 на accept).
  4. Бизнес-правила и маппинг. Создаётся приглашение CompanyInvitationEntity: компания — из claims, телефон в E.164, целевая роль, автор приглашения, срок now + ExpiresInDays, Status = Pending, Version = 1. Поднимается доменный факт CompanyInvitationCreatedEvent.
  5. Что меняется. Появляется запись приглашения.
  6. Какие факты/события фиксируются. CompanyInvitationChangedKafkaEvent (топик pin.identity.company-invitation-changed.v1, ключ InvitationId) — snapshot Status = Pending.
  7. Согласованность. Приглашение, отметка идемпотентности и событие фиксируются в одном согласованном изменении. Гонка одновременного приглашения того же телефона/роли разрешается уникальным индексом ux_company_invitations_activeIDENTITY_INVITATION_DUPLICATE.
  8. Результат. Идентификатор приглашения, статус Pending, срок жизни (201).

Диаграмма

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

ТипДетали
Интеграционные событияCompanyInvitationChangedKafkaEventpin.identity.company-invitation-changed.v1 (Status = Pending)
УведомленияСМС адресату «Вас пригласили в <Название>» — через контекст notifications (event-driven; identity сам СМС не шлёт)

Интеграционная последовательность и согласованность

Всё асинхронно после фиксации Pending-приглашения:

  1. Публикуется CompanyInvitationChangedKafkaEvent (Status = Pending) → топик pin.identity.company-invitation-changed.v1, ключ InvitationId (порядок переходов одного приглашения сохраняется партиционированием по Id).
  2. Контекст notifications потребляет событие и отправляет адресату СМС «Вас пригласили в <Название>» со ссылкой на подтверждение. identity сам СМС не шлёт (нет прямого вызова провайдера).
  3. Адресат позже вызывает accept-invitation.md (проверка совпадения телефона — INV-CI2) — тогда в одном согласованном изменении открывается/переоткрывается AgentMembership и создаётся RoleAssignment (роль agent), что порождает уже UserAccessChangedKafkaEvent; либо decline-invitation.md.
Что видит пользователь в промежутке

Приглашение отображается у приглашающей стороны как Pending; у адресата — до доставки СМС ничего, после — экран подтверждения. Пока приглашение Pending, роль/членство не выданы. Если адресат не отреагирует до ExpiresAt, background-job переведёт приглашение в Expired (вне этой операции).

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400Формат телефона / диапазон срокаСписок ошибокno retry
IDENTITY_INVITATION_DUPLICATE409Уже есть Pending на (компания, телефон, роль) (INV-CI3)«Приглашение уже отправлено»no retry
IDENTITY_ROLE_NOT_FOUND400TargetRoleId не существует«Роль не найдена»no retry
IDENTITY_ASSIGNMENT_COMPANY_MISMATCH409Роль другой компании (INV-CI7/R2)«Роль принадлежит другой компании»no retry
IDENTITY_ASSIGNMENT_PLATFORM_SCOPE_FORBIDDEN403Платформенная роль ПИН (INV-R3)«Роль недоступна для назначения»no retry
IDENTITY_USER_TYPE_COMPANY_MISMATCH400Компания не Agency/Broker/Partner (INV-CI6)«Приглашение агентов недоступно»no retry
IDENTITY_COMPANY_SUSPENDED409Компания не Active«Компания заблокирована»no retry
FORBIDDEN403Нет identity.users.manage«Недостаточно прав»no retry

Совместимость и наблюдаемость

  • Новый эндпоинт, additive. Контракт события — company-invitation-changed.md, версия контракта — 1.
  • Logs: CompanyId, ActorUserId, InvitationId, маскированный телефон; metrics: identity_agent_invite_total{result}; traces: OTel; dashboard identity-overview.

Acceptance Criteria

  • Given агентство Active и актор с identity.users.manage, When POST с телефоном без TargetRoleId, Then 201; приглашение Pending на системную роль agent, срок now + 14 дней; опубликован CompanyInvitationChangedKafkaEvent (Status = Pending).
  • Given TargetRoleId — Active внутренняя роль этой компании, When POST, Then приглашение создаётся на эту роль (INV-CI7).
  • Given телефон в местном формате, When POST, Then он приводится к E.164; в логах маскируется (+7***1234).
  • Given уже есть Pending-приглашение на (компания, телефон, роль), When повторный POST, Then IDENTITY_INVITATION_DUPLICATE (409).
  • Given компания актора Developer/Pin, When POST, Then IDENTITY_USER_TYPE_COMPANY_MISMATCH (400) — приглашение агентов недоступно (INV-CI6).
  • Given TargetRoleId — платформенная роль ПИН, When POST, Then IDENTITY_ASSIGNMENT_PLATFORM_SCOPE_FORBIDDEN (403) — INV-R3.
  • Given TargetRoleId — внутренняя роль чужой компании, When POST, Then IDENTITY_ASSIGNMENT_COMPANY_MISMATCH (409).
  • Given первый вызов создал приглашение по ключу K, When повтор с тем же Idempotency-Key = K, Then возвращается тот же InvitationId, второе приглашение не создаётся.
  • Given телефон не соответствует ни одной учётке, When POST, Then приглашение создаётся успешно (наличие учётки не проверяется — сверка телефона будет на accept, INV-CI2).
  • Given два параллельных приглашения на тот же телефон/роль, When оба прошли предпроверку, Then ровно одно создаётся, второе → IDENTITY_INVITATION_DUPLICATE (409) на уникальном индексе.

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

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

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