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

PUT /api/investors/me/referral-links/{id} — Обновить реферальную ссылку

Назначение

Изменяет адрес (slug) и/или описание собственной реферальной ссылки. Семантика partial-update: не переданное поле не меняется. Смена slug инвалидирует ранее розданные cookie со старым slug (история slug-ов не ведётся — INV-REF-L6); журнал регистраций и связи не рвутся (ссылаются по Id).

Metadata

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

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

ПроверкаЗначение
Actor typeUser (авторизованный пользователь)
Auth policy / Permissionsreferral.links.manage-self
Scopeself
Record-levelINV-REF-L4: только владелец (OwnerUserId = UserId); чужая ссылка неотличима от несуществующей (REFERRAL_LINK_NOT_FOUND)
Tenant isolationне применяется в запросе (record-level по владельцу); TenantId ссылки не меняется (историческая принадлежность)
Auditupdate: actor-stamping (UpdatedByUserId) + событие ReferralLinkUpdatedEvent (старый/новый Url)

HTTP-контракт

ПолеЗначение
MethodPUT
Route/api/investors/me/referral-links/{id}
Success status200
Idempotencyповтор того же PUT идемпотентен по эффекту (те же значения)
CorrelationOpenTelemetry trace (traceparent)

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

Body — форма UpdateReferralLinkForm (JSON) + route-параметр.

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity claims (sub)GuidДавалидный JWT401
idrouteGuidДавалидный Guid; ссылка существует, не удалена, принадлежит акторуrecord-level INV-REF-L4
urlbodystring?НетmaxLength 64; при задании — lowercase-нормализация, формат ^[a-z0-9][a-z0-9-]{2,63}$ (INV-REF-L3), не зарезервированный, уникален среди не удалённых (INV-REF-L1). null/отсутствует → адрес не меняетсяuntrusted input
descriptionbodystring?НетmaxLength 256. null/отсутствует → описание не меняется (очистка описания контрактом не предусмотрена)не логируется дословно

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

ПолеТипОбязательностьИсточникMaskingОписание
idstring (Guid)ДаReferralLinkEntity.IdИдентификатор ссылки.
urlstringДаReferralLinkEntity.Url (после изменения)Актуальный slug.
descriptionstring?НетReferralLinkEntity.Description (после изменения)Актуальное описание.

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

  • Предусловия. Пользователь авторизован. Ссылка существует, не удалена (DeletedAt == null) и принадлежит актору. Новый slug (если задан) — валидного формата, не зарезервирован и свободен среди не удалённых ссылок.
  • Постусловия. При успехе: Url и/или Description обновлены по семантике partial-update (не переданное поле сохранено); поднято доменное ReferralLinkUpdatedEvent (старый/новый Url); при смене slug старый адрес перестаёт резолвиться (розданные cookie дают no-op привязки); IsDefault, Channel, VisitsCount, OwnerUserId, TenantId не изменены; журнал регистраций и связи сохранены (ссылаются по Id, INV-REF-L6). Интеграционные Kafka-события ссылка не порождает.

Алгоритм

  1. Контекст и доступ. Операцию выполняет авторизованный пользователь; UserId — из claim sub; требуется право referral.links.manage-self.
  2. Проверка входа. id — непустой Guid; url (если задан) — длина/формат/не зарезервирован; description — maxLength 256. Нарушение → ValidationError.
  3. Существование и принадлежность. Загрузить собственную ссылку по Id = id, OwnerUserId = UserId, не удалённую (DeletedAt == null). Отсутствие / чужая / удалённая → REFERRAL_LINK_NOT_FOUND (единый код без раскрытия чужих ссылок, INV-REF-L4). Отдельного enum-статуса у ссылки нет — soft-delete проверяется фильтром.
  4. Уникальность нового slug. Если url задан и нормализованное значение отличается от текущего — убедиться, что среди не удалённых ссылок такого slug ещё нет; занят → REFERRAL_LINK_URL_TAKEN (INV-REF-L1).
  5. Изменение. Обновить Url (если передан) и Description (если передан); IsDefault, Channel, VisitsCount, OwnerUserId, TenantId этой операцией не меняются. Изменение поднимает событие ReferralLinkUpdatedEvent (старый/новый Url). Правка не должна затирать чужую: при конкурентном изменении той же ссылки — CONCURRENCY_CONFLICT (клиент перечитывает и повторяет); конфликт уникальности slug под гонку → REFERRAL_LINK_URL_TAKEN.
  6. Результат. UpdateReferralLinkResultid, актуальные url/description → HTTP 200.

Диаграмма

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

  • Старый slug перестаёт резолвиться: cookie referralUrl со старым slug дают no-op привязки (guard G1 attach-rules.md, edge case «ссылка переименована»).

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
— (auth)401Нет/невалиден JWTre-login
ValidationError400Формат/длина url/descriptionсписок ошибок полейисправить
REFERRAL_LINK_NOT_FOUND404Ссылка не существует / чужая / удалена (INV-REF-L4, без раскрытия существования)«Ссылка не найдена»no retry
REFERRAL_LINK_URL_TAKEN409Новый slug занят (INV-REF-L1) — конфликт уникальности«Адрес ссылки уже занят»сменить slug
CONCURRENCY_CONFLICT409Конкурентное изменение той же ссылки (version-guard)«Данные изменены, обновите страницу»reload + retry

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

  • Given владелец и валидный новый url, When PUT, Then 200, url обновлён, description без изменений (поле не передано), поднято ReferralLinkUpdatedEvent (старый/новый Url).
  • Given передан только description, When PUT, Then url без изменений, description обновлён.
  • Given url и description не переданы (оба null), When PUT, Then эффект-no-op, 200 с текущими значениями (идемпотентно).
  • Given новый url после нормализации равен текущему, When PUT, Then проверка уникальности пропускается, 200.
  • Given новый slug занят другой не удалённой ссылкой, When PUT, Then отказ REFERRAL_LINK_URL_TAKEN (INV-REF-L1).
  • Given ссылка чужая / удалена / не существует, When PUT, Then отказ REFERRAL_LINK_NOT_FOUND — без раскрытия чужих ссылок (INV-REF-L4).
  • Given url неверного формата / зарезервирован / длиннее 64, When PUT, Then ValidationError.
  • Given параллельное изменение той же ссылки, When PUT, Then проигравший получает CONCURRENCY_CONFLICT (перечитать и повторить); гонка уникальности slug → REFERRAL_LINK_URL_TAKEN.
  • Given повтор того же PUT, When PUT, Then идемпотентный по эффекту результат (те же значения).
  • Given slug сменён, When старый slug приходит из cookie при регистрации/входе, Then привязка не происходит (guard G1 attach-rules.md); ранее записанные журнал и связи сохранены (ссылки по Id).

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

  • Контракт стабилен. Семантика «null = не менять» блокирует очистку description этим методом — зафиксированное ограничение; очистка потребует явного маркера (breaking для семантики — через новую версию формы).
  • Logs: UserId, LinkId, факт смены slug (old → new — допустимо, PII нет); metrics: referral_links_updated_total; traces — OpenTelemetry.

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

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

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