pcm_engine/
eligibility.rs

1//! Product eligibility validation
2
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6/// Eligibility rule for a product offering
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct EligibilityRule {
9    pub id: Uuid,
10    pub product_offering_id: Uuid,
11    pub conditions: Vec<EligibilityCondition>,
12    pub rule_type: EligibilityRuleType,
13}
14
15/// Eligibility rule type
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
17#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
18pub enum EligibilityRuleType {
19    /// All conditions must be met
20    All,
21    /// At least one condition must be met
22    Any,
23}
24
25/// Eligibility condition
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct EligibilityCondition {
28    pub field: String,
29    pub operator: EligibilityConditionOperator,
30    pub value: String,
31}
32
33/// Eligibility condition operator
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
35#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
36pub enum EligibilityConditionOperator {
37    Equals,
38    NotEquals,
39    GreaterThan,
40    LessThan,
41    Contains,
42    NotContains,
43    In,
44    NotIn,
45}
46
47/// Eligibility context for validation
48#[derive(Debug, Clone)]
49pub struct EligibilityContext {
50    pub customer_id: Option<Uuid>,
51    pub customer_segment: Option<String>,
52    pub existing_products: Vec<Uuid>,
53    pub customer_attributes: std::collections::HashMap<String, String>,
54}
55
56/// Check if a product offering is eligible for a customer
57pub fn is_eligible(rule: &EligibilityRule, context: &EligibilityContext) -> bool {
58    match rule.rule_type {
59        EligibilityRuleType::All => rule
60            .conditions
61            .iter()
62            .all(|condition| evaluate_condition(condition, context)),
63        EligibilityRuleType::Any => rule
64            .conditions
65            .iter()
66            .any(|condition| evaluate_condition(condition, context)),
67    }
68}
69
70fn evaluate_condition(condition: &EligibilityCondition, context: &EligibilityContext) -> bool {
71    let field_value = get_field_value(&condition.field, context);
72
73    match condition.operator {
74        EligibilityConditionOperator::Equals => field_value == condition.value,
75        EligibilityConditionOperator::NotEquals => field_value != condition.value,
76        EligibilityConditionOperator::GreaterThan => {
77            if let (Ok(field_num), Ok(cond_num)) =
78                (field_value.parse::<f64>(), condition.value.parse::<f64>())
79            {
80                field_num > cond_num
81            } else {
82                false
83            }
84        }
85        EligibilityConditionOperator::LessThan => {
86            if let (Ok(field_num), Ok(cond_num)) =
87                (field_value.parse::<f64>(), condition.value.parse::<f64>())
88            {
89                field_num < cond_num
90            } else {
91                false
92            }
93        }
94        EligibilityConditionOperator::Contains => field_value.contains(&condition.value),
95        EligibilityConditionOperator::NotContains => !field_value.contains(&condition.value),
96        EligibilityConditionOperator::In => {
97            condition.value.split(',').any(|v| v.trim() == field_value)
98        }
99        EligibilityConditionOperator::NotIn => {
100            !condition.value.split(',').any(|v| v.trim() == field_value)
101        }
102    }
103}
104
105fn get_field_value(field: &str, context: &EligibilityContext) -> String {
106    match field {
107        "customer_segment" => context.customer_segment.clone().unwrap_or_default(),
108        "has_product" => {
109            // Check if customer has a specific product
110            // This would need product_id in the condition value
111            "false".to_string()
112        }
113        _ => context
114            .customer_attributes
115            .get(field)
116            .cloned()
117            .unwrap_or_default(),
118    }
119}