| 1 |
//! Multi-Currency Payment Processing Engine |
| 2 |
//! |
| 3 |
//! Handles payouts in multiple currencies for ZephyrFS volunteers |
| 4 |
|
| 5 |
use anyhow::Result; |
| 6 |
use serde::{Deserialize, Serialize}; |
| 7 |
use std::collections::{HashMap, VecDeque}; |
| 8 |
use chrono::{DateTime, Utc, Duration}; |
| 9 |
|
| 10 |
/// Multi-currency payment processor |
| 11 |
#[derive(Debug, Clone, Serialize, Deserialize)] |
| 12 |
pub struct PaymentProcessor { |
| 13 |
/// Supported payment methods |
| 14 |
pub payment_methods: HashMap<PaymentMethod, PaymentMethodConfig>, |
| 15 |
/// Exchange rates for currency conversion |
| 16 |
pub exchange_rates: HashMap<Currency, f64>, |
| 17 |
/// Payment processing fees |
| 18 |
pub fee_structure: PaymentFeeStructure, |
| 19 |
/// Pending payments queue |
| 20 |
pub pending_payments: VecDeque<PaymentRequest>, |
| 21 |
/// Payment history |
| 22 |
pub payment_history: HashMap<String, Vec<PaymentRecord>>, |
| 23 |
/// Minimum payout thresholds |
| 24 |
pub min_payout_thresholds: HashMap<Currency, u64>, |
| 25 |
/// Processor configuration |
| 26 |
pub config: ProcessorConfig, |
| 27 |
} |
| 28 |
|
| 29 |
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)] |
| 30 |
pub enum PaymentMethod { |
| 31 |
Cryptocurrency(CryptoNetwork), |
| 32 |
BankTransfer(BankTransferType), |
| 33 |
DigitalWallet(WalletProvider), |
| 34 |
StableCoin(StableCoinType), |
| 35 |
} |
| 36 |
|
| 37 |
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)] |
| 38 |
pub enum CryptoNetwork { |
| 39 |
Bitcoin, |
| 40 |
Ethereum, |
| 41 |
Polygon, |
| 42 |
BinanceSmartChain, |
| 43 |
Solana, |
| 44 |
Cardano, |
| 45 |
ZephyrCoin, // Native token |
| 46 |
} |
| 47 |
|
| 48 |
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)] |
| 49 |
pub enum BankTransferType { |
| 50 |
ACH, // US |
| 51 |
SEPA, // Europe |
| 52 |
FasterPayments, // UK |
| 53 |
Interac, // Canada |
| 54 |
PIX, // Brazil |
| 55 |
UPI, // India |
| 56 |
} |
| 57 |
|
| 58 |
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)] |
| 59 |
pub enum WalletProvider { |
| 60 |
PayPal, |
| 61 |
Wise, |
| 62 |
Revolut, |
| 63 |
CashApp, |
| 64 |
Venmo, |
| 65 |
Zelle, |
| 66 |
} |
| 67 |
|
| 68 |
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)] |
| 69 |
pub enum StableCoinType { |
| 70 |
USDC, |
| 71 |
USDT, |
| 72 |
DAI, |
| 73 |
BUSD, |
| 74 |
} |
| 75 |
|
| 76 |
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)] |
| 77 |
pub enum Currency { |
| 78 |
USD, |
| 79 |
EUR, |
| 80 |
GBP, |
| 81 |
CAD, |
| 82 |
AUD, |
| 83 |
BRL, |
| 84 |
INR, |
| 85 |
JPY, |
| 86 |
ZephyrCoin, |
| 87 |
Bitcoin, |
| 88 |
Ethereum, |
| 89 |
} |
| 90 |
|
| 91 |
#[derive(Debug, Clone, Serialize, Deserialize)] |
| 92 |
pub struct PaymentMethodConfig { |
| 93 |
pub enabled: bool, |
| 94 |
pub min_amount: u64, |
| 95 |
pub max_amount: u64, |
| 96 |
pub processing_time_hours: u32, |
| 97 |
pub supported_currencies: Vec<Currency>, |
| 98 |
pub geographic_restrictions: Vec<String>, // ISO country codes |
| 99 |
pub requires_kyc: bool, |
| 100 |
pub fee_structure: MethodFeeStructure, |
| 101 |
} |
| 102 |
|
| 103 |
#[derive(Debug, Clone, Serialize, Deserialize)] |
| 104 |
pub struct MethodFeeStructure { |
| 105 |
pub fixed_fee: u64, // Fixed fee in cents/wei |
| 106 |
pub percentage_fee: f64, // Percentage fee (0.01 = 1%) |
| 107 |
pub network_fee: u64, // Blockchain network fees |
| 108 |
pub exchange_fee: f64, // Currency conversion fee |
| 109 |
} |
| 110 |
|
| 111 |
#[derive(Debug, Clone, Serialize, Deserialize)] |
| 112 |
pub struct PaymentFeeStructure { |
| 113 |
/// Base processing fee (0.5%) |
| 114 |
pub base_processing_fee: f64, |
| 115 |
/// Currency conversion fees |
| 116 |
pub conversion_fees: HashMap<Currency, f64>, |
| 117 |
/// Express processing fee (1%) |
| 118 |
pub express_fee: f64, |
| 119 |
/// KYC verification fee |
| 120 |
pub kyc_fee: u64, |
| 121 |
} |
| 122 |
|
| 123 |
#[derive(Debug, Clone, Serialize, Deserialize)] |
| 124 |
pub struct PaymentRequest { |
| 125 |
pub request_id: String, |
| 126 |
pub volunteer_id: String, |
| 127 |
pub amount_tokens: u64, |
| 128 |
pub target_currency: Currency, |
| 129 |
pub payment_method: PaymentMethod, |
| 130 |
pub recipient_info: RecipientInfo, |
| 131 |
pub priority: PaymentPriority, |
| 132 |
pub created_at: DateTime<Utc>, |
| 133 |
pub scheduled_for: Option<DateTime<Utc>>, |
| 134 |
pub metadata: HashMap<String, String>, |
| 135 |
} |
| 136 |
|
| 137 |
#[derive(Debug, Clone, Serialize, Deserialize)] |
| 138 |
pub struct RecipientInfo { |
| 139 |
pub wallet_address: Option<String>, |
| 140 |
pub bank_account: Option<BankAccountInfo>, |
| 141 |
pub digital_wallet: Option<DigitalWalletInfo>, |
| 142 |
pub kyc_verified: bool, |
| 143 |
pub tax_info: Option<TaxInfo>, |
| 144 |
} |
| 145 |
|
| 146 |
#[derive(Debug, Clone, Serialize, Deserialize)] |
| 147 |
pub struct BankAccountInfo { |
| 148 |
pub account_holder: String, |
| 149 |
pub account_number: String, |
| 150 |
pub routing_number: String, |
| 151 |
pub bank_name: String, |
| 152 |
pub swift_code: Option<String>, |
| 153 |
pub iban: Option<String>, |
| 154 |
pub country_code: String, |
| 155 |
} |
| 156 |
|
| 157 |
#[derive(Debug, Clone, Serialize, Deserialize)] |
| 158 |
pub struct DigitalWalletInfo { |
| 159 |
pub provider: WalletProvider, |
| 160 |
pub wallet_id: String, |
| 161 |
pub verified: bool, |
| 162 |
} |
| 163 |
|
| 164 |
#[derive(Debug, Clone, Serialize, Deserialize)] |
| 165 |
pub struct TaxInfo { |
| 166 |
pub tax_id: String, |
| 167 |
pub tax_country: String, |
| 168 |
pub tax_exempt: bool, |
| 169 |
pub withholding_rate: f64, |
| 170 |
} |
| 171 |
|
| 172 |
#[derive(Debug, Clone, Serialize, Deserialize)] |
| 173 |
pub enum PaymentPriority { |
| 174 |
Standard, |
| 175 |
Express, |
| 176 |
Immediate, |
| 177 |
} |
| 178 |
|
| 179 |
#[derive(Debug, Clone, Serialize, Deserialize)] |
| 180 |
pub struct PaymentRecord { |
| 181 |
pub payment_id: String, |
| 182 |
pub request_id: String, |
| 183 |
pub volunteer_id: String, |
| 184 |
pub amount_tokens: u64, |
| 185 |
pub amount_paid: u64, |
| 186 |
pub currency: Currency, |
| 187 |
pub payment_method: PaymentMethod, |
| 188 |
pub status: PaymentStatus, |
| 189 |
pub fees_paid: u64, |
| 190 |
pub exchange_rate: f64, |
| 191 |
pub processed_at: DateTime<Utc>, |
| 192 |
pub confirmation_hash: Option<String>, |
| 193 |
pub error_message: Option<String>, |
| 194 |
} |
| 195 |
|
| 196 |
#[derive(Debug, Clone, Serialize, Deserialize)] |
| 197 |
pub enum PaymentStatus { |
| 198 |
Pending, |
| 199 |
Processing, |
| 200 |
Completed, |
| 201 |
Failed, |
| 202 |
Cancelled, |
| 203 |
RequiresAction, |
| 204 |
} |
| 205 |
|
| 206 |
#[derive(Debug, Clone, Serialize, Deserialize)] |
| 207 |
pub struct ProcessorConfig { |
| 208 |
pub batch_processing_enabled: bool, |
| 209 |
pub batch_size: usize, |
| 210 |
pub processing_interval_minutes: u32, |
| 211 |
pub max_retries: u32, |
| 212 |
pub retry_delay_minutes: u32, |
| 213 |
pub auto_currency_conversion: bool, |
| 214 |
pub fraud_detection_enabled: bool, |
| 215 |
} |
| 216 |
|
| 217 |
impl Default for PaymentFeeStructure { |
| 218 |
fn default() -> Self { |
| 219 |
let mut conversion_fees = HashMap::new(); |
| 220 |
conversion_fees.insert(Currency::USD, 0.005); |
| 221 |
conversion_fees.insert(Currency::EUR, 0.005); |
| 222 |
conversion_fees.insert(Currency::GBP, 0.005); |
| 223 |
conversion_fees.insert(Currency::Bitcoin, 0.01); |
| 224 |
conversion_fees.insert(Currency::Ethereum, 0.008); |
| 225 |
conversion_fees.insert(Currency::ZephyrCoin, 0.0); |
| 226 |
|
| 227 |
Self { |
| 228 |
base_processing_fee: 0.005, // 0.5% |
| 229 |
conversion_fees, |
| 230 |
express_fee: 0.01, // 1% |
| 231 |
kyc_fee: 500, // $5 equivalent |
| 232 |
} |
| 233 |
} |
| 234 |
} |
| 235 |
|
| 236 |
impl Default for ProcessorConfig { |
| 237 |
fn default() -> Self { |
| 238 |
Self { |
| 239 |
batch_processing_enabled: true, |
| 240 |
batch_size: 100, |
| 241 |
processing_interval_minutes: 30, |
| 242 |
max_retries: 3, |
| 243 |
retry_delay_minutes: 60, |
| 244 |
auto_currency_conversion: true, |
| 245 |
fraud_detection_enabled: true, |
| 246 |
} |
| 247 |
} |
| 248 |
} |
| 249 |
|
| 250 |
impl PaymentProcessor { |
| 251 |
/// Create new payment processor |
| 252 |
pub fn new() -> Self { |
| 253 |
let mut processor = Self { |
| 254 |
payment_methods: HashMap::new(), |
| 255 |
exchange_rates: HashMap::new(), |
| 256 |
fee_structure: PaymentFeeStructure::default(), |
| 257 |
pending_payments: VecDeque::new(), |
| 258 |
payment_history: HashMap::new(), |
| 259 |
min_payout_thresholds: HashMap::new(), |
| 260 |
config: ProcessorConfig::default(), |
| 261 |
}; |
| 262 |
|
| 263 |
processor.initialize_payment_methods(); |
| 264 |
processor.initialize_exchange_rates(); |
| 265 |
processor.initialize_payout_thresholds(); |
| 266 |
|
| 267 |
processor |
| 268 |
} |
| 269 |
|
| 270 |
/// Initialize supported payment methods |
| 271 |
fn initialize_payment_methods(&mut self) { |
| 272 |
// Cryptocurrency methods |
| 273 |
self.payment_methods.insert( |
| 274 |
PaymentMethod::Cryptocurrency(CryptoNetwork::ZephyrCoin), |
| 275 |
PaymentMethodConfig { |
| 276 |
enabled: true, |
| 277 |
min_amount: 1_000_000_000_000_000_000, // 1 ZEPH |
| 278 |
max_amount: 1_000_000 * 1_000_000_000_000_000_000, // 1M ZEPH |
| 279 |
processing_time_hours: 1, |
| 280 |
supported_currencies: vec![Currency::ZephyrCoin], |
| 281 |
geographic_restrictions: vec![], |
| 282 |
requires_kyc: false, |
| 283 |
fee_structure: MethodFeeStructure { |
| 284 |
fixed_fee: 0, |
| 285 |
percentage_fee: 0.0, |
| 286 |
network_fee: 100_000_000_000_000, // 0.0001 ZEPH |
| 287 |
exchange_fee: 0.0, |
| 288 |
}, |
| 289 |
}, |
| 290 |
); |
| 291 |
|
| 292 |
self.payment_methods.insert( |
| 293 |
PaymentMethod::Cryptocurrency(CryptoNetwork::Ethereum), |
| 294 |
PaymentMethodConfig { |
| 295 |
enabled: true, |
| 296 |
min_amount: 10_000_000_000_000_000, // 0.01 ETH |
| 297 |
max_amount: 1000 * 1_000_000_000_000_000_000, // 1000 ETH |
| 298 |
processing_time_hours: 1, |
| 299 |
supported_currencies: vec![Currency::Ethereum, Currency::USD], |
| 300 |
geographic_restrictions: vec![], |
| 301 |
requires_kyc: false, |
| 302 |
fee_structure: MethodFeeStructure { |
| 303 |
fixed_fee: 0, |
| 304 |
percentage_fee: 0.003, |
| 305 |
network_fee: 5_000_000_000_000_000, // ~$10 gas fee |
| 306 |
exchange_fee: 0.005, |
| 307 |
}, |
| 308 |
}, |
| 309 |
); |
| 310 |
|
| 311 |
// Bank transfer methods |
| 312 |
self.payment_methods.insert( |
| 313 |
PaymentMethod::BankTransfer(BankTransferType::ACH), |
| 314 |
PaymentMethodConfig { |
| 315 |
enabled: true, |
| 316 |
min_amount: 1000, // $10 |
| 317 |
max_amount: 1_000_000_00, // $10,000 |
| 318 |
processing_time_hours: 48, |
| 319 |
supported_currencies: vec![Currency::USD], |
| 320 |
geographic_restrictions: vec!["US".to_string()], |
| 321 |
requires_kyc: true, |
| 322 |
fee_structure: MethodFeeStructure { |
| 323 |
fixed_fee: 100, // $1 |
| 324 |
percentage_fee: 0.001, |
| 325 |
network_fee: 0, |
| 326 |
exchange_fee: 0.005, |
| 327 |
}, |
| 328 |
}, |
| 329 |
); |
| 330 |
|
| 331 |
self.payment_methods.insert( |
| 332 |
PaymentMethod::BankTransfer(BankTransferType::SEPA), |
| 333 |
PaymentMethodConfig { |
| 334 |
enabled: true, |
| 335 |
min_amount: 1000, // €10 |
| 336 |
max_amount: 1_000_000_00, // €10,000 |
| 337 |
processing_time_hours: 24, |
| 338 |
supported_currencies: vec![Currency::EUR], |
| 339 |
geographic_restrictions: vec!["EU".to_string()], |
| 340 |
requires_kyc: true, |
| 341 |
fee_structure: MethodFeeStructure { |
| 342 |
fixed_fee: 50, // €0.50 |
| 343 |
percentage_fee: 0.001, |
| 344 |
network_fee: 0, |
| 345 |
exchange_fee: 0.005, |
| 346 |
}, |
| 347 |
}, |
| 348 |
); |
| 349 |
|
| 350 |
// Digital wallet methods |
| 351 |
self.payment_methods.insert( |
| 352 |
PaymentMethod::DigitalWallet(WalletProvider::PayPal), |
| 353 |
PaymentMethodConfig { |
| 354 |
enabled: true, |
| 355 |
min_amount: 500, // $5 |
| 356 |
max_amount: 500_000_00, // $5,000 |
| 357 |
processing_time_hours: 2, |
| 358 |
supported_currencies: vec![Currency::USD, Currency::EUR, Currency::GBP], |
| 359 |
geographic_restrictions: vec![], // Global |
| 360 |
requires_kyc: true, |
| 361 |
fee_structure: MethodFeeStructure { |
| 362 |
fixed_fee: 30, // $0.30 |
| 363 |
percentage_fee: 0.029, // 2.9% |
| 364 |
network_fee: 0, |
| 365 |
exchange_fee: 0.035, // 3.5% for currency conversion |
| 366 |
}, |
| 367 |
}, |
| 368 |
); |
| 369 |
|
| 370 |
// Stablecoin methods |
| 371 |
self.payment_methods.insert( |
| 372 |
PaymentMethod::StableCoin(StableCoinType::USDC), |
| 373 |
PaymentMethodConfig { |
| 374 |
enabled: true, |
| 375 |
min_amount: 1_000_000, // 1 USDC |
| 376 |
max_amount: 100_000 * 1_000_000, // 100k USDC |
| 377 |
processing_time_hours: 1, |
| 378 |
supported_currencies: vec![Currency::USD], |
| 379 |
geographic_restrictions: vec![], |
| 380 |
requires_kyc: false, |
| 381 |
fee_structure: MethodFeeStructure { |
| 382 |
fixed_fee: 0, |
| 383 |
percentage_fee: 0.001, |
| 384 |
network_fee: 2_000_000_000_000_000, // ~$2 gas fee |
| 385 |
exchange_fee: 0.001, |
| 386 |
}, |
| 387 |
}, |
| 388 |
); |
| 389 |
} |
| 390 |
|
| 391 |
/// Initialize exchange rates (would fetch from APIs in production) |
| 392 |
fn initialize_exchange_rates(&mut self) { |
| 393 |
self.exchange_rates.insert(Currency::ZephyrCoin, 0.10); // $0.10 per ZEPH |
| 394 |
self.exchange_rates.insert(Currency::USD, 1.0); |
| 395 |
self.exchange_rates.insert(Currency::EUR, 0.85); |
| 396 |
self.exchange_rates.insert(Currency::GBP, 0.73); |
| 397 |
self.exchange_rates.insert(Currency::Bitcoin, 45000.0); |
| 398 |
self.exchange_rates.insert(Currency::Ethereum, 2500.0); |
| 399 |
} |
| 400 |
|
| 401 |
/// Initialize minimum payout thresholds |
| 402 |
fn initialize_payout_thresholds(&mut self) { |
| 403 |
self.min_payout_thresholds.insert(Currency::ZephyrCoin, 10 * 1_000_000_000_000_000_000); // 10 ZEPH |
| 404 |
self.min_payout_thresholds.insert(Currency::USD, 1000); // $10 |
| 405 |
self.min_payout_thresholds.insert(Currency::EUR, 850); // €8.50 |
| 406 |
self.min_payout_thresholds.insert(Currency::Bitcoin, 22222); // ~$10 worth |
| 407 |
self.min_payout_thresholds.insert(Currency::Ethereum, 400000); // ~$10 worth |
| 408 |
} |
| 409 |
|
| 410 |
/// Submit payment request |
| 411 |
pub fn submit_payment_request(&mut self, mut request: PaymentRequest) -> Result<String> { |
| 412 |
// Validate payment method |
| 413 |
let method_config = self.payment_methods.get(&request.payment_method) |
| 414 |
.ok_or_else(|| anyhow::anyhow!("Payment method not supported"))?; |
| 415 |
|
| 416 |
if !method_config.enabled { |
| 417 |
return Err(anyhow::anyhow!("Payment method temporarily disabled")); |
| 418 |
} |
| 419 |
|
| 420 |
// Convert amount to target currency |
| 421 |
let amount_in_currency = self.convert_tokens_to_currency( |
| 422 |
request.amount_tokens, |
| 423 |
&request.target_currency, |
| 424 |
)?; |
| 425 |
|
| 426 |
// Check minimum threshold |
| 427 |
if let Some(&min_threshold) = self.min_payout_thresholds.get(&request.target_currency) { |
| 428 |
if amount_in_currency < min_threshold { |
| 429 |
return Err(anyhow::anyhow!( |
| 430 |
"Amount below minimum payout threshold: {} < {}", |
| 431 |
amount_in_currency, min_threshold |
| 432 |
)); |
| 433 |
} |
| 434 |
} |
| 435 |
|
| 436 |
// Check method limits |
| 437 |
if amount_in_currency < method_config.min_amount || amount_in_currency > method_config.max_amount { |
| 438 |
return Err(anyhow::anyhow!( |
| 439 |
"Amount outside payment method limits: {} not in [{}, {}]", |
| 440 |
amount_in_currency, method_config.min_amount, method_config.max_amount |
| 441 |
)); |
| 442 |
} |
| 443 |
|
| 444 |
// Validate recipient info |
| 445 |
self.validate_recipient_info(&request.recipient_info, &request.payment_method)?; |
| 446 |
|
| 447 |
// Generate request ID |
| 448 |
request.request_id = format!("pay_{}_{}", |
| 449 |
chrono::Utc::now().timestamp(), |
| 450 |
&request.volunteer_id[..8] |
| 451 |
); |
| 452 |
|
| 453 |
self.pending_payments.push_back(request.clone()); |
| 454 |
|
| 455 |
tracing::info!("Payment request submitted: {} for {} tokens", |
| 456 |
request.request_id, request.amount_tokens); |
| 457 |
|
| 458 |
Ok(request.request_id) |
| 459 |
} |
| 460 |
|
| 461 |
/// Convert tokens to target currency |
| 462 |
fn convert_tokens_to_currency(&self, tokens: u64, target_currency: &Currency) -> Result<u64> { |
| 463 |
if *target_currency == Currency::ZephyrCoin { |
| 464 |
return Ok(tokens); |
| 465 |
} |
| 466 |
|
| 467 |
let zeph_rate = self.exchange_rates.get(&Currency::ZephyrCoin) |
| 468 |
.ok_or_else(|| anyhow::anyhow!("ZephyrCoin exchange rate not available"))?; |
| 469 |
|
| 470 |
let target_rate = self.exchange_rates.get(target_currency) |
| 471 |
.ok_or_else(|| anyhow::anyhow!("Target currency exchange rate not available"))?; |
| 472 |
|
| 473 |
// Convert tokens to USD value, then to target currency |
| 474 |
let usd_value = (tokens as f64 / 1_000_000_000_000_000_000.0) * zeph_rate; |
| 475 |
let target_value = usd_value / target_rate; |
| 476 |
|
| 477 |
// Convert to smallest units (cents, wei, etc.) |
| 478 |
let target_amount = match target_currency { |
| 479 |
Currency::USD | Currency::EUR | Currency::GBP => (target_value * 100.0) as u64, |
| 480 |
Currency::Bitcoin => (target_value * 100_000_000.0) as u64, // Satoshis |
| 481 |
Currency::Ethereum => (target_value * 1_000_000_000_000_000_000.0) as u64, // Wei |
| 482 |
_ => (target_value * 1_000_000.0) as u64, // Default 6 decimals |
| 483 |
}; |
| 484 |
|
| 485 |
Ok(target_amount) |
| 486 |
} |
| 487 |
|
| 488 |
/// Validate recipient information |
| 489 |
fn validate_recipient_info(&self, info: &RecipientInfo, method: &PaymentMethod) -> Result<()> { |
| 490 |
match method { |
| 491 |
PaymentMethod::Cryptocurrency(_) => { |
| 492 |
if info.wallet_address.is_none() { |
| 493 |
return Err(anyhow::anyhow!("Wallet address required for crypto payments")); |
| 494 |
} |
| 495 |
}, |
| 496 |
PaymentMethod::BankTransfer(_) => { |
| 497 |
if info.bank_account.is_none() { |
| 498 |
return Err(anyhow::anyhow!("Bank account info required for bank transfers")); |
| 499 |
} |
| 500 |
if !info.kyc_verified { |
| 501 |
return Err(anyhow::anyhow!("KYC verification required for bank transfers")); |
| 502 |
} |
| 503 |
}, |
| 504 |
PaymentMethod::DigitalWallet(_) => { |
| 505 |
if info.digital_wallet.is_none() { |
| 506 |
return Err(anyhow::anyhow!("Digital wallet info required")); |
| 507 |
} |
| 508 |
}, |
| 509 |
PaymentMethod::StableCoin(_) => { |
| 510 |
if info.wallet_address.is_none() { |
| 511 |
return Err(anyhow::anyhow!("Wallet address required for stablecoin payments")); |
| 512 |
} |
| 513 |
}, |
| 514 |
} |
| 515 |
|
| 516 |
Ok(()) |
| 517 |
} |
| 518 |
|
| 519 |
/// Process pending payments |
| 520 |
pub async fn process_pending_payments(&mut self) -> Result<Vec<PaymentRecord>> { |
| 521 |
let mut processed = Vec::new(); |
| 522 |
let batch_size = if self.config.batch_processing_enabled { |
| 523 |
self.config.batch_size |
| 524 |
} else { |
| 525 |
1 |
| 526 |
}; |
| 527 |
|
| 528 |
for _ in 0..batch_size { |
| 529 |
if let Some(request) = self.pending_payments.pop_front() { |
| 530 |
// Check if scheduled for future |
| 531 |
if let Some(scheduled_time) = request.scheduled_for { |
| 532 |
if Utc::now() < scheduled_time { |
| 533 |
// Put back in queue |
| 534 |
self.pending_payments.push_front(request); |
| 535 |
break; |
| 536 |
} |
| 537 |
} |
| 538 |
|
| 539 |
match self.process_single_payment(request).await { |
| 540 |
Ok(record) => { |
| 541 |
processed.push(record.clone()); |
| 542 |
|
| 543 |
// Add to history |
| 544 |
self.payment_history |
| 545 |
.entry(record.volunteer_id.clone()) |
| 546 |
.or_insert_with(Vec::new) |
| 547 |
.push(record); |
| 548 |
}, |
| 549 |
Err(e) => { |
| 550 |
tracing::error!("Payment processing failed: {}", e); |
| 551 |
// Could implement retry logic here |
| 552 |
} |
| 553 |
} |
| 554 |
} else { |
| 555 |
break; |
| 556 |
} |
| 557 |
} |
| 558 |
|
| 559 |
Ok(processed) |
| 560 |
} |
| 561 |
|
| 562 |
/// Process single payment |
| 563 |
async fn process_single_payment(&self, request: PaymentRequest) -> Result<PaymentRecord> { |
| 564 |
let payment_id = format!("tx_{}_{}", |
| 565 |
chrono::Utc::now().timestamp_millis(), |
| 566 |
&request.volunteer_id[..6] |
| 567 |
); |
| 568 |
|
| 569 |
// Calculate fees |
| 570 |
let fees = self.calculate_payment_fees(&request)?; |
| 571 |
|
| 572 |
// Convert amount |
| 573 |
let amount_in_currency = self.convert_tokens_to_currency( |
| 574 |
request.amount_tokens, |
| 575 |
&request.target_currency, |
| 576 |
)?; |
| 577 |
|
| 578 |
let net_amount = amount_in_currency.saturating_sub(fees.total_fee); |
| 579 |
|
| 580 |
// Execute payment based on method |
| 581 |
let (status, confirmation_hash, error_message) = match request.payment_method { |
| 582 |
PaymentMethod::Cryptocurrency(_) => { |
| 583 |
self.execute_crypto_payment(&request, net_amount).await? |
| 584 |
}, |
| 585 |
PaymentMethod::BankTransfer(_) => { |
| 586 |
self.execute_bank_transfer(&request, net_amount).await? |
| 587 |
}, |
| 588 |
PaymentMethod::DigitalWallet(_) => { |
| 589 |
self.execute_wallet_payment(&request, net_amount).await? |
| 590 |
}, |
| 591 |
PaymentMethod::StableCoin(_) => { |
| 592 |
self.execute_stablecoin_payment(&request, net_amount).await? |
| 593 |
}, |
| 594 |
}; |
| 595 |
|
| 596 |
let record = PaymentRecord { |
| 597 |
payment_id, |
| 598 |
request_id: request.request_id, |
| 599 |
volunteer_id: request.volunteer_id, |
| 600 |
amount_tokens: request.amount_tokens, |
| 601 |
amount_paid: net_amount, |
| 602 |
currency: request.target_currency, |
| 603 |
payment_method: request.payment_method, |
| 604 |
status, |
| 605 |
fees_paid: fees.total_fee, |
| 606 |
exchange_rate: self.get_exchange_rate(&Currency::ZephyrCoin, &request.target_currency)?, |
| 607 |
processed_at: Utc::now(), |
| 608 |
confirmation_hash, |
| 609 |
error_message, |
| 610 |
}; |
| 611 |
|
| 612 |
tracing::info!("Payment processed: {} - {} {:?}", |
| 613 |
record.payment_id, record.amount_paid, record.currency); |
| 614 |
|
| 615 |
Ok(record) |
| 616 |
} |
| 617 |
|
| 618 |
/// Calculate payment fees |
| 619 |
fn calculate_payment_fees(&self, request: &PaymentRequest) -> Result<PaymentFees> { |
| 620 |
let method_config = self.payment_methods.get(&request.payment_method) |
| 621 |
.ok_or_else(|| anyhow::anyhow!("Payment method not found"))?; |
| 622 |
|
| 623 |
let amount_in_currency = self.convert_tokens_to_currency( |
| 624 |
request.amount_tokens, |
| 625 |
&request.target_currency, |
| 626 |
)?; |
| 627 |
|
| 628 |
let base_fee = (amount_in_currency as f64 * self.fee_structure.base_processing_fee) as u64; |
| 629 |
let method_percentage_fee = (amount_in_currency as f64 * method_config.fee_structure.percentage_fee) as u64; |
| 630 |
let method_fixed_fee = method_config.fee_structure.fixed_fee; |
| 631 |
let network_fee = method_config.fee_structure.network_fee; |
| 632 |
|
| 633 |
let exchange_fee = if request.target_currency != Currency::ZephyrCoin { |
| 634 |
let exchange_rate = method_config.fee_structure.exchange_fee; |
| 635 |
(amount_in_currency as f64 * exchange_rate) as u64 |
| 636 |
} else { |
| 637 |
0 |
| 638 |
}; |
| 639 |
|
| 640 |
let express_fee = if matches!(request.priority, PaymentPriority::Express | PaymentPriority::Immediate) { |
| 641 |
(amount_in_currency as f64 * self.fee_structure.express_fee) as u64 |
| 642 |
} else { |
| 643 |
0 |
| 644 |
}; |
| 645 |
|
| 646 |
let total_fee = base_fee + method_percentage_fee + method_fixed_fee + network_fee + exchange_fee + express_fee; |
| 647 |
|
| 648 |
Ok(PaymentFees { |
| 649 |
base_fee, |
| 650 |
method_percentage_fee, |
| 651 |
method_fixed_fee, |
| 652 |
network_fee, |
| 653 |
exchange_fee, |
| 654 |
express_fee, |
| 655 |
total_fee, |
| 656 |
}) |
| 657 |
} |
| 658 |
|
| 659 |
/// Execute cryptocurrency payment |
| 660 |
async fn execute_crypto_payment( |
| 661 |
&self, |
| 662 |
request: &PaymentRequest, |
| 663 |
amount: u64, |
| 664 |
) -> Result<(PaymentStatus, Option<String>, Option<String>)> { |
| 665 |
// Simulate crypto transaction |
| 666 |
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; |
| 667 |
|
| 668 |
let confirmation_hash = format!("0x{:x}", rand::random::<u64>()); |
| 669 |
|
| 670 |
Ok((PaymentStatus::Completed, Some(confirmation_hash), None)) |
| 671 |
} |
| 672 |
|
| 673 |
/// Execute bank transfer |
| 674 |
async fn execute_bank_transfer( |
| 675 |
&self, |
| 676 |
request: &PaymentRequest, |
| 677 |
amount: u64, |
| 678 |
) -> Result<(PaymentStatus, Option<String>, Option<String>)> { |
| 679 |
// Simulate bank transfer processing |
| 680 |
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; |
| 681 |
|
| 682 |
let reference = format!("TXN{}", chrono::Utc::now().timestamp()); |
| 683 |
|
| 684 |
Ok((PaymentStatus::Processing, Some(reference), None)) |
| 685 |
} |
| 686 |
|
| 687 |
/// Execute digital wallet payment |
| 688 |
async fn execute_wallet_payment( |
| 689 |
&self, |
| 690 |
request: &PaymentRequest, |
| 691 |
amount: u64, |
| 692 |
) -> Result<(PaymentStatus, Option<String>, Option<String>)> { |
| 693 |
// Simulate wallet payment |
| 694 |
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await; |
| 695 |
|
| 696 |
let transaction_id = format!("WLT{}", chrono::Utc::now().timestamp()); |
| 697 |
|
| 698 |
Ok((PaymentStatus::Completed, Some(transaction_id), None)) |
| 699 |
} |
| 700 |
|
| 701 |
/// Execute stablecoin payment |
| 702 |
async fn execute_stablecoin_payment( |
| 703 |
&self, |
| 704 |
request: &PaymentRequest, |
| 705 |
amount: u64, |
| 706 |
) -> Result<(PaymentStatus, Option<String>, Option<String>)> { |
| 707 |
// Simulate stablecoin transfer |
| 708 |
tokio::time::sleep(tokio::time::Duration::from_millis(150)).await; |
| 709 |
|
| 710 |
let tx_hash = format!("0x{:x}", rand::random::<u64>()); |
| 711 |
|
| 712 |
Ok((PaymentStatus::Completed, Some(tx_hash), None)) |
| 713 |
} |
| 714 |
|
| 715 |
/// Get exchange rate between currencies |
| 716 |
fn get_exchange_rate(&self, from: &Currency, to: &Currency) -> Result<f64> { |
| 717 |
if from == to { |
| 718 |
return Ok(1.0); |
| 719 |
} |
| 720 |
|
| 721 |
let from_rate = self.exchange_rates.get(from) |
| 722 |
.ok_or_else(|| anyhow::anyhow!("Exchange rate not found for {:?}", from))?; |
| 723 |
|
| 724 |
let to_rate = self.exchange_rates.get(to) |
| 725 |
.ok_or_else(|| anyhow::anyhow!("Exchange rate not found for {:?}", to))?; |
| 726 |
|
| 727 |
Ok(from_rate / to_rate) |
| 728 |
} |
| 729 |
|
| 730 |
/// Get payment history for volunteer |
| 731 |
pub fn get_payment_history(&self, volunteer_id: &str) -> Vec<&PaymentRecord> { |
| 732 |
self.payment_history.get(volunteer_id) |
| 733 |
.map(|records| records.iter().collect()) |
| 734 |
.unwrap_or_default() |
| 735 |
} |
| 736 |
|
| 737 |
/// Update exchange rates |
| 738 |
pub fn update_exchange_rates(&mut self, rates: HashMap<Currency, f64>) { |
| 739 |
for (currency, rate) in rates { |
| 740 |
self.exchange_rates.insert(currency, rate); |
| 741 |
} |
| 742 |
} |
| 743 |
|
| 744 |
/// Get supported payment methods for region |
| 745 |
pub fn get_supported_methods(&self, country_code: &str) -> Vec<&PaymentMethod> { |
| 746 |
self.payment_methods.iter() |
| 747 |
.filter(|(_, config)| { |
| 748 |
config.enabled && |
| 749 |
(config.geographic_restrictions.is_empty() || |
| 750 |
config.geographic_restrictions.contains(&country_code.to_string())) |
| 751 |
}) |
| 752 |
.map(|(method, _)| method) |
| 753 |
.collect() |
| 754 |
} |
| 755 |
} |
| 756 |
|
| 757 |
#[derive(Debug, Clone, Serialize, Deserialize)] |
| 758 |
struct PaymentFees { |
| 759 |
pub base_fee: u64, |
| 760 |
pub method_percentage_fee: u64, |
| 761 |
pub method_fixed_fee: u64, |
| 762 |
pub network_fee: u64, |
| 763 |
pub exchange_fee: u64, |
| 764 |
pub express_fee: u64, |
| 765 |
pub total_fee: u64, |
| 766 |
} |
| 767 |
|
| 768 |
#[cfg(test)] |
| 769 |
mod tests { |
| 770 |
use super::*; |
| 771 |
|
| 772 |
#[test] |
| 773 |
fn test_payment_processor_creation() { |
| 774 |
let processor = PaymentProcessor::new(); |
| 775 |
assert!(!processor.payment_methods.is_empty()); |
| 776 |
assert!(!processor.exchange_rates.is_empty()); |
| 777 |
} |
| 778 |
|
| 779 |
#[tokio::test] |
| 780 |
async fn test_payment_submission() { |
| 781 |
let mut processor = PaymentProcessor::new(); |
| 782 |
|
| 783 |
let request = PaymentRequest { |
| 784 |
request_id: String::new(), |
| 785 |
volunteer_id: "test_volunteer".to_string(), |
| 786 |
amount_tokens: 100 * 1_000_000_000_000_000_000, // 100 ZEPH |
| 787 |
target_currency: Currency::USD, |
| 788 |
payment_method: PaymentMethod::DigitalWallet(WalletProvider::PayPal), |
| 789 |
recipient_info: RecipientInfo { |
| 790 |
wallet_address: None, |
| 791 |
bank_account: None, |
| 792 |
digital_wallet: Some(DigitalWalletInfo { |
| 793 |
provider: WalletProvider::PayPal, |
| 794 |
wallet_id: "test@example.com".to_string(), |
| 795 |
verified: true, |
| 796 |
}), |
| 797 |
kyc_verified: true, |
| 798 |
tax_info: None, |
| 799 |
}, |
| 800 |
priority: PaymentPriority::Standard, |
| 801 |
created_at: Utc::now(), |
| 802 |
scheduled_for: None, |
| 803 |
metadata: HashMap::new(), |
| 804 |
}; |
| 805 |
|
| 806 |
let request_id = processor.submit_payment_request(request).unwrap(); |
| 807 |
assert!(!request_id.is_empty()); |
| 808 |
assert_eq!(processor.pending_payments.len(), 1); |
| 809 |
} |
| 810 |
|
| 811 |
#[test] |
| 812 |
fn test_currency_conversion() { |
| 813 |
let processor = PaymentProcessor::new(); |
| 814 |
let tokens = 100 * 1_000_000_000_000_000_000; // 100 ZEPH |
| 815 |
|
| 816 |
let usd_amount = processor.convert_tokens_to_currency(tokens, &Currency::USD).unwrap(); |
| 817 |
assert_eq!(usd_amount, 1000); // $10.00 (100 ZEPH * $0.10 * 100 cents) |
| 818 |
|
| 819 |
let zeph_amount = processor.convert_tokens_to_currency(tokens, &Currency::ZephyrCoin).unwrap(); |
| 820 |
assert_eq!(zeph_amount, tokens); // Same amount in ZEPH |
| 821 |
} |
| 822 |
} |