pcm_engine/
complex_pricing.rs

1//! Complex Pricing Models
2//!
3//! Supports tiered pricing, volume-based pricing, subscription models, and dynamic pricing
4
5use crate::pricing::Money;
6use chrono::{DateTime, Timelike, Utc};
7use serde::{Deserialize, Serialize};
8use uuid::Uuid;
9
10/// Complex pricing model
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub enum ComplexPricingModel {
13    /// Tiered pricing - different prices for different quantity tiers
14    Tiered(TieredPricing),
15    /// Volume-based pricing - price decreases with volume
16    VolumeBased(VolumePricing),
17    /// Subscription pricing - recurring charges
18    Subscription(SubscriptionPricing),
19    /// Dynamic pricing - price changes based on demand/time
20    Dynamic(DynamicPricing),
21    /// Bundle pricing - special pricing for product bundles
22    Bundle(BundlePricing),
23}
24
25/// Tiered pricing structure
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct TieredPricing {
28    pub tiers: Vec<PricingTier>,
29}
30
31/// Pricing tier
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct PricingTier {
34    pub min_quantity: u32,
35    pub max_quantity: Option<u32>,
36    pub price: Money,
37    pub price_per_unit: Option<Money>,
38}
39
40/// Volume-based pricing
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct VolumePricing {
43    pub base_price: Money,
44    pub volume_discounts: Vec<VolumeDiscount>,
45}
46
47/// Volume discount
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct VolumeDiscount {
50    pub min_volume: u32,
51    pub discount_percentage: f64,
52}
53
54/// Subscription pricing
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct SubscriptionPricing {
57    pub recurring_price: Money,
58    pub billing_cycle: BillingCycle,
59    pub setup_fee: Option<Money>,
60    pub trial_period_days: Option<u32>,
61    pub cancellation_policy: CancellationPolicy,
62}
63
64/// Billing cycle
65#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
66#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
67pub enum BillingCycle {
68    Monthly,
69    Quarterly,
70    SemiAnnual,
71    Annual,
72    Weekly,
73    Daily,
74}
75
76/// Cancellation policy
77#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
78#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
79pub enum CancellationPolicy {
80    Immediate,
81    EndOfPeriod,
82    ProRated,
83}
84
85/// Dynamic pricing
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct DynamicPricing {
88    pub base_price: Money,
89    pub factors: Vec<PricingFactor>,
90    pub adjustment_rules: Vec<PriceAdjustmentRule>,
91}
92
93/// Pricing factor that affects price
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct PricingFactor {
96    pub factor_type: FactorType,
97    pub weight: f64,
98    pub adjustment_percentage: f64,
99}
100
101/// Factor type
102#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
103#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
104pub enum FactorType {
105    Demand,
106    TimeOfDay,
107    DayOfWeek,
108    Season,
109    Inventory,
110    Competition,
111}
112
113/// Price adjustment rule
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct PriceAdjustmentRule {
116    pub condition: String, // JSON condition
117    pub adjustment_type: AdjustmentType,
118    pub value: f64,
119}
120
121/// Adjustment type
122#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
123#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
124pub enum AdjustmentType {
125    Percentage,
126    FixedAmount,
127    Multiplier,
128}
129
130/// Bundle pricing
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct BundlePricing {
133    pub bundle_id: Uuid,
134    pub component_prices: Vec<ComponentPrice>,
135    pub bundle_discount: f64,
136    pub minimum_components: Option<u32>,
137}
138
139/// Component price in a bundle
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct ComponentPrice {
142    pub product_offering_id: Uuid,
143    pub price: Money,
144    pub required: bool,
145}
146
147/// Calculate price using complex pricing model
148pub fn calculate_complex_price(
149    model: &ComplexPricingModel,
150    quantity: u32,
151    context: &PricingContext,
152) -> Money {
153    match model {
154        ComplexPricingModel::Tiered(tiered) => calculate_tiered_price(tiered, quantity),
155        ComplexPricingModel::VolumeBased(volume) => calculate_volume_price(volume, quantity),
156        ComplexPricingModel::Subscription(sub) => calculate_subscription_price(sub),
157        ComplexPricingModel::Dynamic(dynamic) => calculate_dynamic_price(dynamic, context),
158        ComplexPricingModel::Bundle(bundle) => calculate_bundle_price(bundle, context),
159    }
160}
161
162/// Pricing context for complex calculations
163#[derive(Debug, Clone)]
164pub struct PricingContext {
165    pub quantity: u32,
166    pub customer_id: Option<Uuid>,
167    pub timestamp: DateTime<Utc>,
168    pub demand_level: Option<f64>,
169    pub inventory_level: Option<f64>,
170    pub existing_subscriptions: Vec<Uuid>,
171}
172
173fn calculate_tiered_price(tiered: &TieredPricing, quantity: u32) -> Money {
174    for tier in &tiered.tiers {
175        if quantity >= tier.min_quantity {
176            if let Some(max) = tier.max_quantity {
177                if quantity <= max {
178                    return apply_tier_price(tier, quantity);
179                }
180            } else {
181                return apply_tier_price(tier, quantity);
182            }
183        }
184    }
185    // Default to first tier if no match
186    tiered
187        .tiers
188        .first()
189        .map(|t| apply_tier_price(t, quantity))
190        .unwrap_or_else(|| Money {
191            value: 0.0,
192            unit: "USD".to_string(),
193        })
194}
195
196fn apply_tier_price(tier: &PricingTier, quantity: u32) -> Money {
197    if let Some(ref per_unit) = tier.price_per_unit {
198        Money {
199            value: per_unit.value * quantity as f64,
200            unit: per_unit.unit.clone(),
201        }
202    } else {
203        tier.price.clone()
204    }
205}
206
207fn calculate_volume_price(volume: &VolumePricing, quantity: u32) -> Money {
208    let mut final_price = volume.base_price.value;
209    let mut best_discount: f64 = 0.0;
210
211    for discount in &volume.volume_discounts {
212        if quantity >= discount.min_volume {
213            best_discount = best_discount.max(discount.discount_percentage);
214        }
215    }
216
217    final_price *= 1.0 - (best_discount / 100.0);
218
219    Money {
220        value: final_price.max(0.0),
221        unit: volume.base_price.unit.clone(),
222    }
223}
224
225fn calculate_subscription_price(sub: &SubscriptionPricing) -> Money {
226    sub.recurring_price.clone()
227}
228
229fn calculate_dynamic_price(dynamic: &DynamicPricing, context: &PricingContext) -> Money {
230    let mut price = dynamic.base_price.value;
231
232    for factor in &dynamic.factors {
233        let adjustment = match factor.factor_type {
234            FactorType::Demand => context
235                .demand_level
236                .map(|d| d * factor.weight * factor.adjustment_percentage / 100.0),
237            FactorType::TimeOfDay => {
238                let hour = context.timestamp.hour();
239                if (9..=17).contains(&hour) {
240                    Some(factor.adjustment_percentage / 100.0)
241                } else {
242                    Some(-factor.adjustment_percentage / 100.0)
243                }
244            }
245            FactorType::Inventory => context.inventory_level.map(|inv| {
246                if inv < 0.2 {
247                    factor.adjustment_percentage / 100.0
248                } else {
249                    -factor.adjustment_percentage / 100.0
250                }
251            }),
252            _ => None,
253        };
254
255        if let Some(adj) = adjustment {
256            price *= 1.0 + adj;
257        }
258    }
259
260    Money {
261        value: price.max(0.0),
262        unit: dynamic.base_price.unit.clone(),
263    }
264}
265
266fn calculate_bundle_price(bundle: &BundlePricing, _context: &PricingContext) -> Money {
267    let total: f64 = bundle
268        .component_prices
269        .iter()
270        .map(|cp| cp.price.value)
271        .sum();
272
273    let discounted = total * (1.0 - bundle.bundle_discount / 100.0);
274
275    Money {
276        value: discounted.max(0.0),
277        unit: bundle
278            .component_prices
279            .first()
280            .map(|cp| cp.price.unit.clone())
281            .unwrap_or_else(|| "USD".to_string()),
282    }
283}