GET /api/admin/referrals/links — Все реферальные ссылки платформы (admin, getReferralLinks)
Назначение
Административный кросс-tenant обзор всех реферальных ссылок PIN с ФИО и телефоном владельца. Даёт
администратору платформы полный постраничный список реферальных ссылок всех пользователей (инвесторы,
агенты, менеджеры застройщика, сотрудники ПИН) с владельцем (ФИО/телефон), каналом, числом регистраций
и признаком default — для контроля партнёрской программы и разбора обращений. В отличие от публичного
../public/get-my-referral-links.md (record-level, обезличенный),
операция доступна только по платформенному permission referral.registrations.view-all и раскрывает
PII владельца из реплики. Дом item-модели AdminReferralLinkEntry — здесь; её переиспользует
get-referral-link-by-id.md. Общий конверт, paging-модели и модель
FullName — README.md.
| Поле | Значение |
|---|
| Сервис/контекст | referral |
| API area | admin |
| Feature group | admin-referral-links |
| Статус | approved |
| Документ | docs/06-services/referral/api/admin/get-referral-links.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | Operator(Admin) (администратор платформы) |
| Auth policy / Permissions | permission referral.registrations.view-all (каталог — permission.md) |
| Scope (RBAC) | платформа (кросс-tenant доступ по платформенному permission) |
| Tenant isolation | кросс-tenant по платформенному permission (tenant-предикат не накладывается); доступ фиксируется в audit |
| Audit | read-sensitive (PII: ФИО/телефон владельца) — access-audit |
HTTP-контракт
| Поле | Значение |
|---|
| Method | GET |
| Route | /api/admin/referrals/links |
| Success status | 200 |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
AdminUserId | identity claims | Guid | Да | непустой | контекст admin-области, audit |
Offset | query | int | Да | min 0, max 2147483647 | — |
Take | query | int | Нет (admin-клиент передаёт явно, README.md) | min 1, max 1000; при отсутствии — server-side default | ограничение размера выдачи |
OwnerUserId | query | Guid? | Нет | непустой если задан — фильтр по владельцу | — |
Url | query | string? | Нет | нормализация lowercase; подстрока slug, max 64 | untrusted — параметризованный запрос |
Модель ответа AdminReferralLinkEntryPagingResult
Container-модель (дом paging-моделей — README.md):
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
meta | PagingResultMeta | Да | projection | — | total/take/offset — README.md. |
items | IReadOnlyList<AdminReferralLinkEntry> | Да (может быть пуст) | projection | — | Страница ссылок, сортировка CreatedAt desc. |
Модель AdminReferralLinkEntry (дом item-модели; переиспользуется get-referral-link-by-id.md)
Проекция дома referral-link.md + данные владельца из реплики
ReferralUserReplica (раздел «Реплика»); модель FullName —
README.md:
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
id | string (Guid) | Да | ReferralLinkEntity.Id | — | Идентификатор ссылки. |
ownerUserId | string (Guid) | Да | ReferralLinkEntity.OwnerUserId | — | Владелец ссылки. |
ownerName | FullName | Да | ReferralUserReplica.Name | — | ФИО владельца; пустые строки при lazy-регистрации. |
ownerPhoneNumber | string? | Нет | ReferralUserReplica.PhoneNumber | в логах +7***1234 | Телефон владельца (E.164); null, если реплика ещё не догнала владельца. |
url | string | Да | ReferralLinkEntity.Url | — | Slug. |
description | string? | Нет | ReferralLinkEntity.Description | — | Описание канала. |
isDefault | bool | Да | ReferralLinkEntity.IsDefault | — | Ссылка «по умолчанию» владельца. |
channel | ReferralChannel (enum строкой) | Да | ReferralLinkEntity.Channel | — | Канал (referral-link.md). |
registrationsCount | number (int) | Да | computed (производный — referral-link.md) | — | Число регистраций по ссылке. |
createdAt | string (ISO 8601) | Да | ReferralLinkEntity.CreatedAt | — | Момент создания. |
email | string? | Нет | всегда null | — | Email в реплику не реплицируется (PII-минимизация, attach-rules.md «Реплика») → всегда null. |
Алгоритм
- Контекст и доступ. Оператор — администратор платформы; требуется permission
referral.registrations.view-all (иначе REFERRAL_ADMIN_FORBIDDEN). Доступ кросс-tenant.
- Проверка входа.
Offset >= 0; при заданном Take — 1 <= Take <= 1000 (иначе server-side
default); OwnerUserId непустой если передан; Url — нормализация к lowercase, <= 64; иначе
ValidationError.
- Выдача. Возвращается кросс-tenant страница не удалённых ссылок (soft-deleted в список не
попадают — детальная карточка по id их показывает, get-referral-link-by-id.md).
Опциональные фильтры комбинируются по И:
OwnerUserId (точное совпадение владельца) и Url
(case-insensitive подстрока slug — contains по нормализованному к lowercase значению). Порядок —
CreatedAt desc, при равных значениях — по Id (стабильный tie-break; правило порядка/консистентности
страницы — README.md). К каждой ссылке присоединяются ФИО/телефон владельца из реплики
(реплика отсутствует → пустой FullName и null телефон, fail-soft) и число регистраций
(registrationsCount, отсутствует → 0); email всегда null. Телефон в логах маскируется, факт
выдачи PII фиксируется в access-audit. meta содержит полное число записей под фильтром, размер и
смещение страницы; конкурентное создание ссылок между страницами сдвигает offset (снимок —
README.md).
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
ValidationError | 400 | Offset/Take/OwnerUserId/Url вне диапазона/формата | перечень полей | исправить |
REFERRAL_ADMIN_FORBIDDEN | 403 | Актор без permission referral.registrations.view-all | «Доступ запрещён» | no retry |
| — (auth) | 401 | Нет/невалиден токен | «Требуется авторизация» | re-login |
Постусловие
Состояние не изменяется (чистое чтение): ссылки, счётчики и реплика не трогаются, события не
публикуются. Побочный факт — access-audit «оператор просматривал кросс-tenant список ссылок с PII».
Acceptance Criteria
- Given администратор с permission
referral.registrations.view-all, When запрашивает GET …/referrals/links, Then 200 со страницей AdminReferralLinkEntry, сортировка createdAt desc,
кросс-tenant; в каждой записи — ownerName/ownerPhoneNumber из реплики, registrationsCount,
channel, isDefault, email = null; телефон в логах маскирован.
- Given заданы
OwnerUserId и подстрока Url одновременно, When запрос, Then фильтры
комбинируются по И: выдаются только ссылки указанного владельца, чей slug содержит подстроку
(case-insensitive, нормализация к lowercase); soft-deleted ссылки в список не попадают (их показывает
карточка по id — get-referral-link-by-id.md); порядок createdAt desc
со стабильным вторичным ключом Id (README.md).
- Given под фильтр не попадает ни одна ссылка, When запрос, Then
200 с meta.total = 0,
items = [].
- Given реплика владельца отсутствует, When запрос, Then
ownerName пустой, ownerPhoneNumber = null, registrationsCount = 0 при отсутствии регистраций (fail-soft).
- Given
Take не передан, When запрос, Then применяется server-side default (admin-параметр
опционален); Take вне 1..1000/Offset < 0/невалидный OwnerUserId/Url > 64 → 400 ValidationError.
- Given актор без permission, When запрос, Then
403 REFERRAL_ADMIN_FORBIDDEN; без токена —
401.
Совместимость и наблюдаемость
- Контракт стабилен. Новые optional поля
AdminReferralLinkEntry — additive; удаление/переименование
полей — breaking (migration + rollout). Unknown ReferralChannel при чтении — fallback Other +
warning (referral-link.md).
- Logs:
admin-referral-links-listed (AdminUserId, total, размер страницы; телефоны — маской
+7***1234, ФИО в лог не пишется). Metrics: referral_admin_links_requests_total, latency p95.
Traces — сквозной trace через traceparent; dashboard/runbook — ../README.md
(раздел Runtime).
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы