GET /internal/v1/referral-links/resolve — Internal: резолв реферальной ссылки по slug
Назначение
Внутренняя m2m-операция: по slug из cookie referralUrl вернуть владельца ссылки и его тип/статус —
данные для правил привязки (attach-rules.md) и обогащения на стороне
вызывающих контекстов. Вызывается сервисами identity и crm; PII не отдаёт; побочных эффектов не
имеет. Правила привязки исполняет сам referral (события — основной канал), но соседним контекстам
нужен синхронный read-only резолв slug до/вне событийного потока:
identity— проверить slugreferrerUrlиз входа/регистрации (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 area | internal |
| Feature group | referral-links |
| Статус | draft |
| Документ | docs/06-services/referral/api/internal/resolve-referral-link.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | System (сервисы identity, crm) |
| Auth policy / Permissions | m2m 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 ссылки) как данные |
url — untrusted (происходит из пользовательской cookie, транзитом через identity/crm): формат
slug валидируется до обращения к данным; невалидный формат → Resolved = false, не ошибка.
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | GET |
| Route | /internal/v1/referral-links/resolve?url={slug} |
| Success status | 200 (и найденная, и не найденная ссылка — 200 с флагом Resolved, чтобы вызывающий не ретраил 404) |
| Correlation | сквозной trace через заголовок traceparent (сквозной от исходного пользовательского запроса identity) |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
Url | query:url | string | Да | 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 | Описание |
|---|---|---|---|---|---|
Resolved | bool | Да | computed | — | true — ссылка найдена и не удалена; false — все остальные исходы (см. FailureReason). |
FailureReason | string? (enum ReferralResolveFailureReason) | Нет | computed | — | Заполнен только при Resolved = false: InvalidSlugFormat / NotFound (не найдена или soft-deleted — не различаются, чтобы не раскрывать историю slug). |
ReferralLinkId | Guid? | Нет | ReferralLinkEntity.Id | — | Идентификатор ссылки (null при Resolved = false; далее все поля — аналогично). |
Url | string? | Нет | ReferralLinkEntity.Url | — | Нормализованный (lowercase) slug. |
Channel | string? (enum ReferralChannel) | Нет | ReferralLinkEntity.Channel | — | Канал ссылки (дом enum — referral-link.md). |
IsDefault | bool? | Нет | ReferralLinkEntity.IsDefault | — | Признак default-ссылки. |
OwnerUserId | Guid? | Нет | ReferralLinkEntity.OwnerUserId | — | Владелец ссылки (потенциальный реферер). |
OwnerCompanyId | Guid? | Нет | ReferralLinkEntity.TenantId | — | Компания владельца на момент создания ссылки; null — инвестор. |
OwnerUserType | string? (enum UserType) | Нет | ReferralUserReplica.Type | — | Тип учётки владельца (дом enum — user.md; роль в правилах — attach-rules.md). null, если реплика владельца ещё не доставлена (лаг consumer-а). |
OwnerStatus | string? (enum UserStatus) | Нет | ReferralUserReplica.Status | — | Статус владельца (guard активности агента для лида). null при отсутствии реплики. |
OwnerIsLeadEligible | bool | Да | computed: OwnerUserType ∈ {Agent, DeveloperManager} и OwnerStatus = Active | — | Готовый ответ на INV-REF-002 + edge case «владелец заблокирован» (attach-rules.md): создаст ли привязка по этой ссылке лид. false при Resolved = false, отсутствии реплики или unknown OwnerUserType (fail-closed). |
Алгоритм
- Контекст и доступ. Actor — system: аутентификация m2m-токеном (policy
internal-services), пользовательский контекст отсутствует; correlation сквозной от исходного запроса identity/crm. - Проверка входа.
Url— required, ≤ 64 символов (нарушение required/длины →ValidationError, 400: это ошибка интеграции вызывающего, не пользовательский мусор). После нормализации (trim + lowercase) несоответствие формату^[a-z0-9][a-z0-9-]{2,63}$→ короткое замыкание:Resolved = false,FailureReason = InvalidSlugFormat(дальнейшие шаги не выполняются). - Резолв ссылки. Ищется не удалённая ссылка по slug (глобально уникальный
Url); нет →Resolved = false,FailureReason = NotFound. Для найденной ссылки берётся владелец из реплики (ReferralUserReplica); реплика может отсутствовать (лаг) — ссылка всё равно резолвится, owner-поля отдаютсяnull. - Вычисление и выдача.
OwnerIsLeadEligible= владелец есть и его тип ∈ {Agent,DeveloperManager} и статус =Active; unknown значениеType/Status→false(fail-closed, правило enum в attach-rules.md). Атрибуты ссылки — из ссылки, атрибуты владельца — из реплики. Никаких мутаций:VisitsCountне инкрементируется (m2m-резолв — не переход пользователя) и триггер T3 привязки не запускается. Оба исхода (Resolved = true/false) —200. Owner-поля отстают на лаг реплики identity → вызывающий трактует их как подсказку, финальное решение «связь/лид» принимаетreferralпо матрице R1–R6 в момент обработки события.
Диаграмма
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
ValidationError | 400 | url отсутствует или длиннее 64 символов (ошибка интеграции вызывающего) | «Параметр url обязателен» | no retry (исправить вызов) |
UNAUTHORIZED | 401 | Нет/невалидный m2m-токен | «Требуется авторизация» | retry после переполучения токена |
FORBIDDEN | 403 | Токен без 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
200cResolved = true, атрибутами ссылки (ReferralLinkId,Url,Channel,IsDefault,OwnerUserId,OwnerCompanyId) и владельца (OwnerUserType,OwnerStatus); PII (ФИО/телефон) не отдаётся. - Given владелец ссылки —
Agent/DeveloperManagerсо статусомActive, When resolve, ThenOwnerIsLeadEligible = true; если тип не агент/застройщик, или статус ≠Active, или реплика отсутствует, илиTypeunknown —OwnerIsLeadEligible = false(fail-closed). - Given slug не резолвится (не найден или soft-deleted), When resolve, Then
200cResolved = false,FailureReason = NotFound(удалённая и несуществующая неразличимы — история slug не раскрывается). - Given slug не проходит формат
^[a-z0-9][a-z0-9-]{2,63}$(мусор из cookie), When resolve, Then200cResolved = false,FailureReason = InvalidSlugFormat(короткое замыкание, без обращения к данным) — не ошибка валидации. - Given ссылка найдена, но реплика владельца отстаёт (лаг consumer-а), When resolve, Then
Resolved = true, но owner-поля (OwnerUserType/OwnerStatus) —null,OwnerIsLeadEligible = false; вызывающий трактует их как подсказку, финальное решение «связь/лид» принимаетreferralпо матрице. - Given параметр
urlотсутствует или длиннее 64 символов (ошибка интеграции вызывающего), When resolve, Then400 ValidationError; без m2m-токена —401; токен без policyinternal-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.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (2): GET /api/investors/referral-links/{url}/validate — Публичная валидация slug + счётчик переходов, Referral API — реестр операций и общие контрактные модели
- registries (1): Реестр API-методов PIN
Связанные документы
- Дома: referral-link.md (slug, INV-REF-L1/L3),
attach-rules.md (типы владельца, INV-REF-002, guard-ы,
ReferralUserReplica); enumUserType/UserStatus: user.md. - Пользовательский аналог с side effect-ами (инкремент
VisitsCount, триггер T3): ../public/validate-referral-link.md. - Вызывающие: sign-up-or-sign-in.md,
confirm-sign-in.md; лиды —
docs/06-services/crm/README.md.