POST /api/v1/companies/my/agent-invitations/{invitationId}/revoke — Отзыв приглашения агента
Приглашающая сторона (руководитель/менеджер) отзывает ещё не разрешённое приглашение
CompanyInvitationEntity: Pending → Revoked (ADR-0061, INV-CI5). Отзыв доступен только пока приглашение
не принято/отклонено/просрочено.
Назначение
Отмена ошибочного или неактуального приглашения агента. Owning-контекст — identity.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | identity |
| API area | company-user |
| Feature group | agent-onboarding |
| Статус | draft |
| Документ | docs/06-services/identity/api/company-user/revoke-invitation.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (руководитель/менеджер агентства) |
| Auth policy / Permissions | identity.users.manage (INV-CI5) |
| Scope (RBAC) | организация: Active RoleAssignment со scope Organization(CompanyId) |
| Record-level | не применяется (приглашение внутри tenant компании) |
| Tenant isolation | TenantId = CompanyId из claims: отозвать можно только приглашение своей компании |
| Audit | update: UpdatedByUserId = actor, RevokedAt, событие CompanyInvitationChangedKafkaEvent |
| MFA/approval | не требуется |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/v1/companies/my/agent-invitations/{invitationId}/revoke |
| Success status | 200 |
| Idempotency header | не применяется (повтор на терминальном Revoked — идемпотентный 200) |
| Correlation | сквозной trace через заголовок traceparent |
Отдельный idempotency-заголовок не нужен: повторный вызов на приглашении, уже переведённом в Revoked,
возвращает 200 с тем же результатом и не публикует новое событие — идемпотентность обеспечивает сам
терминальный статус.
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
ActorUserId | identity claims (sub) | Guid | Да | Required | — |
CompanyId | identity claims (tenant_id) | Guid | Да | Required | Tenant isolation |
InvitationId | route | Guid | Да | существующее приглашение своей компании | — |
Модель ответа RevokeInvitationCommandResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
InvitationId | Guid | Да | CompanyInvitationEntity.Id | — | Id приглашения. |
Status | string (enum CompanyInvitationStatus) | Да | CompanyInvitationEntity.Status | — | Revoked. |
RevokedAt | DateTimeOffset | Да | CompanyInvitationEntity.RevokedAt | — | Момент отзыва. |
Предусловия и постусловия
Предусловия. Актор аутентифицирован и имеет identity.users.manage в scope Organization(CompanyId)
(INV-CI5); приглашение существует, принадлежит компании актора и находится в статусе Pending.
Постусловия при успехе. Приглашение переведено Pending → Revoked, зафиксированы момент и автор
отзыва; опубликовано CompanyInvitationChangedKafkaEvent (Status = Revoked). Адресат больше не сможет
принять приглашение (accept из Revoked невозможен — INV-CI1). Повтор на уже Revoked — идемпотентный
200 без нового события.
Постусловия при отказе. Статус приглашения не изменился, событие не публикуется.
Алгоритм
- Контекст и доступ. Операцию выполняет руководитель/менеджер агентства; tenant — его компания
(
CompanyId); требуется правоidentity.users.manageв scopeOrganization(CompanyId)(INV-CI5); иначе —FORBIDDEN. - Проверка входа и предусловий.
InvitationId— корректный Guid; иначе —ValidationError. Приглашение существует и принадлежит компании актора (invitation.CompanyId == CompanyId); иначе —IDENTITY_INVITATION_NOT_FOUND(cross-tenant не раскрывается). Отзыв допустим только из статусаPending(INV-CI1);Accepted/Declined/Expired→IDENTITY_INVITATION_INVALID_STATUS; повтор на ужеRevoked— идемпотентный200. - Что читаем. Приглашение (для проверок выше).
- Бизнес-правила и переходы. Приглашение переходит
Pending → Revoked: фиксируются момент и автор отзыва. Поднимается доменный фактCompanyInvitationResolvedEvent. - Что меняется. Обновляется запись приглашения.
- Какие факты/события фиксируются.
CompanyInvitationChangedKafkaEvent(топикpin.identity.company-invitation-changed.v1, ключInvitationId) — snapshotStatus = Revoked. - Согласованность. Приглашение и событие фиксируются в одном согласованном изменении.
- Результат. Идентификатор приглашения, статус
Revoked, момент отзыва (200).
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | CompanyInvitationChangedKafkaEvent → pin.identity.company-invitation-changed.v1 (Status = Revoked) |
| Уведомления | опциональное СМС адресату «приглашение отозвано» через notifications |
Интеграционная последовательность и согласованность
После фиксации перехода Pending → Revoked публикуется CompanyInvitationChangedKafkaEvent (Status = Revoked) → топик pin.identity.company-invitation-changed.v1, ключ InvitationId. Порядок переходов
одного приглашения гарантируется партиционированием по Id — потребители (лента найма crm,
notifications) увидят Revoked после Pending. notifications может отправить адресату опциональное
СМС «приглашение отозвано». Гонка с параллельным accept/decline/expire: побеждает первый успешный
переход (INV-CI1), проигравший получает CONCURRENCY_CONFLICT или, если приглашение уже терминально, —
IDENTITY_INVITATION_INVALID_STATUS.
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
ValidationError | 400 | Битый InvitationId | Список ошибок | no retry |
FORBIDDEN | 403 | Нет identity.users.manage (INV-CI5) | «Недостаточно прав» | no retry |
IDENTITY_INVITATION_NOT_FOUND | 404 | Приглашение не существует или вне tenant | «Приглашение не найдено» | no retry |
IDENTITY_INVITATION_INVALID_STATUS | 409 | Приглашение уже Accepted/Declined/Expired (INV-CI1) | «Приглашение уже разрешено» | no retry |
CONCURRENCY_CONFLICT | 409 | Конфликт оптимистичной блокировки с accept/decline | «Данные изменены, повторите» | retry после перечитки |
Совместимость и наблюдаемость
- Новый эндпоинт, additive. Версия контракта события — 1.
- Logs:
CompanyId,ActorUserId,InvitationId; metrics:identity_agent_invite_revoke_total{result}; traces: OTel.
Acceptance Criteria
- Given
Pending-приглашение своей компании и актор сidentity.users.manage, WhenPOST /revoke, Then200; приглашениеRevoked,RevokedAtзаполнен; опубликованCompanyInvitationChangedKafkaEvent (Status = Revoked). - Given приглашение уже
Revoked, When повторныйPOST /revoke, Then200с тем же результатом, нового события нет. - Given приглашение уже
Accepted/Declined/Expired, WhenPOST /revoke, ThenIDENTITY_INVITATION_INVALID_STATUS(409) — INV-CI1. - Given
InvitationIdпринадлежит другой компании, WhenPOST /revoke, ThenIDENTITY_INVITATION_NOT_FOUND(404) — cross-tenant не раскрывается. - Given актор без
identity.users.manage, WhenPOST /revoke, ThenFORBIDDEN(403) — INV-CI5. - Given адресат одновременно принимает приглашение, When
POST /revokeпроигрывает гонку, ThenCONCURRENCY_CONFLICT(409) с рекомендацией перечитать (или ужеIDENTITY_INVITATION_INVALID_STATUS, если accept зафиксировался первым).
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: CompanyInvitationEntity
- API (4): Identity Company-User API — реестр методов и общие правила области, POST /api/v1/companies/my/agent-invitations — Приглашение агента по номеру телефона, POST /api/v1/me/agent-invitations/{invitationId}/accept — Принятие приглашения в агентство, POST /api/v1/me/agent-invitations/{invitationId}/decline — Отклонение приглашения в агентство
- Use Cases (1): UC-IDN-009. Приглашение агента по телефону и его принятие
- События (1): Event Contract: pin.identity.company-invitation-changed (CompanyInvitationChangedKafkaEvent)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность: company-invitation.md (INV-CI1, CI5, stateDiagram).
- Приглашение: invite-agent-by-phone.md; подтверждение/отклонение: accept-invitation.md, decline-invitation.md.
- Событие: company-invitation-changed.md.