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 area | company-user |
| Feature group | agent-onboarding |
| Статус | draft |
| Документ | docs/06-services/identity/api/company-user/invite-agent-by-phone.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (руководитель/менеджер агентства) |
| Auth policy / Permissions | identity.users.manage (дефолт: agency-owner) |
| Scope (RBAC) | организация: Active RoleAssignment со scope Organization(CompanyId) |
| Record-level | не применяется |
| Tenant isolation | TenantId = CompanyId из claims: приглашение создаётся в своей компании |
| Audit | create: InvitedByUserId = CreatedByUserId = actor, событие CompanyInvitationChangedKafkaEvent |
| MFA/approval | не требуется |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/v1/companies/my/agent-invitations |
| Success status | 201 |
| Idempotency header | Idempotency-Key (Guid) — повтор с тем же ключом возвращает первый результат |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
ActorUserId | identity claims (sub) | Guid | Да | Required | — |
CompanyId | identity claims (tenant_id) | Guid | Да | Required | Tenant isolation |
IdempotencyKey | header Idempotency-Key | Guid | Да | Required | — |
PhoneNumber | body | string | Да | E.164 после нормализации, <= 32 | PII — в логах маскируется |
TargetRoleId | body | Guid? | Нет (default — системная роль agent) | Active; системная agent или внутренняя роль этой компании (INV-CI7) | — |
ExpiresInDays | body | int? | Нет (default 14) | 1..30 | — |
Модель ответа InviteAgentByPhoneCommandResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
InvitationId | Guid | Да | CompanyInvitationEntity.Id | — | Id созданного приглашения. |
Status | string (enum CompanyInvitationStatus) | Да | CompanyInvitationEntity.Status | — | Всегда Pending. |
ExpiresAt | DateTimeOffset | Да | 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; учётка адресата не
создаётся и не изменяется — вступление подтверждает сам адресат позже. Отметка идемпотентности сохранена.
Постусловия при отказе. Приглашение не создано, событие не публикуется.
Алгоритм
- Контекст и доступ. Операцию выполняет руководитель/менеджер агентства; tenant — его компания
(
CompanyId); требуется правоidentity.users.manageв scopeOrganization(CompanyId). Повтор с тем жеIdempotency-Keyвозвращает сохранённый результат. - Проверка входа и предусловий.
- Синтаксис: телефон нормализуется в 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.
- Синтаксис: телефон нормализуется в E.164,
- Что читаем. Компанию актора и целевую роль (для проверок выше). Наличие учётки по телефону не проверяется — адресат может ещё не быть пользователем и подтвердит вступление позже (INV-CI2 на accept).
- Бизнес-правила и маппинг. Создаётся приглашение
CompanyInvitationEntity: компания — из claims, телефон в E.164, целевая роль, автор приглашения, срокnow + ExpiresInDays,Status = Pending,Version = 1. Поднимается доменный фактCompanyInvitationCreatedEvent. - Что меняется. Появляется запись приглашения.
- Какие факты/события фиксируются.
CompanyInvitationChangedKafkaEvent(топикpin.identity.company-invitation-changed.v1, ключInvitationId) — snapshotStatus = Pending. - Согласованность. Приглашение, отметка идемпотентности и событие фиксируются в одном согласованном
изменении. Гонка одновременного приглашения того же телефона/роли разрешается уникальным индексом
ux_company_invitations_active→IDENTITY_INVITATION_DUPLICATE. - Результат. Идентификатор приглашения, статус
Pending, срок жизни (201).
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | CompanyInvitationChangedKafkaEvent → pin.identity.company-invitation-changed.v1 (Status = Pending) |
| Уведомления | СМС адресату «Вас пригласили в <Название>» — через контекст notifications (event-driven; identity сам СМС не шлёт) |
Интеграционная последовательность и согласованность
Всё асинхронно после фиксации Pending-приглашения:
- Публикуется
CompanyInvitationChangedKafkaEvent(Status = Pending) → топикpin.identity.company-invitation-changed.v1, ключInvitationId(порядок переходов одного приглашения сохраняется партиционированием поId). - Контекст
notificationsпотребляет событие и отправляет адресату СМС «Вас пригласили в <Название>» со ссылкой на подтверждение. identity сам СМС не шлёт (нет прямого вызова провайдера). - Адресат позже вызывает accept-invitation.md (проверка совпадения
телефона — INV-CI2) — тогда в одном согласованном изменении открывается/переоткрывается
AgentMembershipи создаётсяRoleAssignment(рольagent), что порождает ужеUserAccessChangedKafkaEvent; либо decline-invitation.md.
Приглашение отображается у приглашающей стороны как Pending; у адресата — до доставки СМС ничего, после —
экран подтверждения. Пока приглашение Pending, роль/членство не выданы. Если адресат не отреагирует до
ExpiresAt, background-job переведёт приглашение в Expired (вне этой операции).
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
ValidationError | 400 | Формат телефона / диапазон срока | Список ошибок | no retry |
IDENTITY_INVITATION_DUPLICATE | 409 | Уже есть Pending на (компания, телефон, роль) (INV-CI3) | «Приглашение уже отправлено» | no retry |
IDENTITY_ROLE_NOT_FOUND | 400 | TargetRoleId не существует | «Роль не найдена» | no retry |
IDENTITY_ASSIGNMENT_COMPANY_MISMATCH | 409 | Роль другой компании (INV-CI7/R2) | «Роль принадлежит другой компании» | no retry |
IDENTITY_ASSIGNMENT_PLATFORM_SCOPE_FORBIDDEN | 403 | Платформенная роль ПИН (INV-R3) | «Роль недоступна для назначения» | no retry |
IDENTITY_USER_TYPE_COMPANY_MISMATCH | 400 | Компания не Agency/Broker/Partner (INV-CI6) | «Приглашение агентов недоступно» | no retry |
IDENTITY_COMPANY_SUSPENDED | 409 | Компания не Active | «Компания заблокирована» | no retry |
FORBIDDEN | 403 | Нет identity.users.manage | «Недостаточно прав» | no retry |
Совместимость и наблюдаемость
- Новый эндпоинт, additive. Контракт события — company-invitation-changed.md, версия контракта — 1.
- Logs:
CompanyId,ActorUserId,InvitationId, маскированный телефон; metrics:identity_agent_invite_total{result}; traces: OTel; dashboardidentity-overview.
Acceptance Criteria
- Given агентство
Activeи актор сidentity.users.manage, WhenPOSTс телефоном безTargetRoleId, Then201; приглашениеPendingна системную рольagent, срокnow + 14 дней; опубликованCompanyInvitationChangedKafkaEvent (Status = Pending). - Given
TargetRoleId— Active внутренняя роль этой компании, WhenPOST, Then приглашение создаётся на эту роль (INV-CI7). - Given телефон в местном формате, When
POST, Then он приводится к E.164; в логах маскируется (+7***1234). - Given уже есть
Pending-приглашение на (компания, телефон, роль), When повторныйPOST, ThenIDENTITY_INVITATION_DUPLICATE(409). - Given компания актора
Developer/Pin, WhenPOST, ThenIDENTITY_USER_TYPE_COMPANY_MISMATCH(400) — приглашение агентов недоступно (INV-CI6). - Given
TargetRoleId— платформенная роль ПИН, WhenPOST, ThenIDENTITY_ASSIGNMENT_PLATFORM_SCOPE_FORBIDDEN(403) — INV-R3. - Given
TargetRoleId— внутренняя роль чужой компании, WhenPOST, ThenIDENTITY_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) на уникальном индексе.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: CompanyInvitationEntity
- API (3): Identity Company-User API — реестр методов и общие правила области, POST /api/v1/companies/my/agent-invitations/{invitationId}/revoke — Отзыв приглашения агента, POST /api/v1/companies/my/users/invitations — Приглашение пользователя в компанию
- Use Cases (2): UC-IDN-009. Приглашение агента по телефону и его принятие, US-реестр «Участники и владение процессами» (US-CRM-PART / US-AG-ADMIN / US-AGT-MULTI / US-INV-CO)
- События (1): Event Contract: pin.identity.company-invitation-changed (CompanyInvitationChangedKafkaEvent)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность: company-invitation.md (INV-CI1..CI7), role.md (INV-R2/R3).
- Отзыв: revoke-invitation.md; подтверждение/отклонение адресатом: accept-invitation.md, decline-invitation.md.
- Событие: company-invitation-changed.md; отличие от легаси: invite-company-user.md.
- Use case: UC-IDN-009-invite-agent-by-phone-and-accept.md.