POST /api/v1/referral-links — Создание реферальной ссылки (с каналом)
Назначение
Пользователь (любой тип учётки: инвестор, агент, менеджер застройщика, менеджер партнёра, сотрудник
ПИН) создаёт новую реферальную ссылку с заданным или случайным slug, описанием и каналом
распространения (ReferralChannel), чтобы распространять её в конкретном канале и отдельно
отслеживать конверсию «переход → регистрация → связь». Один пользователь создаёт много ссылок под
разные каналы. Owning-контекст — referral (дом полей — referral-link.md).
| Поле | Значение |
|---|
| Сервис/контекст | referral |
| API area | public |
| Feature group | referral-links |
| Статус | draft |
| Документ | docs/06-services/referral/api/public/create-referral-link.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (любой тип учётки — реферальную ссылку может формировать любой пользователь) |
| Auth policy / Permissions | referral.links.manage-self (каталог — permission.md) |
| Scope (RBAC) | self-service |
| Record-level | ссылка создаётся строго на актора: OwnerUserId = actor.UserId; чужой ownerUserId в запросе не принимается |
| Tenant isolation | TenantId = CompanyId актора на момент создания (null у инвестора — referral-link.md) |
| Audit | create: actor-stamping (CreatedByUserId = actor.UserId) + событие ReferralLinkCreatedEvent |
HTTP-контракт
| Поле | Значение |
|---|
| Method | POST |
| Route | /api/v1/referral-links |
| Success status | 200 (тело CreateReferralLinkResult) |
| Idempotency | при заданном url дубль отсекает уникальность slug (INV-REF-L1), при случайном — повторный вызов осознанно создаёт новую ссылку |
| Correlation | сквозной trace через заголовок traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
UserId | identity claims (sub) | Guid | Да | Required | становится OwnerUserId; из body не принимается |
Url | body.url | string? | Нет | maxLength 64; после trim + lowercase — формат ^[a-z0-9][a-z0-9-]{2,63}$ (INV-REF-L3); стоп-лист зарезервированных slug (admin, api, www); null/пусто → генерация случайного slug | untrusted; хранится в lowercase |
Description | body.description | string? | Нет | maxLength 256; trim; пустая строка → null | untrusted; PII не ожидается, в логах обрезка 64 симв. |
Channel | body.channel | string (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 | Описание |
|---|
Id | Guid | Да | ReferralLinkEntity.Id | — | Идентификатор созданной ссылки. |
Url | string | Да | ReferralLinkEntity.Url | — | Итоговый slug (заданный или сгенерированный), lowercase. Полный URL собирает фронт. |
Description | string? | Нет | ReferralLinkEntity.Description | — | Описание. |
Channel | string (enum ReferralChannel) | Да | ReferralLinkEntity.Channel | — | Канал распространения. |
IsDefault | bool | Да | ReferralLinkEntity.IsDefault | — | Всегда false для этой операции: default-ссылка создаётся только getOrCreateDefaultReferralLink (get-or-create-default-referral-link.md, INV-REF-L2). |
VisitsCount | long | Да | ReferralLinkEntity.VisitsCount | — | Всегда 0 при создании. |
CreatedAt | DateTimeOffset | Да | ReferralLinkEntity.CreatedAt | — | Момент создания. |
Алгоритм
- Контекст и доступ. Пользователь авторизован; требуется permission
referral.links.manage-self
(иначе FORBIDDEN).
- Проверка входа. Если
Url задан — trim, lowercase, длина и формат ^[a-z0-9][a-z0-9-]{2,63}$
(INV-REF-L3), не в стоп-листе (admin, api, www); Description ≤ 256; Channel — известное
значение enum ReferralChannel (неизвестное → ValidationError), не передан → Direct.
- Определение владельца и tenant.
OwnerUserId = actor.UserId; TenantId — компания актора
(null у инвестора). При заданном Url: если slug уже занят активной ссылкой → отказ
REFERRAL_LINK_URL_TAKEN (INV-REF-L1).
- Генерация slug (если не задан). Случайный slug из 10 символов алфавита
[a-z0-9]; при
коллизии повтор до 3 раз, после — отказ REFERRAL_LINK_URL_TAKEN (правило генерации —
referral-link.md, поле Url).
- Создание. Появляется ссылка (
IsDefault = false, VisitsCount = 0, CreatedAt = now(),
канал из входа или Direct), фиксируется событие ReferralLinkCreatedEvent. Конкурентное создание
того же slug арбитрируется уникальностью: заданный Url → REFERRAL_LINK_URL_TAKEN;
сгенерированный → повтор генерации (до 3 раз). Возвращается карточка созданной ссылки.
Диаграмма
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
ValidationError | 400 | Невалидный slug (формат/длина/стоп-лист), description > 256, неизвестный channel | «Некорректные данные ссылки» + перечень полей | no retry (исправить ввод) |
REFERRAL_LINK_URL_TAKEN | 409 | Slug занят активной ссылкой (INV-REF-L1) — pre-check или конфликт уникальности; исчерпаны 3 попытки генерации | «Адрес ссылки уже занят» | retry с другим slug |
FORBIDDEN | 403 | Нет referral.links.manage-self | «Недостаточно прав» | no retry |
UNAUTHORIZED | 401 | Нет/просрочен 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; неизвестная строка
Channel → 400 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 — согласуется с продуктом).
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы