ADR-0036: PriceRule aggregate + auto-derivation

Status: accepted Date: 2026-04-25 Deciders: backend team, pricing context owner

Контекст

P1 proposal pipeline использовал PriceResolverStub — выбирал первую non-nil PriceSet.Net из observations без применения каких-либо rule’ов. Pricing context имел target design (PriceRule, Transformation, RuleScope, AppliesTo, ValidityWindow), но не было реализации.

Параллельно копились observations с разными ценами для одинаковых canonical products через разные customer credentials. Эти данные содержат implicit правила pricing (наценки, скидки, gold-tier discounts), которые supplier применяет — мы видим их только пост-фактум через credential-bound observations.

Цели P2:

  1. Заменить P1 stub на реальный pricing engine.
  2. Reverse-engineer правила из observations, чтобы наполнить engine автоматически без manual entry для каждой пары (customer, canonical).

Решение

Storage: PG discriminator + typed columns

Один table price_rules с дискриминатором transform_kind и типизированными колонками для каждого варианта (pct_value для percentage, fixed_value/precision/ currency/mode для fixed, tiers JSONB для tiers). DB CHECK constraints гарантируют что ровно один из вариантов non-NULL. Pre-production — schema может меняться без JSONB rigidity.

Snapshot: M2 polling

Kafka наполняет БД через outbox (write path). API server читает БД через PollingLoader (10s incremental + 5min full reload safety net), хранит snapshot в atomic.Pointer. Single source of truth — БД; memory всегда производное. Нет Listen/Notify, нет invalidation messaging.

Rules combination: O1 stacked

Все matching rules применяются в canonical sequence (fixed → tiers → percentage) по убыванию priority. Stable sort tie-break: scope specificity (customer > group > supplier > tag), source (manual < derived), RuleID asc.

Overlap validation: V1 sync 409

OverlapValidator runs synchronously in CrudService.Create/Update. Conflict = same scope + same priority + same kind + overlapping qty/customer_type/region/ window → возвращает 409 + conflicting_rule_ids. Self-update (same RuleID) — не conflict.

Moderation BC отложен. Async approval flow (V2) не реализован.

Scopes (S3): supplier | customer_pricing_group | customer | classification_tag

Scope — текстовый дискриминатор (kind, ref). Ref для customer/group/tag — UUID, для supplier — string.

Derivation: T3 event + cron, AA3 auto-apply ≥ 0.9

  • Event-driven: Kafka offers.events.v1 → DebouncedQueue (LRU 10000 / 5min window) → drain каждые 30s → Deriver.Execute(trigger=“event”).
  • Weekly cron: Sunday 03:00 UTC, recompute всех (supplier, canonical) pairs.
  • Detectors: D1 RatioDetector (Percentage), D2 DeltaDetector (Fixed), D3 TiersDetector (Tiers).
  • Baseline (B1): system-credential observation с самым свежим observed_at. Без system credential — no_baseline skip.
  • Confidence (CF3): weighted CV + recency (half-life ≈ 14 days) + sample bonus (samples/10). Threshold 0.9 → auto-apply (derived_auto priority 50). Иначе draft в price_rule_drafts для review.
  • Scope inference (SI2): group-first если supporting customers покрывают всю group; иначе per-customer.
  • Min credentials: ≥ 3 distinct credentials (insufficient_observations иначе).
  • Auto-deprecate: derived rules для (supplier, canonical), чьих fingerprint больше нет в новом run и без disabled_by_operator, marked deprecated.

Feature flags

PRICING_ENGINE_ENABLED — отдельно от derivation, контролирует replace stub. PRICING_DERIVATION_ENABLED — Kafka consumer + cron + scheduler. Rollback без code changes.

Последствия

Плюсы

  • Auto-pricing закрывает 80%+ customer-specific rules без manual entry.
  • Engine extensible: добавить новый TransformKind = новый column + Apply branch + detector.
  • Invariants (privacy, snapshot atomicity, deterministic SourceHash, preview no-persist) explicit в коде + tests.
  • Two-flag rollback path — instant rollback без redeploy stub.

Минусы

  • Snapshot reload lag ≤10s (incremental) + до 5 min (full reload). Eventual consistency для admin updates — видны не моментально.
  • Derivation false positives risk — patterns могут быть случайной correlation, не реальное rule. CF3 + draft review mitigate, но не устраняют.
  • Increased complexity — 10+ packages, 4 migrations, 2 feature flags. Higher cognitive load для новых разработчиков.
  • Confidence calibration (CF3) requires production tuning — текущая formula даёт conf ≈ 0.4 на 12 stable samples, что ниже 0.9 threshold. Production rollout требует наблюдения и iteration на коэффициентах.

Нейтральные

  • Outbox events публикуются через LogRepo (M1 placeholder). Real Kafka bridge деferred — не блокирует engine, но draft suggestions не доходят до Kafka consumers.
  • Migration 0021 добавила qty_observed на offer_observations с default 1 для backfill совместимости.

Рассмотренные альтернативы

A. JSONB polymorphic storage

Single transformation JSONB column с shape {kind, payload}. Отвергнуто — теряем типизацию + DB CHECK constraints; pre-production schema часто меняется, typed columns с CHECK дают immediate constraint feedback.

B. LISTEN/NOTIFY вместо polling

PG LISTEN/NOTIFY уведомляет api-server о изменениях напрямую. Отвергнуто — single SoT через polling проще; еventual consistency 10s acceptable; NOTIFY теряет события при connection drop.

C. Full auto без threshold

AA1 — все detected patterns auto-apply без confidence gate. Отвергнуто — высокий risk false positives при noise; draft review обязателен для low-confidence patterns.

D. Async approval flow (V2)

Все mutations через Moderation BC. Отвергнуто — Moderation BC ещё не существует, sync 409 достаточен для P2; V2 — отдельный future ADR.

E. Per-customer derivation always (skip group inference)

Отвергнуто — рост table size + дубликаты equivalent rules. SI2 group-first сохраняет cardinality.

Ссылки

  • Spec: docs/superpowers/specs/2026-04-24-p2-pricing-rule-aggregate-design.md
  • Plan: docs/superpowers/plans/2026-04-24-p2-pricing-rule-aggregate.md
  • ADR-0020: pricing context boundaries
  • ADR-0025: outbox pattern
  • ADR-0026: customer credential scope
  • ADR-0030: runtime DI via uber/fx
  • ADR-0031: microkernel submodules per BC
  • ADR-0035: proposal pipeline layering
  • Migrations: 0018, 0019, 0020, 0021