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

POST /api/v1/referral-links — Создание реферальной ссылки (с каналом)

Назначение

Пользователь (любой тип учётки: инвестор, агент, менеджер застройщика, менеджер партнёра, сотрудник ПИН) создаёт новую реферальную ссылку с заданным или случайным slug, описанием и каналом распространения (ReferralChannel), чтобы распространять её в конкретном канале и отдельно отслеживать конверсию «переход → регистрация → связь». Один пользователь создаёт много ссылок под разные каналы. Owning-контекст — referral (дом полей — referral-link.md).

Metadata

ПолеЗначение
Сервис/контекстreferral
API areapublic
Feature groupreferral-links
Статусdraft
Документdocs/06-services/referral/api/public/create-referral-link.md

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

ПроверкаЗначение
Actor typeUser (любой тип учётки — реферальную ссылку может формировать любой пользователь)
Auth policy / Permissionsreferral.links.manage-self (каталог — permission.md)
Scope (RBAC)self-service
Record-levelссылка создаётся строго на актора: OwnerUserId = actor.UserId; чужой ownerUserId в запросе не принимается
Tenant isolationTenantId = CompanyId актора на момент создания (null у инвестора — referral-link.md)
Auditcreate: actor-stamping (CreatedByUserId = actor.UserId) + событие ReferralLinkCreatedEvent

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/referral-links
Success status200 (тело CreateReferralLinkResult)
Idempotencyпри заданном url дубль отсекает уникальность slug (INV-REF-L1), при случайном — повторный вызов осознанно создаёт новую ссылку
Correlationсквозной trace через заголовок traceparent

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity claims (sub)GuidДаRequiredстановится OwnerUserId; из body не принимается
Urlbody.urlstring?НетmaxLength 64; после trim + lowercase — формат ^[a-z0-9][a-z0-9-]{2,63}$ (INV-REF-L3); стоп-лист зарезервированных slug (admin, api, www); null/пусто → генерация случайного sluguntrusted; хранится в lowercase
Descriptionbody.descriptionstring?НетmaxLength 256; trim; пустая строка → nulluntrusted; PII не ожидается, в логах обрезка 64 симв.
Channelbody.channelstring (enum ReferralChannel)Нетодно из значений enum ReferralChannel (дом — referral-link.md: Direct, Telegram, WhatsApp, Vk, Instagram, YouTube, Email, Sms, QrOffline, Blog, Partner, Other); не передан → Direct; неизвестная строка → ValidationError

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

Дом полей — referral-link.md.

ПолеТипОбязательностьИсточникMaskingОписание
IdGuidДаReferralLinkEntity.IdИдентификатор созданной ссылки.
UrlstringДаReferralLinkEntity.UrlИтоговый slug (заданный или сгенерированный), lowercase. Полный URL собирает фронт.
Descriptionstring?НетReferralLinkEntity.DescriptionОписание.
Channelstring (enum ReferralChannel)ДаReferralLinkEntity.ChannelКанал распространения.
IsDefaultboolДаReferralLinkEntity.IsDefaultВсегда false для этой операции: default-ссылка создаётся только getOrCreateDefaultReferralLink (get-or-create-default-referral-link.md, INV-REF-L2).
VisitsCountlongДаReferralLinkEntity.VisitsCountВсегда 0 при создании.
CreatedAtDateTimeOffsetДаReferralLinkEntity.CreatedAtМомент создания.

Алгоритм

  1. Контекст и доступ. Пользователь авторизован; требуется permission referral.links.manage-self (иначе FORBIDDEN).
  2. Проверка входа. Если Url задан — trim, lowercase, длина и формат ^[a-z0-9][a-z0-9-]{2,63}$ (INV-REF-L3), не в стоп-листе (admin, api, www); Description ≤ 256; Channel — известное значение enum ReferralChannel (неизвестное → ValidationError), не передан → Direct.
  3. Определение владельца и tenant. OwnerUserId = actor.UserId; TenantId — компания актора (null у инвестора). При заданном Url: если slug уже занят активной ссылкой → отказ REFERRAL_LINK_URL_TAKEN (INV-REF-L1).
  4. Генерация slug (если не задан). Случайный slug из 10 символов алфавита [a-z0-9]; при коллизии повтор до 3 раз, после — отказ REFERRAL_LINK_URL_TAKEN (правило генерации — referral-link.md, поле Url).
  5. Создание. Появляется ссылка (IsDefault = false, VisitsCount = 0, CreatedAt = now(), канал из входа или Direct), фиксируется событие ReferralLinkCreatedEvent. Конкурентное создание того же slug арбитрируется уникальностью: заданный UrlREFERRAL_LINK_URL_TAKEN; сгенерированный → повтор генерации (до 3 раз). Возвращается карточка созданной ссылки.

Диаграмма

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400Невалидный slug (формат/длина/стоп-лист), description > 256, неизвестный channel«Некорректные данные ссылки» + перечень полейno retry (исправить ввод)
REFERRAL_LINK_URL_TAKEN409Slug занят активной ссылкой (INV-REF-L1) — pre-check или конфликт уникальности; исчерпаны 3 попытки генерации«Адрес ссылки уже занят»retry с другим slug
FORBIDDEN403Нет referral.links.manage-self«Недостаточно прав»no retry
UNAUTHORIZED401Нет/просрочен access-токен«Требуется авторизация»retry после refresh-token

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

Предусловие: пользователь авторизован (любой тип учётки). При заданном Url — slug не занят активной ссылкой (INV-REF-L1); при незаданном — генерируется случайный уникальный.

Постусловия (при успехе): появилась ссылка владельца (OwnerUserId = actor, TenantId = компания актора или null у инвестора, IsDefault = false — INV-REF-L2, VisitsCount = 0, Channel из входа или Direct); поднято доменное ReferralLinkCreatedEvent. Интеграционных Kafka-событий у ссылки нет — другим контекстам нужны факты привязки, не сама ссылка. Число ссылок владельца увеличилось на 1.

Acceptance Criteria

  • Given авторизованный пользователь и свободный валидный Url, When POST /api/v1/referral-links, Then 200 c CreateReferralLinkResult (IsDefault = false, VisitsCount = 0, Channel из входа); slug сохранён в lowercase; доменное событие создания поднято.
  • Given Url не задан, When запрос, Then сгенерирован случайный уникальный slug (10 символов [a-z0-9]); при коллизии — до 3 повторов, после — 409 REFERRAL_LINK_URL_TAKEN.
  • Given заданный Url уже занят активной ссылкой (INV-REF-L1) — pre-check или гонка уникальности, When запрос, Then 409 REFERRAL_LINK_URL_TAKEN (retry с другим slug).
  • Given Channel не передан, When запрос, Then канал = Direct; неизвестная строка Channel400 ValidationError.
  • Given Url в стоп-листе (admin/api/www), нарушает формат ^[a-z0-9][a-z0-9-]{2,63}$ или Description > 256, When запрос, Then 400 ValidationError с перечнем полей.
  • Given чужой ownerUserId в теле, When запрос, Then он игнорируется — ссылка создаётся на актора (OwnerUserId = actor.UserId).
  • Given актор без referral.links.manage-self, When запрос, Then 403; без токена — 401.

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

  • Расширение enum ReferralChannel новыми значениями — additive; unknown на чтении — fallback Other + warning (referral-link.md). Удаление/переименование/смена типа полей — breaking (migration + rollout).
  • Logs: UserId, LinkId, Channel, факт генерации slug (без полного Description); metrics: referral_link_create_total{channel, result}; traces — сквозной trace через traceparent. Dashboard — referral-overview (README.md, Runtime).
  • Объём ссылок на владельца жёстким инвариантом не ограничен (модель допускает много ссылок по каналам). Защита от массового создания slug (перебор адресного пространства, злоупотребление) — не бизнес-правило, а эксплуатационный контроль: rate-limiting на операцию + алерт на аномальный рост referral_link_create_total по владельцу; при необходимости мягкий порог вводится additive-политикой без смены контракта (assumption — согласуется с продуктом).

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

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

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