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

POST /api/v1/companies/my/users/invitations — Приглашение пользователя в компанию

Создание учётки сотрудника своей компании в статусе Invited с опциональным немедленным назначением ролей. Агенты добавляются только приглашением, если AllowAgentSelfRegistration = false (workspace-настройка, company.md).

Назначение

Руководитель агентства/менеджер застройщика добавляет сотрудника: указывает телефон, ФИО, роли. Учётка создаётся сразу (без отдельной pending-invitation-сущности — приглашение = UserEntity в статусе Invited, stateDiagram user.md); активация — первым успешным входом по СМС. Owning-контекст — identity. Feature group — companies.

Metadata

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

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

ПроверкаЗначение
Actor typeUser (пользователь компании)
Auth policy / Permissionsidentity.users.manage (дефолт: agency-owner, developer-manager — только своя компания)
Scope (RBAC)организация: Active RoleAssignment со scope Platform или Organization(CompanyId); роли назначаются дополнительно по INV-R8 (см. шаг 2)
Record-levelне применяется
Tenant isolationTenantId = CompanyId из claims: пригласить можно только в свою компанию
Auditcreate: CreatedByUserId = actor, событие UserUpsertedKafkaEvent; назначения ролей — GrantedByUserId
MFA/approvalне требуется (операция в границах организации; платформенные роли этим методом не выдаются — INV-R3)

HTTP-контракт

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

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorUserIdidentity claimsGuidДаRequired
CompanyIdidentity claims (tenant_id)GuidДаRequiredTenant isolation
IdempotencyKeyheader Idempotency-KeyGuidДаRequired
PhoneNumberbodystringДаE.164 после нормализации, <= 32; уникальность INV-1 user.mdPII — в логах маскируется
Emailbodystring?Нетформат email, <= 256; уникальность INV-3PII — маскируется
FirstNamebodystringДа1..128PII
LastNamebodystringДа1..128PII
MiddleNamebodystring?Нет<= 128PII
RoleIdsbodyIReadOnlyList<Guid>Нет (default — WorkspaceSettings.DefaultRoleId, если задан)<= 10 элементов; каждая роль — Active, этой компании или системная назначимая в Organization

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

ПолеТипОбязательностьИсточникMaskingОписание
UserIdGuidДасозданный UserEntity.IdId учётки.
Statusstring (enum UserStatus)ДаUserEntity.StatusВсегда Invited.
AssignedRoleIdsIReadOnlyList<Guid>Дасозданные RoleAssignment.RoleIdФактически назначенные роли.
CreatedAtDateTimeOffsetДаUserEntity.CreatedAtМомент создания.

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

Предусловия. Актор аутентифицирован и имеет identity.users.manage в scope Organization(CompanyId); для назначения ролей — дополнительно identity.role-assignments.manage (INV-R8); компания актора Active; телефон не занят активной учёткой (INV-1), email (если задан) свободен (INV-3).

Постусловия при успехе. Создана учётка сотрудника Status = Invited, Version = 1, с типом по типу компании (INV-2); для каждой роли — активное назначение в scope Organization(CompanyId); опубликованы UserUpsertedKafkaEvent и (при непустых назначениях) UserAccessChangedKafkaEvent. Учётка активируется первым успешным входом по СМС (вне этой операции). Отметка идемпотентности сохранена.

Постусловия при отказе. Учётка и назначения не созданы, события не публикуются.

Алгоритм

  1. Контекст и доступ. Операцию выполняет пользователь компании; tenant — его компания (CompanyId); требуется право identity.users.manage в scope Organization(CompanyId); для назначения ролей — также identity.role-assignments.manage в этом scope (INV-R8), иначе — FORBIDDEN. Повтор с тем же Idempotency-Key возвращает сохранённый результат.
  2. Проверка входа и предусловий.
    • Синтаксис по таблице входных данных (телефон E.164, email, ФИО, лимит ролей); иначе — ValidationError.
    • Компания актора существует и активна; Status != ActiveIDENTITY_COMPANY_SUSPENDED (INV-9 user.md).
    • Телефон свободен (INV-1 user.md): активная учётка с таким телефоном уже есть → IDENTITY_USER_PHONE_TAKEN (в отличие от входа по СМС, здесь дубль — ошибка).
    • Email, если задан, свободен (INV-3) → иначе IDENTITY_USER_EMAIL_TAKEN.
    • Роли: каждая — Active (IDENTITY_ROLE_ARCHIVED); внутренняя роль — только этой компании (INV-R2, IDENTITY_ASSIGNMENT_COMPANY_MISMATCH); системная — только организационная (Code ∈ {agent, agency-owner, developer-manager}), совместимая с company.Type; платформенные роли (pin-manager/care-operator/investor) запрещены (INV-R3 → IDENTITY_ASSIGNMENT_PLATFORM_SCOPE_FORBIDDEN).
  3. Что читаем. Компанию актора и запрошенные роли. При пустом списке ролей — роль по умолчанию WorkspaceSettings.DefaultRoleId компании (если задана).
  4. Бизнес-правила и маппинг. Создаётся учётка (INV-2 user.md — согласованность UserTypeCompany.Type): Type — по типу компании (Agency|Broker → Agent, Developer → DeveloperManager, Partner → PartnerManager, Pin → PinEmployee), CompanyId — из claims, Status = Invited, ФИО, телефон в E.164, email в нижнем регистре (не подтверждён), контакты управляются самостоятельно, регистрация через admin API, источник Pin, Version = 1. Поднимается доменный факт UserRegisteredEvent. Для каждой роли создаётся назначение RoleAssignment в scope Organization(CompanyId) (инварианты INV-R2/R3/R4/R6; дубликаты в списке схлопываются) с фактом RoleAssignmentGrantedEvent.
  5. Что меняется. Появляются учётка и её назначения ролей.
  6. Какие факты/события фиксируются.
    • UserUpsertedKafkaEvent (топик pin.identity.users.v1, ключ UserId) — snapshot Id/Type/CompanyId/Status/Name/PhoneNumber/RegisteredVia/Version;
    • UserAccessChangedKafkaEvent (топик pin.identity.access.v1, ключ UserId) — список активных назначений {RoleCode, ScopeType, ScopeIds} (при непустых назначениях).
  7. Согласованность. Учётка, назначения, отметка идемпотентности и события фиксируются в одном согласованном изменении. Гонка одновременного приглашения того же телефона разрешается уникальным индексом ux_users_phone_numberIDENTITY_USER_PHONE_TAKEN.
  8. Результат. Идентификатор учётки, статус Invited, фактически назначенные роли, момент создания (201). Телефон в ответ не возвращается (клиент его знает), в логах — маска.

Диаграмма

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

ТипДетали
Интеграционные событияUserUpsertedKafkaEventpin.identity.users.v1; UserAccessChangedKafkaEventpin.identity.access.v1
Уведомленияприглашающая СМС «Вас пригласили в <Название>» — через контекст notifications (event-driven; identity сам СМС не шлёт)

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

Всё асинхронно после фиксации учётки и назначений (в одном согласованном изменении):

  1. UserUpsertedKafkaEvent → топик pin.identity.users.v1, ключ UserId — snapshot профиля (Id/Type/CompanyId/Status/Name/PhoneNumber/RegisteredVia/Version). Потребители: поисковая проекция UserSearchDocument (сотрудник появляется в поиске «Команда»), crm/прочие реплики профиля.
  2. При непустых назначениях — UserAccessChangedKafkaEvent → топик pin.identity.access.v1, ключ UserId — список активных назначений; потребители авторизации (crm, catalog, chessboard, gateway) наполняют доступ нового сотрудника.
  3. Контекст notifications по событию отправляет приглашающую СМС.
Отличие от invite-agent-by-phone

Здесь учётка создаётся сразу (Invited) без самоподтверждения адресата — путь для внутренних сотрудников (сравнить с invite-agent-by-phone.md). Окно eventual-consistency (появление в поиске/доступах у потребителей) — секунды; до входа по СМС учётка остаётся Invited.

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400Формат телефона/email/ФИО/лимит ролейСписок ошибокno retry
IDENTITY_USER_PHONE_TAKEN409Телефон занят активной учёткой (INV-1)«Пользователь с таким телефоном уже существует»no retry
IDENTITY_USER_EMAIL_TAKEN409Email занят (INV-3)«Email уже используется»no retry
IDENTITY_ROLE_NOT_FOUND400Роль из RoleIds не существует«Роль не найдена»no retry
IDENTITY_ROLE_ARCHIVED409Роль в статусе Archived«Роль архивирована»no retry
IDENTITY_ASSIGNMENT_COMPANY_MISMATCH409Роль другой компании (INV-R2)«Роль принадлежит другой компании»no retry
IDENTITY_ASSIGNMENT_PLATFORM_SCOPE_FORBIDDEN403Попытка выдать платформенную роль (INV-R3)«Роль недоступна для назначения»no retry
IDENTITY_COMPANY_SUSPENDED409Компания не Active«Компания заблокирована»no retry
FORBIDDEN403Нет identity.users.manage / identity.role-assignments.manage«Недостаточно прав»no retry

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

  • Новые optional поля формы — additive. Контракт событий — по domain/user.md и domain/role.md, версия контракта событий — 1.
  • Logs: CompanyId, ActorUserId, UserId результата, маскированный телефон; metrics: identity_user_invite_total{result}; traces: OTel; dashboard identity-overview; runbook: merge учёток при конфликте телефона — README identity, R6.

Acceptance Criteria

  • Given компания Active, актор с identity.users.manage и identity.role-assignments.manage, When POST с телефоном/ФИО и RoleIds, Then 201; учётка Invited типа по типу компании, назначения созданы; опубликованы UserUpsertedKafkaEvent и UserAccessChangedKafkaEvent.
  • Given RoleIds не задан и в WorkspaceSettings.DefaultRoleId есть роль, When POST, Then учётка получает роль по умолчанию; AssignedRoleIds содержит её.
  • Given актор имеет identity.users.manage, но не identity.role-assignments.manage, а RoleIds непуст, When POST, Then FORBIDDEN (403) — INV-R8.
  • Given активная учётка с таким телефоном уже есть, When POST, Then IDENTITY_USER_PHONE_TAKEN (409) — INV-1.
  • Given email задан и уже используется, When POST, Then IDENTITY_USER_EMAIL_TAKEN (409) — INV-3.
  • Given в RoleIds — платформенная роль (pin-manager/care-operator/investor), When POST, Then IDENTITY_ASSIGNMENT_PLATFORM_SCOPE_FORBIDDEN (403) — INV-R3.
  • Given в RoleIds — внутренняя роль чужой компании, When POST, Then IDENTITY_ASSIGNMENT_COMPANY_MISMATCH (409) — INV-R2.
  • Given компания актора Suspended, When POST, Then IDENTITY_COMPANY_SUSPENDED (409) — INV-9.
  • Given первый вызов создал учётку по ключу K, When повтор с тем же Idempotency-Key = K, Then возвращается тот же UserId, вторая учётка не создаётся.
  • Given два параллельных приглашения того же телефона, When оба прошли предпроверку, Then ровно одно создаёт учётку, второе → IDENTITY_USER_PHONE_TAKEN (409) на индексе.
  • Given RoleIds = [] и DefaultRoleId не задан, When POST, Then учётка создаётся Invited без назначений; UserAccessChangedKafkaEvent не публикуется (эффективные права пусты).

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

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

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