GET /api/investors/me/referrer — свой реферер пользователя
Назначение
Показывает авторизованному пользователю его реферера («кто меня привёл») — на странице «Мои
рефералы» и в профиле. Возвращает факт наличия связи, id реферера, ссылку привязки, момент привязки
и имя реферера для отображения. У пользователя не более одной активной реферальной связи (инвариант
INV-REF-001, referral-bond.md): активной связи либо нет
(hasReferrer = false), либо ровно одна. Операция только читает — сущности не меняет и событий не
производит.
| Поле | Значение |
|---|
| Сервис/контекст | referral |
| API area | public |
| Feature group | referral-links |
| Статус | draft |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (авторизованный пользователь) |
| Auth policy / Permissions | referral.links.view-self (каталог — permission.md) |
| Scope | платформа (self-service) |
| Record-level | Да — читается только связь с ReferredUserId = UserId актора; чужой реферер недоступен и неотличим от отсутствия (нет user enumeration) |
| Tenant isolation | не применяется (инвестор вне организации, ADR-0052); изоляция — record-level |
| Audit | none (несенситивное чтение id + даты + имя реферера, показываемое самому приглашённому) |
HTTP-контракт
| Поле | Значение |
|---|
| Method | GET |
| Route | /api/investors/me/referrer |
| Success status | 200 (всегда, в т.ч. при hasReferrer = false — отсутствие связи не 404) |
| Idempotency | не применяется (безопасный read) |
| Correlation | OpenTelemetry trace (traceparent) |
Входные данные / параметры запроса
Тело/route/query отсутствуют: единственный вход — идентичность актора.
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
UserId | identity claim sub | Guid | Да | != Guid.Empty | Record-level: жёстко фиксирует ReferredUserId фильтра (клиент id не передаёт) |
Модель ответа GetMyReferrerQueryResult
Дом модели (первый владелец). Все Guid?/DateTimeOffset?/string? = null при
hasReferrer = false.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
hasReferrer | bool | Да | computed | — | true — активная связь найдена; false — реферера нет. |
referrerInvestorId | Guid? | Нет | ReferralBondEntity.ReferrerUserId | — | Id реферера (реферером может быть любой пользователь платформы). |
referralLinkId | Guid? | Нет | ReferralBondEntity.ReferralLinkId | — | Ссылка, через которую произошла привязка. |
boundAt | DateTimeOffset? | Нет | ReferralBondEntity.BoundAt | — | Момент привязки. |
referrerFirstName | string? | Нет | ReferralUserReplica.Name.FirstName | не логируется (PII) | Имя реферера для отображения; null — реплика ещё не догнала (лаг consumer-а) или имя пустое (lazy-регистрация). |
linkUrl | string? | Нет | ReferralLinkEntity.Url | — | Slug ссылки привязки (если запрошен join ссылки). |
linkDescription | string? | Нет | 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): до фиксации замены читается прежний
реферер.
- Постусловия. Состояние не меняется (чтение); события не публикуются.
Алгоритм
- Контекст и доступ. Операцию выполняет авторизованный пользователь;
UserId — из claim sub;
требуется право referral.links.view-self. При отсутствии/невалидности JWT → 401, при
отсутствии права → 403.
- Проверка входа.
UserId != Guid.Empty — иначе VALIDATION_FAILED. Существование связи по id
не проверяется: её отсутствие — валидный исход hasReferrer = false, а не ошибка.
- Что читаем. Активную реферальную связь актора (
ReferredUserId = UserId, Status = Active);
единственность гарантирует partial unique-индекс (INV-REF-001). Если активной связи нет —
hasReferrer = false, остальные поля null. Если связь есть — дополнительно имя реферера из
реплики ReferralUserReplica (может отсутствовать при лаге реплики → referrerFirstName = null)
и, опционально, url/description ссылки привязки.
- Маппинг.
hasReferrer = true; referrerInvestorId = ReferrerUserId;
referralLinkId = ReferralLinkId; boundAt = BoundAt; referrerFirstName — имя из реплики;
linkUrl/linkDescription — из ссылки. Защитная проверка: если активных связей найдено больше
одной (нарушение partial unique / INV-REF-001) → REFERRAL_MULTIPLE_ACTIVE_BONDS.
- Результат.
GetMyReferrerQueryResult → HTTP 200 (в том числе при hasReferrer = false).
Диаграмма
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
VALIDATION_FAILED | 400 | UserId пуст/невалиден в claim sub | «Некорректный контекст пользователя» | no retry |
401 Unauthorized | 401 | нет/просрочен JWT | — (фронт: очистка сессии) | no retry |
403 Forbidden | 403 | нет права referral.links.view-self | «Недостаточно прав» | no retry |
REFERRAL_MULTIPLE_ACTIVE_BONDS | 500 | найдено > 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.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы