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 roledelivery_admin. - Snapshot polling —
atomic.Pointer[Snapshot], 10s incremental + 5min full reload. - Transactional outbox: каждая Create/Update/Deprecate в single pgx.Tx с
outbox.Append. Topicdelivery.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 enforced —
delivery-admin.yaml↔infra/http/*.goparity. - Extends
prop.DeliveryOutcomeсCost *money.Amountfield — non-breaking domain change. NewTraceReason("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.
Costfield 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.Costfield — 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