commerce_theory/
pricing.rs1use 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});