GET /api/v1/referral-links/{referralLinkId}/stats — Статистика одной реферальной ссылки
Назначение
Детальная статистика конкретной ссылки владельца: переходы (VisitsCount), регистрации по журналу,
запрошенные лиды, реферальные связи (активные/всего) и сделки (фаза 2). Владелец оценивает
эффективность конкретного канала/размещения — экран деталей ссылки на странице /profile/partner.
Реферальная связь — основание будущих выплат (referral-bond.md).
Показатели — агрегаты по журналу/связям + счётчик VisitsCount (решение №6
README.md: без ClickHouse/Redis).
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | referral |
| API area | public |
| Feature group | partner-stats |
| Статус | draft |
| Документ | docs/06-services/referral/api/public/get-link-stats.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (владелец ссылки) |
| Auth policy / Permissions | referral.links.view-self (каталог — permission.md) |
| Scope (RBAC) | self-service |
| Record-level | строго своя ссылка: Id == referralLinkId && OwnerUserId == actor.UserId; чужая/несуществующая → REFERRAL_LINK_NOT_FOUND без раскрытия существования (INV-REF-L4) |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | GET |
| Route | /api/v1/referral-links/{referralLinkId}/stats |
| Success status | 200 |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
UserId | identity claims (sub) | Guid | Да | Required | record-level фильтр OwnerUserId |
ReferralLinkId | route:referralLinkId | Guid | Да | непустой; невалидный Guid → 400 | untrusted route-параметр |
Body и query-параметры отсутствуют. Период (from/to) в первой итерации не поддерживается:
показатели накопительные с момента создания ссылки — VisitsCount не имеет истории по датам
(единственный счётчик-колонка, referral-link.md); периодные срезы по
регистрациям/связям — кандидат на additive-расширение (from/to по RegisteredAt/BoundAt) без
breaking change.
Модель ответа GetReferralLinkStatsQueryResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
ReferralLinkId | Guid | Да | ReferralLinkEntity.Id | — | Идентификатор ссылки. |
Url | string | Да | ReferralLinkEntity.Url | — | Текущий slug (заголовок карточки статистики). |
Description | string? | Нет | ReferralLinkEntity.Description | — | Описание. |
Channel | string (enum ReferralChannel) | Да | ReferralLinkEntity.Channel | — | Канал ссылки (дом enum — referral-link.md). |
IsDefault | bool | Да | ReferralLinkEntity.IsDefault | — | Признак default-ссылки. |
CreatedAt | DateTimeOffset | Да | ReferralLinkEntity.CreatedAt | — | С какого момента копятся показатели. |
VisitsCount | long | Да | ReferralLinkEntity.VisitsCount | — | Переходы: инкременты validateReferralLink (validate-referral-link.md). |
RegistrationsCount | int | Да | computed по журналу | — | Регистрации/привязки (Initial + Rebind); та же семантика, что registrationsCount. |
LeadsRequestedCount | int | Да | computed по журналу (LeadRequested = true) | — | Сколько привязок запросило карточку лида в CRM (referral-registration.md). Факт создания/жизни лида — в CRM. |
ActiveBondsCount | int | Да | computed по связям (Status = Active) | — | Действующие реферальные связи через ссылку (производный — referral-link.md). |
TotalBondsCount | int | Да | computed по связям (все статусы) | — | Все связи, включая Replaced/Revoked (разбор admin-коррекций). |
DealsCount | int? | Нет | фаза 2 (см. «Совместимость») | — | Сделки CRM, дошедшие из лидов этой ссылки. В первой итерации всегда null — referral не располагает данными сделок. |
Предусловия и постусловия
- Предусловия. Пользователь авторизован. Запрашиваемая ссылка существует в системе (в т.ч.
soft-deleted) и принадлежит актору. Журнал регистраций и реферальные связи наполняются асинхронно
привязкой (consumer-ы identity, attach-rules.md) — показатели свежи с
точностью до лага привязки (SLO < 1 мин, README.md);
VisitsCountактуален на момент последнего успешного validate-referral-link.md. - Постусловия. Состояние не меняется (чистое чтение): ни ссылка, ни журнал, ни связи не
модифицируются, события не публикуются,
VisitsCountэтой операцией не инкрементируется. Ответ — снимок счётчиков на момент запроса.
Алгоритм
- Контекст и доступ. Пользователь авторизован; требуется permission
referral.links.view-self(иначеFORBIDDEN). - Проверка входа.
ReferralLinkIdнепустой валидный Guid, иначеValidationError. - Проверка владения. Ссылка должна принадлежать актору (
OwnerUserId == UserId); иначе →REFERRAL_LINK_NOT_FOUND(record-level, INV-REF-L4: чужая ссылка неотличима от несуществующей). Статистика доступна и для soft-deleted ссылки владельца (история канала не пропадает после удаления ссылки). - Сбор показателей. По ссылке считаются: число регистраций (
RegistrationsCount), число запрошенных лидов (LeadsRequestedCount— записи журнала сLeadRequested = true), число действующих связей (ActiveBondsCount—Status = Active) и всех связей (TotalBondsCount).VisitsCountберётся из счётчика ссылки;DealsCount = null(фаза 2). Расхождение счётчиков на единицы при конкурентной привязке допустимо для статистики.- Скоуп по ссылке. Все показатели журнала и связей считаются строго по этой ссылке
(
ReferralLinkId = referralLinkId) — в отличие от get-partner-stats.md, где агрегация идёт по владельцу (ReferrerUserId) и по каналам. - Семантика воронки. Открытия ссылки (правило R4, validate-referral-link.md)
растят только
VisitsCount, но не создают записи журнала и связи. Поэтому штатноVisitsCount >= RegistrationsCount >= ActiveBondsCount— это воронка «переход → регистрация → связь», а не рассинхрон. Обратное соотношение (RegistrationsCount > VisitsCount) возможно лишь как краевой эффект best-effort-инкрементаVisitsCount(потеря инкремента, ADR-0056) и трактуется клиентом как норма, а не ошибка. LeadsRequestedCount— не число живых лидов. Показатель считает факты запроса лида по привязке (журналLeadRequested = true), а не активные карточки лидов CRM: лид мог быть закрыт/ объединён на стороне CRM. Для актуального состояния лидов клиент обращается к CRM.
- Скоуп по ссылке. Все показатели журнала и связей считаются строго по этой ссылке
(
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
ValidationError | 400 | ReferralLinkId пустой/невалидный Guid | «Некорректный идентификатор ссылки» | no retry |
REFERRAL_LINK_NOT_FOUND | 404 | Ссылка не существует или принадлежит другому пользователю (INV-REF-L4 — без раскрытия) | «Ссылка не найдена» | no retry |
FORBIDDEN | 403 | Нет referral.links.view-self | «Недостаточно прав» | no retry |
UNAUTHORIZED | 401 | Нет/просрочен access-токен | «Требуется авторизация» | retry после refresh-token |
Критерии приёмки
- Given авторизованный владелец ссылки, по которой были переходы, регистрации и активные связи,
When запрашивается статистика ссылки, Then
200сVisitsCountиз счётчика ссылки,RegistrationsCount/LeadsRequestedCountиз журнала,ActiveBondsCount/TotalBondsCountиз связей иDealsCount = null(фаза 2). - Given только что созданная ссылка без активности, When запрос, Then
200со всеми счётчиками= 0(не404),DealsCount = null. - Given ссылка принадлежит другому пользователю или не существует, When запрос, Then
REFERRAL_LINK_NOT_FOUND(404) — существование не раскрывается (INV-REF-L4). - Given soft-deleted ссылка владельца, When запрос, Then
200с исторической статистикой — показатели канала удалением ссылки не обнуляются. - Given актор без permission
referral.links.view-self, When запрос, ThenFORBIDDEN(403); при отсутствии/просрочке токена —UNAUTHORIZED(401). - Given конкурентная привязка ровно в момент чтения, When запрос, Then допускается расхождение счётчиков на единицы — статистика eventually consistent, чтение не блокирует привязку.
- Given по ссылке было N переходов, M < N из которых завершились регистрацией, When запрос,
Then
VisitsCount = N,RegistrationsCount = M,VisitsCount >= RegistrationsCount— переходы без регистрации (в т.ч. правило R4) не создают записей журнала, воронка сохраняется.
Совместимость и наблюдаемость
- Новые optional поля ответа (в т.ч. периодные срезы
from/to) — additive; удаление/ переименование/смена типа — breaking (migration + rollout). DealsCount(фаза 2): контрактно поле присутствует с первой версии какint?и равноnull, чтобы включение фазы 2 не меняло схему ответа. Источник фазы 2 — событие завершённой сделки контекстаcrm(топикpin.crm.deals.v1): CRM ведётReferralLinkIdна карточке лида (получен изReferralLeadAttachRequestedKafkaEvent— attach-rules.md, шаг 6) и включает его в событие сделки;referralзаведёт consumer и таблицу фактов сделок для подсчёта per ссылка/канал. До согласования контракта сcrmфаза 2 не активируется; расширение реестра consumed-событий фиксируется в README.md при активации.- Unknown
Channelпри чтении — fallbackOther+ warning (referral-link.md). - Logs:
UserId,LinkId, длительность счётных запросов (без PII); metrics:referral_link_stats_get_total{result}, histogramreferral_stats_query_duration_seconds; traces — сквозной trace черезtraceparent. Dashboard —referral-overview; алерт на p95 длительности счётных запросов > 300 мс (SLO public API, README.md).
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Правила реферальной привязки (attach-rules) — дом доменных правил
- API (5): GET /api/investors/me/referral-links/registrations — Обезличенный журнал по всем моим ссылкам (getAllRegistrations), GET /api/investors/referral-links/{url}/validate — Публичная валидация slug + счётчик переходов, GET /api/v1/partner/stats — Партнёрская сводка по каналам, POST /api/v1/referral-links — Создание реферальной ссылки (с каналом), Referral API — реестр операций и общие контрактные модели
- registries (1): Реестр API-методов PIN
Связанные документы
- Дома полей: referral-link.md (производные показатели),
referral-registration.md (
LeadRequested), referral-bond.md (статусы связей). - Сводка по всем каналам: get-partner-stats.md; источник переходов: validate-referral-link.md; список ссылок: get-my-referral-links.md.
- Лиды:
docs/06-services/crm/README.md; выплаты по связям:docs/06-services/rewards/README.md.