ADR-0038: Delivery Rule Aggregate (P2c)

Status: accepted Date: 2026-04-25 Deciders: Maxim Belkanov (architect), agent-claude (implementer)

Контекст

Layer 2c proposal pipeline (ADR-0035) поднялся с P1-стабом DeliveryResolverStub — adapter читает delivery_term из observation, без бизнес-правил:

  • Operators не могут блокировать carriers (e.g. «не работаем с DHL для customer X»).
  • Нет logistics markup — supplier-quoted lead_time + zero cost.
  • Нет free delivery thresholds.
  • Нет lead_time adjustments (e.g. «+1 day handling buffer»).

P2a pricing + P2b stock уже шипят full rule aggregates — нужен зеркальный DeliveryRule.

Решение

DeliveryRule aggregate в core/delivery/{domain,app,infra}/:

  • 4 transform kinds в discriminated union:
    • carrier_filter — allowlist/blocklist по carrier name (mismatch → reject с reason=carrier_blocked)
    • lead_time_override — Mode absolute (Min+Max) / add_days (signed AddDays) / multiply (Multiplier > 0)
    • logistics_markup — Mode flat (FlatAmount) / per_warehouse (× WarehouseCount). per_kg deferred до Phase 5+ (нет weight ingestion)
    • free_delivery_threshold — Cost = 0 при order_value (Price.Base × qty) ≥ MinOrderValue
  • RuleScope identical к pricing/stock (4 kinds); AppliesTo identical (qty_min, customer_type, region).
  • Engine.Apply — canonical phase order carrier_filter → lead_time_override → logistics_markup → free_delivery_threshold. Predictable независимо от operator priority.
  • Engine always-on с дня one (no rollout flag).
  • Admin HTTP на canonical layout /api/v1/admin/delivery/delivery-rules/*. JWT role delivery_admin.
  • Snapshot pollingatomic.Pointer[Snapshot], 10s incremental + 5min full reload.
  • Transactional outbox: каждая Create/Update/Deprecate в single pgx.Tx с outbox.Append. Topic delivery.events.v1.
  • Overlap validator — sync 409, fingerprint = sha256(scope+priority+kind+applies_to+window).
  • Health.Checker delivery.snapshot — fails until Bootstrap.
  • Metrics interface + AtomicMetrics + slog mirror.
  • openapilint enforceddelivery-admin.yamlinfra/http/*.go parity.
  • Extends prop.DeliveryOutcome с Cost *money.Amount field — non-breaking domain change. New TraceReason("delivery_carrier_blocked") enum (classified leak — visibility-adjacent).

DeliveryResolverStub deleted (c028439); DeliveryResolverEngine adapter wraps engine + reads delivery_term для baseline.

Последствия

Плюсы

  • Operator может задавать carrier blocklist + logistics markup без deploy.
  • Cost field surfaces в pipeline → Layer 3 (proposal ranking) может использовать total = price + cost для ordering.
  • Engine always-on — никакого rollout drift.
  • Mirror pricing/stock pattern — однообразный mental model.

Минусы

  • per_kg mode rejected в P2c — operator пишет rule expecting work, гет error в Validate. Documented в operator runbook + spec §12.
  • Carrier blocklist может конфликтовать между уровнями scope (supplier-level allow + customer-level block) — no logical overlap detection (только same-fingerprint). Risk низкий — operator проверяет через preview.
  • FreeDeliveryThreshold currency mismatch с Price.Base — engine errors out, outcome status unknown. Real ExchangeRate (P6a) разрулит.

Нейтральные последствия

  • New JWT role delivery_admin — отдельная permission boundary.
  • Extends prop.DeliveryOutcome.Cost field — non-breaking (existing consumers ignore nil).

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

A. Generic Rule[T] abstraction

Отклонено: преждевременная абстракция across pricing/stock/delivery (3 BCs may need shared engine — but overhead > benefit at current scale).

B. Carrier API integration в P2c (validate cost против DHL/SDEK API)

Отклонено: scope creep. Phase 6+ — отдельная задача (real carrier ratings).

C. Volumetric pricing (per_m3)

Отклонено: requires upstream volume ingestion (no observation field). Phase 5+.

D. Multi-leg delivery routing

Отклонено: out of scope; current model = single offer → single delivery quote.

E. Auto-derivation для delivery patterns

Отклонено: similar reasoning к Stock (operator-curated). Possible reconsider в Phase 6+ if historical observations consistently encode carrier patterns.

Ссылки

  • Spec: docs/superpowers/specs/2026-04-25-p2c-delivery-rule-aggregate-design.md
  • Plan: docs/superpowers/plans/2026-04-25-p2c-delivery-rule-aggregate.md
  • ADR-0035: Proposal Pipeline Layering
  • ADR-0036: Pricing Rule Aggregate (sister BC)
  • ADR-0037: Stock Rule Aggregate (sister BC)
  • Operator runbook: docs/docs/30-services/delivery/operator-runbook.md
  • OpenAPI: docs/docs/20-architecture/schemas/api/delivery-admin.yaml
  • Migration: backend/migrations/0024_create_delivery_rules.sql