PUT /api/developers/v1/ranking-rules/{ruleId}/priority — Изменение приоритета правила
Назначение
Менеджер застройщика меняет значение Priority существующего правила ранжирования
(ranking-rule.md). Если правило Active, новый приоритет в той же
транзакции денормализуется в DeveloperPriority затронутых карточек (INV-4); для Disabled-правила
меняется только сохранённое значение (применится при Enable).
| Поле | Значение |
|---|
| Сервис/контекст | catalog |
| API area | developers |
| Feature group | ranking |
| Статус | draft |
| Документ | docs/06-services/catalog/api/developers/update-ranking-rule-priority.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | User (менеджер застройщика — JWT identity) |
| Auth policy / Permissions | permission catalog.ranking.manage в scope организации/группы проектов/проекта |
| Scope (RBAC) | организация и ниже |
| Tenant isolation | TenantId = CompanyId (ADR-0052); чужое правило невидимо (404) |
| Audit | actor-stamping UpdatedByUserId, Version++ |
HTTP-контракт
| Поле | Значение |
|---|
| Method | PUT |
| Route | /api/developers/v1/ranking-rules/{ruleId}/priority |
| Success status | 200 |
| Idempotency header | не применяется (PUT абсолютного значения идемпотентен по смыслу) |
| Correlation | сквозной trace через traceparent |
Входные данные / параметры запроса
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
TenantId | identity claims | Guid | Да | Required | tenant isolation |
RuleId | route | Guid | Да | != Guid.Empty; существование — шаг 2 | — |
Priority | body | int | Да | 0..1000 (INV-3) | — |
ExpectedVersion | body | long? | Нет | >= 1; optimistic-check против Version правила | — |
Модель ответа RankingRuleResult
Модель — дом create-ranking-rule.md (та же структура: Id, TargetKind,
TargetId, Priority, Status, CardsRecomputed, Version). Дельта: Version — инкремент;
CardsRecomputed = 0 для Disabled-правила.
Алгоритм
- Контекст и доступ. Актор — менеджер застройщика (JWT +
catalog.ranking.manage); TenantId из
claims.
- Проверка входа.
Priority 0..1000 (INV-3), RuleId != Guid.Empty → ValidationError.
Существование: правило (в рамках своего tenant) найдено; нет → CATALOG_RANKING_RULE_NOT_FOUND
(404). Состояние: Active и Disabled допустимы (soft-deleted отфильтрован). Optimistic-check:
заданный ExpectedVersion не совпал с версией правила → CONCURRENCY_CONFLICT (409). No-op:
значение приоритета не изменилось → идемпотентный успех без мутаций (CardsRecomputed = 0).
- Что читаем. При
Status == Active — данные каскада как в
create-ranking-rule.md шаг 3: TargetKind=Project → объекты недвижимости
проекта + их активные правила объектов; TargetKind=PropertyObject → карточка объекта недвижимости.
При Disabled — ничего.
- Правила и переходы. Меняется
Priority (guard INV-3), Version++, UpdatedByUserId = актор;
фиксируется RankingRuleChangedEvent. Для Active-правила — каскад INV-5 (правило объекта
недвижимости ?? проектное ?? 0): маппинг идентичен create — карточка проекта DeveloperPriority = Priority + объекты недвижимости без собственного Active-правила, либо один объект недвижимости →
Priority.
- Что меняется. Правило обновляется; для
Active — обновляются затронутые карточки (счётчик →
CardsRecomputed). Правка не должна затирать чужую (конкурентный доступ).
- События. Только доменное
RankingRuleChangedEvent (тот же коммит); интеграционных нет.
- Согласованность. Правило + карточки — в одном согласованном изменении (INV-4).
- Результат.
RankingRuleResult (200); отказ → ProblemDetails.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|
| Проекции / поиск | обновление DeveloperPriority карточек (индекс ix_*_feed) — только для Active-правила |
Интеграционные события не публикуются; внешних вызовов и уведомлений нет.
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
ValidationError | 400 | Priority вне 0..1000 | Ошибка валидации запроса | no retry |
UNAUTHORIZED | 401 | Нет/просрочен JWT | Требуется авторизация | no retry |
FORBIDDEN | 403 | Нет permission в scope | Доступ запрещён | no retry |
CATALOG_RANKING_RULE_NOT_FOUND | 404 | Правило не существует / чужой tenant / soft-deleted | Правило не найдено | no retry |
CONCURRENCY_CONFLICT | 409 | ExpectedVersion не совпал / гонка версий | Данные изменены параллельно | retry после перечитывания |
Acceptance Criteria
- Happy path (
Active-правило, каскад). Дано активное правило проекта Priority = 100 и три
опубликованных объекта недвижимости проекта без собственных правил. Когда менеджер с
catalog.ranking.manage ставит Priority = 300. Тогда правило получает Priority = 300, Version
инкрементируется, DeveloperPriority карточки проекта и трёх объектов недвижимости = 300,
CardsRecomputed = 4, ответ 200; сортировка «Рекомендованные» отражает новый вес.
- Happy path (
Disabled-правило). Дано Disabled-правило. Когда меняется Priority. Тогда
сохранённое значение обновлено, каскада в карточки нет, CardsRecomputed = 0, витринная сортировка не
меняется до Enable.
- No-op (значение не изменилось). Дано правило
Priority = 200. Когда приходит Priority = 200.
Тогда мутаций нет, CardsRecomputed = 0, ответ 200 (идемпотентно по смыслу PUT).
- Конкурентная правка (optimistic-check). Дано две параллельные правки с одинаковым
ExpectedVersion. Когда обе доходят. Тогда первая применяется, вторая получает CONCURRENCY_CONFLICT
(409) и должна перечитать актуальную версию правила.
- Гонка с консюмером каскада. Дано параллельный пересчёт затронутой карточки консюмером
chessboard-sync. Когда правка каскадит DeveloperPriority. Тогда конфликт версий карточки
разрешается повтором (проигравший перечитывает), итоговый DeveloperPriority согласован.
- Чужой tenant. Дано правило другого застройщика. Когда менеджер обращается по его
ruleId. Тогда
CATALOG_RANKING_RULE_NOT_FOUND (404) — существование чужого правила не раскрывается.
- Валидация диапазона. Дано
Priority = 1500. Когда приходит запрос. Тогда ValidationError
(400), правило не изменено.
Совместимость и наблюдаемость
- Новые optional поля — additive; удаление/переименование — breaking.
- Logs:
ranking-rule-priority-updated (ruleId, tenantId, oldPriority → newPriority,
cardsRecomputed); metrics:
catalog_developers_operations_total{operation="update-ranking-rule-priority"}, duration;
traces + correlation — сквозной trace через traceparent. Dashboard catalog-overview.
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы