pcm_engine/
pricing.rs

1//! Pricing rules and calculations
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7/// Pricing rule for a product offering
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct PricingRule {
10    pub id: Uuid,
11    pub product_offering_id: Uuid,
12    pub price_type: PriceType,
13    pub base_price: Money,
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub discount_rules: Option<Vec<DiscountRule>>,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub valid_for: Option<TimePeriod>,
18}
19
20/// Price type
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
22#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
23pub enum PriceType {
24    Recurring,
25    OneTime,
26    Usage,
27    Tiered,
28}
29
30/// Money representation
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct Money {
33    pub value: f64,
34    pub unit: String,
35}
36
37/// Discount rule
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct DiscountRule {
40    pub name: String,
41    pub discount_type: DiscountType,
42    pub value: f64,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub conditions: Option<Vec<DiscountCondition>>,
45}
46
47/// Discount type
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
49#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
50pub enum DiscountType {
51    Percentage,
52    FixedAmount,
53}
54
55/// Discount condition
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct DiscountCondition {
58    pub field: String,
59    pub operator: PricingConditionOperator,
60    pub value: String,
61}
62
63/// Pricing condition operator
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
65#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
66pub enum PricingConditionOperator {
67    Equals,
68    GreaterThan,
69    LessThan,
70    Contains,
71}
72
73/// Time period
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct TimePeriod {
76    pub start_date_time: DateTime<Utc>,
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub end_date_time: Option<DateTime<Utc>>,
79}
80
81/// Calculate final price after applying discounts
82pub fn calculate_final_price(rule: &PricingRule, context: &PricingContext) -> Money {
83    let mut final_price = rule.base_price.value;
84
85    if let Some(ref discounts) = rule.discount_rules {
86        for discount in discounts {
87            if is_discount_applicable(discount, context) {
88                final_price = apply_discount(final_price, discount);
89            }
90        }
91    }
92
93    Money {
94        value: final_price.max(0.0),
95        unit: rule.base_price.unit.clone(),
96    }
97}
98
99/// Pricing context for discount evaluation
100#[derive(Debug, Clone)]
101pub struct PricingContext {
102    pub customer_segment: Option<String>,
103    pub quantity: u32,
104    pub existing_products: Vec<Uuid>,
105}
106
107fn is_discount_applicable(discount: &DiscountRule, context: &PricingContext) -> bool {
108    if let Some(ref conditions) = discount.conditions {
109        conditions
110            .iter()
111            .all(|condition| evaluate_condition(condition, context))
112    } else {
113        true
114    }
115}
116
117fn evaluate_condition(condition: &DiscountCondition, context: &PricingContext) -> bool {
118    match condition.field.as_str() {
119        "customer_segment" => {
120            if let Some(ref segment) = context.customer_segment {
121                match condition.operator {
122                    PricingConditionOperator::Equals => segment == &condition.value,
123                    PricingConditionOperator::Contains => segment.contains(&condition.value),
124                    _ => false,
125                }
126            } else {
127                false
128            }
129        }
130        "quantity" => {
131            let qty: u32 = condition.value.parse().unwrap_or(0);
132            match condition.operator {
133                PricingConditionOperator::GreaterThan => context.quantity > qty,
134                PricingConditionOperator::LessThan => context.quantity < qty,
135                PricingConditionOperator::Equals => context.quantity == qty,
136                _ => false,
137            }
138        }
139        _ => false,
140    }
141}
142
143fn apply_discount(base_price: f64, discount: &DiscountRule) -> f64 {
144    match discount.discount_type {
145        DiscountType::Percentage => base_price * (1.0 - discount.value / 100.0),
146        DiscountType::FixedAmount => (base_price - discount.value).max(0.0),
147    }
148}