POST /api/v1/me/agent-invitations/{invitationId}/decline — Отклонение приглашения в агентство
Адресат отклоняет приглашение: приглашение переходит Pending → Declined (ADR-0061). Учётка, членство и
роли не меняются — только фиксируется отказ.
Назначение
Приглашённый (инвестор или агент другой компании) отказывается вступать. Вызывается адресатом
приглашения; на выходе — приглашение в статусе Declined. Авторизация — по совпадению телефона (INV-CI2),
а не по tenant. Owning-контекст — identity.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | identity |
| API area | investor |
| Feature group | agent-onboarding |
| Статус | approved |
| Документ | docs/06-services/identity/api/investor/decline-invitation.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (адресат приглашения) |
| Auth policy / Permissions | authenticated; self-действие — по совпадению телефона (INV-CI2) |
| Scope (RBAC) | self; приглашение вне tenant актора (record-level по телефону) |
| Record-level | приглашение доступно только пользователю c PhoneNumber == invitation.PhoneNumber |
| Tenant isolation | не применяется |
| Audit | update: UpdatedByUserId = actor, DeclinedAt, событие CompanyInvitationChangedKafkaEvent |
| MFA/approval | не требуется |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/v1/me/agent-invitations/{invitationId}/decline |
| Success status | 200 |
| Idempotency header | не применяется (повтор на терминальном Declined — идемпотентный 200) |
| Correlation | сквозной trace через traceparent |
Идемпотентный повтор
Отдельный idempotency-заголовок не нужен: повторный вызов на приглашении, уже переведённом в Declined,
возвращает 200 с тем же результатом и не публикует новое событие — идемпотентность обеспечивает сам
терминальный статус.
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
ActorUserId | identity claims (sub) | Guid | Да | валидный JWT | 401 без токена |
InvitationId | route | Guid | Да | существующее приглашение | — |
Модель ответа DeclineInvitationCommandResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
InvitationId | Guid | Да | CompanyInvitationEntity.Id | — | Id приглашения. |
Status | string (enum CompanyInvitationStatus) | Да | CompanyInvitationEntity.Status | — | Declined. |
DeclinedAt | DateTimeOffset | Да | CompanyInvitationEntity.DeclinedAt | — | Момент отклонения. |
Алгоритм
- Контекст и доступ. Операцию выполняет аутентифицированный пользователь — адресат приглашения
(
ActorUserIdиз токена). - Проверка входа. Идентификатор приглашения корректен — иначе
ValidationError; актор аутентифицирован — иначе401. - Проверки сущностей. Получить приглашение по
InvitationId(не найдено →IDENTITY_INVITATION_NOT_FOUND) и пользователя-актора. Условия: телефон (INV-CI2) — телефон актора совпадает с телефоном приглашения, иначеIDENTITY_INVITATION_PHONE_MISMATCH; статус/срок — приглашениеPendingи не истекло (INV-CI1/CI4), иначеIDENTITY_INVITATION_INVALID_STATUS/IDENTITY_INVITATION_EXPIRED. - Переходы и маппинг. Приглашение отклоняется (guard INV-CI1; повтор на
Declined— идемпотентный no-op):Status = Declined,DeclinedAt = now,UpdatedByUserId = actor; фиксируется доменное событие о разрешении приглашения (Declined). - Что меняется. В одном согласованном изменении обновляется приглашение; гонка accept/revoke разрешается оптимистической блокировкой.
- События.
CompanyInvitationChangedKafkaEvent→pin.identity.company-invitation-changed.v1(ключ =InvitationId, снапшотStatus = Declined). - Результат.
InvitationId,Status = Declined,DeclinedAt; HTTP 200.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | CompanyInvitationChangedKafkaEvent → pin.identity.company-invitation-changed.v1 (Status = Declined) |
| Внешние вызовы | none |
| Проекции / поиск | none |
| Уведомления | опциональное СМС руководителю «приглашение отклонено» через сервис notifications |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
ValidationError | 400 | Битый InvitationId | Список ошибок | no retry |
IDENTITY_INVITATION_NOT_FOUND | 404 | Приглашение не существует | «Приглашение не найдено» | no retry |
IDENTITY_INVITATION_PHONE_MISMATCH | 403 | Телефон актора ≠ телефон приглашения (INV-CI2) | «Приглашение адресовано другому номеру» | no retry |
IDENTITY_INVITATION_INVALID_STATUS | 409 | Не Pending (INV-CI1) | «Приглашение уже разрешено» | no retry |
IDENTITY_INVITATION_EXPIRED | 409 | Истёк ExpiresAt (INV-CI4) | «Срок приглашения истёк» | no retry |
CONCURRENCY_CONFLICT | 409 | Гонка 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 приглашения не существует, ThenIDENTITY_INVITATION_NOT_FOUND(404). - Given приглашение
Accepted/Revoked(неPending), When decline, ThenIDENTITY_INVITATION_INVALID_STATUS(409); Given истёкшийExpiresAt, ThenIDENTITY_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.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: CompanyInvitationEntity
- API (4): Identity API — карта областей и реестр investor-области, POST /api/v1/companies/my/agent-invitations — Приглашение агента по номеру телефона, POST /api/v1/companies/my/agent-invitations/{invitationId}/revoke — Отзыв приглашения агента, POST /api/v1/me/agent-invitations/{invitationId}/accept — Принятие приглашения в агентство
- Use Cases (1): UC-IDN-009. Приглашение агента по телефону и его принятие
- События (1): Event Contract: pin.identity.company-invitation-changed (CompanyInvitationChangedKafkaEvent)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность: company-invitation.md (INV-CI1/CI2/CI4, stateDiagram).
- Принятие: accept-invitation.md; отзыв приглашающей стороной: revoke-invitation.md.
- Событие: company-invitation-changed.md.