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

GET /api/investors/me/referrer — свой реферер пользователя

Назначение

Показывает авторизованному пользователю его реферера («кто меня привёл») — на странице «Мои рефералы» и в профиле. Возвращает факт наличия связи, id реферера, ссылку привязки, момент привязки и имя реферера для отображения. У пользователя не более одной активной реферальной связи (инвариант INV-REF-001, referral-bond.md): активной связи либо нет (hasReferrer = false), либо ровно одна. Операция только читает — сущности не меняет и событий не производит.

Metadata

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

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

ПроверкаЗначение
Actor typeUser (авторизованный пользователь)
Auth policy / Permissionsreferral.links.view-self (каталог — permission.md)
Scopeплатформа (self-service)
Record-levelДа — читается только связь с ReferredUserId = UserId актора; чужой реферер недоступен и неотличим от отсутствия (нет user enumeration)
Tenant isolationне применяется (инвестор вне организации, ADR-0052); изоляция — record-level
Auditnone (несенситивное чтение id + даты + имя реферера, показываемое самому приглашённому)

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/investors/me/referrer
Success status200 (всегда, в т.ч. при hasReferrer = false — отсутствие связи не 404)
Idempotencyне применяется (безопасный read)
CorrelationOpenTelemetry trace (traceparent)

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

Тело/route/query отсутствуют: единственный вход — идентичность актора.

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity claim subGuidДа!= Guid.EmptyRecord-level: жёстко фиксирует ReferredUserId фильтра (клиент id не передаёт)

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

Дом модели (первый владелец). Все Guid?/DateTimeOffset?/string? = null при hasReferrer = false.

ПолеТипОбязательностьИсточникMaskingОписание
hasReferrerboolДаcomputedtrue — активная связь найдена; false — реферера нет.
referrerInvestorIdGuid?НетReferralBondEntity.ReferrerUserIdId реферера (реферером может быть любой пользователь платформы).
referralLinkIdGuid?НетReferralBondEntity.ReferralLinkIdСсылка, через которую произошла привязка.
boundAtDateTimeOffset?НетReferralBondEntity.BoundAtМомент привязки.
referrerFirstNamestring?НетReferralUserReplica.Name.FirstNameне логируется (PII)Имя реферера для отображения; null — реплика ещё не догнала (лаг consumer-а) или имя пустое (lazy-регистрация).
linkUrlstring?НетReferralLinkEntity.UrlSlug ссылки привязки (если запрошен join ссылки).
linkDescriptionstring?НетReferralLinkEntity.DescriptionОписание ссылки.

Предусловия и постусловия

  • Предусловия. Пользователь авторизован. Связь (если есть) создана процессом привязки (attach-rules.md); её единственность в статусе Active гарантирует partial unique-индекс (INV-REF-001). Имя реферера отдаётся из реплики ReferralUserReplica, наполняемой consumer-ом user-upserted; при лаге/отсутствии реплики имя = null (graceful degradation, не ошибка).
  • Промежуточное состояние (eventual consistency). Связь создаётся асинхронно consumer-ами привязки (user-upserted.md — T1, user-signed-in.md — T2) после регистрации/входа по ссылке. В окне между регистрацией и фиксацией связи (SLO привязки < 1 мин, README.md) эта операция штатно возвращает hasReferrer = false — не ошибка; клиент может перезапросить. То же окно применимо к смене реферера admin-коррекцией (Replaced): до фиксации замены читается прежний реферер.
  • Постусловия. Состояние не меняется (чтение); события не публикуются.

Алгоритм

  1. Контекст и доступ. Операцию выполняет авторизованный пользователь; UserId — из claim sub; требуется право referral.links.view-self. При отсутствии/невалидности JWT → 401, при отсутствии права → 403.
  2. Проверка входа. UserId != Guid.Empty — иначе VALIDATION_FAILED. Существование связи по id не проверяется: её отсутствие — валидный исход hasReferrer = false, а не ошибка.
  3. Что читаем. Активную реферальную связь актора (ReferredUserId = UserId, Status = Active); единственность гарантирует partial unique-индекс (INV-REF-001). Если активной связи нет — hasReferrer = false, остальные поля null. Если связь есть — дополнительно имя реферера из реплики ReferralUserReplica (может отсутствовать при лаге реплики → referrerFirstName = null) и, опционально, url/description ссылки привязки.
  4. Маппинг. hasReferrer = true; referrerInvestorId = ReferrerUserId; referralLinkId = ReferralLinkId; boundAt = BoundAt; referrerFirstName — имя из реплики; linkUrl/linkDescription — из ссылки. Защитная проверка: если активных связей найдено больше одной (нарушение partial unique / INV-REF-001) → REFERRAL_MULTIPLE_ACTIVE_BONDS.
  5. Результат. GetMyReferrerQueryResult → HTTP 200 (в том числе при hasReferrer = false).

Диаграмма

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
VALIDATION_FAILED400UserId пуст/невалиден в claim sub«Некорректный контекст пользователя»no retry
401 Unauthorized401нет/просрочен JWT— (фронт: очистка сессии)no retry
403 Forbidden403нет права referral.links.view-self«Недостаточно прав»no retry
REFERRAL_MULTIPLE_ACTIVE_BONDS500найдено > 1 активной связи (нарушение INV-REF-001 / partial unique)«Внутренняя ошибка»retry (после разбора данных)

Отсутствие связи — не ошибка: 200 c hasReferrer = false. Отставание реплики (referrerFirstName = null) — graceful degradation, не failure.

Критерии приёмки

  • Given у пользователя есть активная реферальная связь и реплика реферера догнала, When запрос своего реферера, Then 200, hasReferrer = true, заполнены referrerInvestorId, referralLinkId, boundAt и referrerFirstName.
  • Given у пользователя нет активной связи, When запрос, Then 200, hasReferrer = false, остальные поля null (отсутствие связи — не 404).
  • Given пользователь только что зарегистрировался/вошёл по реф-ссылке, но consumer привязки ещё не зафиксировал связь (окно eventual consistency), When запрос, Then 200, hasReferrer = false — не ошибка; при повторном запросе после фиксации (SLO < 1 мин) вернётся hasReferrer = true.
  • Given активная связь есть, но реплика реферера ещё не догнала (лаг consumer-а) или FirstName пуст (lazy-регистрация), When запрос, Then 200, hasReferrer = true, referrerFirstName = null — деградация, не ошибка.
  • Given нарушена единственность (найдено > 1 активной связи — сбой partial unique / INV-REF-001), When запрос, Then REFERRAL_MULTIPLE_ACTIVE_BONDS (500), требуется разбор данных.
  • Given пустой/невалидный sub в токене, When запрос, Then VALIDATION_FAILED (400); при отсутствии права — 403; при отсутствии токена — 401.

Совместимость и наблюдаемость

  • referrerInvestorId/referralLinkId/boundAt/referrerFirstName не переименовывать без migration-плана; linkUrl/linkDescription — additive optional (без breaking). Unknown-enum неприменим (в ответе enum нет).
  • Logs: get-my-referrer served (UserId, hasReferrer, referralLinkId; referrerFirstName не пишется — PII); metrics: referral_get_my_referrer_total{has_referrer}, latency p95 (< 300 мс, SLO README); traces — OpenTelemetry; dashboard referral-overview.

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

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

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