GET /api/admin/referrals/links/{linkId}/registrations — Журнал ссылки с PII (admin getLinkRegistrations)
Назначение
Административный постраничный журнал регистраций по одной реферальной ссылке с персональными данными
приглашённых (ФИО, телефон): проекция append-only журнала
ReferralRegistrationEntity. В отличие от обезличенной
public-выдачи владельца ../public/get-link-registrations.md
(только referredInvestorId), администратор платформы видит ФИО/телефон приглашённого из реплики
пользователей ReferralUserReplica. Используется в разборе привязок,
поддержке и подготовке коррекций связей. Общий конверт, paging- и FullName-модели —
README.md.
| Поле | Значение |
|---|
| Сервис/контекст | referral |
| API area | admin |
| Feature group | referral-registrations |
| Статус | approved |
| Документ | docs/06-services/referral/api/admin/get-link-registrations.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | Operator(Admin) (администратор платформы) |
| Auth policy / Permissions | authenticated; permission referral.registrations.view-all (permission.md) |
| Scope (RBAC) | платформа (cross-tenant): admin видит журнал любой ссылки |
| Tenant isolation | кросс-tenant по платформенному permission (tenant-предикат не накладывается) |
| Audit | read-sensitive: выдача PII (ФИО/телефон) пишется в access-audit |
HTTP-контракт
| Поле | Значение |
|---|
| Method | GET |
| Route | /api/admin/referrals/links/{linkId}/registrations |
| Success status | 200 |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
OperatorId | identity claims (sub) | Guid | Да | валидный токен + permission referral.registrations.view-all | 401 / 403 |
linkId | route | Guid | Да | непустой; существующая ссылка (шаг 3) | — |
Offset | query | int | Да | min 0, max 2147483647 | — |
Take | query | int | Нет (admin) | min 1, max 1000; клиент админки передаёт явно | ограничение размера выдачи |
Модель ответа AdminReferralRegistrationEntryPagingResult
Контейнер offset-paging (дом paging-моделей — README.md):
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
meta | PagingResultMeta | Да | computed | — | total / take / offset — README.md. |
items | AdminReferralRegistrationEntry[] | Да | projection + join | — | Страница записей журнала ссылки. |
Модель AdminReferralRegistrationEntry (дом item-модели)
Проекция журнала ReferralRegistrationEntity +
denormalized-поля + данные приглашённого из реплики ReferralUserReplica:
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
registeredAt | string (ISO 8601) | Да | ReferralRegistrationEntity.RegisteredAt | — | Момент регистрации/привязки (сортировка по нему desc). |
referralLinkId | string (Guid) | Да | ReferralRegistrationEntity.ReferralLinkId | — | Ссылка записи (= linkId запроса). |
linkUrl | string | Да | ReferralRegistrationEntity.LinkUrl (денормализация) | — | Исторический slug на момент записи; переживает переименование/удаление ссылки. |
referredInvestorId | string (Guid) | Да | ReferralRegistrationEntity.ReferredUserId | — | Приглашённый пользователь. |
name | FullName | Да | ReferralUserReplica.Name (по ReferredUserId) | PII (raw для admin) | ФИО приглашённого из реплики (README.md — FullName); реплики нет (лаг consumer-а) → пустые строки. |
phoneNumber | string? | Нет | ReferralUserReplica.PhoneNumber | PII; в логах маска +7***1234 | Телефон приглашённого (E.164); null, если реплики ещё нет. |
email | string? | Нет | всегда null | — | Email в реплику не реплицируется (PII-минимизация, attach-rules.md «Реплика») → всегда null. |
registrationType | string (ReferralRegistrationType) | Да | ReferralRegistrationEntity.RegistrationType | — | Initial / Rebind (enum строкой, referral-registration.md). |
Алгоритм
- Контекст и доступ. Оператор — администратор платформы; требуется permission
referral.registrations.view-all (иначе 403). Доступ кросс-tenant.
- Проверка входа.
linkId непустой, Offset >= 0, 1 <= Take <= 1000; иначе ValidationError.
- Проверка ссылки. Ссылка должна существовать (журнал включает soft-deleted ссылки — история
сохраняется); если ссылки по
linkId нет → REFERRAL_LINK_NOT_FOUND (чтобы не отдавать пустую
страницу как валидный результат по несуществующему id).
- Выдача. Возвращается страница записей журнала по ссылке (
ReferralLinkId == linkId),
упорядоченная по дате регистрации RegisteredAt desc, при равных значениях — по Id (стабильный
tie-break; правило порядка/консистентности — README.md). Журнал append-only:
исторические страницы стабильны, новые записи (Initial при привязке, Rebind при admin-коррекции)
появляются только в «голове» ленты. К каждой записи присоединяются ФИО/телефон приглашённого из
реплики (реплика отсутствует, лаг consumer-а → name пустой FullName, phoneNumber = null,
деградация, не ошибка); email всегда null. PII отдаётся сырой (permission
referral.registrations.view-all), в логах телефон маскируется. meta содержит полное число
записей под фильтром, размер и смещение страницы.
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
| — (auth) | 401 | Нет/невалиден токен | «Требуется авторизация» | re-login |
| — (policy) | 403 | Нет permission referral.registrations.view-all | «Недостаточно прав» | no retry |
ValidationError | 400 | linkId/Offset/Take вне ограничений | перечень полей | исправить |
REFERRAL_LINK_NOT_FOUND | 404 | Ссылка по linkId не существует | «Реферальная ссылка не найдена» | no retry |
Реплика приглашённого отсутствует (consumer lag) — не ошибка: name/phoneNumber деградируют,
запись журнала всё равно возвращается.
Постусловие
Состояние не изменяется (append-only журнал только читается): записи, ссылка и реплика не трогаются,
события не публикуются. Побочный факт — access-audit «оператор просмотрел журнал ссылки linkId с PII».
Acceptance Criteria
- Given администратор с permission
referral.registrations.view-all и существующая ссылка с
регистрациями, When запрашивает GET …/links/{linkId}/registrations, Then 200 со страницей
AdminReferralRegistrationEntry, упорядоченной по registeredAt desc; в теле — ФИО/телефон
приглашённого (raw), linkUrl — исторический slug на момент записи, registrationType ∈
{Initial, Rebind}; телефон в логах маскирован, email = null.
- Given ссылка soft-deleted, но с историческими записями, When запрос, Then
200 — журнал
переживает удаление ссылки (записи возвращаются).
- Given
linkId не существует ни у одной ссылки, When запрос, Then 404 REFERRAL_LINK_NOT_FOUND (пустая страница по несуществующему id не выдаётся как валидный результат).
- Given реплика приглашённого отстаёт от журнала, When запрос, Then запись возвращается с
пустым
name и phoneNumber = null (graceful degradation).
- Given актор без permission, When запрос, Then
403; без токена — 401.
- Given
linkId/Offset/Take вне ограничений, When запрос, Then 400 ValidationError.
- Given в журнале записи с равным
registeredAt, When постраничный обход, Then порядок
стабилен по вторичному ключу Id; append-only журнал — исторические страницы не смещаются, новые
записи появляются только в «голове» ленты (README.md).
Совместимость и наблюдаемость
- Контракт стабилен (имена полей
AdminReferralRegistrationEntry не меняются). Удаление/
переименование/смена типа поля — breaking (migration + rollout admin-клиента).
- Unknown значение
RegistrationType при чтении — трактуется консервативно (лог warning), в выдачу
идёт как есть строкой.
- Logs:
OperatorId, linkId, размер страницы, факт выдачи PII (audit read-sensitive); телефоны —
маской +7***1234. Metrics: referral_admin_link_registrations_requests_total, latency p95.
Traces — сквозной trace через traceparent. Dashboard/runbook — ../README.md (Runtime).
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы