use crate::bundling::{
calculate_bundle_price, validate_bundle, validate_bundle_selection, validate_relationships,
Bundle, ProductRelationship,
};
use crate::complex_pricing::{
calculate_complex_price, ComplexPricingModel, PricingContext as ComplexPricingContext,
};
use crate::eligibility::{
evaluate_eligibility, EligibilityContext, EligibilityOutcome, EligibilityRule,
};
use crate::pricing::{
calculate_best_price, Money, PricingContext, PricingRule,
};
use crate::rules::{evaluate_rule, CatalogRule, RuleContext};
use crate::versioning::{CatalogSnapshot, CatalogVersion, VersionManager};
use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct QualifyAndPriceResult {
pub eligible: bool,
pub eligibility: EligibilityOutcome,
pub price: Option<Money>,
pub relationship_errors: Vec<String>,
}
pub struct CatalogEngine {
pricing_rules: Vec<PricingRule>,
eligibility_rules: Vec<EligibilityRule>,
bundles: Vec<Bundle>,
catalog_rules: Vec<CatalogRule>,
relationships: Vec<ProductRelationship>,
complex_models: Vec<(Uuid, ComplexPricingModel)>,
versions: VersionManager,
}
impl CatalogEngine {
pub fn new() -> Self {
Self {
pricing_rules: Vec::new(),
eligibility_rules: Vec::new(),
bundles: Vec::new(),
catalog_rules: Vec::new(),
relationships: Vec::new(),
complex_models: Vec::new(),
versions: VersionManager::new(),
}
}
pub fn add_pricing_rule(&mut self, rule: PricingRule) {
self.pricing_rules.push(rule);
}
pub fn add_eligibility_rule(&mut self, rule: EligibilityRule) {
self.eligibility_rules.push(rule);
}
pub fn add_bundle(&mut self, bundle: Bundle) -> Result<(), String> {
validate_bundle(&bundle)?;
self.bundles.push(bundle);
Ok(())
}
pub fn add_catalog_rule(&mut self, rule: CatalogRule) {
self.catalog_rules.push(rule);
}
pub fn add_relationship(&mut self, relationship: ProductRelationship) {
self.relationships.push(relationship);
}
pub fn add_complex_pricing_model(&mut self, product_offering_id: Uuid, model: ComplexPricingModel) {
self.complex_models.push((product_offering_id, model));
}
pub fn current_snapshot(&self) -> CatalogSnapshot {
CatalogSnapshot {
pricing_rules: self.pricing_rules.clone(),
eligibility_rules: self.eligibility_rules.clone(),
bundles: self.bundles.clone(),
catalog_rules: self.catalog_rules.clone(),
relationships: self.relationships.clone(),
}
}
pub fn create_version(
&mut self,
catalog_id: Uuid,
version: String,
description: Option<String>,
created_by: Option<Uuid>,
) -> CatalogVersion {
let snapshot = self.current_snapshot();
self.versions
.create_version(catalog_id, version, description, created_by, snapshot)
}
pub fn publish_version(&mut self, version_id: Uuid) -> Result<(), String> {
let published = self.versions.publish_version(version_id)?.clone();
self.load_snapshot(&published.snapshot);
Ok(())
}
pub fn rollback_to_version(&mut self, version_id: Uuid) -> Result<(), String> {
self.publish_version(version_id)
}
pub fn versions(&self) -> &VersionManager {
&self.versions
}
fn load_snapshot(&mut self, snapshot: &CatalogSnapshot) {
self.pricing_rules = snapshot.pricing_rules.clone();
self.eligibility_rules = snapshot.eligibility_rules.clone();
self.bundles = snapshot.bundles.clone();
self.catalog_rules = snapshot.catalog_rules.clone();
self.relationships = snapshot.relationships.clone();
}
pub fn check_eligibility(
&self,
product_offering_id: Uuid,
context: &EligibilityContext,
) -> bool {
self.explain_eligibility(product_offering_id, context)
.eligible
}
pub fn explain_eligibility(
&self,
product_offering_id: Uuid,
context: &EligibilityContext,
) -> EligibilityOutcome {
let mut failed = Vec::new();
let mut any_rule = false;
for rule in self
.eligibility_rules
.iter()
.filter(|rule| rule.product_offering_id == product_offering_id)
{
any_rule = true;
let outcome = evaluate_eligibility(rule, context);
if !outcome.eligible {
failed.extend(outcome.failed_conditions);
}
}
EligibilityOutcome {
eligible: !any_rule || failed.is_empty(),
failed_conditions: failed,
}
}
pub fn calculate_price(
&self,
product_offering_id: Uuid,
context: &PricingContext,
) -> Option<Money> {
if let Some((_, model)) = self
.complex_models
.iter()
.find(|(id, _)| *id == product_offering_id)
{
let complex_ctx = ComplexPricingContext {
quantity: context.quantity,
customer_id: None,
timestamp: context.as_of,
demand_level: None,
inventory_level: None,
existing_subscriptions: context.existing_products.clone(),
};
return Some(calculate_complex_price(
model,
context.quantity,
&complex_ctx,
));
}
calculate_best_price(&self.pricing_rules, product_offering_id, context)
}
pub fn qualify_and_price(
&self,
product_offering_id: Uuid,
eligibility: &EligibilityContext,
pricing: &PricingContext,
cart: &[Uuid],
) -> QualifyAndPriceResult {
let eligibility_outcome = self.explain_eligibility(product_offering_id, eligibility);
let mut relationship_errors = Vec::new();
if let Err(e) = validate_relationships(&self.relationships, cart) {
relationship_errors.push(e);
}
let eligible = eligibility_outcome.eligible && relationship_errors.is_empty();
let price = if eligible {
self.calculate_price(product_offering_id, pricing)
} else {
None
};
QualifyAndPriceResult {
eligible,
eligibility: eligibility_outcome,
price,
relationship_errors,
}
}
pub fn validate_bundle_selection(
&self,
bundle_id: Uuid,
selected: &[Uuid],
) -> Result<(), String> {
let bundle = self
.bundles
.iter()
.find(|b| b.id == bundle_id)
.ok_or_else(|| "Bundle not found".to_string())?;
validate_bundle_selection(bundle, selected)
}
pub fn calculate_bundle_price(
&self,
bundle_id: Uuid,
individual_prices: &[(Uuid, f64)],
) -> Result<f64, String> {
let bundle = self
.bundles
.iter()
.find(|b| b.id == bundle_id)
.ok_or_else(|| "Bundle not found".to_string())?;
calculate_bundle_price(bundle, individual_prices)
}
pub fn get_bundles_for_product(&self, product_offering_id: Uuid) -> Vec<&Bundle> {
self.bundles
.iter()
.filter(|bundle| {
bundle
.products
.iter()
.any(|bp| bp.product_offering_id == product_offering_id)
})
.collect()
}
pub fn evaluate_rules(&self, context: &RuleContext) -> Vec<&CatalogRule> {
self.catalog_rules
.iter()
.filter(|rule| {
matches!(
evaluate_rule(rule, context),
crate::rules::RuleResult::Matched { .. }
)
})
.collect()
}
}
impl Default for CatalogEngine {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::eligibility::{EligibilityCondition, EligibilityConditionOperator, EligibilityRuleType};
use crate::pricing::{PriceType, PricingRule};
#[test]
fn qualify_and_price_happy_path() {
let offering = Uuid::new_v4();
let mut engine = CatalogEngine::new();
engine.add_pricing_rule(PricingRule {
id: Uuid::new_v4(),
product_offering_id: offering,
price_type: PriceType::OneTime,
base_price: Money {
value: 25.0,
unit: "USD".into(),
},
priority: 10,
discount_rules: None,
valid_for: None,
});
engine.add_eligibility_rule(EligibilityRule {
id: Uuid::new_v4(),
product_offering_id: offering,
rule_type: EligibilityRuleType::All,
conditions: vec![EligibilityCondition {
field: "customer_segment".into(),
operator: EligibilityConditionOperator::Equals,
value: "premium".into(),
}],
});
let mut elig = EligibilityContext::new();
elig.customer_segment = Some("premium".into());
let result = engine.qualify_and_price(offering, &elig, &PricingContext::new(1), &[offering]);
assert!(result.eligible);
assert_eq!(result.price.unwrap().value, 25.0);
}
}