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

GET /api/investors/referral-links/{url}/validate — Публичная валидация slug + счётчик переходов

Дом item-модели ValidateReferralLinkResult (на неё ссылаются ../internal/resolve-referral-link.md, get-link-stats.md, attach-rules.md шаг 8) и точка входа триггера T3 (правило R4 LinkOpenAuthorized) процесса привязки.

Назначение

Аноним или авторизованный пользователь открывает реферальную ссылку — фронт вызывает эту операцию, чтобы показать «вас пригласил <имя>» и решить, класть ли slug в cookie перед входом/регистрацией. Помимо валидации выполняются два побочных эффекта без изменения полей ответа: (1) атомарный инкремент ReferralLinkEntity.VisitsCount (база конверсии «переход → регистрация → связь»); (2) при авторизованном инвесторе и включённом флаге — запуск нормативного процесса привязки по правилу R4 (связь не создаётся, лид — если владелец агент).

Metadata

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

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

ПроверкаЗначение
Actor typeAnonymous (публичный метод); JWT опционален — авторизованный инвестор включает путь T3
Auth policy / Permissionsnone; rate limit по IP (защита от перебора slug)
Scopeне применяется (аноним вне организации)
Record-levelне применяется — ссылка ищется по глобальному slug (INV-REF-L1)
Tenant isolationне применяется к выборке; TenantId ссылки наружу не отдаётся
Auditnone (read + некритичный инкремент счётчика; T3 — через событие)
Trusted/untrusted boundaryurluntrusted (slug из пользовательской cookie referralUrl): нормализация lowercase + формат ^[a-z0-9][a-z0-9-]{2,63}$ (INV-REF-L3) до обращения к хранилищу; несоответствие → isValid=false, не 400

HTTP-контракт

ПолеЗначение
MethodGET
Route/api/investors/referral-links/{url}/validate
Success status200 — валидна/невалидна/не найдена всегда даёт 200 с флагом isValid
Idempotencyне применяется
CorrelationOpenTelemetry trace (traceparent)

GET с намеренным побочным эффектом (инкремент VisitsCount, опц. T3) — осознанное отклонение от «GET безопасен» ради обратной совместимости маршрута: инкремент — некритичная аналитика (потеря/двойной счёт допустимы, ADR-0056), T3 идемпотентен через дедуп лида в CRM.

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
Urlroute:{url}stringДанормализация Trim().ToLowerInvariant(); формат ^[a-z0-9][a-z0-9-]{2,63}$ (INV-REF-L3); несоответствие/длина > 64 → isValid=false (не 400 — мусор из cookie штатен)untrusted (cookie-origin slug); в логах — усечённо и только при isValid=false
ActorUserIdidentity claims (sub)Guid?НетJWT опционален; != null + инвестор + флаг Referral:LinkOpenAttachEnabled → путь T3

Body и query-параметры отсутствуют.

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

Канонический дом item-модели. Наружу отдаётся только публичное имя реферера — фамилия/телефон/TenantId не раскрываются (PII-минимизация).

ПолеТипОбязательностьИсточникMaskingОписание
isValidboolДаcomputedtrue — ссылка найдена и не удалена (DeletedAt == null); false — невалидный формат / не найдена / soft-deleted (не различаются — не раскрываем историю slug).
urlstringДаecho нормализованного slugЭхо запрошенного slug (lowercase) — фронт сверяет с cookie и очищает её при isValid=false.
referrerDisplayNamestring?НетReferralUserReplica.Name.FirstName владельцапубличное имя (только FirstName)Имя пригласившего для баннера «вас пригласил …». null при isValid=false, отсутствии реплики (лаг consumer-а) или пустом FirstName (lazy-регистрация).

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

  • Предусловия. Актор — аноним либо авторизованный (JWT опционален); права не требуются. url — untrusted slug из cookie referralUrl. Путь T3 активен только при включённом флаге Referral:LinkOpenAttachEnabled (default off, README.md).
  • Постусловия. При найденной ссылке VisitsCount инкрементирован атомарно (best-effort: потеря/ двойной счёт допустимы, ADR-0056). По правилу R4 связь и запись журнала не создаются (ReferralBondCreatedKafkaEvent не публикуется). Если сработал T3 и владелец — Agent/DeveloperManager в статусе Active (INV-REF-002) — опубликован ReferralLeadAttachRequestedKafkaEvent (AppliedRule = R4, BondId = null). При невалидном формате / не найденной / soft-deleted ссылке состояние не меняется: инкремента и событий нет.

Алгоритм

Путь T3 делегирует нормативному процессу привязки (attach-rules.md, «Процесс привязки»); здесь — только дельта входа T3 (синхронный, actor из контекста запроса).

  1. Контекст и доступ. Актор — Anonymous либо авторизованный (ActorUserId может быть null); права не требуются. Нормализация slug = Url.Trim().ToLowerInvariant().
  2. Проверка входа (soft). Формат slug (^[a-z0-9][a-z0-9-]{2,63}$) и длина ≤ 64 проверяются как soft-исход, а не hard-ошибка: несоответствие → короткое замыкание с isValid=false, url=slug, referrerDisplayName=null (хранилище не трогаем; 400 не возвращается — untrusted cookie-slug).
  3. Что читаем. Ссылку по Url = slug, не удалённую (DeletedAt == null). Не найдена → isValid=false, referrerDisplayName=null, счётчик не инкрементируется. Найдена — дополнительно владельца из реплики ReferralUserReplica (для referrerDisplayName; допускается null при лаге реплики).
  4. Правила и запуск T3. isValid=true; referrerDisplayName = имя владельца (null, если владелец не найден или FirstName пуст). Путь T3 запускается, если актор авторизован, флаг Referral:LinkOpenAttachEnabled включён и выполнены guard-ы: G2 (актор — Investor), G3 (ActorUserId != OwnerUserId), G4 (актор активен). При запуске применяется правило R4: связь и журнал не создаются (матрица attach-rules); признак лида leadEligible — владелец агент/застройщик (шаг 6). Флаг выключен / актор аноним / не инвестор / self → T3 не запускается, остаётся чистая валидация.
  5. Инкремент счётчика. При найденной ссылке — атомарный visits_count = visits_count + 1 без загрузки агрегата (ADR-0056). Связь и запись журнала по R4 не добавляются.
  6. Событие лида (только при T3 + владелец-агент). Если путь T3 сработал и владелец активен и его тип ∈ {Agent, DeveloperManager} (INV-REF-002) — публикуется ReferralLeadAttachRequestedKafkaEvent (topic pin.referral.lead-attachments.v1, ContractVersion = 1, key = ActorUserId; payload по attach-rules шаг 6: AppliedRule=R4, BondId=null, AgentUserId=OwnerUserId, AgentCompanyId=TenantId ссылки, ReferralLinkId, Channel; контракт — events/lead-attach-requested.md). ReferralBondCreatedKafkaEvent не публикуется (R4 связь не создаёт). Иначе событий нет.
  7. Согласованность. Инкремент VisitsCount и (опц.) публикация lead-attach фиксируются вместе. Чистый путь (аноним): сбой инкремента логируется warning-ом и не роняет ответ валидации (счётчик — некритичная аналитика). Путь T3: сбой сохранения → REFERRAL_ATTACH_PERSIST_FAILED (500), клиент/шлюз ретраит — повторный инкремент допустим, лид дедуплицируется CRM по паре «инвестор+агент».
  8. Результат. ValidateReferralLinkResult { isValid, url = slug, referrerDisplayName }200. Все исходы — 200. Masking: referrerDisplayName — только имя; фамилия/телефон/tenant не отдаются.

Диаграмма

Побочные эффекты

ТипДетали
Интеграционные событияReferralLeadAttachRequestedKafkaEventpin.referral.lead-attachments.v1 (только при T3 + владелец-агент); иначе нет
Проекции / счётчикиатомарный инкремент колонки VisitsCount (потребители — get-link-stats.md, get-partner-stats.md)
Уведомленияопосредованно: по lead-attach CRM создаёт карточку лида; уведомление агенту — вне referral

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
TOO_MANY_REQUESTS429Превышен per-IP rate limit (защита от перебора slug); заголовок Retry-After«Слишком много запросов»retry после Retry-After
REFERRAL_LINK_STORE_UNAVAILABLE503Хранилище недоступно при чтении ссылки/реплики«Сервис временно недоступен»retry
REFERRAL_ATTACH_PERSIST_FAILED500Сбой сохранения на пути T3 (инкремент + lead-attach)retry (лид дедуплицируется CRM; повторный инкремент допустим)

Невалидный формат / не найденная / soft-deleted ссылка — не ошибки: 200 c isValid=false (фронт очищает cookie). 400 из этой операции не возвращается by design (untrusted cookie-slug). Сбой инкремента на чистом (анонимном) пути — logged warning, ответ валидации отдаётся (best-effort счётчик, ADR-0056).

Критерии приёмки

  • Given аноним открывает валидный slug существующей ссылки, When валидация, Then 200, isValid = true, referrerDisplayName = имя владельца из реплики (null при лаге реплики или пустом FirstName), VisitsCount инкрементирован (best-effort), событий нет.
  • Given мусорный по формату slug из cookie, When валидация, Then 200, isValid = false, url = эхо slug, referrerDisplayName = null; хранилище не читается, инкремента нет, 400 не возвращается (untrusted cookie-slug).
  • Given slug не найден / soft-deleted, When валидация, Then 200, isValid = false, инкремента и событий нет.
  • Given авторизованный инвестор, флаг T3 включён, владелец — Agent/DeveloperManager (Active), guard-ы G2–G4 пройдены, When валидация, Then 200, isValid = true, VisitsCount +1, опубликован ReferralLeadAttachRequestedKafkaEvent (AppliedRule = R4, BondId = null); связь и ReferralBondCreatedKafkaEvent — нет (R4).
  • Given авторизованный инвестор, флаг включён, владелец — инвестор/партнёр (не агент), When валидация, Then 200, инкремент есть, лид не запрашивается (R5/INV-REF-002).
  • Given флаг T3 выключен или актор аноним/не инвестор или self (ActorUserId == OwnerUserId), When валидация, Then T3 не запускается — чистая валидация, только инкремент.
  • Given хранилище недоступно при чтении, When валидация, Then REFERRAL_LINK_STORE_UNAVAILABLE (503), retry.
  • Given путь T3 и сбой сохранения, When валидация, Then REFERRAL_ATTACH_PERSIST_FAILED (500), retry; лид дедуплицируется CRM по паре «инвестор+агент», повторный инкремент допустим.
  • Given сбой инкремента на анонимном пути, When валидация, Then ответ всё равно 200 (счётчик best-effort, warning в лог).
  • Given один IP превышает лимит запросов (перебор slug), When валидация, Then TOO_MANY_REQUESTS (429) с Retry-After — хранилище не читается, инкремента и T3 нет.

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

  • Контракт ValidateReferralLinkResult (isValid/url/referrerDisplayName) стабилен; новые optional поля — additive; удаление/переименование/смена типа — breaking (migration + rollout). Побочные эффекты (VisitsCount, T3) — поведенческое расширение; T3 под feature flag Referral:LinkOpenAttachEnabled (default off до релиза, README.md).
  • Unknown Channel/Type владельца при чтении — fallback (Other / «не агент», fail-closed, attach-rules.md) — на isValid/referrerDisplayName не влияет.
  • Logs: validate resolved/not-found/invalid-format, LinkId, isValid, actor (anon/authed), t3_fired + AppliedRule=R4 + guard-исходы; slug — только при isValid=false, усечённо; PII нет. Metrics: referral_link_validate_total{result, t3_fired}, referral_link_visits_incremented_total, referral_link_validate_rate_limited_total (перебор slug по IP); traces — OpenTelemetry. Dashboard — referral-overview; алерт «рост invalid-format» = битые slug в cookie, алерт «рост 429 c одного IP» = перебор slug (README.md, Alerts).

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

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

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