GET /api/v1/partner/stats — Партнёрская сводка по каналам
Назначение
Сводная партнёрская статистика владельца по всем его реферальным ссылкам с разбивкой по каналам
(ReferralChannel): переходы, регистрации, запрошенные лиды, реферальные связи, сделки (фаза 2).
Основа страницы /profile/partner: пользователь-партнёр сравнивает конверсию «переход → регистрация
→ связь» между каналами (Telegram, QrOffline, Email и т.д.), чтобы понять, какие каналы
привлечения работают.
| Поле | Значение |
|---|
| Сервис/контекст | referral |
| API area | public |
| Feature group | partner-stats |
| Статус | draft |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (владелец ссылок; тип учётки любой) |
| Auth policy / Permissions | referral.links.view-self (каталог — permission.md) |
| Scope | платформа (self-service) |
| Record-level | агрегируются только собственные данные: все выборки фильтруются OwnerUserId/ReferrerUserId = actor.UserId; параметр «чужой пользователь» не принимается |
| Tenant isolation | не применяется как фильтр запроса: record-level по владельцу строже (ADR-0052; у инвестора TenantId = null) |
| Audit | none (чтение собственных агрегатов без PII) |
HTTP-контракт
| Поле | Значение |
|---|
| Method | GET |
| Route | /api/v1/partner/stats |
| Success status | 200 |
| Idempotency | не применяется (безопасный read) |
| Correlation | OpenTelemetry trace (traceparent) |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
UserId | identity claims (sub) | Guid | Да | Required | единственный фильтр всех агрегатов (record-level) |
Body и query-параметры отсутствуют. Показатели накопительные (без периода from/to) — то же
ограничение и тот же путь additive-расширения, что у get-link-stats.md.
Модель ответа GetPartnerStatsQueryResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
Totals | PartnerStatsTotalsModel | Да | computed (сумма строк Channels) | — | Итого по всем каналам владельца. |
Channels | IReadOnlyList<PartnerChannelStatsItem> | Да | computed (группировка по каналу) | — | Разбивка по каналам; каналы без ссылок владельца не включаются; сортировка — по RegistrationsCount desc, затем Channel asc (детерминированно). |
GeneratedAt | DateTimeOffset | Да | момент расчёта | — | Freshness для кэширования на фронте. |
PartnerStatsTotalsModel
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
LinksCount | int | Да | число действующих ссылок владельца (deleted_at is null) | — | Действующие ссылки владельца. |
VisitsCount | long | Да | сумма VisitsCount по ссылкам владельца (включая soft-deleted) | — | Все переходы по ссылкам владельца. |
RegistrationsCount | int | Да | число записей журнала регистраций с referrer_user_id = UserId | — | Регистрации/привязки по журналу (Initial + Rebind; денормализованный ReferrerUserId — метрики не зависят от последующих изменений ссылок, referral-registration.md). |
LeadsRequestedCount | int | Да | число записей журнала с lead_requested | — | Привязки, запросившие карточку лида в CRM (LeadRequested = true). |
ActiveBondsCount | int | Да | число связей владельца со Status = Active | — | Действующие реферальные связи (основание будущих выплат — rewards). |
TotalBondsCount | int | Да | число всех связей владельца (все статусы) | — | Все связи, включая Replaced/Revoked. |
DealsCount | int? | Нет | фаза 2 | — | Сделки из лидов ссылок владельца; всегда null до внедрения consumer-а pin.crm.deals.v1 (правило фазы 2 — get-link-stats.md). |
PartnerChannelStatsItem
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
Channel | string (enum ReferralChannel) | Да | ReferralLinkEntity.Channel (группировка) | — | Канал (дом enum — referral-link.md); unknown при чтении — fallback Other + warning. |
LinksCount | int | Да | действующие ссылки канала (deleted_at is null) | — | Действующие ссылки канала. |
VisitsCount | long | Да | сумма VisitsCount ссылок канала (включая soft-deleted) | — | Переходы канала. |
RegistrationsCount | int | Да | регистрации по ссылкам канала | — | Регистрации канала. |
LeadsRequestedCount | int | Да | то же с фильтром lead_requested | — | Запрошенные лиды канала. |
ActiveBondsCount | int | Да | связи канала со Status = Active | — | Действующие связи канала. |
TotalBondsCount | int | Да | все связи канала (все статусы) | — | Все связи канала. |
DealsCount | int? | Нет | фаза 2 | — | Всегда null до фазы 2. |
Правило учёта soft-deleted ссылок: удалённая ссылка исключается из LinksCount, но её
исторические VisitsCount/регистрации/связи остаются в показателях канала — история канала не
обнуляется удалением ссылки (журнал и связи живут после soft-delete, INV-REF-R2 и restrict-связи в
referral-link.md).
Семантика воронки и лидов (единая с get-link-stats.md): открытия ссылки (правило
R4, validate-referral-link.md) растят только VisitsCount, но не пишут
журнал и связь — поэтому по каналу штатно VisitsCount >= RegistrationsCount >= ActiveBondsCount
(воронка «переход → регистрация → связь»). LeadsRequestedCount считает факты запроса лида
(журнал LeadRequested = true), а не активные карточки лидов CRM (лид мог быть закрыт/объединён на
стороне CRM).
Предусловия и постусловия
- Предусловия. Пользователь авторизован. У владельца может не быть ни одной ссылки (валидный кейс —
пустая сводка). Регистрации и связи наполняются асинхронно привязкой (лаг consumer-ов identity, SLO
< 1 мин, README.md) — сводка свежа с точностью до этого лага.
- Постусловия. Состояние не меняется (чтение); события не публикуются;
GeneratedAt фиксирует
момент среза для клиентского кэша.
Алгоритм
- Контекст и доступ. Операцию выполняет владелец ссылок;
UserId — из claim sub; требуется
право referral.links.view-self, отказ → 403.
- Проверка входа.
UserId != Guid.Empty. Существование сущностей не проверяется: отсутствие
ссылок у владельца — валидный кейс (пустая сводка, не ошибка).
- Что читаем. Три групповые (по каналу) выборки, все фильтруются владельцем:
- ссылки: по ссылкам владельца —
LinksCount (не удалённые) и VisitsCount (сумма);
- журнал: по записям с
referrer_user_id = UserId (канал берётся из связанной ссылки) —
RegistrationsCount и LeadsRequestedCount (записи с lead_requested);
- связи: по связям владельца (канал из ссылки) —
ActiveBondsCount (Status = Active) и
TotalBondsCount (все статусы).
- Маппинг. Слить три группировки в разбивку по
Channel (отсутствующая метрика канала → 0);
построить PartnerChannelStatsItem поле-в-поле; DealsCount = null (фаза 2); Totals —
суммирование строк каналов поле-в-поле; сортировка Channels по RegistrationsCount desc,
затем Channel asc; GeneratedAt = момент расчёта.
- Результат.
GetPartnerStatsQueryResult → 200; при отсутствии ссылок — Totals c нулями и
пустой Channels (не 404). Masking не требуется (агрегаты без PII). Freshness: лаг привязок = лаг
consumer-ов identity (SLO < 1 мин, README.md); GeneratedAt — маркер среза.
Диаграмма
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
FORBIDDEN | 403 | Нет referral.links.view-self | «Недостаточно прав» | no retry |
UNAUTHORIZED | 401 | Нет/просрочен access-токен | — | retry после refresh-token |
Доменных ошибок нет: пустые данные — валидный ответ с нулями.
Критерии приёмки
- Given владелец с ссылками в нескольких каналах и активностью, When запрос сводки, Then
200: Channels содержит по строке на канал с ненулевой активностью, отсортированы по
RegistrationsCount desc → Channel asc; Totals = поле-в-поле сумма строк; DealsCount = null.
- Given владелец без единой ссылки, When запрос, Then
200 с Totals из нулей и пустым
Channels (не 404).
- Given у владельца есть soft-deleted ссылка с историей, When запрос, Then её
VisitsCount,
регистрации и связи остаются в показателях своего канала, но LinksCount её не считает.
- Given ссылка с неизвестным (после additive-расширения) значением
Channel, When запрос,
Then её метрики агрегируются в строку Other + warning (без сбоя ответа).
- Given актор без permission
referral.links.view-self, When запрос, Then FORBIDDEN
(403); при отсутствии/просрочке токена — UNAUTHORIZED (401).
- Given параметр «чужой пользователь» в запросе, When запрос, Then он игнорируется —
агрегируются только собственные данные (record-level по
UserId).
- Given канал с переходами без регистраций (в т.ч. открытия по правилу R4), When запрос,
Then его
VisitsCount >= RegistrationsCount >= ActiveBondsCount (воронка сохраняется), а
Totals — поле-в-поле сумма строк с той же семантикой.
Совместимость и наблюдаемость
- Новые optional поля (
DealsCount-заполнение, периодные срезы from/to, элементы enum
ReferralChannel) — additive; удаление/переименование/смена типа полей, изменение семантики
счётчиков — breaking (migration + rollout).
- Unknown
Channel при чтении — строки агрегируются в Other + warning (правило enum —
referral-link.md).
- Logs:
UserId, число каналов, длительность каждой из трёх группировок (без PII); metrics:
referral_partner_stats_get_total{result}, histogram referral_stats_query_duration_seconds
(общая с get-link-stats.md); traces — OpenTelemetry. Dashboard —
referral-overview (панель «конверсия по каналам»); алерт — p95 > 300 мс (SLO public API,
README.md).
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы