Rust · 23992 bytes Raw Blame History
1 //! Real-Time Earnings Calculation System
2 //!
3 //! Comprehensive earnings tracking and calculation 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 // Moved from legacy token_model for backward compatibility
11
12 /// Reason for reward calculation
13 #[derive(Debug, Clone, Serialize, Deserialize)]
14 pub enum RewardReason {
15 StorageContribution,
16 UptimeBonus,
17 PerformanceBonus,
18 GeographicBonus,
19 NetworkHealthBonus,
20 }
21
22 /// Network health metrics for earnings calculation
23 #[derive(Debug, Clone, Serialize, Deserialize)]
24 pub struct NetworkHealthMetrics {
25 pub total_storage_tb: f64,
26 pub total_capacity_gb: f64,
27 pub active_nodes: u32,
28 pub active_volunteers: u32,
29 pub average_uptime_percentage: f64,
30 pub average_uptime: f64,
31 pub network_utilization_percentage: f64,
32 pub utilization_rate: f64,
33 pub geographic_distribution_score: f64,
34 pub geographic_diversity: f64,
35 pub data_redundancy_factor: f64,
36 pub data_durability: f64,
37 }
38
39 /// Real-time earnings calculator for volunteers
40 #[derive(Debug, Clone, Serialize, Deserialize)]
41 pub struct EarningsCalculator {
42 /// Volunteer performance tracking
43 pub volunteer_metrics: HashMap<String, VolunteerMetrics>,
44 /// Earnings rates configuration
45 pub rates: EarningsRates,
46 /// Bonus multipliers
47 pub bonuses: BonusStructure,
48 /// Performance history for analytics
49 pub performance_history: HashMap<String, VecDeque<PerformanceRecord>>,
50 /// Network-wide metrics
51 pub network_metrics: NetworkHealthMetrics,
52 /// Daily earnings summary
53 pub daily_earnings: HashMap<String, DailyEarnings>,
54 }
55
56 #[derive(Debug, Clone, Serialize, Deserialize)]
57 pub struct VolunteerMetrics {
58 pub volunteer_id: String,
59 pub total_storage_gb: u64,
60 pub available_storage_gb: u64,
61 pub used_storage_gb: u64,
62 pub uptime_hours_24h: f64,
63 pub uptime_percentage: f64,
64 pub response_time_ms: u64,
65 pub transfer_speed_mbps: f64,
66 pub successful_transfers: u64,
67 pub failed_transfers: u64,
68 pub geographic_region: GeographicRegion,
69 pub connection_quality: ConnectionQuality,
70 pub reliability_score: f64,
71 pub last_seen: DateTime<Utc>,
72 pub joined_at: DateTime<Utc>,
73 }
74
75 #[derive(Debug, Clone, Serialize, Deserialize)]
76 pub struct EarningsRates {
77 /// Base rate per GB per day (in wei-equivalent tokens)
78 pub base_storage_rate: u64,
79 /// Uptime bonus rate (per hour of 100% uptime)
80 pub uptime_bonus_rate: u64,
81 /// Performance bonus rate (based on speed/reliability)
82 pub performance_bonus_rate: u64,
83 /// Geographic diversity bonus
84 pub geographic_bonus_rate: u64,
85 /// Longevity bonus (tenure rewards)
86 pub longevity_bonus_rate: u64,
87 /// Network contribution bonus
88 pub network_contribution_rate: u64,
89 }
90
91 #[derive(Debug, Clone, Serialize, Deserialize)]
92 pub struct BonusStructure {
93 /// Uptime thresholds and multipliers
94 pub uptime_tiers: Vec<UptimeTier>,
95 /// Performance score multipliers
96 pub performance_multipliers: PerformanceMultipliers,
97 /// Geographic diversity bonuses
98 pub geographic_bonuses: HashMap<GeographicRegion, f64>,
99 /// Tenure bonuses (loyalty rewards)
100 pub tenure_bonuses: Vec<TenureTier>,
101 /// Network health bonuses
102 pub network_health_bonus: f64,
103 }
104
105 #[derive(Debug, Clone, Serialize, Deserialize)]
106 pub struct UptimeTier {
107 pub threshold_percent: f64,
108 pub multiplier: f64,
109 pub name: String,
110 }
111
112 #[derive(Debug, Clone, Serialize, Deserialize)]
113 pub struct PerformanceMultipliers {
114 pub excellent_speed: f64, // >100 Mbps
115 pub good_speed: f64, // 50-100 Mbps
116 pub average_speed: f64, // 10-50 Mbps
117 pub low_response_time: f64, // <100ms
118 pub high_reliability: f64, // >99% success rate
119 }
120
121 #[derive(Debug, Clone, Serialize, Deserialize)]
122 pub struct TenureTier {
123 pub months: u32,
124 pub multiplier: f64,
125 pub name: String,
126 }
127
128 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
129 pub enum GeographicRegion {
130 NorthAmerica,
131 Europe,
132 Asia,
133 SouthAmerica,
134 Africa,
135 Oceania,
136 Rare, // Underrepresented regions
137 }
138
139 #[derive(Debug, Clone, Serialize, Deserialize)]
140 pub enum ConnectionQuality {
141 Excellent, // Fiber, low latency
142 Good, // Broadband, stable
143 Fair, // Adequate speed
144 Poor, // Slow or unstable
145 }
146
147 #[derive(Debug, Clone, Serialize, Deserialize)]
148 pub struct PerformanceRecord {
149 pub timestamp: DateTime<Utc>,
150 pub uptime_hours: f64,
151 pub storage_provided_gb: u64,
152 pub transfer_speed_mbps: f64,
153 pub response_time_ms: u64,
154 pub success_rate: f64,
155 pub earnings_tokens: u64,
156 pub bonus_tokens: u64,
157 }
158
159 #[derive(Debug, Clone, Serialize, Deserialize)]
160 pub struct DailyEarnings {
161 pub date: DateTime<Utc>,
162 pub base_earnings: u64,
163 pub uptime_bonus: u64,
164 pub performance_bonus: u64,
165 pub geographic_bonus: u64,
166 pub longevity_bonus: u64,
167 pub network_bonus: u64,
168 pub total_earnings: u64,
169 pub storage_gb_hours: u64,
170 pub actual_uptime_hours: f64,
171 }
172
173 #[derive(Debug, Clone, Serialize, Deserialize)]
174 pub struct EarningsProjection {
175 pub daily_estimate: u64,
176 pub weekly_estimate: u64,
177 pub monthly_estimate: u64,
178 pub annual_estimate: u64,
179 pub factors: ProjectionFactors,
180 }
181
182 #[derive(Debug, Clone, Serialize, Deserialize)]
183 pub struct ProjectionFactors {
184 pub current_performance: f64,
185 pub network_demand: f64,
186 pub seasonal_adjustment: f64,
187 pub growth_factor: f64,
188 }
189
190 impl Default for EarningsRates {
191 fn default() -> Self {
192 Self {
193 base_storage_rate: 20_000_000_000_000_000, // 0.02 tokens per GB per day
194 uptime_bonus_rate: 1_000_000_000_000_000, // 0.001 tokens per hour
195 performance_bonus_rate: 5_000_000_000_000_000, // 0.005 tokens for high performance
196 geographic_bonus_rate: 3_000_000_000_000_000, // 0.003 tokens for rare regions
197 longevity_bonus_rate: 2_000_000_000_000_000, // 0.002 tokens longevity bonus
198 network_contribution_rate: 1_000_000_000_000_000, // 0.001 tokens network contribution
199 }
200 }
201 }
202
203 impl Default for BonusStructure {
204 fn default() -> Self {
205 Self {
206 uptime_tiers: vec![
207 UptimeTier { threshold_percent: 99.5, multiplier: 2.0, name: "Platinum".to_string() },
208 UptimeTier { threshold_percent: 98.0, multiplier: 1.5, name: "Gold".to_string() },
209 UptimeTier { threshold_percent: 95.0, multiplier: 1.2, name: "Silver".to_string() },
210 UptimeTier { threshold_percent: 90.0, multiplier: 1.0, name: "Bronze".to_string() },
211 ],
212 performance_multipliers: PerformanceMultipliers {
213 excellent_speed: 1.3,
214 good_speed: 1.1,
215 average_speed: 1.0,
216 low_response_time: 1.2,
217 high_reliability: 1.25,
218 },
219 geographic_bonuses: HashMap::from([
220 (GeographicRegion::Rare, 0.5),
221 (GeographicRegion::Africa, 0.3),
222 (GeographicRegion::SouthAmerica, 0.2),
223 (GeographicRegion::Oceania, 0.2),
224 (GeographicRegion::Asia, 0.1),
225 (GeographicRegion::Europe, 0.05),
226 (GeographicRegion::NorthAmerica, 0.0),
227 ]),
228 tenure_bonuses: vec![
229 TenureTier { months: 24, multiplier: 1.5, name: "Veteran".to_string() },
230 TenureTier { months: 12, multiplier: 1.3, name: "Experienced".to_string() },
231 TenureTier { months: 6, multiplier: 1.15, name: "Established".to_string() },
232 TenureTier { months: 3, multiplier: 1.05, name: "Regular".to_string() },
233 ],
234 network_health_bonus: 0.1, // 10% bonus when network is healthy
235 }
236 }
237 }
238
239 impl EarningsCalculator {
240 /// Create new earnings calculator
241 pub fn new() -> Self {
242 Self {
243 volunteer_metrics: HashMap::new(),
244 rates: EarningsRates::default(),
245 bonuses: BonusStructure::default(),
246 performance_history: HashMap::new(),
247 network_metrics: NetworkHealthMetrics {
248 total_storage_tb: 0.0,
249 total_capacity_gb: 0.0,
250 active_nodes: 0,
251 active_volunteers: 0,
252 average_uptime_percentage: 0.0,
253 average_uptime: 0.0,
254 network_utilization_percentage: 0.0,
255 utilization_rate: 0.0,
256 geographic_distribution_score: 0.0,
257 geographic_diversity: 0.0,
258 data_redundancy_factor: 0.0,
259 data_durability: 0.0,
260 },
261 daily_earnings: HashMap::new(),
262 }
263 }
264
265 /// Update volunteer metrics
266 pub fn update_volunteer_metrics(&mut self, metrics: VolunteerMetrics) {
267 let volunteer_id = metrics.volunteer_id.clone();
268
269 // Record performance history
270 let record = PerformanceRecord {
271 timestamp: Utc::now(),
272 uptime_hours: metrics.uptime_hours_24h,
273 storage_provided_gb: metrics.used_storage_gb,
274 transfer_speed_mbps: metrics.transfer_speed_mbps,
275 response_time_ms: metrics.response_time_ms,
276 success_rate: if metrics.successful_transfers + metrics.failed_transfers > 0 {
277 metrics.successful_transfers as f64 / (metrics.successful_transfers + metrics.failed_transfers) as f64
278 } else {
279 1.0
280 },
281 earnings_tokens: 0, // Will be calculated
282 bonus_tokens: 0,
283 };
284
285 self.performance_history
286 .entry(volunteer_id.clone())
287 .or_insert_with(|| VecDeque::with_capacity(720)) // 30 days of hourly records
288 .push_back(record);
289
290 // Keep only last 30 days
291 if let Some(history) = self.performance_history.get_mut(&volunteer_id) {
292 while history.len() > 720 {
293 history.pop_front();
294 }
295 }
296
297 self.volunteer_metrics.insert(volunteer_id, metrics);
298 }
299
300 /// Calculate real-time earnings for a volunteer
301 pub fn calculate_real_time_earnings(&self, volunteer_id: &str) -> Result<u64> {
302 let metrics = self.volunteer_metrics.get(volunteer_id)
303 .ok_or_else(|| anyhow::anyhow!("Volunteer metrics not found"))?;
304
305 // Calculate base storage earnings
306 let base_earnings = self.calculate_base_storage_earnings(metrics);
307
308 // Calculate uptime bonus
309 let uptime_bonus = self.calculate_uptime_bonus(metrics);
310
311 // Calculate performance bonus
312 let performance_bonus = self.calculate_performance_bonus(metrics);
313
314 // Calculate geographic bonus
315 let geographic_bonus = self.calculate_geographic_bonus(metrics);
316
317 // Calculate longevity bonus
318 let longevity_bonus = self.calculate_longevity_bonus(metrics);
319
320 // Calculate network health bonus
321 let network_bonus = self.calculate_network_health_bonus(base_earnings);
322
323 let total_earnings = base_earnings + uptime_bonus + performance_bonus
324 + geographic_bonus + longevity_bonus + network_bonus;
325
326 Ok(total_earnings)
327 }
328
329 /// Calculate base storage earnings
330 fn calculate_base_storage_earnings(&self, metrics: &VolunteerMetrics) -> u64 {
331 // Earnings based on storage provided and utilization
332 let utilization_factor = if metrics.total_storage_gb > 0 {
333 metrics.used_storage_gb as f64 / metrics.total_storage_gb as f64
334 } else {
335 0.0
336 };
337
338 // Base rate * storage * utilization (with minimum guarantee)
339 let base = metrics.total_storage_gb * self.rates.base_storage_rate;
340 let utilization_earnings = (base as f64 * utilization_factor) as u64;
341
342 // Guarantee at least 20% of base rate even with no utilization
343 let minimum = base / 5;
344
345 utilization_earnings.max(minimum)
346 }
347
348 /// Calculate uptime bonus
349 fn calculate_uptime_bonus(&self, metrics: &VolunteerMetrics) -> u64 {
350 let uptime_tier = self.bonuses.uptime_tiers.iter()
351 .find(|tier| metrics.uptime_percentage >= tier.threshold_percent)
352 .unwrap_or(&self.bonuses.uptime_tiers[self.bonuses.uptime_tiers.len() - 1]);
353
354 let base_bonus = (metrics.uptime_hours_24h * self.rates.uptime_bonus_rate as f64) as u64;
355 (base_bonus as f64 * uptime_tier.multiplier) as u64
356 }
357
358 /// Calculate performance bonus
359 fn calculate_performance_bonus(&self, metrics: &VolunteerMetrics) -> u64 {
360 let mut multiplier = 1.0;
361
362 // Speed bonus
363 if metrics.transfer_speed_mbps >= 100.0 {
364 multiplier *= self.bonuses.performance_multipliers.excellent_speed;
365 } else if metrics.transfer_speed_mbps >= 50.0 {
366 multiplier *= self.bonuses.performance_multipliers.good_speed;
367 } else if metrics.transfer_speed_mbps >= 10.0 {
368 multiplier *= self.bonuses.performance_multipliers.average_speed;
369 }
370
371 // Response time bonus
372 if metrics.response_time_ms < 100 {
373 multiplier *= self.bonuses.performance_multipliers.low_response_time;
374 }
375
376 // Reliability bonus
377 let success_rate = if metrics.successful_transfers + metrics.failed_transfers > 0 {
378 metrics.successful_transfers as f64 / (metrics.successful_transfers + metrics.failed_transfers) as f64
379 } else {
380 1.0
381 };
382
383 if success_rate >= 0.99 {
384 multiplier *= self.bonuses.performance_multipliers.high_reliability;
385 }
386
387 (self.rates.performance_bonus_rate as f64 * multiplier) as u64
388 }
389
390 /// Calculate geographic diversity bonus
391 fn calculate_geographic_bonus(&self, metrics: &VolunteerMetrics) -> u64 {
392 let bonus_multiplier = self.bonuses.geographic_bonuses
393 .get(&metrics.geographic_region)
394 .copied()
395 .unwrap_or(0.0);
396
397 (self.rates.geographic_bonus_rate as f64 * (1.0 + bonus_multiplier)) as u64
398 }
399
400 /// Calculate longevity bonus
401 fn calculate_longevity_bonus(&self, metrics: &VolunteerMetrics) -> u64 {
402 let months_active = (Utc::now() - metrics.joined_at).num_days() / 30;
403
404 let tenure_tier = self.bonuses.tenure_bonuses.iter()
405 .find(|tier| months_active >= tier.months as i64)
406 .cloned()
407 .unwrap_or(TenureTier { months: 0, multiplier: 1.0, name: "New".to_string() });
408
409 (self.rates.longevity_bonus_rate as f64 * tenure_tier.multiplier) as u64
410 }
411
412 /// Calculate network health bonus
413 fn calculate_network_health_bonus(&self, base_earnings: u64) -> u64 {
414 // Bonus when network is performing well
415 let health_score = self.calculate_network_health_score();
416
417 if health_score >= 95.0 {
418 (base_earnings as f64 * self.bonuses.network_health_bonus) as u64
419 } else {
420 0
421 }
422 }
423
424 /// Calculate overall network health score
425 fn calculate_network_health_score(&self) -> f64 {
426 let metrics = &self.network_metrics;
427
428 // Weighted health score
429 let uptime_score = metrics.average_uptime.min(100.0);
430 let diversity_score = metrics.geographic_diversity.min(100.0);
431 let durability_score = metrics.data_durability.min(100.0);
432
433 (uptime_score * 0.4 + diversity_score * 0.3 + durability_score * 0.3)
434 }
435
436 /// Calculate hourly earnings for a volunteer
437 pub fn calculate_hourly_earnings(&self, volunteer_id: &str) -> Result<u64> {
438 let daily_earnings = self.calculate_real_time_earnings(volunteer_id)?;
439 Ok(daily_earnings / 24) // Hourly rate
440 }
441
442 /// Calculate earnings projection
443 pub fn calculate_earnings_projection(&self, volunteer_id: &str) -> Result<EarningsProjection> {
444 let daily_earnings = self.calculate_real_time_earnings(volunteer_id)?;
445
446 // Get performance trend
447 let performance_trend = self.calculate_performance_trend(volunteer_id)?;
448
449 // Network demand factor (based on utilization)
450 let demand_factor = (self.network_metrics.utilization_rate / 100.0).min(1.2); // Cap at 120%
451
452 // Seasonal adjustment (placeholder - would use historical data)
453 let seasonal_factor = 1.0;
454
455 // Growth factor based on network expansion
456 let growth_factor = if self.network_metrics.active_volunteers < 100 {
457 1.2 // Early network bonus
458 } else {
459 1.0
460 };
461
462 let factors = ProjectionFactors {
463 current_performance: performance_trend,
464 network_demand: demand_factor,
465 seasonal_adjustment: seasonal_factor,
466 growth_factor,
467 };
468
469 let adjusted_daily = (daily_earnings as f64 * performance_trend * demand_factor * growth_factor) as u64;
470
471 Ok(EarningsProjection {
472 daily_estimate: adjusted_daily,
473 weekly_estimate: adjusted_daily * 7,
474 monthly_estimate: adjusted_daily * 30,
475 annual_estimate: adjusted_daily * 365,
476 factors,
477 })
478 }
479
480 /// Calculate performance trend for projections
481 fn calculate_performance_trend(&self, volunteer_id: &str) -> Result<f64> {
482 let history = self.performance_history.get(volunteer_id)
483 .ok_or_else(|| anyhow::anyhow!("No performance history found"))?;
484
485 if history.len() < 2 {
486 return Ok(1.0); // No trend data
487 }
488
489 // Calculate trend over last 7 days
490 let recent_records: Vec<_> = history.iter().rev().take(168).collect(); // Last 7 days (hourly)
491
492 if recent_records.len() < 24 {
493 return Ok(1.0);
494 }
495
496 let recent_performance = recent_records.iter().take(24)
497 .map(|r| r.success_rate * (r.transfer_speed_mbps / 50.0).min(2.0))
498 .sum::<f64>() / 24.0;
499
500 let older_performance = recent_records.iter().skip(24).take(24)
501 .map(|r| r.success_rate * (r.transfer_speed_mbps / 50.0).min(2.0))
502 .sum::<f64>() / 24.0;
503
504 if older_performance > 0.0 {
505 Ok((recent_performance / older_performance).max(0.5).min(2.0)) // Cap trend between 0.5x and 2x
506 } else {
507 Ok(1.0)
508 }
509 }
510
511 /// Update daily earnings summary
512 pub fn update_daily_earnings(&mut self, volunteer_id: &str) -> Result<()> {
513 let metrics = self.volunteer_metrics.get(volunteer_id)
514 .ok_or_else(|| anyhow::anyhow!("Volunteer metrics not found"))?;
515
516 let today = Utc::now().date_naive();
517 let key = format!("{}_{}", volunteer_id, today.format("%Y-%m-%d"));
518
519 let base_earnings = self.calculate_base_storage_earnings(metrics);
520 let uptime_bonus = self.calculate_uptime_bonus(metrics);
521 let performance_bonus = self.calculate_performance_bonus(metrics);
522 let geographic_bonus = self.calculate_geographic_bonus(metrics);
523 let longevity_bonus = self.calculate_longevity_bonus(metrics);
524 let network_bonus = self.calculate_network_health_bonus(base_earnings);
525
526 let daily_earnings = DailyEarnings {
527 date: Utc::now(),
528 base_earnings,
529 uptime_bonus,
530 performance_bonus,
531 geographic_bonus,
532 longevity_bonus,
533 network_bonus,
534 total_earnings: base_earnings + uptime_bonus + performance_bonus
535 + geographic_bonus + longevity_bonus + network_bonus,
536 storage_gb_hours: metrics.used_storage_gb * 24, // GB-hours for the day
537 actual_uptime_hours: metrics.uptime_hours_24h,
538 };
539
540 self.daily_earnings.insert(key, daily_earnings);
541
542 Ok(())
543 }
544
545 /// Get earnings breakdown for display
546 pub fn get_earnings_breakdown(&self, volunteer_id: &str) -> Result<HashMap<String, u64>> {
547 let metrics = self.volunteer_metrics.get(volunteer_id)
548 .ok_or_else(|| anyhow::anyhow!("Volunteer metrics not found"))?;
549
550 let mut breakdown = HashMap::new();
551 breakdown.insert("base_storage".to_string(), self.calculate_base_storage_earnings(metrics));
552 breakdown.insert("uptime_bonus".to_string(), self.calculate_uptime_bonus(metrics));
553 breakdown.insert("performance_bonus".to_string(), self.calculate_performance_bonus(metrics));
554 breakdown.insert("geographic_bonus".to_string(), self.calculate_geographic_bonus(metrics));
555 breakdown.insert("longevity_bonus".to_string(), self.calculate_longevity_bonus(metrics));
556 breakdown.insert("network_bonus".to_string(), self.calculate_network_health_bonus(self.calculate_base_storage_earnings(metrics)));
557
558 Ok(breakdown)
559 }
560
561 /// Update network metrics
562 pub fn update_network_metrics(&mut self, metrics: NetworkHealthMetrics) {
563 self.network_metrics = metrics;
564 }
565 }
566
567 #[cfg(test)]
568 mod tests {
569 use super::*;
570
571 #[test]
572 fn test_earnings_calculation() {
573 let mut calculator = EarningsCalculator::new();
574
575 let metrics = VolunteerMetrics {
576 volunteer_id: "test_volunteer".to_string(),
577 total_storage_gb: 100,
578 available_storage_gb: 50,
579 used_storage_gb: 50,
580 uptime_hours_24h: 24.0,
581 uptime_percentage: 100.0,
582 response_time_ms: 50,
583 transfer_speed_mbps: 100.0,
584 successful_transfers: 100,
585 failed_transfers: 0,
586 geographic_region: GeographicRegion::Europe,
587 connection_quality: ConnectionQuality::Excellent,
588 reliability_score: 1.0,
589 last_seen: Utc::now(),
590 joined_at: Utc::now() - Duration::days(365),
591 };
592
593 calculator.update_volunteer_metrics(metrics);
594
595 let earnings = calculator.calculate_real_time_earnings("test_volunteer").unwrap();
596 assert!(earnings > 0);
597
598 let breakdown = calculator.get_earnings_breakdown("test_volunteer").unwrap();
599 assert!(breakdown.contains_key("base_storage"));
600 assert!(breakdown.contains_key("uptime_bonus"));
601 }
602
603 #[test]
604 fn test_performance_multipliers() {
605 let calculator = EarningsCalculator::new();
606
607 let high_performance_metrics = VolunteerMetrics {
608 volunteer_id: "high_perf".to_string(),
609 total_storage_gb: 100,
610 available_storage_gb: 0,
611 used_storage_gb: 100,
612 uptime_hours_24h: 24.0,
613 uptime_percentage: 99.9,
614 response_time_ms: 25,
615 transfer_speed_mbps: 150.0,
616 successful_transfers: 1000,
617 failed_transfers: 1,
618 geographic_region: GeographicRegion::Rare,
619 connection_quality: ConnectionQuality::Excellent,
620 reliability_score: 1.0,
621 last_seen: Utc::now(),
622 joined_at: Utc::now() - Duration::days(730),
623 };
624
625 let high_perf_bonus = calculator.calculate_performance_bonus(&high_performance_metrics);
626
627 let low_performance_metrics = VolunteerMetrics {
628 volunteer_id: "low_perf".to_string(),
629 total_storage_gb: 100,
630 available_storage_gb: 80,
631 used_storage_gb: 20,
632 uptime_hours_24h: 20.0,
633 uptime_percentage: 83.3,
634 response_time_ms: 200,
635 transfer_speed_mbps: 5.0,
636 successful_transfers: 80,
637 failed_transfers: 20,
638 geographic_region: GeographicRegion::NorthAmerica,
639 connection_quality: ConnectionQuality::Fair,
640 reliability_score: 0.8,
641 last_seen: Utc::now(),
642 joined_at: Utc::now() - Duration::days(30),
643 };
644
645 let low_perf_bonus = calculator.calculate_performance_bonus(&low_performance_metrics);
646
647 assert!(high_perf_bonus > low_perf_bonus);
648 }
649 }