pcm_engine/
bundling.rs

1//! Product bundling and relationship management
2
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6/// Bundle definition
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct Bundle {
9    pub id: Uuid,
10    pub name: String,
11    pub bundle_type: BundleType,
12    pub products: Vec<BundleProduct>,
13    pub bundle_price: Option<BundlePrice>,
14}
15
16/// Bundle type
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
18#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
19pub enum BundleType {
20    /// All products must be included
21    Mandatory,
22    /// At least one product must be included
23    Optional,
24    /// Products are mutually exclusive
25    Exclusive,
26}
27
28/// Product in a bundle
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct BundleProduct {
31    pub product_offering_id: Uuid,
32    pub quantity: u32,
33    pub is_required: bool,
34}
35
36/// Bundle pricing
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct BundlePrice {
39    pub discount_type: BundleDiscountType,
40    pub value: f64,
41}
42
43/// Bundle discount type
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
45#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
46pub enum BundleDiscountType {
47    /// Percentage discount on total
48    PercentageOff,
49    /// Fixed amount discount
50    FixedAmountOff,
51    /// Fixed price for the bundle
52    FixedPrice,
53}
54
55/// Validate bundle configuration
56pub fn validate_bundle(bundle: &Bundle) -> Result<(), String> {
57    if bundle.products.is_empty() {
58        return Err("Bundle must contain at least one product".to_string());
59    }
60
61    match bundle.bundle_type {
62        BundleType::Mandatory => {
63            if bundle.products.iter().any(|p| !p.is_required) {
64                return Err("Mandatory bundles cannot have optional products".to_string());
65            }
66        }
67        BundleType::Exclusive => {
68            if bundle.products.len() < 2 {
69                return Err("Exclusive bundles must have at least 2 products".to_string());
70            }
71        }
72        _ => {}
73    }
74
75    Ok(())
76}
77
78/// Calculate bundle price
79pub fn calculate_bundle_price(
80    bundle: &Bundle,
81    individual_prices: &[(Uuid, f64)],
82) -> Result<f64, String> {
83    let total_individual_price: f64 = bundle
84        .products
85        .iter()
86        .map(|bp| {
87            individual_prices
88                .iter()
89                .find(|(id, _)| *id == bp.product_offering_id)
90                .map(|(_, price)| *price * bp.quantity as f64)
91                .unwrap_or(0.0)
92        })
93        .sum();
94
95    match &bundle.bundle_price {
96        Some(bp) => match bp.discount_type {
97            BundleDiscountType::PercentageOff => {
98                Ok(total_individual_price * (1.0 - bp.value / 100.0))
99            }
100            BundleDiscountType::FixedAmountOff => Ok((total_individual_price - bp.value).max(0.0)),
101            BundleDiscountType::FixedPrice => Ok(bp.value),
102        },
103        None => Ok(total_individual_price),
104    }
105}