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

GET /api/v1/internal/profile/verification-status/{userId} — Статус верификации для m2m (getVerificationStatus)

Internal m2m-запрос: по userId отдаёт денормализованный KYC-статус аккаунта из ProfileEntity (поле VerificationStatus, дом — profile.md). Синхронная альтернатива событию VerificationCompletedKafkaEvent (../events/verification-completed.md): потребители (crm, catalog) держат признак «верифицирован» на своей реплике по событию, а этот endpoint используют для холодного старта, сверки и точечного дочитывания, когда реплики ещё нет. Снапшот заявки и любые PII не отдаются (расшифровка снапшота — только аудируемая admin-операция ревью, verification-request.md).

Назначение

Дать m2m-потребителям (crm — бейдж «верифицированный инвестор» на карточке клиента/сделки; catalog — гейтинг функций для верифицированных инвесторов) синхронно читать актуальный статус KYC одного аккаунта без подписки на Kafka. Owning-контекст — profile, feature group verification. Данные — денормализованное поле ProfileEntity.VerificationStatus, обновляемое в той же транзакции, что и решение по заявке (INV-PRF-6, profile.md), поэтому синхронный ответ согласован с событием.

Metadata

ПолеЗначение
Сервис/контекстprofile
API areainternal (m2m; потребители — crm, catalog)
Feature groupverification
Статусdraft
Документdocs/06-services/profile/api/internal/get-verification-status.md

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

ПроверкаЗначение
Actor typeServiceAccount (m2m internal; вызывающие сервисы — crm, catalog)
Auth policy / Permissionsm2m internal (service token / gateway mTLS); scope internal; permission profile.verification.read-internal (m2m-scope токена, permission.md)
Scope (RBAC)не применяется (актор вне tenant; данные инвестора вне tenant, ADR-0052)
Record-levelизоляция по userId из маршрута; проверку прав на конкретного инвестора выполняет вызывающий сервис (delegated) — статус верификации не является чувствительными данными
Tenant isolationне применяется (ADR-0052; tenant-интерсепторы не подключаются)
Auditread: не аудируется — наружу идёт только денормализованный статус (без PII, без расшифровки SnapshotCipher)

IUserActor(sub) self-scope здесь не применяется: userId приходит маршрутом (доверенный m2m-вызов), а не из JWT actor-context, как в public-методах ЛК.

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/v1/internal/profile/verification-status/{userId}
Success status200
Idempotency headerне применяется (GET — safe/idempotent по природе)
CorrelationOpenTelemetry trace (traceparent)

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdroute {userId}GuidДа!= Guid.Empty → иначе ValidationError; существование в identity не проверяется (чужой контекст)record-level по владельцу; авторизацию на конкретного инвестора делает вызывающий сервис (delegated)

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

Дом полей — profile.md (ProfileVerificationStatus, INV-PRF-6); здесь только проекция (дельта) денормализованного статуса, без чувствительных значений снапшота.

ПолеТипОбязательностьИсточникMaskingОписание
userIdGuidДаProfileEntity.Id (= UserId)Эхо запрошенного аккаунта.
verificationStatusProfileVerificationStatusДаProfileEntity.VerificationStatusNotVerified / Pending / Verified / Rejected (enum ниже).
verifiedAtDateTimeOffset?НетProfileEntity.VerifiedAtМомент одобрения последней заявки; null при verificationStatus != Verified.

Enum ProfileVerificationStatus

Значения идентичны profile.md (.HasConversion<string>() на стороне контракта):

ЗначениеУсловиеКак трактует потребитель
NotVerifiedВерификация не проходилась (default).Бейдж не показывать; функции для верифицированных закрыты.
PendingЕсть заявка в Submitted/InReview.Бейдж не показывать; можно показать «на проверке».
VerifiedПоследняя заявка Approved.Бейдж «верифицирован»; функции открыты (verifiedAt заполнен).
RejectedПоследняя заявка Rejected.Бейдж не показывать; функции закрыты.

VerifiedRequestId и снапшот данных заявки наружу не отдаются (внутренняя ссылка/PII контекста profile); связь с событием потребитель ведёт по VerificationCompletedKafkaEvent.Id собственной реплики.

Алгоритм

  1. Контекст и доступ. Вызывает m2m service-account (crm/catalog), scope internal + permission profile.verification.read-internal (иначе 401/403). Tenant не применяется (ADR-0052).
  2. Проверка входа. UserId не пустой (!= Guid.Empty) — иначе ValidationError. Состояние чтение не ограничивает: статус читается для любого ProfileStatus (в т.ч. Blocked/Archived) — гейтинг у потребителя, не здесь.
  3. Что читаем. Профиль по UserId; если профиль ещё не создан консюмером pin.identity.users.v1 (лаг реплики, consumers/identity-users-sync.md) → PROFILE_NOT_FOUND (вызывающий повторяет с backoff). Заявка и снапшот не читаются — статус берётся из денормализованного поля профиля.
  4. Результат. Проекция ProfileEntity → VerificationStatusModel: userId = profile.Id, verificationStatus = profile.VerificationStatus, verifiedAt = profile.VerifiedAt. Снапшот не расшифровывается. HTTP 200. Freshness: VerificationStatus денормализован и меняется в одном согласованном изменении с решением по заявке (INV-PRF-6), поэтому синхронный ответ согласован с VerificationCompletedKafkaEvent; потребитель может кэшировать значение (см. «Совместимость и наблюдаемость»).

Диаграмма

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
UNAUTHENTICATED401Нет/просрочен m2m service tokenбез тела (ветка Other)обновить service token; повтор
FORBIDDEN403Токен без scope internal / permission profile.verification.read-internalбез телаno retry (исправить m2m-credential)
ValidationError400Пустой UserId в маршрутеRestApiValidationErrorResponseno retry
PROFILE_NOT_FOUND404Профиль ещё не создан консюмером identity (лаг реплики)«Профиль не найден»retry с backoff (1–5 с)

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

  • Новые optional поля ответа — additive, без breaking change; удаление/переименование/смена типа — breaking (migration + rollout координируется с crm/catalog). Unknown значение verificationStatus у потребителя — fail closed: трактовать как «не верифицирован» (бейдж скрыть, функции не открывать).
  • Кэширование: значение стабильно между терминальными решениями KYC; потребителю рекомендован короткий TTL с инвалидацией по VerificationCompletedKafkaEvent (event-driven push + этот sync-read как reconciliation).
  • Logs: UserId, verificationStatus, actor(service), latency (без PII); metrics: profile_api_operations_total{operation="getVerificationStatus",result}, latency p95 (SLO README — internal p99 < 200 мс); traces — OTel. Dashboard profile-overview; runbook — replay DLQ identity-sync (при всплеске PROFILE_NOT_FOUND).

Acceptance Criteria

  • Happy path. Given профиль существует, m2m-вызов crm/catalog с profile.verification.read-internal; When get; Then 200, VerificationStatusModel с денормализованным verificationStatus и verifiedAt.
  • Лаг реплики. Given профиль ещё не создан консюмером pin.identity.users.v1; When get; Then 404 PROFILE_NOT_FOUND; потребитель повторяет с backoff (1–5 с).
  • Нет прав m2m. Given токен без scope internal или без permission; When get; Then 403 FORBIDDEN; без токена — 401 UNAUTHENTICATED.
  • Пустой id. Given userId = Guid.Empty; When get; Then 400 ValidationError.
  • Гейтинг у потребителя. Given профиль Blocked/Archived; When get; Then статус всё равно отдаётся (200) — ограничение функций решает потребитель.
  • Синхронность с решением. Given заявка только что одобрена; When get сразу после; Then возвращается Verified немедленно (обновлено в той же транзакции, INV-PRF-6) — sync-read согласован с VerificationCompletedKafkaEvent и служит reconciliation.
  • Неизвестный статус у клиента. Given будущее значение verificationStatus; When старый потребитель разбирает ответ; Then трактует его консервативно как «не верифицирован» (бейдж скрыт, функции закрыты).

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

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

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