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

POST /api/v1/companies/my/agent-invitations/{invitationId}/revoke — Отзыв приглашения агента

Приглашающая сторона (руководитель/менеджер) отзывает ещё не разрешённое приглашение CompanyInvitationEntity: Pending → Revoked (ADR-0061, INV-CI5). Отзыв доступен только пока приглашение не принято/отклонено/просрочено.

Назначение

Отмена ошибочного или неактуального приглашения агента. Owning-контекст — identity.

Metadata

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

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

ПроверкаЗначение
Actor typeUser (руководитель/менеджер агентства)
Auth policy / Permissionsidentity.users.manage (INV-CI5)
Scope (RBAC)организация: Active RoleAssignment со scope Organization(CompanyId)
Record-levelне применяется (приглашение внутри tenant компании)
Tenant isolationTenantId = CompanyId из claims: отозвать можно только приглашение своей компании
Auditupdate: UpdatedByUserId = actor, RevokedAt, событие CompanyInvitationChangedKafkaEvent
MFA/approvalне требуется

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/companies/my/agent-invitations/{invitationId}/revoke
Success status200
Idempotency headerне применяется (повтор на терминальном Revoked — идемпотентный 200)
Correlationсквозной trace через заголовок traceparent
Идемпотентный повтор

Отдельный idempotency-заголовок не нужен: повторный вызов на приглашении, уже переведённом в Revoked, возвращает 200 с тем же результатом и не публикует новое событие — идемпотентность обеспечивает сам терминальный статус.

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
ActorUserIdidentity claims (sub)GuidДаRequired
CompanyIdidentity claims (tenant_id)GuidДаRequiredTenant isolation
InvitationIdrouteGuidДасуществующее приглашение своей компании

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

ПолеТипОбязательностьИсточникMaskingОписание
InvitationIdGuidДаCompanyInvitationEntity.IdId приглашения.
Statusstring (enum CompanyInvitationStatus)ДаCompanyInvitationEntity.StatusRevoked.
RevokedAtDateTimeOffsetДаCompanyInvitationEntity.RevokedAtМомент отзыва.

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

Предусловия. Актор аутентифицирован и имеет identity.users.manage в scope Organization(CompanyId) (INV-CI5); приглашение существует, принадлежит компании актора и находится в статусе Pending.

Постусловия при успехе. Приглашение переведено Pending → Revoked, зафиксированы момент и автор отзыва; опубликовано CompanyInvitationChangedKafkaEvent (Status = Revoked). Адресат больше не сможет принять приглашение (accept из Revoked невозможен — INV-CI1). Повтор на уже Revoked — идемпотентный 200 без нового события.

Постусловия при отказе. Статус приглашения не изменился, событие не публикуется.

Алгоритм

  1. Контекст и доступ. Операцию выполняет руководитель/менеджер агентства; tenant — его компания (CompanyId); требуется право identity.users.manage в scope Organization(CompanyId) (INV-CI5); иначе — FORBIDDEN.
  2. Проверка входа и предусловий. InvitationId — корректный Guid; иначе — ValidationError. Приглашение существует и принадлежит компании актора (invitation.CompanyId == CompanyId); иначе — IDENTITY_INVITATION_NOT_FOUND (cross-tenant не раскрывается). Отзыв допустим только из статуса Pending (INV-CI1); Accepted/Declined/ExpiredIDENTITY_INVITATION_INVALID_STATUS; повтор на уже Revoked — идемпотентный 200.
  3. Что читаем. Приглашение (для проверок выше).
  4. Бизнес-правила и переходы. Приглашение переходит Pending → Revoked: фиксируются момент и автор отзыва. Поднимается доменный факт CompanyInvitationResolvedEvent.
  5. Что меняется. Обновляется запись приглашения.
  6. Какие факты/события фиксируются. CompanyInvitationChangedKafkaEvent (топик pin.identity.company-invitation-changed.v1, ключ InvitationId) — snapshot Status = Revoked.
  7. Согласованность. Приглашение и событие фиксируются в одном согласованном изменении.
  8. Результат. Идентификатор приглашения, статус Revoked, момент отзыва (200).

Диаграмма

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

ТипДетали
Интеграционные событияCompanyInvitationChangedKafkaEventpin.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
ValidationError400Битый InvitationIdСписок ошибокno retry
FORBIDDEN403Нет identity.users.manage (INV-CI5)«Недостаточно прав»no retry
IDENTITY_INVITATION_NOT_FOUND404Приглашение не существует или вне tenant«Приглашение не найдено»no retry
IDENTITY_INVITATION_INVALID_STATUS409Приглашение уже Accepted/Declined/Expired (INV-CI1)«Приглашение уже разрешено»no retry
CONCURRENCY_CONFLICT409Конфликт оптимистичной блокировки с 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, When POST /revoke, Then 200; приглашение Revoked, RevokedAt заполнен; опубликован CompanyInvitationChangedKafkaEvent (Status = Revoked).
  • Given приглашение уже Revoked, When повторный POST /revoke, Then 200 с тем же результатом, нового события нет.
  • Given приглашение уже Accepted/Declined/Expired, When POST /revoke, Then IDENTITY_INVITATION_INVALID_STATUS (409) — INV-CI1.
  • Given InvitationId принадлежит другой компании, When POST /revoke, Then IDENTITY_INVITATION_NOT_FOUND (404) — cross-tenant не раскрывается.
  • Given актор без identity.users.manage, When POST /revoke, Then FORBIDDEN (403) — INV-CI5.
  • Given адресат одновременно принимает приглашение, When POST /revoke проигрывает гонку, Then CONCURRENCY_CONFLICT (409) с рекомендацией перечитать (или уже IDENTITY_INVITATION_INVALID_STATUS, если accept зафиксировался первым).

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

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

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