1use crate::bundling::{
4 calculate_bundle_price, validate_bundle, validate_bundle_selection, validate_relationships,
5 Bundle, ProductRelationship,
6};
7use crate::complex_pricing::{
8 calculate_complex_price, ComplexPricingModel, PricingContext as ComplexPricingContext,
9};
10use crate::eligibility::{
11 evaluate_eligibility, EligibilityContext, EligibilityOutcome, EligibilityRule,
12};
13use crate::pricing::{
14 calculate_best_price, Money, PricingContext, PricingRule,
15};
16use crate::rules::{evaluate_rule, CatalogRule, RuleContext};
17use crate::versioning::{CatalogSnapshot, CatalogVersion, VersionManager};
18use uuid::Uuid;
19
20#[derive(Debug, Clone)]
22pub struct QualifyAndPriceResult {
23 pub eligible: bool,
24 pub eligibility: EligibilityOutcome,
25 pub price: Option<Money>,
26 pub relationship_errors: Vec<String>,
27}
28
29pub struct CatalogEngine {
31 pricing_rules: Vec<PricingRule>,
32 eligibility_rules: Vec<EligibilityRule>,
33 bundles: Vec<Bundle>,
34 catalog_rules: Vec<CatalogRule>,
35 relationships: Vec<ProductRelationship>,
36 complex_models: Vec<(Uuid, ComplexPricingModel)>,
37 versions: VersionManager,
38}
39
40impl CatalogEngine {
41 pub fn new() -> Self {
43 Self {
44 pricing_rules: Vec::new(),
45 eligibility_rules: Vec::new(),
46 bundles: Vec::new(),
47 catalog_rules: Vec::new(),
48 relationships: Vec::new(),
49 complex_models: Vec::new(),
50 versions: VersionManager::new(),
51 }
52 }
53
54 pub fn add_pricing_rule(&mut self, rule: PricingRule) {
56 self.pricing_rules.push(rule);
57 }
58
59 pub fn add_eligibility_rule(&mut self, rule: EligibilityRule) {
61 self.eligibility_rules.push(rule);
62 }
63
64 pub fn add_bundle(&mut self, bundle: Bundle) -> Result<(), String> {
66 validate_bundle(&bundle)?;
67 self.bundles.push(bundle);
68 Ok(())
69 }
70
71 pub fn add_catalog_rule(&mut self, rule: CatalogRule) {
73 self.catalog_rules.push(rule);
74 }
75
76 pub fn add_relationship(&mut self, relationship: ProductRelationship) {
78 self.relationships.push(relationship);
79 }
80
81 pub fn add_complex_pricing_model(&mut self, product_offering_id: Uuid, model: ComplexPricingModel) {
83 self.complex_models.push((product_offering_id, model));
84 }
85
86 pub fn current_snapshot(&self) -> CatalogSnapshot {
88 CatalogSnapshot {
89 pricing_rules: self.pricing_rules.clone(),
90 eligibility_rules: self.eligibility_rules.clone(),
91 bundles: self.bundles.clone(),
92 catalog_rules: self.catalog_rules.clone(),
93 relationships: self.relationships.clone(),
94 }
95 }
96
97 pub fn create_version(
99 &mut self,
100 catalog_id: Uuid,
101 version: String,
102 description: Option<String>,
103 created_by: Option<Uuid>,
104 ) -> CatalogVersion {
105 let snapshot = self.current_snapshot();
106 self.versions
107 .create_version(catalog_id, version, description, created_by, snapshot)
108 }
109
110 pub fn publish_version(&mut self, version_id: Uuid) -> Result<(), String> {
112 let published = self.versions.publish_version(version_id)?.clone();
113 self.load_snapshot(&published.snapshot);
114 Ok(())
115 }
116
117 pub fn rollback_to_version(&mut self, version_id: Uuid) -> Result<(), String> {
119 self.publish_version(version_id)
120 }
121
122 pub fn versions(&self) -> &VersionManager {
124 &self.versions
125 }
126
127 fn load_snapshot(&mut self, snapshot: &CatalogSnapshot) {
128 self.pricing_rules = snapshot.pricing_rules.clone();
129 self.eligibility_rules = snapshot.eligibility_rules.clone();
130 self.bundles = snapshot.bundles.clone();
131 self.catalog_rules = snapshot.catalog_rules.clone();
132 self.relationships = snapshot.relationships.clone();
133 }
134
135 pub fn check_eligibility(
137 &self,
138 product_offering_id: Uuid,
139 context: &EligibilityContext,
140 ) -> bool {
141 self.explain_eligibility(product_offering_id, context)
142 .eligible
143 }
144
145 pub fn explain_eligibility(
147 &self,
148 product_offering_id: Uuid,
149 context: &EligibilityContext,
150 ) -> EligibilityOutcome {
151 let mut failed = Vec::new();
152 let mut any_rule = false;
153 for rule in self
154 .eligibility_rules
155 .iter()
156 .filter(|rule| rule.product_offering_id == product_offering_id)
157 {
158 any_rule = true;
159 let outcome = evaluate_eligibility(rule, context);
160 if !outcome.eligible {
161 failed.extend(outcome.failed_conditions);
162 }
163 }
164 EligibilityOutcome {
165 eligible: !any_rule || failed.is_empty(),
166 failed_conditions: failed,
167 }
168 }
169
170 pub fn calculate_price(
172 &self,
173 product_offering_id: Uuid,
174 context: &PricingContext,
175 ) -> Option<Money> {
176 if let Some((_, model)) = self
177 .complex_models
178 .iter()
179 .find(|(id, _)| *id == product_offering_id)
180 {
181 let complex_ctx = ComplexPricingContext {
182 quantity: context.quantity,
183 customer_id: None,
184 timestamp: context.as_of,
185 demand_level: None,
186 inventory_level: None,
187 existing_subscriptions: context.existing_products.clone(),
188 };
189 return Some(calculate_complex_price(
190 model,
191 context.quantity,
192 &complex_ctx,
193 ));
194 }
195 calculate_best_price(&self.pricing_rules, product_offering_id, context)
196 }
197
198 pub fn qualify_and_price(
200 &self,
201 product_offering_id: Uuid,
202 eligibility: &EligibilityContext,
203 pricing: &PricingContext,
204 cart: &[Uuid],
205 ) -> QualifyAndPriceResult {
206 let eligibility_outcome = self.explain_eligibility(product_offering_id, eligibility);
207 let mut relationship_errors = Vec::new();
208 if let Err(e) = validate_relationships(&self.relationships, cart) {
209 relationship_errors.push(e);
210 }
211 let eligible = eligibility_outcome.eligible && relationship_errors.is_empty();
212 let price = if eligible {
213 self.calculate_price(product_offering_id, pricing)
214 } else {
215 None
216 };
217 QualifyAndPriceResult {
218 eligible,
219 eligibility: eligibility_outcome,
220 price,
221 relationship_errors,
222 }
223 }
224
225 pub fn validate_bundle_selection(
227 &self,
228 bundle_id: Uuid,
229 selected: &[Uuid],
230 ) -> Result<(), String> {
231 let bundle = self
232 .bundles
233 .iter()
234 .find(|b| b.id == bundle_id)
235 .ok_or_else(|| "Bundle not found".to_string())?;
236 validate_bundle_selection(bundle, selected)
237 }
238
239 pub fn calculate_bundle_price(
241 &self,
242 bundle_id: Uuid,
243 individual_prices: &[(Uuid, f64)],
244 ) -> Result<f64, String> {
245 let bundle = self
246 .bundles
247 .iter()
248 .find(|b| b.id == bundle_id)
249 .ok_or_else(|| "Bundle not found".to_string())?;
250 calculate_bundle_price(bundle, individual_prices)
251 }
252
253 pub fn get_bundles_for_product(&self, product_offering_id: Uuid) -> Vec<&Bundle> {
255 self.bundles
256 .iter()
257 .filter(|bundle| {
258 bundle
259 .products
260 .iter()
261 .any(|bp| bp.product_offering_id == product_offering_id)
262 })
263 .collect()
264 }
265
266 pub fn evaluate_rules(&self, context: &RuleContext) -> Vec<&CatalogRule> {
268 self.catalog_rules
269 .iter()
270 .filter(|rule| {
271 matches!(
272 evaluate_rule(rule, context),
273 crate::rules::RuleResult::Matched { .. }
274 )
275 })
276 .collect()
277 }
278}
279
280impl Default for CatalogEngine {
281 fn default() -> Self {
282 Self::new()
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289 use crate::eligibility::{EligibilityCondition, EligibilityConditionOperator, EligibilityRuleType};
290 use crate::pricing::{PriceType, PricingRule};
291
292 #[test]
293 fn qualify_and_price_happy_path() {
294 let offering = Uuid::new_v4();
295 let mut engine = CatalogEngine::new();
296 engine.add_pricing_rule(PricingRule {
297 id: Uuid::new_v4(),
298 product_offering_id: offering,
299 price_type: PriceType::OneTime,
300 base_price: Money {
301 value: 25.0,
302 unit: "USD".into(),
303 },
304 priority: 10,
305 discount_rules: None,
306 valid_for: None,
307 });
308 engine.add_eligibility_rule(EligibilityRule {
309 id: Uuid::new_v4(),
310 product_offering_id: offering,
311 rule_type: EligibilityRuleType::All,
312 conditions: vec![EligibilityCondition {
313 field: "customer_segment".into(),
314 operator: EligibilityConditionOperator::Equals,
315 value: "premium".into(),
316 }],
317 });
318
319 let mut elig = EligibilityContext::new();
320 elig.customer_segment = Some("premium".into());
321 let result = engine.qualify_and_price(offering, &elig, &PricingContext::new(1), &[offering]);
322 assert!(result.eligible);
323 assert_eq!(result.price.unwrap().value, 25.0);
324 }
325}