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

GET /api/admin/referrals/links/{linkId} — Одна реферальная ссылка (admin, getReferralLinkById)

Назначение

Детальная admin-карточка одной реферальной ссылки: по linkId вернуть ссылку с данными владельца (ФИО, телефон) и числом регистраций. Кросс-tenant чтение: администратор платформы видит любую ссылку независимо от TenantId владельца. Дом item-модели AdminReferralLinkEntry — соседняя списочная операция get-referral-links.md; здесь та же модель под detail-карточку. Общий конверт и модели — README.md.

Metadata

ПолеЗначение
Сервис/контекстreferral
API areaadmin
Feature groupadmin-referrals
Статусapproved
Документdocs/06-services/referral/api/admin/get-referral-link-by-id.md

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

ПроверкаЗначение
Actor typeOperator(Admin) (администратор платформы)
Auth policy / Permissionspermission referral.registrations.view-all (каталог — permission.md)
Scope (RBAC)платформа (кросс-tenant)
Tenant isolationкросс-tenant по платформенному permission (tenant-предикат не накладывается); включая soft-deleted ссылки — admin-журнал
Auditread-sensitive: доступ к PII владельца (ownerName, ownerPhoneNumber) — access-audit

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/admin/referrals/links/{linkId}
Success status200
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
AdminUserIdidentity claims (sub)GuidДавалидный токен + permission referral.registrations.view-all401/403
linkIdrouteGuidДанепустой; существование — шаг 3

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

Контрактный дом модели — списочная операция get-referral-links.md; здесь та же item-модель под detail-карточку. Источники полей — агрегат referral-link.md и реплика ReferralUserReplica (attach-rules.md, раздел «Реплика»); FullNameREADME.md.

ПолеТипОбязательностьИсточникMaskingОписание
idstring (Guid)ДаReferralLinkEntity.IdИдентификатор ссылки.
ownerUserIdstring (Guid)ДаReferralLinkEntity.OwnerUserIdВладелец.
ownerNameFullNameДаReferralUserReplica.NameФИО владельца (README.md); пустые строки при lazy-регистрации.
ownerPhoneNumberstring?НетReferralUserReplica.PhoneNumberлог +7***1234Телефон владельца (E.164). PII, только admin; отдаётся в открытом виде.
urlstringДаReferralLinkEntity.UrlSlug ссылки.
descriptionstring?НетReferralLinkEntity.DescriptionОписание канала/назначения.
isDefaultboolДаReferralLinkEntity.IsDefaultСсылка «по умолчанию» владельца.
channelReferralChannel (enum строкой)ДаReferralLinkEntity.ChannelКанал; дом enum — referral-link.md.
registrationsCountnumber (int)Даcomputed (производный — referral-link.md)Число регистраций по ссылке.
createdAtstring (ISO 8601)ДаReferralLinkEntity.CreatedAtМомент создания.
emailstring?Нетвсегда nullEmail в реплику не реплицируется (PII-минимизация) → всегда null.

Алгоритм

  1. Контекст и доступ. Оператор — администратор платформы; требуется permission referral.registrations.view-all. Доступ кросс-tenant, включая soft-deleted ссылки (admin-журнал).
  2. Проверка входа. linkId непустой валидный Guid, иначе ValidationError.
  3. Выдача. Ищется ссылка по linkId (кросс-tenant, включая удалённые); если не найдена → REFERRAL_LINK_NOT_FOUND. К найденной ссылке присоединяются ФИО/телефон владельца из реплики (реплика ещё не догнала событие identity → ownerName пустой FullName, ownerPhoneNumber = null, fail-soft) и число регистраций (registrationsCount — live COUNT по журналу, денормализованного счётчика нет: отражает все зафиксированные регистрации на момент чтения и совпадает с meta.total журнала ссылки get-link-registrations.md); email всегда null; channel — enum строкой (unknown при чтении → Other + warning). Телефон оператору отдаётся в открытом виде, в логах маскируется; факт доступа к PII фиксируется в access-audit.

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400linkId пустой / не Guidперечень полейисправить
REFERRAL_LINK_NOT_FOUND404ссылки с таким linkId не существует (в т.ч. кросс-tenant)«Ссылка не найдена»no retry
— (auth)401нет/невалиден токен«Требуется авторизация»re-login
— (authz)403нет permission referral.registrations.view-all«Недостаточно прав»no retry

Постусловие

Состояние не изменяется (чистое чтение): ссылка, счётчики и реплика не трогаются, события не публикуются. Побочный факт — access-audit «оператор просмотрел карточку ссылки linkId с PII владельца».

Acceptance Criteria

  • Given администратор с permission referral.registrations.view-all и существующая ссылка, When запрашивает GET …/links/{linkId}, Then 200 c AdminReferralLinkEntry: ownerName/ ownerPhoneNumber из реплики, registrationsCount — число регистраций, channel — enum строкой, email = null; телефон в логах маскирован, доступ к PII — в access-audit.
  • Given ссылка принадлежит другому tenant или soft-deleted, When запрос, Then 200 — доступ кросс-tenant и к удалённым (admin-журнал).
  • Given linkId не существует, When запрос, Then 404 REFERRAL_LINK_NOT_FOUND.
  • Given реплика владельца ещё не догнала событие identity, When запрос, Then ownerName — пустой FullName, ownerPhoneNumber = null (fail-soft), ссылка всё равно отдаётся.
  • Given в хранилище неизвестное значение Channel (после additive-расширения), When чтение, Then channel = Other + warning в логах (без ошибки клиенту).
  • Given актор без permission, When запрос, Then 403; без токена — 401; linkId не Guid — 400 ValidationError.

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

  • Контракт стабилен. Новые optional поля (в т.ч. channel) — без breaking change; удаление/ переименование полей — breaking (migration + rollout).
  • Logs: AdminUserId, linkId, ownerUserId (телефон — маской +7***1234, ФИО в лог не пишется); metrics: referral_admin_link_by_id_requests_total{result}, latency p95; traces — сквозной trace через traceparent; access-audit; runbook — referral/README.md (Runtime).

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

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

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