Spaces:
Running
Running
| use crate::domain::error::{AppError, AppResult}; | |
| use crate::domain::models::OrderRecord; | |
| use crate::infrastructure::db::DbPool; | |
| use crate::infrastructure::repositories::{MerchantRepository, OrderRepository, ProductRepository}; | |
| use crate::infrastructure::storage::assets::AssetProvider; | |
| use crate::interfaces::http::api::RealtimeEvent; | |
| use std::sync::Arc; | |
| use tokio::sync::broadcast::Sender; | |
| use uuid::Uuid; | |
| pub async fn execute_checkout_helper( | |
| product_repo: &Arc<dyn ProductRepository>, | |
| merchant_repo: &Arc<dyn MerchantRepository>, | |
| order_repo: &Arc<dyn OrderRepository>, | |
| pool: &DbPool, | |
| tx_sender: &Sender<RealtimeEvent>, | |
| link_id: &str, | |
| buyer_phone: &str, | |
| buyer_name: &str, | |
| buyer_email: &str, | |
| shipping_pincode: &str, | |
| delivery_address: &str, | |
| coupon_code: Option<String>, | |
| request_id: Option<String>, | |
| client_ip: &str, | |
| lat: Option<f64>, | |
| lng: Option<f64>, | |
| device_fingerprint: Option<String>, | |
| ) -> AppResult<OrderRecord> { | |
| let product = product_repo.find_by_id(link_id).await?; | |
| let mut product = | |
| product.ok_or_else(|| AppError::NotFound("Product link not found".to_string()))?; | |
| let _ = product_repo.increment_views(link_id).await; | |
| product.image_data = crate::core::utils::hydrate_file_to_base64(product.image_data).await; | |
| let merchant = merchant_repo | |
| .find_by_id(&product.merchant_id) | |
| .await? | |
| .ok_or_else(|| AppError::NotFound("Merchant not found".to_string()))?; | |
| if merchant.is_frozen { | |
| return Err(AppError::Forbidden("Merchant account is frozen due to unpaid outstanding invoices.".to_string())); | |
| } | |
| // 1. Secure Logistics Circuit Breaker | |
| // Check if the pincode has high smart volatility (recent violations) | |
| let volatility_count = sqlx::query_scalar::<_, i64>( | |
| "SELECT COUNT(*) FROM risk_audit_logs WHERE details LIKE $1 AND created_at > NOW() - INTERVAL '1 hour'" | |
| ) | |
| .bind(format!("%{}%", shipping_pincode)) | |
| .fetch_one(order_repo.find_pool()) | |
| .await | |
| .unwrap_or(0); | |
| if volatility_count > 5 { | |
| return Err(AppError::Forbidden(format!( | |
| "Logistics Circuit Breaker Active for zone {}. High volatility detected in recent smart audit cycles.", | |
| shipping_pincode | |
| ))); | |
| } | |
| // 2. Institutional Velocity Guard (Anti-Abuse) | |
| let intelligence = | |
| crate::application::services::intelligence::IntelligenceService::new(pool.clone()); | |
| let velocity_risk = intelligence | |
| .evaluate_velocity_risk( | |
| device_fingerprint.as_deref(), | |
| Some(client_ip), | |
| &product.merchant_id, | |
| ) | |
| .await?; | |
| if velocity_risk >= 90.0 { | |
| // Log Critical Security Event | |
| let _ = sqlx::query( | |
| "INSERT INTO risk_audit_logs (merchant_id, event_type, risk_level, details, device_fingerprint) VALUES ($1, $2, $3, $4, $5)" | |
| ) | |
| .bind(&product.merchant_id) | |
| .bind("VELOCITY_BLOCK") | |
| .bind("CRITICAL") | |
| .bind(format!("Transaction blocked due to high velocity risk ({}). Fingerprint: {:?}, IP: {}", velocity_risk, device_fingerprint, client_ip)) | |
| .bind(device_fingerprint.as_deref()) | |
| .execute(pool) | |
| .await; | |
| return Err(AppError::Forbidden( | |
| "Security Protocol Active: High-frequency transaction activity detected from this device/network. Access restricted for protocol safety.".to_string() | |
| )); | |
| } | |
| let distance_km = | |
| crate::domain::distance::estimate_distance_km(&merchant.base_pincode, shipping_pincode); | |
| let pricing_features = crate::application::services::pricing::PricingFeatures { | |
| distance_km, | |
| user_rate_per_km: merchant.delivery_rate_per_km, | |
| product_weight: product.expected_weight, | |
| base_charge: merchant.delivery_base_fee, | |
| config: serde_json::from_value(merchant.logistics_config.clone()).unwrap_or_default(), | |
| }; | |
| let delivery_fee = crate::application::services::pricing::PricingEngine::estimate_delivery_fee( | |
| pricing_features, | |
| ); | |
| // 3. Precision Geofence Check | |
| let mut geofence_verified = None; | |
| if let (Some(l_lat), Some(l_lng)) = (lat, lng) { | |
| let intelligence = | |
| crate::application::services::intelligence::IntelligenceService::new(pool.clone()); | |
| if !intelligence | |
| .verify_geofence_with_precision(shipping_pincode, l_lat, l_lng) | |
| .await? | |
| { | |
| return Err(AppError::Forbidden(format!( | |
| "Geofence Verification Failed: Your current GPS coordinates do not match the shipping pincode {}. Forensic integrity active.", | |
| shipping_pincode | |
| ))); | |
| } | |
| geofence_verified = Some(true); | |
| } | |
| let order_count = order_repo | |
| .count_by_buyer(&product.merchant_id, buyer_phone) | |
| .await?; | |
| let transaction_id = Uuid::new_v4().to_string(); | |
| // 2. Merchant Transaction Limits | |
| if merchant.plan == "FREE" && product.price_inr > 10000.0 { | |
| return Err(AppError::Forbidden( | |
| "Transaction value exceeds the ₹10,000 limit for merchants on the FREE plan. Upgrade to PRO to accept higher-value payments without limitations.".to_string() | |
| )); | |
| } | |
| if product.price_inr > merchant.max_order_value_inr { | |
| return Err(AppError::Forbidden(format!( | |
| "Transaction value ₹{} exceeds the current limit for this merchant (₹{}). Increase merchant verification level to lift this restriction.", | |
| product.price_inr, merchant.max_order_value_inr | |
| ))); | |
| } | |
| // Start Transaction for Atomicity | |
| let mut tx = pool.begin().await.map_err(AppError::Database)?; | |
| // 3. Inventory Enforcement | |
| if !product.is_unlimited { | |
| let rows_affected = sqlx::query( | |
| "UPDATE product_links SET inventory_count = inventory_count - 1 WHERE link_id = $1 AND inventory_count > 0", | |
| ) | |
| .bind(&product.link_id) | |
| .execute(&mut *tx) | |
| .await | |
| .map_err(AppError::Database)? | |
| .rows_affected(); | |
| if rows_affected == 0 { | |
| return Err(AppError::BadRequest( | |
| "Product is currently out of stock.".to_string(), | |
| )); | |
| } | |
| } | |
| let current_price = | |
| if let (Some(sale_price), Some(ends_at)) = (product.sale_price_inr, product.sale_ends_at) { | |
| if ends_at > chrono::Utc::now().naive_utc() { | |
| sale_price | |
| } else { | |
| product.price_inr | |
| } | |
| } else { | |
| product.price_inr | |
| }; | |
| let platform_fee = crate::application::services::pricing::PricingEngine::calculate_platform_fee( | |
| current_price, | |
| merchant.trust_score, | |
| ); | |
| // Calculate preliminary risk score | |
| let mut calculated_risk = 0.0; | |
| calculated_risk += (volatility_count as f64 * 5.0).min(30.0); | |
| calculated_risk += velocity_risk * 0.4; // Incorporate velocity guard signal | |
| if geofence_verified == Some(true) { | |
| calculated_risk *= 0.8; // Lower risk if GPS verified | |
| } | |
| let mut order = OrderRecord { | |
| transaction_id: transaction_id.clone(), | |
| merchant_id: product.merchant_id.clone(), | |
| link_id: link_id.to_string(), | |
| buyer_phone: buyer_phone.to_string(), | |
| buyer_phone_hash: None, // populated by encrypt_pii() at persist time | |
| buyer_name: buyer_name.to_string(), | |
| buyer_email: buyer_email.to_string(), | |
| shipping_pincode: Some(shipping_pincode.to_string()), | |
| delivery_address: Some(delivery_address.to_string()), | |
| price_inr: current_price, | |
| status: crate::domain::constants::ORDER_STATUS_PENDING_PAYMENT.to_string(), | |
| vpa: None, | |
| payu_id: String::new(), | |
| outbound_weight: product.expected_weight, | |
| return_weight: 0.0, | |
| proof_data: None, | |
| settled_at: None, | |
| shipped_at: None, | |
| delivered_at: None, | |
| shipping_method: None, | |
| estimated_delivery_at: None, | |
| is_payment: false, | |
| platform_fee_paid: false, | |
| platform_fee, | |
| delivery_fee, | |
| distance_km, | |
| risk_score: calculated_risk, | |
| risk_flags: None, | |
| cgst: 0.0, | |
| sgst: 0.0, | |
| igst: 0.0, | |
| utr_number: None, | |
| platform_fee_utr: None, | |
| delivery_gps_lat: None, | |
| delivery_gps_lng: None, | |
| is_geofence_verified: geofence_verified, | |
| pincode_volatility_at_checkout: 0.0, | |
| discount_amount: 0.0, | |
| coupon_code: None, | |
| checkout_gps_lat: lat, | |
| checkout_gps_lng: lng, | |
| device_fingerprint: device_fingerprint.clone(), | |
| paid_at: None, | |
| proof_received_at: None, | |
| created_at: None, | |
| brand_name: None, | |
| }; | |
| if let Some(ref code) = coupon_code { | |
| let coupon = sqlx::query_as::<_, crate::domain::models::Coupon>( | |
| "SELECT * FROM coupons WHERE merchant_id = $1 AND code = $2 AND is_active = TRUE", | |
| ) | |
| .bind(&product.merchant_id) | |
| .bind(code.to_uppercase()) | |
| .fetch_optional(&mut *tx) | |
| .await?; | |
| if let Some(c) = coupon { | |
| if c.is_valid(order.price_inr) { | |
| let discount = c.calculate_discount(order.price_inr); | |
| order.discount_amount = discount; | |
| order.price_inr -= discount; | |
| order.coupon_code = Some(code.clone()); | |
| let _ = | |
| sqlx::query("UPDATE coupons SET usage_count = usage_count + 1 WHERE id = $1") | |
| .bind(c.id) | |
| .execute(&mut *tx) | |
| .await; | |
| } | |
| } | |
| } | |
| let gst_breakdown = crate::application::services::india_tax::IndiaTaxService::calculate_gst( | |
| order.price_inr, | |
| merchant.state_code.unwrap_or(29), | |
| order.shipping_pincode.as_deref().unwrap_or_default(), | |
| 0.18, // 18% standard rate | |
| ); | |
| order.cgst = gst_breakdown.cgst; | |
| order.sgst = gst_breakdown.sgst; | |
| order.igst = gst_breakdown.igst; | |
| let volatility = | |
| crate::application::services::intelligence::IntelligenceService::new(pool.clone()) | |
| .get_pincode_volatility(order.shipping_pincode.as_deref().unwrap_or_default()) | |
| .await | |
| .unwrap_or(0.0); | |
| order.pincode_volatility_at_checkout = volatility; | |
| if volatility > 0.5 { | |
| let _ = tx_sender.send( | |
| crate::interfaces::http::api::RealtimeEvent::NetworkVolatilityAlert { | |
| pincode: order.shipping_pincode.clone().unwrap_or_default(), | |
| volatility_score: volatility, | |
| message: format!( | |
| "High logistics volatility detected for pincode {}.", | |
| order.shipping_pincode.clone().unwrap_or_default() | |
| ), | |
| }, | |
| ); | |
| } | |
| let (risk_score, risk_flags) = | |
| crate::application::services::risk::RiskEngine::calculate_risk_score( | |
| &order, | |
| order_count, | |
| volatility, | |
| ); | |
| order.risk_score = risk_score; | |
| order.risk_flags = Some(risk_flags); | |
| if order.risk_score >= 80.0 { | |
| // Log Critical/High Security Event outside the transaction so it's persisted even when rolled back | |
| if let Ok(mut conn) = pool.acquire().await { | |
| crate::domain::audit::log_risk_event( | |
| &mut *conn, | |
| Some(&transaction_id), | |
| &product.merchant_id, | |
| "HIGH_RISK_BLOCK", | |
| "CRITICAL", | |
| Some(&format!( | |
| "Transaction blocked due to high risk score ({:.1}) during checkout for link {}. Flags: {:?}", | |
| order.risk_score, link_id, order.risk_flags | |
| )), | |
| Some(order.risk_score), | |
| request_id.as_deref(), | |
| device_fingerprint.as_deref(), | |
| Some(tx_sender), | |
| ) | |
| .await; | |
| } | |
| if order.risk_score > 90.0 { | |
| crate::interfaces::http::middleware::block_ip_persistently( | |
| pool, | |
| client_ip, | |
| &format!("Automated Defense: High Risk Score ({:.1}) detected during checkout for link {}", order.risk_score, link_id), | |
| Some(tx_sender) | |
| ).await; | |
| } | |
| return Err(AppError::Forbidden(format!( | |
| "Security restriction: High risk profile detected (Score: {:.1}). This transaction has been blocked to prevent potential fraud.", | |
| order.risk_score | |
| ))); | |
| } else if order.risk_score > 60.0 { | |
| crate::domain::audit::log_risk_event( | |
| &mut tx, | |
| Some(&transaction_id), | |
| &product.merchant_id, | |
| "HIGH_RISK_ORDER", | |
| "HIGH", | |
| Some(&format!( | |
| "Order {} flagged with risk score {}", | |
| transaction_id, order.risk_score | |
| )), | |
| Some(order.risk_score), | |
| request_id.as_deref(), | |
| device_fingerprint.as_deref(), | |
| Some(tx_sender), | |
| ) | |
| .await; | |
| } | |
| // Persist Order using transaction | |
| order.created_at = Some(chrono::Utc::now().naive_utc()); | |
| crate::domain::models::OrderRecord::create_with_tx(&mut tx, &order).await?; | |
| tx.commit().await.map_err(AppError::Database)?; | |
| Ok(order) | |
| } | |
| pub async fn execute_submit_delivery_proof_helper( | |
| order_repo: &Arc<dyn OrderRepository>, | |
| assets: &Arc<dyn AssetProvider>, | |
| tx_sender: &Sender<RealtimeEvent>, | |
| transaction_id: &str, | |
| proof_data: &str, | |
| proof_token: &str, | |
| lat: Option<f64>, | |
| lng: Option<f64>, | |
| ) -> AppResult<()> { | |
| let transaction_id = crate::domain::validation::sanitize_filename(transaction_id); | |
| if crate::core::session::verify_proof_token(proof_token, &transaction_id).is_err() { | |
| return Err(AppError::Forbidden( | |
| "Invalid proof authorization token".to_string(), | |
| )); | |
| } | |
| let order = order_repo.find_by_id(&transaction_id).await?; | |
| let order = order.ok_or_else(|| AppError::NotFound("Order not found".to_string()))?; | |
| if order.status != crate::domain::constants::ORDER_STATUS_PAID_PENDING_DELIVERY { | |
| return Err(AppError::BadRequest( | |
| "Order is not in a state to accept delivery proof".to_string(), | |
| )); | |
| } | |
| let mut tx = order_repo | |
| .find_pool() | |
| .begin() | |
| .await | |
| .map_err(AppError::Database)?; | |
| let is_video = proof_data.contains("video") && proof_data.contains("mp4"); | |
| let is_png = proof_data.contains("image/png"); | |
| let file_extension = if is_video { | |
| "mp4" | |
| } else if is_png { | |
| "png" | |
| } else { | |
| "jpg" | |
| }; | |
| let filename = format!("proof_{}.{}", Uuid::new_v4(), file_extension); | |
| let bytes = crate::domain::validation::validate_base64_payload(proof_data, 10 * 1024 * 1024) | |
| .map_err(|e| AppError::BadRequest(e.message))?; | |
| // Institutional Enforcement: Require Video for High-Value Orders (> ₹5,000) | |
| if order.price_inr > 5000.0 && !is_video { | |
| return Err(AppError::BadRequest( | |
| "High-value order detected. Smart video proof is mandatory for this transaction." | |
| .to_string(), | |
| )); | |
| } | |
| if !is_video { | |
| let allowed_headers: [Vec<u8>; 2] = [ | |
| vec![0xFF, 0xD8, 0xFF], | |
| vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A], | |
| ]; | |
| if !allowed_headers | |
| .iter() | |
| .any(|header| bytes.starts_with(header)) | |
| { | |
| return Err(AppError::BadRequest("Invalid image format".to_string())); | |
| } | |
| } | |
| let merchant_plan: String = | |
| sqlx::query_scalar("SELECT plan FROM merchants WHERE merchant_id = $1") | |
| .bind(&order.merchant_id) | |
| .fetch_one(&mut *tx) | |
| .await | |
| .map_err(AppError::Database)?; | |
| let lat_val = lat.unwrap_or(0.0); | |
| let lng_val = lng.unwrap_or(0.0); | |
| let mut zk_proof = crate::core::crypto::CryptoService::generate_zk_telemetry_proof( | |
| &transaction_id, | |
| &bytes, | |
| lat_val, | |
| lng_val, | |
| ); | |
| if merchant_plan == "PRO" { | |
| let asset_path = assets | |
| .store_asset(&filename, &bytes) | |
| .await | |
| .map_err(AppError::Internal)?; | |
| if let Some(obj) = zk_proof.as_object_mut() { | |
| obj.insert("is_zero_storage".to_string(), serde_json::json!(false)); | |
| obj.insert("asset_path".to_string(), serde_json::json!(asset_path)); | |
| } | |
| } | |
| let mut is_geofence_verified = None; | |
| if let (Some(l_lat), Some(l_lng), Some(pincode)) = (lat, lng, &order.shipping_pincode) { | |
| let (p_lat, p_lng) = crate::domain::geofence::GeofenceService::get_coordinates(pincode) | |
| .unwrap_or((12.9716, 77.5946)); // Default to Bangalore Central | |
| let distance = crate::domain::geofence::GeofenceService::calculate_distance_km( | |
| l_lat, l_lng, p_lat, p_lng, | |
| ); | |
| is_geofence_verified = Some(distance < 5.0); // 5km tolerance | |
| if !is_geofence_verified.unwrap_or(false) { | |
| crate::domain::audit::log_risk_event( | |
| &mut tx, | |
| Some(&transaction_id), | |
| &order.merchant_id, | |
| "GEOFENCE_VIOLATION", | |
| "MEDIUM", | |
| Some(&format!( | |
| "Delivery proof submitted from {} km away from shipping pincode {}.", | |
| distance.round(), | |
| pincode | |
| )), | |
| Some(distance), | |
| None, | |
| order.device_fingerprint.as_deref(), | |
| Some(tx_sender), | |
| ) | |
| .await; | |
| // Trust Score Penalty for logistics deviations | |
| let _ = sqlx::query("UPDATE merchants SET trust_score = GREATEST(0.0, trust_score - 2.0) WHERE merchant_id = $1") | |
| .bind(&order.merchant_id) | |
| .execute(&mut *tx) | |
| .await; | |
| } | |
| } | |
| sqlx::query( | |
| "UPDATE orders SET proof_data = proof_data || $1::jsonb, status = $2, delivered_at = CURRENT_TIMESTAMP, delivery_gps_lat = $3, delivery_gps_lng = $4, is_geofence_verified = $5 WHERE transaction_id = $6 AND status = $7", | |
| ) | |
| .bind(serde_json::json!([zk_proof])) | |
| .bind(crate::domain::constants::ORDER_STATUS_DELIVERED_PENDING_APPROVAL) | |
| .bind(lat) | |
| .bind(lng) | |
| .bind(is_geofence_verified) | |
| .bind(&transaction_id) | |
| .bind(crate::domain::constants::ORDER_STATUS_PAID_PENDING_DELIVERY) | |
| .execute(&mut *tx) | |
| .await | |
| .map_err(AppError::Database)?; | |
| tx.commit().await.map_err(AppError::Database)?; | |
| let _ = tx_sender.send( | |
| crate::interfaces::http::api::RealtimeEvent::OrderStatusChanged { | |
| transaction_id: transaction_id.to_string(), | |
| merchant_id: order.merchant_id, | |
| new_status: crate::domain::constants::ORDER_STATUS_DELIVERED_PENDING_APPROVAL | |
| .to_string(), | |
| }, | |
| ); | |
| Ok(()) | |
| } | |
| pub async fn execute_cart_checkout_helper( | |
| product_repo: &Arc<dyn crate::infrastructure::repositories::ProductRepository>, | |
| merchant_repo: &Arc<dyn crate::infrastructure::repositories::MerchantRepository>, | |
| _order_repo: &Arc<dyn crate::infrastructure::repositories::OrderRepository>, | |
| pool: &crate::infrastructure::db::DbPool, | |
| tx_sender: &tokio::sync::broadcast::Sender<crate::interfaces::http::api::RealtimeEvent>, | |
| items: Vec<(String, u32)>, | |
| buyer_phone: &str, | |
| buyer_name: &str, | |
| buyer_email: &str, | |
| shipping_pincode: &str, | |
| delivery_address: &str, | |
| coupon_code: Option<String>, | |
| _request_id: Option<String>, | |
| client_ip: &str, | |
| lat: Option<f64>, | |
| lng: Option<f64>, | |
| device_fingerprint: Option<String>, | |
| ) -> AppResult<crate::domain::models::OrderRecord> { | |
| let transaction_id = format!( | |
| "TX_{}", | |
| uuid::Uuid::new_v4().to_string()[..8].to_uppercase() | |
| ); | |
| let mut total_price = 0.0; | |
| let mut total_weight = 0.0; | |
| let mut first_merchant_id = String::new(); | |
| let mut product_details = Vec::new(); | |
| for (link_id, qty) in &items { | |
| let product = product_repo | |
| .find_by_id(link_id) | |
| .await? | |
| .ok_or_else(|| AppError::NotFound(format!("Product {} not found", link_id)))?; | |
| if first_merchant_id.is_empty() { | |
| first_merchant_id = product.merchant_id.clone(); | |
| } else if first_merchant_id != product.merchant_id { | |
| return Err(AppError::BadRequest( | |
| "Cross-merchant checkout not allowed in single cart".into(), | |
| )); | |
| } | |
| let current_price = if let (Some(sale_price), Some(ends_at)) = | |
| (product.sale_price_inr, product.sale_ends_at) | |
| { | |
| if ends_at > chrono::Utc::now().naive_utc() { | |
| sale_price | |
| } else { | |
| product.price_inr | |
| } | |
| } else { | |
| product.price_inr | |
| }; | |
| total_price += current_price * (*qty as f64); | |
| total_weight += product.expected_weight * (*qty as f64); | |
| // Inventory Enforcement for Cart (Preliminary Check) | |
| if !product.is_unlimited && product.inventory_count < *qty as i32 { | |
| return Err(AppError::BadRequest(format!( | |
| "Product '{}' is low on stock ({} available).", | |
| product.product_name, product.inventory_count | |
| ))); | |
| } | |
| product_details.push((product, *qty)); | |
| } | |
| if first_merchant_id.is_empty() { | |
| return Err(AppError::BadRequest("Cart is empty".into())); | |
| } | |
| let merchant = merchant_repo | |
| .find_by_id(&first_merchant_id) | |
| .await? | |
| .ok_or_else(|| AppError::NotFound("Merchant not found".into()))?; | |
| if merchant.is_frozen { | |
| return Err(AppError::Forbidden("Merchant account is frozen due to unpaid outstanding invoices.".to_string())); | |
| } | |
| // 2. Institutional Velocity Guard (Anti-Abuse) | |
| let intelligence = | |
| crate::application::services::intelligence::IntelligenceService::new(pool.clone()); | |
| let velocity_risk = intelligence | |
| .evaluate_velocity_risk( | |
| device_fingerprint.as_deref(), | |
| Some(client_ip), | |
| &first_merchant_id, | |
| ) | |
| .await?; | |
| if velocity_risk >= 90.0 { | |
| return Err(AppError::Forbidden( | |
| "Security Protocol Active: High-frequency transaction activity detected from this device/network. Access restricted for protocol safety.".to_string() | |
| )); | |
| } | |
| let distance_km = | |
| crate::domain::distance::estimate_distance_km(&merchant.base_pincode, shipping_pincode); | |
| let pricing_features = crate::application::services::pricing::PricingFeatures { | |
| distance_km, | |
| user_rate_per_km: merchant.delivery_rate_per_km, | |
| product_weight: total_weight, | |
| base_charge: merchant.delivery_base_fee, | |
| config: serde_json::from_value(merchant.logistics_config.clone()).unwrap_or_default(), | |
| }; | |
| let delivery_fee = crate::application::services::pricing::PricingEngine::estimate_delivery_fee( | |
| pricing_features, | |
| ); | |
| // Precision Geofence Check | |
| let mut geofence_verified = None; | |
| if let (Some(l_lat), Some(l_lng)) = (lat, lng) { | |
| let intelligence = | |
| crate::application::services::intelligence::IntelligenceService::new(pool.clone()); | |
| if !intelligence | |
| .verify_geofence_with_precision(shipping_pincode, l_lat, l_lng) | |
| .await? | |
| { | |
| return Err(AppError::Forbidden(format!( | |
| "Geofence Verification Failed: Your current GPS coordinates do not match the shipping pincode {}. Forensic integrity active.", | |
| shipping_pincode | |
| ))); | |
| } | |
| geofence_verified = Some(true); | |
| } | |
| let platform_fee = crate::application::services::pricing::PricingEngine::calculate_platform_fee( | |
| total_price, | |
| merchant.trust_score, | |
| ); | |
| let gst_breakdown = crate::application::services::india_tax::IndiaTaxService::calculate_gst( | |
| total_price, | |
| merchant.state_code.unwrap_or(29), | |
| shipping_pincode, | |
| 0.18, | |
| ); | |
| let volatility = | |
| crate::application::services::intelligence::IntelligenceService::new(pool.clone()) | |
| .get_pincode_volatility(shipping_pincode) | |
| .await | |
| .unwrap_or(0.0); | |
| // Calculate preliminary risk score | |
| let mut calculated_risk = 0.0; | |
| calculated_risk += (volatility * 50.0).min(30.0); | |
| calculated_risk += velocity_risk * 0.4; | |
| if geofence_verified == Some(true) { | |
| calculated_risk *= 0.8; | |
| } | |
| let mut order = crate::domain::models::OrderRecord { | |
| transaction_id: transaction_id.clone(), | |
| merchant_id: first_merchant_id.clone(), | |
| link_id: "CART_TRANSACTION".into(), | |
| buyer_phone: buyer_phone.to_string(), | |
| buyer_phone_hash: None, // populated by encrypt_pii() at persist time | |
| buyer_name: buyer_name.to_string(), | |
| buyer_email: buyer_email.to_string(), | |
| shipping_pincode: Some(shipping_pincode.to_string()), | |
| delivery_address: Some(delivery_address.to_string()), | |
| price_inr: total_price, | |
| status: crate::domain::constants::ORDER_STATUS_PENDING_PAYMENT.to_string(), | |
| vpa: Some(String::new()), | |
| outbound_weight: total_weight, | |
| return_weight: 0.0, | |
| proof_data: Some(serde_json::json!([])), | |
| settled_at: None, | |
| shipped_at: None, | |
| delivered_at: None, | |
| shipping_method: None, | |
| estimated_delivery_at: None, | |
| payu_id: String::new(), | |
| is_payment: false, | |
| platform_fee_paid: false, | |
| platform_fee, | |
| delivery_fee, | |
| distance_km, | |
| risk_score: calculated_risk, | |
| risk_flags: None, | |
| cgst: gst_breakdown.cgst, | |
| sgst: gst_breakdown.sgst, | |
| igst: gst_breakdown.igst, | |
| utr_number: None, | |
| platform_fee_utr: None, | |
| delivery_gps_lat: None, | |
| delivery_gps_lng: None, | |
| is_geofence_verified: geofence_verified, | |
| pincode_volatility_at_checkout: volatility, | |
| discount_amount: 0.0, | |
| coupon_code: None, | |
| checkout_gps_lat: lat, | |
| checkout_gps_lng: lng, | |
| device_fingerprint: device_fingerprint.clone(), | |
| paid_at: None, | |
| proof_received_at: None, | |
| created_at: None, | |
| brand_name: None, | |
| }; | |
| let mut tx = pool.begin().await.map_err(AppError::Database)?; | |
| // Enforce cart inventory atomically within the transaction | |
| for (p, qty) in &product_details { | |
| if !p.is_unlimited { | |
| let rows_affected = sqlx::query( | |
| "UPDATE product_links SET inventory_count = inventory_count - $1 WHERE link_id = $2 AND inventory_count >= $3", | |
| ) | |
| .bind(*qty as i32) | |
| .bind(&p.link_id) | |
| .bind(*qty as i32) | |
| .execute(&mut *tx) | |
| .await | |
| .map_err(AppError::Database)? | |
| .rows_affected(); | |
| if rows_affected == 0 { | |
| return Err(AppError::BadRequest(format!( | |
| "Product '{}' went out of stock or has insufficient quantity.", | |
| p.product_name | |
| ))); | |
| } | |
| } | |
| } | |
| if let Some(ref code) = coupon_code { | |
| let coupon = sqlx::query_as::<_, crate::domain::models::Coupon>( | |
| "SELECT * FROM coupons WHERE merchant_id = $1 AND code = $2 AND is_active = TRUE", | |
| ) | |
| .bind(&first_merchant_id) | |
| .bind(code.to_uppercase()) | |
| .fetch_optional(&mut *tx) | |
| .await?; | |
| if let Some(c) = coupon { | |
| if c.is_valid(order.price_inr) { | |
| let discount = c.calculate_discount(order.price_inr); | |
| order.discount_amount = discount; | |
| order.price_inr -= discount; | |
| order.coupon_code = Some(code.clone()); | |
| let _ = | |
| sqlx::query("UPDATE coupons SET usage_count = usage_count + 1 WHERE id = $1") | |
| .bind(c.id) | |
| .execute(&mut *tx) | |
| .await; | |
| } | |
| } | |
| } | |
| let (risk_score, risk_flags) = | |
| crate::application::services::risk::RiskEngine::calculate_risk_score(&order, 0, volatility); | |
| order.risk_score = risk_score; | |
| order.risk_flags = Some(risk_flags); | |
| if order.risk_score >= 80.0 { | |
| // Log Critical/High Security Event outside the transaction so it's persisted even when rolled back | |
| if let Ok(mut conn) = pool.acquire().await { | |
| crate::domain::audit::log_risk_event( | |
| &mut *conn, | |
| Some(&transaction_id), | |
| &first_merchant_id, | |
| "HIGH_RISK_BLOCK", | |
| "CRITICAL", | |
| Some(&format!( | |
| "Cart transaction blocked due to high risk score ({:.1}) during checkout. Flags: {:?}", | |
| order.risk_score, order.risk_flags | |
| )), | |
| Some(order.risk_score), | |
| None, | |
| device_fingerprint.as_deref(), | |
| Some(tx_sender), | |
| ) | |
| .await; | |
| } | |
| if order.risk_score > 90.0 { | |
| crate::interfaces::http::middleware::block_ip_persistently( | |
| pool, | |
| client_ip, | |
| &format!( | |
| "Automated Defense: High Risk Score ({:.1}) detected during cart checkout", | |
| order.risk_score | |
| ), | |
| Some(tx_sender), | |
| ) | |
| .await; | |
| } | |
| return Err(AppError::Forbidden(format!( | |
| "Security restriction: High risk profile detected (Score: {:.1}). This transaction has been blocked to prevent potential fraud.", | |
| order.risk_score | |
| ))); | |
| } else if order.risk_score > 60.0 { | |
| crate::domain::audit::log_risk_event( | |
| &mut tx, | |
| Some(&transaction_id), | |
| &first_merchant_id, | |
| "HIGH_RISK_ORDER", | |
| "HIGH", | |
| Some(&format!( | |
| "Cart order {} flagged with risk score {}", | |
| transaction_id, order.risk_score | |
| )), | |
| Some(order.risk_score), | |
| None, | |
| device_fingerprint.as_deref(), | |
| Some(tx_sender), | |
| ) | |
| .await; | |
| } | |
| // Persist Order | |
| order.created_at = Some(chrono::Utc::now().naive_utc()); | |
| crate::domain::models::OrderRecord::create_with_tx(&mut tx, &order).await?; | |
| // Persist Order Items | |
| for (p, qty) in product_details { | |
| sqlx::query( | |
| "INSERT INTO order_items (transaction_id, product_id, product_name, quantity, price_at_checkout, weight_at_checkout) VALUES ($1, $2, $3, $4, $5, $6)" | |
| ) | |
| .bind(&transaction_id) | |
| .bind(&p.link_id) | |
| .bind(&p.product_name) | |
| .bind(qty as i32) | |
| .bind(p.price_inr) | |
| .bind(p.expected_weight) | |
| .execute(&mut *tx) | |
| .await | |
| .map_err(AppError::Database)?; | |
| } | |
| tx.commit().await.map_err(AppError::Database)?; | |
| let _ = tx_sender.send(crate::interfaces::http::api::RealtimeEvent::NewOrder { | |
| transaction_id: transaction_id.clone(), | |
| merchant_id: order.merchant_id.clone(), | |
| amount: total_price, | |
| buyer_phone: buyer_phone.to_string(), | |
| }); | |
| Ok(order) | |
| } | |