POST /api/investors/me/referral-links/default — Ссылка по умолчанию
Назначение
Идемпотентно выдаёт реферальную ссылку «по умолчанию» текущего пользователя: если default-ссылка
уже существует — возвращает её; если нет — создаёт со случайным slug и возвращает. Используется
перед шарингом (кнопки «Поделиться», SharePopover подборок, /profile/partner): фронт гарантирует
наличие ссылки одним вызовом, не различая «первый раз / повторно».
| Поле | Значение |
|---|
| Сервис/контекст | referral |
| API area | public |
| Feature group | referral-links |
| Статус | draft |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (авторизованный пользователь) |
| Auth policy / Permissions | referral.links.manage-self |
| Scope | self (роль не требуется) |
| Record-level | только собственная default-ссылка: OwnerUserId = UserId актора |
| Tenant isolation | инвестор вне организации (TenantId = null, ADR-0052); для пользователя компании TenantId = CompanyId из реплики фиксируется в создаваемой ссылке |
| Audit | create (в ветке создания): actor-stamping + событие ReferralLinkCreatedEvent |
HTTP-контракт
| Поле | Значение |
|---|
| Method | POST |
| Route | /api/investors/me/referral-links/default |
| Success status | 200 (и при возврате существующей, и при создании — клиент не различает ветки) |
| Idempotency | операция идемпотентна по построению (upsert-семантика INV-REF-L2) |
| Correlation | OpenTelemetry trace (traceparent) |
Входные данные / параметры запроса
Body/route/query-параметров нет.
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
UserId | identity claims (sub) | Guid | Да | валидный JWT (подпись, exp, notBefore) | 401 при отсутствии/просрочке |
Модель ответа GetOrCreateDefaultReferralLinkResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
entry | ReferralLinkLookupEntry | Да | ReferralLinkEntity (найденный или созданный) | — | Lookup-представление default-ссылки; всегда присутствует. |
Модель ReferralLinkLookupEntry (дом; переиспользуется lookup-операциями)
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
id | string (Guid) | Да | ReferralLinkEntity.Id | — | Идентификатор ссылки. |
url | string | Да | ReferralLinkEntity.Url | — | Slug (lowercase); полный URL собирает фронт. |
description | string? | Нет | ReferralLinkEntity.Description | — | Описание; у автосозданной default-ссылки null. |
isDefault | bool | Да | ReferralLinkEntity.IsDefault | — | Здесь всегда true. |
Предусловия и постусловия
- Предусловия. Пользователь авторизован. У владельца допускается не более одной default-ссылки
(INV-REF-L2); на входе default-ссылки может ещё не быть.
- Постусловия. После операции у владельца гарантированно ровно одна действующая default-ссылка
(
IsDefault = true, не удалена), и она возвращена в entry. В ветке создания добавлена ссылка
(Channel = Direct, VisitsCount = 0) и поднято доменное ReferralLinkCreatedEvent; в ветке
возврата существующей состояние не меняется и событие не публикуется. Интеграционные Kafka-события
ссылка не порождает (referral-link.md).
Алгоритм
- Контекст и доступ. Операцию выполняет авторизованный пользователь;
UserId — из claim sub
(иначе 401); требуется право referral.links.manage-self. Идемпотентность обеспечивается самой
семантикой шага 2 (find-first), отдельный ключ не нужен.
- Что читаем. Действующую default-ссылку владельца (
OwnerUserId = UserId, IsDefault = true,
не удалена). Если найдена — возвращаем её (шаг 5), создание не выполняется.
- Создание (если default-ссылки нет). Сгенерировать slug: 10 символов алфавита
[a-z0-9]
(CSPRNG); при коллизии с существующей ссылкой (INV-REF-L1) — повтор до 3 раз, после исчерпания →
REFERRAL_LINK_SLUG_GENERATION_FAILED. Создать ссылку: OwnerUserId = UserId;
TenantId = CompanyId владельца из реплики (null — инвестор или реплика отсутствует);
Url = сгенерированный slug; Description = null; IsDefault = true (INV-REF-L2);
Channel = Direct; VisitsCount = 0. Создание поднимает событие ReferralLinkCreatedEvent.
- Конкурентное создание. Гонка двух одновременных вызовов разрешается partial unique-индексом
ux_referral_links_owner_default: проигравший конфликт перечитывает default-ссылку и возвращает
созданную конкурентом — идемпотентный исход, не ошибка (INV-REF-L2).
- Результат.
GetOrCreateDefaultReferralLinkResult с entry (id, url, description,
isDefault = true) из найденной/созданной ссылки → HTTP 200.
Диаграмма
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
| — (auth) | 401 | Нет/просрочен/невалиден JWT | без тела схемы (фронт очищает сессию) | re-login |
REFERRAL_LINK_SLUG_GENERATION_FAILED | 400 | 3 подряд коллизии сгенерированного slug (практически невозможно: пространство 36^10) | «Не удалось создать ссылку, повторите попытку» | retry |
Конфликт гонки создания — не ошибка (идемпотентный retry, шаг 4).
Критерии приёмки
- Given у владельца уже есть default-ссылка, When вызов, Then
200 с существующей entry
(isDefault = true), новая ссылка не создаётся, событие не публикуется.
- Given у владельца нет default-ссылки, When вызов, Then
200 с созданной entry
(Channel = Direct, VisitsCount = 0, description = null), поднято ReferralLinkCreatedEvent.
- Given два одновременных первых вызова одного владельца, When оба доходят до создания, Then
выигрывает один (partial unique
ux_referral_links_owner_default), проигравший перечитывает и
возвращает ту же default-ссылку — оба получают 200 с одинаковым entry (идемпотентный исход, не
ошибка, INV-REF-L2).
- Given три подряд коллизии сгенерированного slug (пространство
36^10, практически невозможно),
When создание, Then REFERRAL_LINK_SLUG_GENERATION_FAILED (400), клиент повторяет.
- Given повторный вызов после успешного создания, When вызов, Then
200 с той же entry
(идемпотентность по построению).
- Given владелец — пользователь компании и его реплика
ReferralUserReplica уже наполнена, When
создание default-ссылки, Then TenantId создаваемой ссылки = CompanyId из реплики (историческая
принадлежность фиксируется в момент создания).
- Given владелец — инвестор или реплика владельца ещё не догнала (лаг consumer-а
user-upserted), When создание, Then TenantId = null — ссылка создаётся без tenant (это не
ошибка; для инвестора TenantId всегда null, ADR-0052).
- Given отсутствует/просрочен токен, When вызов, Then
401.
Совместимость и наблюдаемость
- Контракт стабилен; additive: у
entry может появиться optional channel (default-ссылка — всегда
Direct) — без breaking change; удаление/переименование полей — breaking (migration + rollout).
- Logs:
UserId, ветка (found/created), LinkId — без PII; metrics:
referral_default_link_requests_total{outcome=found|created}; traces — OpenTelemetry.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы