PUT /api/investors/me/referral-links/{id} — Обновить реферальную ссылку
Назначение
Изменяет адрес (slug) и/или описание собственной реферальной ссылки. Семантика partial-update: не
переданное поле не меняется. Смена slug инвалидирует ранее розданные cookie со старым slug
(история slug-ов не ведётся — INV-REF-L6); журнал регистраций и связи не рвутся (ссылаются по Id).
| Поле | Значение |
|---|
| Сервис/контекст | referral |
| API area | public |
| Feature group | referral-links |
| Статус | draft |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (авторизованный пользователь) |
| Auth policy / Permissions | referral.links.manage-self |
| Scope | self |
| Record-level | INV-REF-L4: только владелец (OwnerUserId = UserId); чужая ссылка неотличима от несуществующей (REFERRAL_LINK_NOT_FOUND) |
| Tenant isolation | не применяется в запросе (record-level по владельцу); TenantId ссылки не меняется (историческая принадлежность) |
| Audit | update: actor-stamping (UpdatedByUserId) + событие ReferralLinkUpdatedEvent (старый/новый Url) |
HTTP-контракт
| Поле | Значение |
|---|
| Method | PUT |
| Route | /api/investors/me/referral-links/{id} |
| Success status | 200 |
| Idempotency | повтор того же PUT идемпотентен по эффекту (те же значения) |
| Correlation | OpenTelemetry trace (traceparent) |
Входные данные / параметры запроса
Body — форма UpdateReferralLinkForm (JSON) + route-параметр.
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
UserId | identity claims (sub) | Guid | Да | валидный JWT | 401 |
id | route | Guid | Да | валидный Guid; ссылка существует, не удалена, принадлежит актору | record-level INV-REF-L4 |
url | body | string? | Нет | maxLength 64; при задании — lowercase-нормализация, формат ^[a-z0-9][a-z0-9-]{2,63}$ (INV-REF-L3), не зарезервированный, уникален среди не удалённых (INV-REF-L1). null/отсутствует → адрес не меняется | untrusted input |
description | body | string? | Нет | maxLength 256. null/отсутствует → описание не меняется (очистка описания контрактом не предусмотрена) | не логируется дословно |
Модель ответа UpdateReferralLinkResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
id | string (Guid) | Да | ReferralLinkEntity.Id | — | Идентификатор ссылки. |
url | string | Да | ReferralLinkEntity.Url (после изменения) | — | Актуальный slug. |
description | string? | Нет | 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-события ссылка не порождает.
Алгоритм
- Контекст и доступ. Операцию выполняет авторизованный пользователь;
UserId — из claim sub;
требуется право referral.links.manage-self.
- Проверка входа.
id — непустой Guid; url (если задан) — длина/формат/не зарезервирован;
description — maxLength 256. Нарушение → ValidationError.
- Существование и принадлежность. Загрузить собственную ссылку по
Id = id,
OwnerUserId = UserId, не удалённую (DeletedAt == null). Отсутствие / чужая / удалённая →
REFERRAL_LINK_NOT_FOUND (единый код без раскрытия чужих ссылок, INV-REF-L4). Отдельного
enum-статуса у ссылки нет — soft-delete проверяется фильтром.
- Уникальность нового slug. Если
url задан и нормализованное значение отличается от текущего —
убедиться, что среди не удалённых ссылок такого slug ещё нет; занят → REFERRAL_LINK_URL_TAKEN
(INV-REF-L1).
- Изменение. Обновить
Url (если передан) и Description (если передан); IsDefault,
Channel, VisitsCount, OwnerUserId, TenantId этой операцией не меняются. Изменение поднимает
событие ReferralLinkUpdatedEvent (старый/новый Url). Правка не должна затирать чужую: при
конкурентном изменении той же ссылки — CONCURRENCY_CONFLICT (клиент перечитывает и повторяет);
конфликт уникальности slug под гонку → REFERRAL_LINK_URL_TAKEN.
- Результат.
UpdateReferralLinkResult — id, актуальные url/description → HTTP 200.
Диаграмма
Побочные эффекты
- Старый slug перестаёт резолвиться: cookie
referralUrl со старым slug дают no-op привязки (guard
G1 attach-rules.md, edge case «ссылка переименована»).
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
| — (auth) | 401 | Нет/невалиден JWT | — | re-login |
ValidationError | 400 | Формат/длина url/description | список ошибок полей | исправить |
REFERRAL_LINK_NOT_FOUND | 404 | Ссылка не существует / чужая / удалена (INV-REF-L4, без раскрытия существования) | «Ссылка не найдена» | no retry |
REFERRAL_LINK_URL_TAKEN | 409 | Новый slug занят (INV-REF-L1) — конфликт уникальности | «Адрес ссылки уже занят» | сменить slug |
CONCURRENCY_CONFLICT | 409 | Конкурентное изменение той же ссылки (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.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы