from __future__ import annotations
from dataclasses import dataclass, field
from decimal import Decimal, ROUND_HALF_UP
from typing import Optional, Protocol
CENTS = Decimal("0.01")
def _money(value: Decimal) -> Decimal:
return value.quantize(CENTS, rounding=ROUND_HALF_UP)
@dataclass(frozen=True)
class LineItem:
sku: str
quantity: int
unit_price: Decimal
@property
def total(self) -> Decimal:
return _money(self.unit_price * self.quantity)
@dataclass(frozen=True)
class AppliedDiscount:
label: str
amount: Decimal
@dataclass(frozen=True)
class PricingContext:
items: tuple[LineItem, ...]
coupon_code: Optional[str] = None
reductions: Decimal = field(default=Decimal("0"))
@property
def subtotal(self) -> Decimal:
return _money(sum((i.total for i in self.items), Decimal("0")))
@property
def discounted_total(self) -> Decimal:
return max(_money(self.subtotal - self.reductions), Decimal("0"))
def with_reduction(self, amount: Decimal) -> "PricingContext":
return PricingContext(self.items, self.coupon_code, self.reductions + amount)
class DiscountRule(Protocol):
def apply(self, ctx: PricingContext) -> Optional[AppliedDiscount]:
...
from __future__ import annotations
from dataclasses import dataclass
from decimal import Decimal
from typing import Optional
from pricing import AppliedDiscount, PricingContext, _money
@dataclass
class PercentageOff:
rate: Decimal
label: str = "Percentage off"
def apply(self, ctx: PricingContext) -> Optional[AppliedDiscount]:
amount = _money(ctx.discounted_total * self.rate)
if amount <= 0:
return None
return AppliedDiscount(self.label, amount)
@dataclass
class FixedAmountOff:
amount: Decimal
label: str = "Fixed amount off"
def apply(self, ctx: PricingContext) -> Optional[AppliedDiscount]:
capped = min(self.amount, ctx.discounted_total)
if capped <= 0:
return None
return AppliedDiscount(self.label, _money(capped))
@dataclass
class BulkQuantityDiscount:
threshold: int
rate: Decimal
label: str = "Bulk order discount"
def apply(self, ctx: PricingContext) -> Optional[AppliedDiscount]:
units = sum(item.quantity for item in ctx.items)
if units < self.threshold:
return None
return AppliedDiscount(self.label, _money(ctx.discounted_total * self.rate))
@dataclass
class CouponDiscount:
code: str
amount: Decimal
label: str = "Coupon"
def apply(self, ctx: PricingContext) -> Optional[AppliedDiscount]:
if ctx.coupon_code != self.code:
return None
capped = min(self.amount, ctx.discounted_total)
if capped <= 0:
return None
return AppliedDiscount(f"{self.label} {self.code}", _money(capped))
from __future__ import annotations
from dataclasses import dataclass
from decimal import Decimal
from typing import Sequence
from pricing import AppliedDiscount, DiscountRule, PricingContext
from rules import BulkQuantityDiscount, CouponDiscount, PercentageOff
@dataclass(frozen=True)
class PricingResult:
subtotal: Decimal
total: Decimal
applied: tuple[AppliedDiscount, ...]
class DiscountPipeline:
def __init__(self, rules: Sequence[DiscountRule]):
self._rules = tuple(rules)
def run(self, ctx: PricingContext) -> PricingResult:
applied: list[AppliedDiscount] = []
for rule in self._rules:
discount = rule.apply(ctx)
if discount is None:
continue
applied.append(discount)
ctx = ctx.with_reduction(discount.amount)
return PricingResult(ctx.subtotal, ctx.discounted_total, tuple(applied))
def build_default_pipeline() -> DiscountPipeline:
return DiscountPipeline([
CouponDiscount(code="SAVE10", amount=Decimal("10.00")),
BulkQuantityDiscount(threshold=10, rate=Decimal("0.05")),
PercentageOff(rate=Decimal("0.02"), label="Loyalty bonus"),
])
This snippet models order-level discounting as a sequence of small, single-responsibility rule objects that are run in order over an accumulating pricing context. The core idea is the pipeline (a variation of the chain-of-responsibility and strategy patterns): each rule inspects the current state, optionally emits a discount, and the pipeline threads the result of one rule into the next. This keeps individual pricing rules tiny and testable while allowing the business to reorder, add, or remove promotions without touching a monolithic if/elif block.
In pricing.py, LineItem and PricingContext hold the immutable-ish state that flows through the pipeline. PricingContext.subtotal derives the raw total from line items, and discounted_total clamps the running total at zero so stacked discounts can never produce a negative charge. Money is handled with Decimal and quantized in _money to avoid the floating-point rounding errors that plague currency math. The DiscountRule Protocol defines the single method every rule must implement, apply, which returns an optional AppliedDiscount — returning None is how a rule cleanly opts out when its conditions are not met.
In rules.py, several concrete rules implement that protocol. PercentageOff and FixedAmountOff are unconditional reductions, while BulkQuantityDiscount and CouponDiscount gate themselves on cart contents or a supplied coupon code. Crucially, each rule computes its reduction against ctx.discounted_total, meaning later rules stack on the already-discounted amount rather than the original subtotal — a deliberate trade-off that mirrors how most stores apply sequential promotions. Because rules are plain objects, they carry their own configuration and can be unit-tested in isolation.
In engine.py, DiscountPipeline orchestrates the run: run folds each rule over the context, appending any AppliedDiscount and rebuilding the context so the next rule sees the updated total. The returned PricingResult exposes both the final total and the ordered applied list, giving callers a full audit trail for receipts or debugging. build_default_pipeline shows how rule ordering encodes policy — coupons before bulk discounts, for instance. A pitfall to watch is order-sensitivity: because reductions compound, changing rule order changes the final price, so ordering should be treated as an explicit business decision rather than an implementation detail.
Related snips
import { ReactNode } from 'react'
interface CardProps {
children: ReactNode
className?: string
}
React component composition over inheritance
from abc import ABC, abstractmethod
class UnknownChannel(Exception):
pass
Pluggable Notification Channels With a Template Registry in Python
package pipeline
import "context"
func generate(ctx context.Context, nums ...int) <-chan int {
out := make(chan int)
Fan-Out/Fan-In Pipeline With Channels and Context Cancellation in Go
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Currency {
pub code: &'static str,
pub symbol: &'static str,
pub exponent: u32,
}
Locale-Aware Money Formatting With Minor Units in Rust
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Currency;
import java.util.Objects;
public final class Money {
Immutable Value Object with a Fluent Builder and build()-Time Validation in Java
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, Dict, List
Parsing a Nested JSON Feed into Normalized Dataclasses in Python
Share this code
Here's the card — post it anywhere.