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

POST /api/v1/me/agent-invitations/{invitationId}/decline — Отклонение приглашения в агентство

Адресат отклоняет приглашение: приглашение переходит Pending → Declined (ADR-0061). Учётка, членство и роли не меняются — только фиксируется отказ.

Назначение

Приглашённый (инвестор или агент другой компании) отказывается вступать. Вызывается адресатом приглашения; на выходе — приглашение в статусе Declined. Авторизация — по совпадению телефона (INV-CI2), а не по tenant. Owning-контекст — identity.

Metadata

ПолеЗначение
Сервис/контекстidentity
API areainvestor
Feature groupagent-onboarding
Статусapproved
Документdocs/06-services/identity/api/investor/decline-invitation.md

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

ПроверкаЗначение
Actor typeUser (адресат приглашения)
Auth policy / Permissionsauthenticated; self-действие — по совпадению телефона (INV-CI2)
Scope (RBAC)self; приглашение вне tenant актора (record-level по телефону)
Record-levelприглашение доступно только пользователю c PhoneNumber == invitation.PhoneNumber
Tenant isolationне применяется
Auditupdate: UpdatedByUserId = actor, DeclinedAt, событие CompanyInvitationChangedKafkaEvent
MFA/approvalне требуется

HTTP-контракт

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

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

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

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

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

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

Алгоритм

  1. Контекст и доступ. Операцию выполняет аутентифицированный пользователь — адресат приглашения (ActorUserId из токена).
  2. Проверка входа. Идентификатор приглашения корректен — иначе ValidationError; актор аутентифицирован — иначе 401.
  3. Проверки сущностей. Получить приглашение по InvitationId (не найдено → IDENTITY_INVITATION_NOT_FOUND) и пользователя-актора. Условия: телефон (INV-CI2) — телефон актора совпадает с телефоном приглашения, иначе IDENTITY_INVITATION_PHONE_MISMATCH; статус/срок — приглашение Pending и не истекло (INV-CI1/CI4), иначе IDENTITY_INVITATION_INVALID_STATUS / IDENTITY_INVITATION_EXPIRED.
  4. Переходы и маппинг. Приглашение отклоняется (guard INV-CI1; повтор на Declined — идемпотентный no-op): Status = Declined, DeclinedAt = now, UpdatedByUserId = actor; фиксируется доменное событие о разрешении приглашения (Declined).
  5. Что меняется. В одном согласованном изменении обновляется приглашение; гонка accept/revoke разрешается оптимистической блокировкой.
  6. События. CompanyInvitationChangedKafkaEventpin.identity.company-invitation-changed.v1 (ключ = InvitationId, снапшот Status = Declined).
  7. Результат. InvitationId, Status = Declined, DeclinedAt; HTTP 200.

Диаграмма

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

ТипДетали
Интеграционные событияCompanyInvitationChangedKafkaEventpin.identity.company-invitation-changed.v1 (Status = Declined)
Внешние вызовыnone
Проекции / поискnone
Уведомленияопциональное СМС руководителю «приглашение отклонено» через сервис notifications

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400Битый InvitationIdСписок ошибокno retry
IDENTITY_INVITATION_NOT_FOUND404Приглашение не существует«Приглашение не найдено»no retry
IDENTITY_INVITATION_PHONE_MISMATCH403Телефон актора ≠ телефон приглашения (INV-CI2)«Приглашение адресовано другому номеру»no retry
IDENTITY_INVITATION_INVALID_STATUS409Не Pending (INV-CI1)«Приглашение уже разрешено»no retry
IDENTITY_INVITATION_EXPIRED409Истёк ExpiresAt (INV-CI4)«Срок приглашения истёк»no retry
CONCURRENCY_CONFLICT409Гонка accept/revoke (оптимистическая блокировка)«Данные изменены, повторите»retry после перечитки

Acceptance Criteria

  • Given Pending-приглашение на телефон актора, When decline, Then приглашение → Declined (DeclinedAt = now, UpdatedByUserId = актор), уходит CompanyInvitationChangedKafkaEvent (Status = Declined), ответ 200; учётка, членство и роли не изменены.
  • Given приглашение уже Declined, When повторный decline, Then идемпотентный 200 без повторной смены и без дублирующего события.
  • Given приглашение с чужим телефоном, When decline, Then IDENTITY_INVITATION_PHONE_MISMATCH (403); Given приглашения не существует, Then IDENTITY_INVITATION_NOT_FOUND (404).
  • Given приглашение Accepted/Revoked (не Pending), When decline, Then IDENTITY_INVITATION_INVALID_STATUS (409); Given истёкший ExpiresAt, Then IDENTITY_INVITATION_EXPIRED (409).
  • Given параллельные decline и accept/revoke, When оба доходят до записи, Then один побеждает, второй получает CONCURRENCY_CONFLICT (409).

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

  • Новый эндпоинт, additive. Контракт события — версия контракта 1.
  • Logs: ActorUserId, InvitationId; metrics: identity_invitation_decline_total{result}; traces: OTel.

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

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

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