use chrono::NaiveDate;
use crate::domain::sector::SectorData;
use crate::ports::compliance::{
ComplianceError, ComplianceErrorKind, ComplianceResult, ComplianceStrategy,
};
#[derive(Debug, Default, Clone, Copy)]
pub struct PassthroughBatteryStrategy;
impl ComplianceStrategy for PassthroughBatteryStrategy {
fn sector_key(&self) -> &str {
"battery"
}
fn compute(
&self,
data: &SectorData,
_law_in_force_on: Option<NaiveDate>,
) -> Result<ComplianceResult, ComplianceError> {
let SectorData::Battery(battery) = data else {
return Err(wrong_sector("battery", data));
};
Ok(ComplianceResult {
co2e_score: Some(battery.co2e_per_unit_kg),
recycled_content_pct: None,
repairability_index: None,
..ComplianceResult::passthrough()
})
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct PassthroughTextileStrategy;
impl ComplianceStrategy for PassthroughTextileStrategy {
fn sector_key(&self) -> &str {
"textile"
}
fn compute(
&self,
data: &SectorData,
_law_in_force_on: Option<NaiveDate>,
) -> Result<ComplianceResult, ComplianceError> {
let SectorData::Textile(textile) = data else {
return Err(wrong_sector("textile", data));
};
Ok(ComplianceResult {
co2e_score: textile.carbon_footprint_kg_co2e,
recycled_content_pct: textile.recycled_content_pct,
repairability_index: textile.repair_score,
..ComplianceResult::passthrough()
})
}
}
fn wrong_sector(expected: &str, got: &SectorData) -> ComplianceError {
ComplianceError {
kind: ComplianceErrorKind::InvalidInput,
message: format!(
"{expected} strategy received {} data",
got.sector().catalog_key()
),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::sector::SectorData;
use crate::ports::compliance::ComplianceStatus;
fn battery() -> SectorData {
SectorData::Battery(Box::new(crate::test_support::sample_battery_data()))
}
fn textile() -> SectorData {
SectorData::Textile(Box::new(crate::domain::sector::TextileData {
carbon_footprint_kg_co2e: Some(8.5),
recycled_content_pct: Some(42.0),
repair_score: Some(7.5),
..crate::test_support::sample_textile_data()
}))
}
#[test]
fn textile_passthrough_lifts_declared_metrics_verbatim() {
let result = PassthroughTextileStrategy
.compute(&textile(), None)
.expect("textile data");
assert_eq!(result.co2e_score, Some(8.5));
assert_eq!(result.recycled_content_pct, Some(42.0));
assert_eq!(result.repairability_index, Some(7.5));
assert_eq!(
result.compliance_status,
ComplianceStatus::PassthroughNoValidation
);
assert!(result.violations.is_empty() && result.warnings.is_empty());
assert!(
result.receipt.is_none() && result.ruleset_version.is_none(),
"no calculation ran, so there is no receipt to show for one"
);
}
#[test]
fn battery_passthrough_leaves_recycled_content_unset() {
let result = PassthroughBatteryStrategy
.compute(&battery(), None)
.expect("battery data");
assert!(
result.recycled_content_pct.is_none(),
"four per-metal percentages must not be collapsed into one"
);
assert!(
result.repairability_index.is_none(),
"no EU repairability index applies to batteries"
);
assert_eq!(
result.compliance_status,
ComplianceStatus::PassthroughNoValidation
);
}
#[test]
fn a_strategy_refuses_another_sectors_data() {
let err = PassthroughBatteryStrategy
.compute(&textile(), None)
.expect_err("battery strategy must refuse textile data");
assert_eq!(err.kind, ComplianceErrorKind::InvalidInput);
assert!(err.message.contains("textile"), "{}", err.message);
}
#[test]
fn sector_keys_match_the_catalog() {
let catalog = crate::SectorCatalog::new();
for key in [
PassthroughBatteryStrategy.sector_key(),
PassthroughTextileStrategy.sector_key(),
] {
assert!(
catalog.get(key).is_some(),
"'{key}' is not a catalog sector key"
);
}
}
}