UC-IDN-006. Приглашение менеджера (агента) в компанию
Назначение
Руководитель агентства/брокера/партнёра пополняет команду: приглашает нового сотрудника по номеру телефона —
создаётся учётка Invited с дефолтной ролью, сотрудник активируется первым входом по СМС (UC-IDN-001).
Менеджеры застройщиков этим потоком не приглашаются — они приходят синком Talent (UC-IDN-007). Связан с
контекстом identity.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-IDN-006 |
| Контекст | identity (см. docs/06-services/identity/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Агентство (руководитель, agency-owner) / уполномоченный менеджер компании; для компании ПИН — МенеджерПИН с identity.users.manage |
| Permissions / scope | identity.users.manage в scope Organization(CompanyId) |
| Tenant | TenantId = 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 |
| Триггерящий элемент | ЛК компании → «Команда» → «Пригласить сотрудника» |
Основной Поток
- Руководитель нажимает «Пригласить сотрудника», вводит телефон, ФИО, выбирает внутреннюю роль
(из ролей компании + системная
agent) и scope (по умолчанию —Organization). - Система принимает запрос приглашения (контракт —
invite-company-user.md;
CompanyId— из claimstenant_id, пригласить можно только в свою компанию) с телом{ phoneNumber (1..32, E.164), firstName (1..128), lastName? (≤128), middleName? (≤128), roleId, scope?: ResourceScopeModel }. - Проверить доступ и вход: у актора есть
identity.users.manageв scopeOrganization(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). - Создать учётку
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). Учётка, назначение и публикация событий — в одном согласованном изменении. - Зафиксировать факты: события
UserRegisteredEvent,RoleAssignmentGrantedEvent; интеграционныеUserUpsertedKafkaEvent→pin.identity.users.v1(snapshot:Id,Type,CompanyId,Status = Invited,Name,PhoneNumber) иUserAccessChangedKafkaEvent→pin.identity.access.v1. - Система возвращает
{ userId, status = Invited, roleAssignmentId }. - Приглашение отправляется через
notifications(СМС со ссылкой на вход) — по событиюpin.identity.users.v1(Status = Invited); identity сам СМС-приглашение не шлёт (границы). - Приглашённый переходит по ссылке, вводит свой номер → UC-IDN-001 (система находит учётку
Invited,outcome = SignedIn,nextStep = RequiresSmsCode). - Успешный вход переводит
Invited → Active(stateDiagram user.md), заполняетPhoneNumberConfirmedAt,LastSignInAt. - Список команды у руководителя показывает статус
Active; consumers (crm, messenger) получили пользователя и назначение — агент может работать с лидами в границах своего scope.
Альтернативные Потоки
A1. Повторное приглашение (учётка уже Invited)
- Шаг 3 находит учётку
Invitedтой же компании → дубль не создаётся, повторно инициируется уведомление-приглашение (идемпотентный ответ с существующимuserId). Завершается шагом 7.
A2. Телефон занят другой учёткой
- Номер принадлежит инвестору или сотруднику другой компании →
IDENTITY_USER_PHONE_TAKEN(409). - Авто-перепривязка запрещена (риск отдать чужой аккаунт); руководитель связывается с сотрудником (пусть сменит номер своей учётки — UC-IDN-004) или обращается в заботу (ручной merge — README «Известные ограничения»). Поток завершается неуспехом.
A3. Приглашённый не входит (просроченное приглашение)
- Учётка остаётся
Invitedбез TTL (вход возможен всегда, пока не отозвана); руководитель может архивировать учётку (административная операция) — назначения отзываются каскадом (user.md «Связи»).
A4. Компания-застройщик пытается пригласить менеджера
Company.Type = Developer→IDENTITY_USER_TYPE_COMPANY_MISMATCH(400): менеджеры застройщика создаются только синком Talent (ContactsManagedBy = TalentSync, UC-IDN-007). UI застройщика не показывает кнопку приглашения.
A5. Приглашение с ролью в узком scope
- Шаг 1: руководитель выбирает scope
Project(X)— валидация и наследование как в UC-IDN-005 шаг 9.
A6. Параллельные приглашения на один номер
- Два руководителя (или один дважды) одновременно приглашают один номер: уникальный индекс телефона
пропускает ровно одну вставку
Invited-учётки, параллельная попытка получаетIDENTITY_USER_PHONE_TAKEN. - Если это та же компания и учётка уже
Invited— конфликт разрешается идемпотентно (A1): дубль не создаётся, повторно инициируется уведомление-приглашение с существующимuserId.
Постусловие
- Создана учётка
UserEntity (Type = Agent|PartnerManager|PinEmployee, Status = Invited, CompanyId = компания, RegisteredVia = AdminApi, CreatedByUserId = актор). - Создано
RoleAssignment (Active)в выбранном scope. - Опубликованы
UserUpsertedKafkaEventиUserAccessChangedKafkaEvent; notifications отправил приглашение. - После первого входа —
Status = Active.
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|---|---|---|
403 FORBIDDEN | 403 | Нет identity.users.manage в scope организации | Скрыть кнопку приглашения |
ValidationError | 400 | Формат телефона/ФИО/scope | Подсветить поле |
IDENTITY_USER_PHONE_TAKEN | 409 | Номер занят чужой учёткой (INV-1, A2) | Сообщение + ссылка на поддержку |
IDENTITY_USER_TYPE_COMPANY_MISMATCH | 400 | Компания Developer (INV-2, A4) | Скрыть фичу для застройщика |
IDENTITY_COMPANY_SUSPENDED | 403 | Компания не Active | «Доступ компании приостановлен» |
IDENTITY_ASSIGNMENT_* | 400/409 | Ошибки назначения роли (INV-R2..R6) | Как в UC-IDN-005 |
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|---|---|
UserRegisteredEvent | Создание учётки Invited | in-proc |
RoleAssignmentGrantedEvent | Дефолтное назначение | in-proc аудит |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
UserUpsertedKafkaEvent.v1 | pin.identity.users.v1 | Создание Invited; повторно при Invited → Active | notifications (приглашение), crm, messenger, profile, user-activity |
UserAccessChangedKafkaEvent.v1 | pin.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 |
| AC6 | Type пользователя выводится из Company.Type (INV-2) | тесты Agency/Broker/Partner/Pin |
| AC7 | Приглашённый до первого входа не аутентифицируется паролем (нет PasswordHash) | негативный тест UC-IDN-002 |
| AC8 | Параллельные приглашения одного номера: ровно одна учётка, вторая попытка → 409 или идемпотентно (A6) | конкурентный тест |
Обратные ссылки
Автоссылки: где используется этот документ.
- API (1): Identity Company-User API — реестр методов и общие правила области
- index.md (1): Identity Service (Pin.Identity)
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- 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 (менеджеры застройщика).