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

POST /api/investors/me/referral-links/default — Ссылка по умолчанию

Назначение

Идемпотентно выдаёт реферальную ссылку «по умолчанию» текущего пользователя: если default-ссылка уже существует — возвращает её; если нет — создаёт со случайным slug и возвращает. Используется перед шарингом (кнопки «Поделиться», SharePopover подборок, /profile/partner): фронт гарантирует наличие ссылки одним вызовом, не различая «первый раз / повторно».

Metadata

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

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

ПроверкаЗначение
Actor typeUser (авторизованный пользователь)
Auth policy / Permissionsreferral.links.manage-self
Scopeself (роль не требуется)
Record-levelтолько собственная default-ссылка: OwnerUserId = UserId актора
Tenant isolationинвестор вне организации (TenantId = null, ADR-0052); для пользователя компании TenantId = CompanyId из реплики фиксируется в создаваемой ссылке
Auditcreate (в ветке создания): actor-stamping + событие ReferralLinkCreatedEvent

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/investors/me/referral-links/default
Success status200 (и при возврате существующей, и при создании — клиент не различает ветки)
Idempotencyоперация идемпотентна по построению (upsert-семантика INV-REF-L2)
CorrelationOpenTelemetry trace (traceparent)

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

Body/route/query-параметров нет.

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity claims (sub)GuidДавалидный JWT (подпись, exp, notBefore)401 при отсутствии/просрочке

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

ПолеТипОбязательностьИсточникMaskingОписание
entryReferralLinkLookupEntryДаReferralLinkEntity (найденный или созданный)Lookup-представление default-ссылки; всегда присутствует.

Модель ReferralLinkLookupEntry (дом; переиспользуется lookup-операциями)

ПолеТипОбязательностьИсточникMaskingОписание
idstring (Guid)ДаReferralLinkEntity.IdИдентификатор ссылки.
urlstringДаReferralLinkEntity.UrlSlug (lowercase); полный URL собирает фронт.
descriptionstring?НетReferralLinkEntity.DescriptionОписание; у автосозданной default-ссылки null.
isDefaultboolДаReferralLinkEntity.IsDefaultЗдесь всегда true.

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

  • Предусловия. Пользователь авторизован. У владельца допускается не более одной default-ссылки (INV-REF-L2); на входе default-ссылки может ещё не быть.
  • Постусловия. После операции у владельца гарантированно ровно одна действующая default-ссылка (IsDefault = true, не удалена), и она возвращена в entry. В ветке создания добавлена ссылка (Channel = Direct, VisitsCount = 0) и поднято доменное ReferralLinkCreatedEvent; в ветке возврата существующей состояние не меняется и событие не публикуется. Интеграционные Kafka-события ссылка не порождает (referral-link.md).

Алгоритм

  1. Контекст и доступ. Операцию выполняет авторизованный пользователь; UserId — из claim sub (иначе 401); требуется право referral.links.manage-self. Идемпотентность обеспечивается самой семантикой шага 2 (find-first), отдельный ключ не нужен.
  2. Что читаем. Действующую default-ссылку владельца (OwnerUserId = UserId, IsDefault = true, не удалена). Если найдена — возвращаем её (шаг 5), создание не выполняется.
  3. Создание (если 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.
  4. Конкурентное создание. Гонка двух одновременных вызовов разрешается partial unique-индексом ux_referral_links_owner_default: проигравший конфликт перечитывает default-ссылку и возвращает созданную конкурентом — идемпотентный исход, не ошибка (INV-REF-L2).
  5. Результат. GetOrCreateDefaultReferralLinkResult с entry (id, url, description, isDefault = true) из найденной/созданной ссылки → HTTP 200.

Диаграмма

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
— (auth)401Нет/просрочен/невалиден JWTбез тела схемы (фронт очищает сессию)re-login
REFERRAL_LINK_SLUG_GENERATION_FAILED4003 подряд коллизии сгенерированного 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.

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

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

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