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

UC-IDN-006. Приглашение менеджера (агента) в компанию

Назначение

Руководитель агентства/брокера/партнёра пополняет команду: приглашает нового сотрудника по номеру телефона — создаётся учётка Invited с дефолтной ролью, сотрудник активируется первым входом по СМС (UC-IDN-001). Менеджеры застройщиков этим потоком не приглашаются — они приходят синком Talent (UC-IDN-007). Связан с контекстом identity.

Metadata

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

Актор

ПолеЗначение
РольАгентство (руководитель, agency-owner) / уполномоченный менеджер компании; для компании ПИН — МенеджерПИН с identity.users.manage
Permissions / scopeidentity.users.manage в scope Organization(CompanyId)
TenantTenantId = CompanyId актора; приглашённый создаётся в этом же tenant (ADR-0052)
Record-levelне применяется (tenant-scope операция)

Предусловие

  • Компания актора Status = Active, Type ∈ {Agency, Broker, Partner, Pin} (для Developer — запрет, см. A4: пользователи застройщика управляются Talent).
  • Телефон приглашаемого не занят учёткой этой же или другой компании (инвестор с этим номером — конфликт, см. A2).
  • WorkspaceSettings.AllowAgentSelfRegistration = false (дефолт) — добавление только приглашением (company.md).

Триггер

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

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

  1. Руководитель нажимает «Пригласить сотрудника», вводит телефон, ФИО, выбирает внутреннюю роль (из ролей компании + системная agent) и scope (по умолчанию — Organization).
  2. Система принимает запрос приглашения (контракт — invite-company-user.md; CompanyId — из claims tenant_id, пригласить можно только в свою компанию) с телом { phoneNumber (1..32, E.164), firstName (1..128), lastName? (≤128), middleName? (≤128), roleId, scope?: ResourceScopeModel }.
  3. Проверить доступ и вход: у актора есть identity.users.manage в scope Organization(CompanyId); формат телефона (E.164) и ФИО; компания Status = Active (иначе IDENTITY_COMPANY_SUSPENDED) и Type != Developer (иначе A4, IDENTITY_USER_TYPE_COMPANY_MISMATCH); телефон не занят среди не-архивных учёток (INV-1, иначе A2, IDENTITY_USER_PHONE_TAKEN); роль существует, Status = Active и (принадлежит компании актора или системная agent, INV-R2); тип пользователя соответствует типу компании (INV-2: Agency|Broker → Agent, Partner → PartnerManager, Pin → PinEmployee).
  4. Создать учётку UserEntity.CreateCompanyUser (phoneNumber, FullName{firstName, lastName?, middleName?}, Type по таблице INV-2, CompanyId из claims, Status = Invited, ContactsManagedBy = SelfService, RegisteredVia = AdminApi, CreatedByUserId = actor) и назначение RoleAssignment (user.Id, roleId, CompanyId, Scope = scope ?? Organization(companyId), GrantedByUserId = actor) (INV-R2..R4, R6). Учётка, назначение и публикация событий — в одном согласованном изменении.
  5. Зафиксировать факты: события UserRegisteredEvent, RoleAssignmentGrantedEvent; интеграционные UserUpsertedKafkaEventpin.identity.users.v1 (snapshot: Id, Type, CompanyId, Status = Invited, Name, PhoneNumber) и UserAccessChangedKafkaEventpin.identity.access.v1.
  6. Система возвращает { userId, status = Invited, roleAssignmentId }.
  7. Приглашение отправляется через notifications (СМС со ссылкой на вход) — по событию pin.identity.users.v1 (Status = Invited); identity сам СМС-приглашение не шлёт (границы).
  8. Приглашённый переходит по ссылке, вводит свой номер → UC-IDN-001 (система находит учётку Invited, outcome = SignedIn, nextStep = RequiresSmsCode).
  9. Успешный вход переводит Invited → Active (stateDiagram user.md), заполняет PhoneNumberConfirmedAt, LastSignInAt.
  10. Список команды у руководителя показывает статус Active; consumers (crm, messenger) получили пользователя и назначение — агент может работать с лидами в границах своего scope.

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

A1. Повторное приглашение (учётка уже Invited)

  1. Шаг 3 находит учётку Invited той же компании → дубль не создаётся, повторно инициируется уведомление-приглашение (идемпотентный ответ с существующим userId). Завершается шагом 7.

A2. Телефон занят другой учёткой

  1. Номер принадлежит инвестору или сотруднику другой компании → IDENTITY_USER_PHONE_TAKEN (409).
  2. Авто-перепривязка запрещена (риск отдать чужой аккаунт); руководитель связывается с сотрудником (пусть сменит номер своей учётки — UC-IDN-004) или обращается в заботу (ручной merge — README «Известные ограничения»). Поток завершается неуспехом.

A3. Приглашённый не входит (просроченное приглашение)

  1. Учётка остаётся Invited без TTL (вход возможен всегда, пока не отозвана); руководитель может архивировать учётку (административная операция) — назначения отзываются каскадом (user.md «Связи»).

A4. Компания-застройщик пытается пригласить менеджера

  1. Company.Type = DeveloperIDENTITY_USER_TYPE_COMPANY_MISMATCH (400): менеджеры застройщика создаются только синком Talent (ContactsManagedBy = TalentSync, UC-IDN-007). UI застройщика не показывает кнопку приглашения.

A5. Приглашение с ролью в узком scope

  1. Шаг 1: руководитель выбирает scope Project(X) — валидация и наследование как в UC-IDN-005 шаг 9.

A6. Параллельные приглашения на один номер

  1. Два руководителя (или один дважды) одновременно приглашают один номер: уникальный индекс телефона пропускает ровно одну вставку Invited-учётки, параллельная попытка получает IDENTITY_USER_PHONE_TAKEN.
  2. Если это та же компания и учётка уже Invited — конфликт разрешается идемпотентно (A1): дубль не создаётся, повторно инициируется уведомление-приглашение с существующим userId.

Постусловие

  • Создана учётка UserEntity (Type = Agent|PartnerManager|PinEmployee, Status = Invited, CompanyId = компания, RegisteredVia = AdminApi, CreatedByUserId = актор).
  • Создано RoleAssignment (Active) в выбранном scope.
  • Опубликованы UserUpsertedKafkaEvent и UserAccessChangedKafkaEvent; notifications отправил приглашение.
  • После первого входа — Status = Active.

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

КодHTTPУсловиеПоведение клиента
403 FORBIDDEN403Нет identity.users.manage в scope организацииСкрыть кнопку приглашения
ValidationError400Формат телефона/ФИО/scopeПодсветить поле
IDENTITY_USER_PHONE_TAKEN409Номер занят чужой учёткой (INV-1, A2)Сообщение + ссылка на поддержку
IDENTITY_USER_TYPE_COMPANY_MISMATCH400Компания Developer (INV-2, A4)Скрыть фичу для застройщика
IDENTITY_COMPANY_SUSPENDED403Компания не Active«Доступ компании приостановлен»
IDENTITY_ASSIGNMENT_*400/409Ошибки назначения роли (INV-R2..R6)Как в UC-IDN-005

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

СобытиеКогда поднимаетсяSubscribers
UserRegisteredEventСоздание учётки Invitedin-proc
RoleAssignmentGrantedEventДефолтное назначениеin-proc аудит

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

СобытиеTopicКогда публикуетсяConsumers
UserUpsertedKafkaEvent.v1pin.identity.users.v1Создание Invited; повторно при Invited → Activenotifications (приглашение), crm, messenger, profile, user-activity
UserAccessChangedKafkaEvent.v1pin.identity.access.v1Дефолтное назначение ролиcrm, catalog, chessboard, gateway
Гарантии доставки и согласованность

Оба события публикуются с ключом партиционирования UserId, несут Version и применяются потребителями идемпотентно (по (UserId, Version) / EventId), поэтому повторная доставка не создаёт дубликатов учётки/назначения у downstream. Отправка СМС-приглашения — асинхронна: notifications реагирует на pin.identity.users.v1 со Status = Invited; до обработки события приглашённый ещё не получил ссылку, а руководитель уже видит учётку Invited — это нормальное промежуточное состояние. Доступ к лидам/клиентской базе агент получает после инвалидации кэшей прав по pin.identity.access.v1 (окно ≤ TTL, UC-IDN-008).

Acceptance Criteria

#КритерийПроверка
AC1Приглашение создаёт учётку Invited + назначение в одном согласованном измененииинтеграционный тест + сбой между шагами
AC2Повторное приглашение идемпотентно (A1)двойной вызов → один пользователь
AC3Занятый номер → 409 без изменений (A2)тест конфликта с инвестором
AC4Застройщик получает 400 (A4)тест типа компании
AC5Первый вход переводит Invited → Activeсквозной тест с UC-IDN-001
AC6Type пользователя выводится из Company.Type (INV-2)тесты Agency/Broker/Partner/Pin
AC7Приглашённый до первого входа не аутентифицируется паролем (нет PasswordHash)негативный тест UC-IDN-002
AC8Параллельные приглашения одного номера: ровно одна учётка, вторая попытка → 409 или идемпотентно (A6)конкурентный тест

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

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

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

  • API метод: invite-company-user.md.
  • Domain entity: ../domain/user.md (INV-1/2, stateDiagram Invited→Active), ../domain/role.md, ../domain/company.md (WorkspaceSettings.AllowAgentSelfRegistration).
  • Уведомление-приглашение: docs/06-services/notifications/.
  • Связанные UC: UC-IDN-001 (активация входом), UC-IDN-005 (роли/scope), UC-IDN-007 (менеджеры застройщика).