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

GET /internal/v1/referral-links/resolve — Internal: резолв реферальной ссылки по slug

Назначение

Внутренняя m2m-операция: по slug из cookie referralUrl вернуть владельца ссылки и его тип/статус — данные для правил привязки (attach-rules.md) и обогащения на стороне вызывающих контекстов. Вызывается сервисами identity и crm; PII не отдаёт; побочных эффектов не имеет. Правила привязки исполняет сам referral (события — основной канал), но соседним контекстам нужен синхронный read-only резолв slug до/вне событийного потока:

  • identity — проверить slug referrerUrl из входа/регистрации (sign-up-or-sign-in.md, confirm-sign-in.md) перед фиксацией в UserEntity.ReferrerUrl: мусор из cookie не сохраняется и не попадает в события pin.identity.users.v1/pin.identity.sign-ins.v1 (снижение no-op по guard G1);
  • crm — при обработке ReferralLeadAttachRequestedKafkaEvent или ручном разборе карточки лида дообогатить её актуальными атрибутами ссылки (канал, признак default, владелец).

Операция ничего не решает за правила привязки: она только резолвит данные; решение «связь/лид» остаётся в referral (матрица R1–R6).

Metadata

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

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

ПроверкаЗначение
Actor typeSystem (сервисы identity, crm)
Auth policy / Permissionsm2m client-credentials токен, policy internal-services (audience referral-internal); пользовательские permissions не применяются; area /internal/* недоступна с внешнего ingress (только внутренняя сеть кластера)
Scope (RBAC)платформа (кросс-tenant read по slug — slug глобально уникален, INV-REF-L1)
Tenant isolationв ответе отдаётся OwnerCompanyId (= TenantId ссылки) как данные

urluntrusted (происходит из пользовательской cookie, транзитом через identity/crm): формат slug валидируется до обращения к данным; невалидный формат → Resolved = false, не ошибка.

HTTP-контракт

ПолеЗначение
MethodGET
Route/internal/v1/referral-links/resolve?url={slug}
Success status200 (и найденная, и не найденная ссылка — 200 с флагом Resolved, чтобы вызывающий не ретраил 404)
Correlationсквозной trace через заголовок traceparent (сквозной от исходного пользовательского запроса identity)

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

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
Urlquery:urlstringДаRequired, maxLength 64; после trim + lowercase проверяется формат ^[a-z0-9][a-z0-9-]{2,63}$ (INV-REF-L3); несоответствие формата — не ошибка валидации, а Resolved = false c FailureReason = InvalidSlugFormat (мусор из cookie — штатный кейс)untrusted вход; в логах пишется усечённо (64 симв.) и только при Resolved = false

Body отсутствует.

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

PII (ФИО/телефон владельца) не отдаётся: identity/crm при необходимости читают собственные источники; публичное отображаемое имя реферера отдаёт операция ../public/validate-referral-link.md (referrerDisplayName).

ПолеТипОбязательностьИсточникMaskingОписание
ResolvedboolДаcomputedtrue — ссылка найдена и не удалена; false — все остальные исходы (см. FailureReason).
FailureReasonstring? (enum ReferralResolveFailureReason)НетcomputedЗаполнен только при Resolved = false: InvalidSlugFormat / NotFound (не найдена или soft-deleted — не различаются, чтобы не раскрывать историю slug).
ReferralLinkIdGuid?НетReferralLinkEntity.IdИдентификатор ссылки (null при Resolved = false; далее все поля — аналогично).
Urlstring?НетReferralLinkEntity.UrlНормализованный (lowercase) slug.
Channelstring? (enum ReferralChannel)НетReferralLinkEntity.ChannelКанал ссылки (дом enum — referral-link.md).
IsDefaultbool?НетReferralLinkEntity.IsDefaultПризнак default-ссылки.
OwnerUserIdGuid?НетReferralLinkEntity.OwnerUserIdВладелец ссылки (потенциальный реферер).
OwnerCompanyIdGuid?НетReferralLinkEntity.TenantIdКомпания владельца на момент создания ссылки; null — инвестор.
OwnerUserTypestring? (enum UserType)НетReferralUserReplica.TypeТип учётки владельца (дом enum — user.md; роль в правилах — attach-rules.md). null, если реплика владельца ещё не доставлена (лаг consumer-а).
OwnerStatusstring? (enum UserStatus)НетReferralUserReplica.StatusСтатус владельца (guard активности агента для лида). null при отсутствии реплики.
OwnerIsLeadEligibleboolДаcomputed: OwnerUserType ∈ {Agent, DeveloperManager} и OwnerStatus = ActiveГотовый ответ на INV-REF-002 + edge case «владелец заблокирован» (attach-rules.md): создаст ли привязка по этой ссылке лид. false при Resolved = false, отсутствии реплики или unknown OwnerUserType (fail-closed).

Алгоритм

  1. Контекст и доступ. Actor — system: аутентификация m2m-токеном (policy internal-services), пользовательский контекст отсутствует; correlation сквозной от исходного запроса identity/crm.
  2. Проверка входа. Url — required, ≤ 64 символов (нарушение required/длины → ValidationError, 400: это ошибка интеграции вызывающего, не пользовательский мусор). После нормализации (trim + lowercase) несоответствие формату ^[a-z0-9][a-z0-9-]{2,63}$ → короткое замыкание: Resolved = false, FailureReason = InvalidSlugFormat (дальнейшие шаги не выполняются).
  3. Резолв ссылки. Ищется не удалённая ссылка по slug (глобально уникальный Url); нет → Resolved = false, FailureReason = NotFound. Для найденной ссылки берётся владелец из реплики (ReferralUserReplica); реплика может отсутствовать (лаг) — ссылка всё равно резолвится, owner-поля отдаются null.
  4. Вычисление и выдача. OwnerIsLeadEligible = владелец есть и его тип ∈ {Agent, DeveloperManager} и статус = Active; unknown значение Type/Statusfalse (fail-closed, правило enum в attach-rules.md). Атрибуты ссылки — из ссылки, атрибуты владельца — из реплики. Никаких мутаций: VisitsCount не инкрементируется (m2m-резолв — не переход пользователя) и триггер T3 привязки не запускается. Оба исхода (Resolved = true/false) — 200. Owner-поля отстают на лаг реплики identity → вызывающий трактует их как подсказку, финальное решение «связь/лид» принимает referral по матрице R1–R6 в момент обработки события.

Диаграмма

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
ValidationError400url отсутствует или длиннее 64 символов (ошибка интеграции вызывающего)«Параметр url обязателен»no retry (исправить вызов)
UNAUTHORIZED401Нет/невалидный m2m-токен«Требуется авторизация»retry после переполучения токена
FORBIDDEN403Токен без policy internal-services / чужой audience«Доступ запрещён»no retry (ошибка конфигурации клиента)

Ненайденный/удалённый slug и мусорный формат — не ошибки: 200 c Resolved = false (вызывающий очищает cookie/поле и продолжает без реферера). Недоступность referral для вызывающих некритична: identity сохраняет slug как есть и полагается на guard G1 событийного пути (graceful degradation).

Постусловие

Состояние не изменяется: VisitsCount не инкрементируется (m2m-резолв — не переход пользователя), триггер T3 не запускается, события не публикуются. Операция идемпотентна и безопасна для retry вызывающего.

Acceptance Criteria

  • Given валидный slug существующей не удалённой ссылки и доставленная реплика владельца, When identity/crm вызывает resolve, Then 200 c Resolved = true, атрибутами ссылки (ReferralLinkId, Url, Channel, IsDefault, OwnerUserId, OwnerCompanyId) и владельца (OwnerUserType, OwnerStatus); PII (ФИО/телефон) не отдаётся.
  • Given владелец ссылки — Agent/DeveloperManager со статусом Active, When resolve, Then OwnerIsLeadEligible = true; если тип не агент/застройщик, или статус ≠ Active, или реплика отсутствует, или Type unknown — OwnerIsLeadEligible = false (fail-closed).
  • Given slug не резолвится (не найден или soft-deleted), When resolve, Then 200 c Resolved = false, FailureReason = NotFound (удалённая и несуществующая неразличимы — история slug не раскрывается).
  • Given slug не проходит формат ^[a-z0-9][a-z0-9-]{2,63}$ (мусор из cookie), When resolve, Then 200 c Resolved = false, FailureReason = InvalidSlugFormat (короткое замыкание, без обращения к данным) — не ошибка валидации.
  • Given ссылка найдена, но реплика владельца отстаёт (лаг consumer-а), When resolve, Then Resolved = true, но owner-поля (OwnerUserType/OwnerStatus) — null, OwnerIsLeadEligible = false; вызывающий трактует их как подсказку, финальное решение «связь/лид» принимает referral по матрице.
  • Given параметр url отсутствует или длиннее 64 символов (ошибка интеграции вызывающего), When resolve, Then 400 ValidationError; без m2m-токена — 401; токен без policy internal-services/ чужой audience — 403.

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

  • Контракт internal-area версионируется путём (/internal/v1/...); новые optional поля ответа — additive; удаление/переименование/смена типа, изменение семантики OwnerIsLeadEligible — breaking (согласованный rollout с identity/crm). Unknown enum на стороне вызывающего — трактовать как null/false (fail-closed, зеркально правилу шага 4).
  • Вызывающему рекомендован таймаут 500 мс + 1 retry (read-only, идемпотентно). Результат допустимо кэшировать на короткий TTL (секунды) как подсказку: атрибуты ссылки (ReferralLinkId, Url, Channel, IsDefault, OwnerUserId, OwnerCompanyId) стабильны в пределах жизни ссылки, но owner-поля из реплики (OwnerUserType/OwnerStatus) и производный OwnerIsLeadEligible могут устареть при смене статуса владельца или доставке реплики — надолго их кэшировать нельзя; финальное решение «связь/лид» всё равно принимает referral по матрице в момент обработки события.
  • Logs: Resolved, FailureReason, ReferralLinkId, caller (client_id токена); slug — только при Resolved = false, усечённый; PII нет. Metrics: referral_resolve_total{caller, resolved, failure_reason} — рост InvalidSlugFormat/NotFound коррелирует с алертом «битые slug в cookie» (README.md, Alerts). Traces — сквозной trace через traceparent. Dashboard — referral-overview.

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

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

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