1use crate::competitor_pricing::*;
2use crate::foundation::*;
3
4#[derive(Clone, Debug, PartialEq, Eq)]
5#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6pub struct BrandPricingPolicy {
7 pub(crate) map_price: Money,
8 pub(crate) msrp: Money,
9}
10
11impl BrandPricingPolicy {
12 pub const fn try_new(map_price: Money, msrp: Money) -> DomainResult<Self> {
13 if map_price > msrp {
14 return Err(ValidationError::Invariant("MAP exceeds MSRP"));
15 }
16 Ok(Self { map_price, msrp })
17 }
18}
19
20#[must_use]
21pub const fn advertised_price_allowed(
22 policy: &BrandPricingPolicy,
23 advertised_price: Money,
24) -> bool {
25 policy.map_price <= advertised_price
26}
27
28#[derive(Clone, Debug, PartialEq, Eq)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30pub struct BundleComponent {
31 pub(crate) sku: Sku,
32 pub(crate) units_per_bundle: Quantity,
33 pub(crate) stock_available: Quantity,
34}
35
36impl BundleComponent {
37 pub const fn try_new(
38 sku: Sku,
39 units_per_bundle: Quantity,
40 stock_available: Quantity,
41 ) -> DomainResult<Self> {
42 if units_per_bundle == 0 {
43 return Err(ValidationError::Invariant(
44 "bundle units per component must be positive",
45 ));
46 }
47 Ok(Self {
48 sku,
49 units_per_bundle,
50 stock_available,
51 })
52 }
53}
54
55pub fn component_required_for_bundles(
56 bundle_qty: Quantity,
57 component: &BundleComponent,
58) -> DomainResult<Quantity> {
59 checked_mul(
60 bundle_qty,
61 component.units_per_bundle,
62 "component_required_for_bundles",
63 )
64}
65
66pub fn component_can_fulfill_bundles(
67 bundle_qty: Quantity,
68 component: &BundleComponent,
69) -> DomainResult<bool> {
70 Ok(component_required_for_bundles(bundle_qty, component)? <= component.stock_available)
71}
72
73#[derive(Clone, Debug, PartialEq, Eq)]
74#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
75pub struct BundleReservation {
76 pub(crate) bundle_qty: Quantity,
77 pub(crate) components: Vec<BundleComponent>,
78}
79
80impl BundleReservation {
81 pub fn try_new(bundle_qty: Quantity, components: Vec<BundleComponent>) -> DomainResult<Self> {
82 for component in &components {
83 if !component_can_fulfill_bundles(bundle_qty, component)? {
84 return Err(ValidationError::Invariant(
85 "bundle component cannot fulfill reservation",
86 ));
87 }
88 }
89 Ok(Self {
90 bundle_qty,
91 components,
92 })
93 }
94}
95
96#[derive(Clone, Copy, Debug, PartialEq, Eq)]
97#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
98pub enum PromotionStackingPolicy {
99 Exclusive,
100 Stackable,
101 StackableWithCap,
102}
103
104#[derive(Clone, Debug, PartialEq, Eq)]
105#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
106pub struct AcceptedPromotionSet {
107 pub(crate) resulting_price: Money,
108 pub(crate) total_discount: Money,
109 pub(crate) discount_cap: Money,
110 pub(crate) profit_floor: Money,
111}
112
113impl AcceptedPromotionSet {
114 pub const fn try_new(
115 resulting_price: Money,
116 total_discount: Money,
117 discount_cap: Money,
118 profit_floor: Money,
119 ) -> DomainResult<Self> {
120 if total_discount > discount_cap {
121 return Err(ValidationError::Invariant("promotion discount exceeds cap"));
122 }
123 if profit_floor > resulting_price {
124 return Err(ValidationError::Invariant(
125 "promotion price below profit floor",
126 ));
127 }
128 Ok(Self {
129 resulting_price,
130 total_discount,
131 discount_cap,
132 profit_floor,
133 })
134 }
135}
136
137#[must_use]
138pub const fn promotion_set_allowed_by_policy(
139 policy: PromotionStackingPolicy,
140 promotion_count: Nat,
141 set: &AcceptedPromotionSet,
142) -> bool {
143 match policy {
144 PromotionStackingPolicy::Exclusive => promotion_count <= 1,
145 PromotionStackingPolicy::Stackable => true,
146 PromotionStackingPolicy::StackableWithCap => set.total_discount <= set.discount_cap,
147 }
148}
149
150domain_struct! {
151 pub struct SearchResultItem {
152 sku: Sku,
153 archived: bool,
154 in_stock: bool,
155 margin_safe: bool,
156 }
157}
158
159#[derive(Clone, Debug, PartialEq, Eq)]
160#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
161pub struct ValidSearchResultItem {
162 pub(crate) item: SearchResultItem,
163}
164
165impl ValidSearchResultItem {
166 pub const fn try_new(item: SearchResultItem) -> DomainResult<Self> {
167 if item.archived || !item.in_stock || !item.margin_safe {
168 return Err(ValidationError::Invariant("search result is not safe"));
169 }
170 Ok(Self { item })
171 }
172}
173
174pub(crate) const fn _competitor_anchor(_: Option<TrustLevel>) {}
175
176impl_getters!(BrandPricingPolicy {
177 map_price: Money,
178 msrp: Money,
179});
180
181impl_getters!(BundleComponent {
182 sku: Sku,
183 units_per_bundle: Quantity,
184 stock_available: Quantity,
185});
186
187impl_getters!(BundleReservation {
188 bundle_qty: Quantity,
189 components: Vec<BundleComponent>,
190});
191
192impl_getters!(AcceptedPromotionSet {
193 resulting_price: Money,
194 total_discount: Money,
195 discount_cap: Money,
196 profit_floor: Money,
197});
198
199impl_getters!(ValidSearchResultItem {
200 item: SearchResultItem,
201});