use crate::error::EvalResult;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EsgThresholds {
pub min_metric_accuracy: f64,
pub min_safety_rate_accuracy: f64,
pub metric_tolerance: f64,
}
impl Default for EsgThresholds {
fn default() -> Self {
Self {
min_metric_accuracy: 0.99,
min_safety_rate_accuracy: 0.999,
metric_tolerance: 0.01,
}
}
}
#[derive(Debug, Clone)]
pub struct WaterUsageData {
pub record_id: String,
pub withdrawal_m3: f64,
pub discharge_m3: f64,
pub consumption_m3: f64,
}
#[derive(Debug, Clone)]
pub struct SafetyMetricData {
pub metric_id: String,
pub total_hours_worked: f64,
pub recordable_incidents: u32,
pub trir: f64,
pub lost_time_incidents: u32,
pub ltir: f64,
}
#[derive(Debug, Clone)]
pub struct GovernanceData {
pub metric_id: String,
pub board_size: u32,
pub independent_directors: u32,
pub independence_ratio: f64,
}
#[derive(Debug, Clone)]
pub struct SupplierEsgData {
pub assessment_id: String,
pub environmental_score: f64,
pub social_score: f64,
pub governance_score: f64,
pub overall_score: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EsgEvaluation {
pub water_accuracy: f64,
pub trir_accuracy: f64,
pub ltir_accuracy: f64,
pub governance_accuracy: f64,
pub supplier_scoring_accuracy: f64,
pub total_water_records: usize,
pub total_safety_records: usize,
pub total_governance_records: usize,
pub total_supplier_assessments: usize,
pub passes: bool,
pub issues: Vec<String>,
}
pub struct EsgEvaluator {
thresholds: EsgThresholds,
}
impl EsgEvaluator {
pub fn new() -> Self {
Self {
thresholds: EsgThresholds::default(),
}
}
pub fn with_thresholds(thresholds: EsgThresholds) -> Self {
Self { thresholds }
}
pub fn evaluate(
&self,
water: &[WaterUsageData],
safety: &[SafetyMetricData],
governance: &[GovernanceData],
suppliers: &[SupplierEsgData],
) -> EvalResult<EsgEvaluation> {
let mut issues = Vec::new();
let tolerance = self.thresholds.metric_tolerance;
let water_ok = water
.iter()
.filter(|w| {
let expected = w.withdrawal_m3 - w.discharge_m3;
(w.consumption_m3 - expected).abs() <= tolerance * w.withdrawal_m3.abs().max(1.0)
})
.count();
let water_accuracy = if water.is_empty() {
1.0
} else {
water_ok as f64 / water.len() as f64
};
let trir_ok = safety
.iter()
.filter(|s| {
if s.total_hours_worked <= 0.0 {
return true;
}
let expected = s.recordable_incidents as f64 * 200_000.0 / s.total_hours_worked;
(s.trir - expected).abs() <= tolerance * expected.abs().max(0.001)
})
.count();
let trir_accuracy = if safety.is_empty() {
1.0
} else {
trir_ok as f64 / safety.len() as f64
};
let ltir_ok = safety
.iter()
.filter(|s| {
if s.total_hours_worked <= 0.0 {
return true;
}
let expected = s.lost_time_incidents as f64 * 200_000.0 / s.total_hours_worked;
(s.ltir - expected).abs() <= tolerance * expected.abs().max(0.001)
})
.count();
let ltir_accuracy = if safety.is_empty() {
1.0
} else {
ltir_ok as f64 / safety.len() as f64
};
let gov_ok = governance
.iter()
.filter(|g| {
if g.board_size == 0 {
return true;
}
let expected = g.independent_directors as f64 / g.board_size as f64;
(g.independence_ratio - expected).abs() <= tolerance
})
.count();
let governance_accuracy = if governance.is_empty() {
1.0
} else {
gov_ok as f64 / governance.len() as f64
};
let supplier_ok = suppliers
.iter()
.filter(|s| {
let expected = (s.environmental_score + s.social_score + s.governance_score) / 3.0;
(s.overall_score - expected).abs() <= tolerance * expected.abs().max(1.0)
})
.count();
let supplier_scoring_accuracy = if suppliers.is_empty() {
1.0
} else {
supplier_ok as f64 / suppliers.len() as f64
};
if water_accuracy < self.thresholds.min_metric_accuracy {
issues.push(format!(
"Water consumption accuracy {:.4} < {:.4}",
water_accuracy, self.thresholds.min_metric_accuracy
));
}
if trir_accuracy < self.thresholds.min_safety_rate_accuracy {
issues.push(format!(
"TRIR accuracy {:.4} < {:.4}",
trir_accuracy, self.thresholds.min_safety_rate_accuracy
));
}
if ltir_accuracy < self.thresholds.min_safety_rate_accuracy {
issues.push(format!(
"LTIR accuracy {:.4} < {:.4}",
ltir_accuracy, self.thresholds.min_safety_rate_accuracy
));
}
if governance_accuracy < self.thresholds.min_metric_accuracy {
issues.push(format!(
"Governance ratio accuracy {:.4} < {:.4}",
governance_accuracy, self.thresholds.min_metric_accuracy
));
}
if supplier_scoring_accuracy < self.thresholds.min_metric_accuracy {
issues.push(format!(
"Supplier ESG scoring accuracy {:.4} < {:.4}",
supplier_scoring_accuracy, self.thresholds.min_metric_accuracy
));
}
let passes = issues.is_empty();
Ok(EsgEvaluation {
water_accuracy,
trir_accuracy,
ltir_accuracy,
governance_accuracy,
supplier_scoring_accuracy,
total_water_records: water.len(),
total_safety_records: safety.len(),
total_governance_records: governance.len(),
total_supplier_assessments: suppliers.len(),
passes,
issues,
})
}
}
impl Default for EsgEvaluator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_valid_esg_data() {
let evaluator = EsgEvaluator::new();
let water = vec![WaterUsageData {
record_id: "W001".to_string(),
withdrawal_m3: 1000.0,
discharge_m3: 700.0,
consumption_m3: 300.0,
}];
let safety = vec![SafetyMetricData {
metric_id: "S001".to_string(),
total_hours_worked: 1_000_000.0,
recordable_incidents: 5,
trir: 1.0, lost_time_incidents: 2,
ltir: 0.4, }];
let governance = vec![GovernanceData {
metric_id: "G001".to_string(),
board_size: 10,
independent_directors: 7,
independence_ratio: 0.7,
}];
let suppliers = vec![SupplierEsgData {
assessment_id: "ESG001".to_string(),
environmental_score: 80.0,
social_score: 70.0,
governance_score: 90.0,
overall_score: 80.0,
}];
let result = evaluator
.evaluate(&water, &safety, &governance, &suppliers)
.unwrap();
assert!(result.passes);
assert_eq!(result.total_water_records, 1);
assert_eq!(result.total_safety_records, 1);
}
#[test]
fn test_wrong_water_consumption() {
let evaluator = EsgEvaluator::new();
let water = vec![WaterUsageData {
record_id: "W001".to_string(),
withdrawal_m3: 1000.0,
discharge_m3: 700.0,
consumption_m3: 500.0, }];
let result = evaluator.evaluate(&water, &[], &[], &[]).unwrap();
assert!(!result.passes);
assert!(result.issues[0].contains("Water consumption"));
}
#[test]
fn test_wrong_trir() {
let evaluator = EsgEvaluator::new();
let safety = vec![SafetyMetricData {
metric_id: "S001".to_string(),
total_hours_worked: 1_000_000.0,
recordable_incidents: 5,
trir: 5.0, lost_time_incidents: 2,
ltir: 0.4,
}];
let result = evaluator.evaluate(&[], &safety, &[], &[]).unwrap();
assert!(!result.passes);
assert!(result.issues.iter().any(|i| i.contains("TRIR")));
}
#[test]
fn test_wrong_supplier_scoring() {
let evaluator = EsgEvaluator::new();
let suppliers = vec![SupplierEsgData {
assessment_id: "ESG001".to_string(),
environmental_score: 80.0,
social_score: 70.0,
governance_score: 90.0,
overall_score: 90.0, }];
let result = evaluator.evaluate(&[], &[], &[], &suppliers).unwrap();
assert!(!result.passes);
assert!(result.issues[0].contains("Supplier ESG"));
}
#[test]
fn test_wrong_ltir() {
let evaluator = EsgEvaluator::new();
let safety = vec![SafetyMetricData {
metric_id: "S001".to_string(),
total_hours_worked: 1_000_000.0,
recordable_incidents: 5,
trir: 1.0, lost_time_incidents: 2,
ltir: 2.0, }];
let result = evaluator.evaluate(&[], &safety, &[], &[]).unwrap();
assert!(!result.passes);
assert!(result.issues.iter().any(|i| i.contains("LTIR")));
}
#[test]
fn test_wrong_governance_ratio() {
let evaluator = EsgEvaluator::new();
let governance = vec![GovernanceData {
metric_id: "G001".to_string(),
board_size: 10,
independent_directors: 7,
independence_ratio: 0.5, }];
let result = evaluator.evaluate(&[], &[], &governance, &[]).unwrap();
assert!(!result.passes);
assert!(result.issues.iter().any(|i| i.contains("Governance ratio")));
}
#[test]
fn test_empty_data() {
let evaluator = EsgEvaluator::new();
let result = evaluator.evaluate(&[], &[], &[], &[]).unwrap();
assert!(result.passes);
}
}