use crate::domain::models::OrderRecord; use chrono::Timelike; use serde_json::Value; /// Strategy trait defining a singular risk checking vector. /// /// Follows the Open-Closed Principle (OCP) of OOPS: adding new risk vectors /// is accomplished by implementing this trait without modifying existing ones. pub trait RiskEvaluationStrategy: Send + Sync { fn evaluate( &self, order: &OrderRecord, history_count: i64, pincode_volatility: f64, ) -> Option<(f64, &'static str, Value)>; } // ======================================================== // CONCRETE STRATEGIES (RISK VECTORS) // ======================================================== /// VECTOR 0: Network Intelligence (Volatility) /// Adjusts risk based on systemic logistics failures in the target pincode. pub struct LogisticsVolatilityStrategy; impl RiskEvaluationStrategy for LogisticsVolatilityStrategy { fn evaluate( &self, _order: &OrderRecord, _history_count: i64, pincode_volatility: f64, ) -> Option<(f64, &'static str, Value)> { if pincode_volatility > 0.3 { let penalty = (pincode_volatility * 50.0).min(40.0); Some(( penalty, "HIGH_LOGISTICS_VOLATILITY", serde_json::json!(penalty), )) } else { None } } } /// VECTOR 1: Account Longevity & Trust /// New accounts have no established behavioral baseline and carry a higher weight. pub struct AccountLongevityStrategy; impl RiskEvaluationStrategy for AccountLongevityStrategy { fn evaluate( &self, _order: &OrderRecord, history_count: i64, _pincode_volatility: f64, ) -> Option<(f64, &'static str, Value)> { if history_count == 0 { Some((30.0, "NEW_BUYER", serde_json::json!(30))) } else if history_count < 3 { Some((10.0, "EMERGING_BUYER", serde_json::json!(10))) } else { None } } } /// VECTOR 2: Volumetric Ticket Analysis /// High-value orders trigger higher smart scrutiny to protect merchant liquidity. pub struct VolumetricTicketValueStrategy; impl RiskEvaluationStrategy for VolumetricTicketValueStrategy { fn evaluate( &self, order: &OrderRecord, _history_count: i64, _pincode_volatility: f64, ) -> Option<(f64, &'static str, Value)> { if order.price_inr > 10000.0 { Some((20.0, "HIGH_TICKET_VALUE", serde_json::json!(20))) } else { None } } } /// VECTOR 3: Logistics & Distance /// Cross-country shipments or long-haul logistics paths increase transit risk. pub struct LogisticsDistanceStrategy; impl RiskEvaluationStrategy for LogisticsDistanceStrategy { fn evaluate( &self, order: &OrderRecord, _history_count: i64, _pincode_volatility: f64, ) -> Option<(f64, &'static str, Value)> { if order.distance_km > 1000.0 { Some((15.0, "LONG_DISTANCE_LOGISTICS", serde_json::json!(15))) } else { None } } } /// VECTOR 4: Correlation Checks /// Combining "New Buyer" with "High Value" creates a significant risk profile. pub struct CorrelationStrategy; impl RiskEvaluationStrategy for CorrelationStrategy { fn evaluate( &self, order: &OrderRecord, history_count: i64, _pincode_volatility: f64, ) -> Option<(f64, &'static str, Value)> { if order.price_inr > 5000.0 && history_count == 0 { Some((15.0, "HIGH_VALUE_NEW_ACCOUNT", serde_json::json!(15))) } else { None } } } /// VECTOR 5: PII Reputation (Domain Integrity) /// Detects suspicious or disposable email providers commonly used in automated fraud. pub struct PiiReputationStrategy; impl RiskEvaluationStrategy for PiiReputationStrategy { fn evaluate( &self, order: &OrderRecord, _history_count: i64, _pincode_volatility: f64, ) -> Option<(f64, &'static str, Value)> { let suspicious_domains = ["tempmail.com", "throwaway.com", "test.com"]; if suspicious_domains .iter() .any(|d| order.buyer_email.ends_with(d)) { Some((25.0, "SUSPICIOUS_EMAIL_DOMAIN", serde_json::json!(25))) } else { None } } } /// VECTOR 6: Temporal Anomalies /// High-value transactions during low-activity windows (late night) are penalized. pub struct TemporalAnomalyStrategy; impl RiskEvaluationStrategy for TemporalAnomalyStrategy { fn evaluate( &self, order: &OrderRecord, _history_count: i64, _pincode_volatility: f64, ) -> Option<(f64, &'static str, Value)> { if let Some(ts) = order.created_at { let hour = ts.time().hour(); if (hour >= 23 || hour <= 4) && order.price_inr > 2000.0 { Some((10.0, "LATE_NIGHT_HIGH_VALUE", serde_json::json!(10))) } else { None } } else { None } } } // ======================================================== // RISK ENGINE CLASS & FACTORY PIPELINE // ======================================================== /// The `RiskEngine` is the security check system of the Rtix platform. /// /// It performs automatic safety checks on every transaction to detect potential fraud /// and protect the merchant's revenue. Refactored using robust OOPS Strategy patterns. pub struct RiskEngine { strategies: Vec>, } impl Default for RiskEngine { fn default() -> Self { Self { strategies: vec![ Box::new(LogisticsVolatilityStrategy), Box::new(AccountLongevityStrategy), Box::new(VolumetricTicketValueStrategy), Box::new(LogisticsDistanceStrategy), Box::new(CorrelationStrategy), Box::new(PiiReputationStrategy), Box::new(TemporalAnomalyStrategy), ], } } } impl RiskEngine { /// Instantiates a new RiskEngine with default strategy vectors registered. pub fn new() -> Self { Self::default() } /// Dynamically register a new risk evaluation strategy vector. pub fn register_strategy(&mut self, strategy: Box) { self.strategies.push(strategy); } /// Calculates a safety score (0-100) for a given order. /// /// Preserves static interface compatibility to ensure other parts of the application /// do not break. /// /// # Arguments /// * `order` - The order record containing price, distance, and buyer PII. /// * `history_count` - Number of successful previous orders by this buyer phone. /// * `pincode_volatility` - Real-time network volatility for the target delivery zone. /// /// # Returns /// A tuple containing the numeric risk score and a JSON object detailing the activated risk flags. pub fn calculate_risk_score( order: &OrderRecord, history_count: i64, pincode_volatility: f64, ) -> (f64, serde_json::Value) { let engine = Self::new(); engine.evaluate_risk(order, history_count, pincode_volatility) } /// Evaluates all registered strategies polmorphicly. pub fn evaluate_risk( &self, order: &OrderRecord, history_count: i64, pincode_volatility: f64, ) -> (f64, serde_json::Value) { let mut score = 0.0; let mut flags = serde_json::Map::new(); for strategy in &self.strategies { if let Some((points, flag_name, detail)) = strategy.evaluate(order, history_count, pincode_volatility) { score += points; flags.insert(flag_name.to_string(), detail); } } // Cap the final score to ensure it fits within the standard 0-100 range. let final_score = score.min(100.0); (final_score, serde_json::Value::Object(flags)) } } #[cfg(test)] mod tests { use super::*; fn mock_order(price: f64, distance: f64) -> OrderRecord { OrderRecord { transaction_id: "txn_123".into(), merchant_id: "m_123".into(), link_id: "lnk_123".into(), buyer_phone: "9999999999".into(), buyer_phone_hash: None, buyer_name: "Test User".into(), buyer_email: "test@example.com".into(), shipping_pincode: Some("110001".into()), delivery_address: Some("123 Test Street, Bangalore".into()), price_inr: price, status: "PENDING".into(), vpa: None, payu_id: String::new(), outbound_weight: 500.0, return_weight: 0.0, proof_data: None, proof_received_at: None, settled_at: None, paid_at: None, shipped_at: None, delivered_at: None, shipping_method: None, estimated_delivery_at: None, is_payment: false, platform_fee_paid: false, platform_fee: 0.0, delivery_fee: 100.0, distance_km: distance, risk_score: 0.0, risk_flags: None, cgst: 0.0, sgst: 0.0, igst: 0.0, utr_number: None, delivery_gps_lat: None, delivery_gps_lng: None, is_geofence_verified: None, pincode_volatility_at_checkout: 0.0, coupon_code: None, discount_amount: 0.0, checkout_gps_lat: None, checkout_gps_lng: None, device_fingerprint: None, created_at: None, platform_fee_utr: None, brand_name: None, } } #[test] fn test_new_buyer_high_value_risk() { // New buyer (30), >10000 (20), >5000 & new (15) => 65.0 let order = mock_order(15000.0, 500.0); let (score, flags) = RiskEngine::calculate_risk_score(&order, 0, 0.0); assert_eq!(score, 65.0); assert!(flags.get("NEW_BUYER").is_some()); } #[test] fn test_trusted_buyer_low_value_risk() { // Returning buyer > 3 orders (0), low value (0), low distance (0) => 0.0 let order = mock_order(1000.0, 50.0); let (score, _) = RiskEngine::calculate_risk_score(&order, 5, 0.0); assert_eq!(score, 0.0); } #[test] fn test_maximum_risk_cap() { let mut order = mock_order(15000.0, 1500.0); order.price_inr = 999999.0; let (score, _) = RiskEngine::calculate_risk_score(&order, 0, 0.0); assert_eq!(score, 80.0); // 30 (new) + 20 (>10k) + 15 (>1k km) + 15 (high value new account) = 80 } #[test] fn test_suspicious_email_risk() { let mut order = mock_order(1000.0, 50.0); order.buyer_email = "scammer@tempmail.com".into(); let (score, flags) = RiskEngine::calculate_risk_score(&order, 5, 0.0); assert_eq!(score, 25.0); assert!(flags.get("SUSPICIOUS_EMAIL_DOMAIN").is_some()); } #[test] fn test_high_volatility_risk() { let order = mock_order(1000.0, 50.0); let (score, flags) = RiskEngine::calculate_risk_score(&order, 5, 0.5); // 50% volatility assert_eq!(score, 25.0); // 0.5 * 50 = 25 assert!(flags.get("HIGH_LOGISTICS_VOLATILITY").is_some()); } #[test] fn test_late_night_risk() { let mut order = mock_order(5000.0, 50.0); // Late night: 1 AM use chrono::NaiveDate; order.created_at = Some( NaiveDate::from_ymd_opt(2026, 5, 6) .unwrap() .and_hms_opt(1, 0, 0) .unwrap(), ); let (score, flags) = RiskEngine::calculate_risk_score(&order, 5, 0.0); assert_eq!(score, 10.0); assert!(flags.get("LATE_NIGHT_HIGH_VALUE").is_some()); } }