Skip to main content

commerce_theory/
pricing.rs

1use crate::foundation::*;
2use crate::inventory::*;
3
4#[derive(Clone, Debug, PartialEq, Eq)]
5#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6pub struct CartLine {
7    pub(crate) sku: Sku,
8    pub(crate) price: Money,
9    pub(crate) cost: Money,
10    pub(crate) quantity: Quantity,
11    pub(crate) discount: Money,
12    pub(crate) weight: Weight,
13}
14
15impl CartLine {
16    pub fn try_new(
17        sku: Sku,
18        price: Money,
19        cost: Money,
20        quantity: Quantity,
21        discount: Money,
22        weight: Weight,
23    ) -> DomainResult<Self> {
24        let gross = checked_mul(price, quantity, "CartLine gross")?;
25        if discount > gross {
26            return Err(ValidationError::Invariant("line discount exceeds gross"));
27        }
28        Ok(Self {
29            sku,
30            price,
31            cost,
32            quantity,
33            discount,
34            weight,
35        })
36    }
37
38    #[must_use]
39    pub const fn quantity(&self) -> Quantity {
40        self.quantity
41    }
42}
43
44pub fn line_gross_total(line: &CartLine) -> DomainResult<Money> {
45    checked_mul(line.price, line.quantity, "line_gross_total")
46}
47
48pub fn line_cost_total(line: &CartLine) -> DomainResult<Money> {
49    checked_mul(line.cost, line.quantity, "line_cost_total")
50}
51
52pub fn line_net_total(line: &CartLine) -> DomainResult<Money> {
53    Ok(nat_sub(line_gross_total(line)?, line.discount))
54}
55
56pub fn line_weight_total(line: &CartLine) -> DomainResult<Weight> {
57    checked_mul(line.weight, line.quantity, "line_weight_total")
58}
59
60pub fn cart_gross_total(items: &[CartLine]) -> DomainResult<Money> {
61    checked_result_sum(items.iter().map(line_gross_total), "cart_gross_total")
62}
63
64pub fn cart_net_total(items: &[CartLine]) -> DomainResult<Money> {
65    checked_result_sum(items.iter().map(line_net_total), "cart_net_total")
66}
67
68pub fn cart_discount_total(items: &[CartLine]) -> DomainResult<Money> {
69    checked_sum(
70        items.iter().map(|line| line.discount),
71        "cart_discount_total",
72    )
73}
74
75pub fn cart_weight_total(items: &[CartLine]) -> DomainResult<Weight> {
76    checked_result_sum(items.iter().map(line_weight_total), "cart_weight_total")
77}
78
79pub fn cart_quantity_total(items: &[CartLine]) -> DomainResult<Quantity> {
80    checked_sum(
81        items.iter().map(|line| line.quantity),
82        "cart_quantity_total",
83    )
84}
85
86domain_struct! {
87    pub struct Coupon {
88        amount: Money,
89        min_subtotal: Money,
90        max_uses: Nat,
91    }
92}
93
94#[must_use]
95pub const fn coupon_can_be_applied(coupon: &Coupon, subtotal: Money, uses_before: Nat) -> bool {
96    coupon.min_subtotal <= subtotal && uses_before < coupon.max_uses
97}
98
99#[must_use]
100pub const fn subtotal_after_coupon_amount(subtotal: Money, coupon_amount: Money) -> Money {
101    nat_sub(subtotal, coupon_amount)
102}
103
104pub fn order_subtotal(items: &[CartLine], coupon_amount: Money) -> DomainResult<Money> {
105    Ok(subtotal_after_coupon_amount(
106        cart_net_total(items)?,
107        coupon_amount,
108    ))
109}
110
111domain_struct! {
112    pub struct ShippingMethod {
113        price: Money,
114        free_threshold: Money,
115        max_weight: Weight,
116    }
117}
118
119#[must_use]
120pub const fn shipping_available(method: &ShippingMethod, weight: Weight) -> bool {
121    weight <= method.max_weight
122}
123
124#[must_use]
125pub const fn shipping_charge(method: &ShippingMethod, subtotal: Money) -> Money {
126    if method.free_threshold <= subtotal {
127        0
128    } else {
129        method.price
130    }
131}
132
133pub fn order_total(
134    method: &ShippingMethod,
135    coupon_amount: Money,
136    tax: Money,
137    items: &[CartLine],
138) -> DomainResult<Money> {
139    let subtotal = order_subtotal(items, coupon_amount)?;
140    checked_add(
141        checked_add(
142            subtotal,
143            shipping_charge(method, subtotal),
144            "order_total shipping",
145        )?,
146        tax,
147        "order_total tax",
148    )
149}
150
151pub(crate) const fn _inventory_anchor(_: &StockState) {}
152
153impl_getters!(CartLine {
154    sku: Sku,
155    price: Money,
156    cost: Money,
157    discount: Money,
158    weight: Weight,
159});