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

GET /api/v1/partner/stats — Партнёрская сводка по каналам

Назначение

Сводная партнёрская статистика владельца по всем его реферальным ссылкам с разбивкой по каналам (ReferralChannel): переходы, регистрации, запрошенные лиды, реферальные связи, сделки (фаза 2). Основа страницы /profile/partner: пользователь-партнёр сравнивает конверсию «переход → регистрация → связь» между каналами (Telegram, QrOffline, Email и т.д.), чтобы понять, какие каналы привлечения работают.

Metadata

ПолеЗначение
Сервис/контекстreferral
API areapublic
Feature grouppartner-stats
Статусdraft

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

ПроверкаЗначение
Actor typeUser (владелец ссылок; тип учётки любой)
Auth policy / Permissionsreferral.links.view-self (каталог — permission.md)
Scopeплатформа (self-service)
Record-levelагрегируются только собственные данные: все выборки фильтруются OwnerUserId/ReferrerUserId = actor.UserId; параметр «чужой пользователь» не принимается
Tenant isolationне применяется как фильтр запроса: record-level по владельцу строже (ADR-0052; у инвестора TenantId = null)
Auditnone (чтение собственных агрегатов без PII)

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/v1/partner/stats
Success status200
Idempotencyне применяется (безопасный read)
CorrelationOpenTelemetry trace (traceparent)

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity claims (sub)GuidДаRequiredединственный фильтр всех агрегатов (record-level)

Body и query-параметры отсутствуют. Показатели накопительные (без периода from/to) — то же ограничение и тот же путь additive-расширения, что у get-link-stats.md.

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

ПолеТипОбязательностьИсточникMaskingОписание
TotalsPartnerStatsTotalsModelДаcomputed (сумма строк Channels)Итого по всем каналам владельца.
ChannelsIReadOnlyList<PartnerChannelStatsItem>Даcomputed (группировка по каналу)Разбивка по каналам; каналы без ссылок владельца не включаются; сортировка — по RegistrationsCount desc, затем Channel asc (детерминированно).
GeneratedAtDateTimeOffsetДамомент расчётаFreshness для кэширования на фронте.

PartnerStatsTotalsModel

ПолеТипОбязательностьИсточникMaskingОписание
LinksCountintДачисло действующих ссылок владельца (deleted_at is null)Действующие ссылки владельца.
VisitsCountlongДасумма VisitsCount по ссылкам владельца (включая soft-deleted)Все переходы по ссылкам владельца.
RegistrationsCountintДачисло записей журнала регистраций с referrer_user_id = UserIdРегистрации/привязки по журналу (Initial + Rebind; денормализованный ReferrerUserId — метрики не зависят от последующих изменений ссылок, referral-registration.md).
LeadsRequestedCountintДачисло записей журнала с lead_requestedПривязки, запросившие карточку лида в CRM (LeadRequested = true).
ActiveBondsCountintДачисло связей владельца со Status = ActiveДействующие реферальные связи (основание будущих выплат — rewards).
TotalBondsCountintДачисло всех связей владельца (все статусы)Все связи, включая Replaced/Revoked.
DealsCountint?Нетфаза 2Сделки из лидов ссылок владельца; всегда null до внедрения consumer-а pin.crm.deals.v1 (правило фазы 2 — get-link-stats.md).

PartnerChannelStatsItem

ПолеТипОбязательностьИсточникMaskingОписание
Channelstring (enum ReferralChannel)ДаReferralLinkEntity.Channel (группировка)Канал (дом enum — referral-link.md); unknown при чтении — fallback Other + warning.
LinksCountintДадействующие ссылки канала (deleted_at is null)Действующие ссылки канала.
VisitsCountlongДасумма VisitsCount ссылок канала (включая soft-deleted)Переходы канала.
RegistrationsCountintДарегистрации по ссылкам каналаРегистрации канала.
LeadsRequestedCountintДато же с фильтром lead_requestedЗапрошенные лиды канала.
ActiveBondsCountintДасвязи канала со Status = ActiveДействующие связи канала.
TotalBondsCountintДавсе связи канала (все статусы)Все связи канала.
DealsCountint?Нетфаза 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 фиксирует момент среза для клиентского кэша.

Алгоритм

  1. Контекст и доступ. Операцию выполняет владелец ссылок; UserId — из claim sub; требуется право referral.links.view-self, отказ → 403.
  2. Проверка входа. UserId != Guid.Empty. Существование сущностей не проверяется: отсутствие ссылок у владельца — валидный кейс (пустая сводка, не ошибка).
  3. Что читаем. Три групповые (по каналу) выборки, все фильтруются владельцем:
    • ссылки: по ссылкам владельца — LinksCount (не удалённые) и VisitsCount (сумма);
    • журнал: по записям с referrer_user_id = UserId (канал берётся из связанной ссылки) — RegistrationsCount и LeadsRequestedCount (записи с lead_requested);
    • связи: по связям владельца (канал из ссылки) — ActiveBondsCount (Status = Active) и TotalBondsCount (все статусы).
  4. Маппинг. Слить три группировки в разбивку по Channel (отсутствующая метрика канала → 0); построить PartnerChannelStatsItem поле-в-поле; DealsCount = null (фаза 2); Totals — суммирование строк каналов поле-в-поле; сортировка Channels по RegistrationsCount desc, затем Channel asc; GeneratedAt = момент расчёта.
  5. Результат. GetPartnerStatsQueryResult200; при отсутствии ссылок — Totals c нулями и пустой Channels (не 404). Masking не требуется (агрегаты без PII). Freshness: лаг привязок = лаг consumer-ов identity (SLO < 1 мин, README.md); GeneratedAt — маркер среза.

Диаграмма

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
FORBIDDEN403Нет referral.links.view-self«Недостаточно прав»no retry
UNAUTHORIZED401Нет/просрочен 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).

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

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

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