1use chrono::NaiveDate;
8use rust_decimal::Decimal;
9use rust_decimal_macros::dec;
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
18#[serde(rename_all = "snake_case")]
19pub enum EmissionScope {
20 #[default]
22 Scope1,
23 Scope2,
25 Scope3,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
31#[serde(rename_all = "snake_case")]
32pub enum Scope3Category {
33 #[default]
34 PurchasedGoods,
35 CapitalGoods,
36 FuelAndEnergy,
37 UpstreamTransport,
38 WasteGenerated,
39 BusinessTravel,
40 EmployeeCommuting,
41 UpstreamLeased,
42 DownstreamTransport,
43 ProcessingOfSoldProducts,
44 UseOfSoldProducts,
45 EndOfLifeTreatment,
46 DownstreamLeased,
47 Franchises,
48 Investments,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
53#[serde(rename_all = "snake_case")]
54pub enum EstimationMethod {
55 #[default]
57 ActivityBased,
58 SpendBased,
60 SupplierSpecific,
62 AverageData,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct EmissionRecord {
69 pub id: String,
70 pub entity_id: String,
71 pub scope: EmissionScope,
72 pub scope3_category: Option<Scope3Category>,
73 pub facility_id: Option<String>,
74 pub period: NaiveDate,
75 pub activity_data: Option<String>,
76 pub activity_unit: Option<String>,
77 #[serde(with = "rust_decimal::serde::str_option")]
78 pub emission_factor: Option<Decimal>,
79 #[serde(with = "rust_decimal::serde::str")]
80 pub co2e_tonnes: Decimal,
81 pub estimation_method: EstimationMethod,
82 pub source: Option<String>,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
91#[serde(rename_all = "snake_case")]
92pub enum EnergySourceType {
93 #[default]
94 Electricity,
95 NaturalGas,
96 Diesel,
97 Coal,
98 SolarPv,
99 WindOnshore,
100 Biomass,
101 Geothermal,
102}
103
104impl EnergySourceType {
105 pub fn is_renewable(&self) -> bool {
107 matches!(
108 self,
109 Self::SolarPv | Self::WindOnshore | Self::Biomass | Self::Geothermal
110 )
111 }
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct EnergyConsumption {
117 pub id: String,
118 pub entity_id: String,
119 pub facility_id: String,
120 pub period: NaiveDate,
121 pub energy_source: EnergySourceType,
122 #[serde(with = "rust_decimal::serde::str")]
123 pub consumption_kwh: Decimal,
124 #[serde(with = "rust_decimal::serde::str")]
125 pub cost: Decimal,
126 pub currency: String,
127 pub is_renewable: bool,
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
136#[serde(rename_all = "snake_case")]
137pub enum WaterSource {
138 #[default]
139 Municipal,
140 Groundwater,
141 SurfaceWater,
142 Rainwater,
143 Recycled,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct WaterUsage {
149 pub id: String,
150 pub entity_id: String,
151 pub facility_id: String,
152 pub period: NaiveDate,
153 pub source: WaterSource,
154 #[serde(with = "rust_decimal::serde::str")]
155 pub withdrawal_m3: Decimal,
156 #[serde(with = "rust_decimal::serde::str")]
157 pub discharge_m3: Decimal,
158 #[serde(with = "rust_decimal::serde::str")]
159 pub consumption_m3: Decimal,
160 pub is_water_stressed_area: bool,
161}
162
163impl WaterUsage {
164 pub fn computed_consumption(&self) -> Decimal {
166 (self.withdrawal_m3 - self.discharge_m3).max(Decimal::ZERO)
167 }
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
176#[serde(rename_all = "snake_case")]
177pub enum WasteType {
178 #[default]
179 General,
180 Hazardous,
181 Electronic,
182 Organic,
183 Construction,
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
188#[serde(rename_all = "snake_case")]
189pub enum DisposalMethod {
190 #[default]
191 Landfill,
192 Recycled,
193 Composted,
194 Incinerated,
195 Reused,
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct WasteRecord {
201 pub id: String,
202 pub entity_id: String,
203 pub facility_id: String,
204 pub period: NaiveDate,
205 pub waste_type: WasteType,
206 pub disposal_method: DisposalMethod,
207 #[serde(with = "rust_decimal::serde::str")]
208 pub quantity_tonnes: Decimal,
209 pub is_diverted_from_landfill: bool,
210}
211
212impl WasteRecord {
213 pub fn computed_diversion(&self) -> bool {
215 !matches!(
216 self.disposal_method,
217 DisposalMethod::Landfill | DisposalMethod::Incinerated
218 )
219 }
220}
221
222#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
228#[serde(rename_all = "snake_case")]
229pub enum DiversityDimension {
230 #[default]
231 Gender,
232 Ethnicity,
233 Age,
234 Disability,
235 VeteranStatus,
236}
237
238#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
240#[serde(rename_all = "snake_case")]
241pub enum OrganizationLevel {
242 #[default]
243 Corporate,
244 Department,
245 Team,
246 Executive,
247 Board,
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct WorkforceDiversityMetric {
253 pub id: String,
254 pub entity_id: String,
255 pub period: NaiveDate,
256 pub dimension: DiversityDimension,
257 pub level: OrganizationLevel,
258 pub category: String,
259 pub headcount: u32,
260 pub total_headcount: u32,
261 #[serde(with = "rust_decimal::serde::str")]
262 pub percentage: Decimal,
263}
264
265impl WorkforceDiversityMetric {
266 pub fn computed_percentage(&self) -> Decimal {
268 if self.total_headcount == 0 {
269 return Decimal::ZERO;
270 }
271 (Decimal::from(self.headcount) / Decimal::from(self.total_headcount)).round_dp(4)
272 }
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize)]
281pub struct PayEquityMetric {
282 pub id: String,
283 pub entity_id: String,
284 pub period: NaiveDate,
285 pub dimension: DiversityDimension,
286 pub reference_group: String,
287 pub comparison_group: String,
288 #[serde(with = "rust_decimal::serde::str")]
289 pub reference_median_salary: Decimal,
290 #[serde(with = "rust_decimal::serde::str")]
291 pub comparison_median_salary: Decimal,
292 #[serde(with = "rust_decimal::serde::str")]
293 pub pay_gap_ratio: Decimal,
294 pub sample_size: u32,
295}
296
297impl PayEquityMetric {
298 pub fn computed_pay_gap_ratio(&self) -> Decimal {
300 if self.reference_median_salary.is_zero() {
301 return dec!(1.00);
302 }
303 (self.comparison_median_salary / self.reference_median_salary).round_dp(4)
304 }
305}
306
307#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
313#[serde(rename_all = "snake_case")]
314pub enum IncidentType {
315 #[default]
316 Injury,
317 Illness,
318 NearMiss,
319 Fatality,
320 PropertyDamage,
321}
322
323#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct SafetyIncident {
326 pub id: String,
327 pub entity_id: String,
328 pub facility_id: String,
329 pub date: NaiveDate,
330 pub incident_type: IncidentType,
331 pub days_away: u32,
332 pub is_recordable: bool,
333 pub description: String,
334}
335
336#[derive(Debug, Clone, Serialize, Deserialize)]
338pub struct SafetyMetric {
339 pub id: String,
340 pub entity_id: String,
341 pub period: NaiveDate,
342 pub total_hours_worked: u64,
343 pub recordable_incidents: u32,
344 pub lost_time_incidents: u32,
345 pub days_away: u32,
346 pub near_misses: u32,
347 pub fatalities: u32,
348 #[serde(with = "rust_decimal::serde::str")]
349 pub trir: Decimal,
350 #[serde(with = "rust_decimal::serde::str")]
351 pub ltir: Decimal,
352 #[serde(with = "rust_decimal::serde::str")]
353 pub dart_rate: Decimal,
354}
355
356impl SafetyMetric {
357 pub fn computed_trir(&self) -> Decimal {
359 if self.total_hours_worked == 0 {
360 return Decimal::ZERO;
361 }
362 (Decimal::from(self.recordable_incidents) * dec!(200000)
363 / Decimal::from(self.total_hours_worked))
364 .round_dp(4)
365 }
366
367 pub fn computed_ltir(&self) -> Decimal {
369 if self.total_hours_worked == 0 {
370 return Decimal::ZERO;
371 }
372 (Decimal::from(self.lost_time_incidents) * dec!(200000)
373 / Decimal::from(self.total_hours_worked))
374 .round_dp(4)
375 }
376
377 pub fn computed_dart_rate(&self) -> Decimal {
379 if self.total_hours_worked == 0 {
380 return Decimal::ZERO;
381 }
382 (Decimal::from(self.days_away) * dec!(200000) / Decimal::from(self.total_hours_worked))
383 .round_dp(4)
384 }
385}
386
387#[derive(Debug, Clone, Serialize, Deserialize)]
393pub struct GovernanceMetric {
394 pub id: String,
395 pub entity_id: String,
396 pub period: NaiveDate,
397 pub board_size: u32,
398 pub independent_directors: u32,
399 pub female_directors: u32,
400 #[serde(with = "rust_decimal::serde::str")]
401 pub board_independence_ratio: Decimal,
402 #[serde(with = "rust_decimal::serde::str")]
403 pub board_gender_diversity_ratio: Decimal,
404 pub ethics_training_completion_pct: f64,
405 pub whistleblower_reports: u32,
406 pub anti_corruption_violations: u32,
407}
408
409impl GovernanceMetric {
410 pub fn computed_independence_ratio(&self) -> Decimal {
412 if self.board_size == 0 {
413 return Decimal::ZERO;
414 }
415 (Decimal::from(self.independent_directors) / Decimal::from(self.board_size)).round_dp(4)
416 }
417}
418
419#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
425#[serde(rename_all = "snake_case")]
426pub enum EsgRiskFlag {
427 #[default]
428 Low,
429 Medium,
430 High,
431 Critical,
432}
433
434#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
436#[serde(rename_all = "snake_case")]
437pub enum AssessmentMethod {
438 #[default]
439 SelfAssessment,
440 ThirdPartyAudit,
441 OnSiteAssessment,
442 DocumentReview,
443}
444
445#[derive(Debug, Clone, Serialize, Deserialize)]
447pub struct SupplierEsgAssessment {
448 pub id: String,
449 pub entity_id: String,
450 pub vendor_id: String,
451 pub assessment_date: NaiveDate,
452 pub method: AssessmentMethod,
453 #[serde(with = "rust_decimal::serde::str")]
454 pub environmental_score: Decimal,
455 #[serde(with = "rust_decimal::serde::str")]
456 pub social_score: Decimal,
457 #[serde(with = "rust_decimal::serde::str")]
458 pub governance_score: Decimal,
459 #[serde(with = "rust_decimal::serde::str")]
460 pub overall_score: Decimal,
461 pub risk_flag: EsgRiskFlag,
462 pub corrective_actions_required: u32,
463}
464
465impl SupplierEsgAssessment {
466 pub fn computed_overall_score(&self) -> Decimal {
468 ((self.environmental_score + self.social_score + self.governance_score) / dec!(3))
469 .round_dp(2)
470 }
471}
472
473#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
479#[serde(rename_all = "snake_case")]
480pub enum EsgFramework {
481 #[default]
483 Gri,
484 Esrs,
486 Sasb,
488 Tcfd,
490 Issb,
492}
493
494#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
496#[serde(rename_all = "snake_case")]
497pub enum AssuranceLevel {
498 #[default]
500 None,
501 Limited,
503 Reasonable,
505}
506
507#[derive(Debug, Clone, Serialize, Deserialize)]
509pub struct EsgDisclosure {
510 pub id: String,
511 pub entity_id: String,
512 pub reporting_period_start: NaiveDate,
513 pub reporting_period_end: NaiveDate,
514 pub framework: EsgFramework,
515 pub assurance_level: AssuranceLevel,
516 pub disclosure_topic: String,
517 pub metric_value: String,
518 pub metric_unit: String,
519 pub is_assured: bool,
520}
521
522#[derive(Debug, Clone, Serialize, Deserialize)]
524pub struct MaterialityAssessment {
525 pub id: String,
526 pub entity_id: String,
527 pub period: NaiveDate,
528 pub topic: String,
529 #[serde(with = "rust_decimal::serde::str")]
531 pub impact_score: Decimal,
532 #[serde(with = "rust_decimal::serde::str")]
534 pub financial_score: Decimal,
535 #[serde(with = "rust_decimal::serde::str")]
537 pub combined_score: Decimal,
538 pub is_material: bool,
539}
540
541impl MaterialityAssessment {
542 pub fn is_material_at_threshold(&self, threshold: Decimal) -> bool {
544 self.impact_score >= threshold || self.financial_score >= threshold
545 }
546}
547
548#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
550#[serde(rename_all = "snake_case")]
551pub enum ScenarioType {
552 #[default]
554 WellBelow2C,
555 Orderly,
557 Disorderly,
559 HotHouse,
561}
562
563#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
565#[serde(rename_all = "snake_case")]
566pub enum TimeHorizon {
567 Short,
569 #[default]
571 Medium,
572 Long,
574}
575
576#[derive(Debug, Clone, Serialize, Deserialize)]
578pub struct ClimateScenario {
579 pub id: String,
580 pub entity_id: String,
581 pub scenario_type: ScenarioType,
582 pub time_horizon: TimeHorizon,
583 pub description: String,
584 #[serde(with = "rust_decimal::serde::str")]
585 pub temperature_rise_c: Decimal,
586 #[serde(with = "rust_decimal::serde::str")]
587 pub transition_risk_impact: Decimal,
588 #[serde(with = "rust_decimal::serde::str")]
589 pub physical_risk_impact: Decimal,
590 #[serde(with = "rust_decimal::serde::str")]
591 pub financial_impact: Decimal,
592}
593
594#[cfg(test)]
599#[allow(clippy::unwrap_used)]
600mod tests {
601 use super::*;
602
603 fn d(s: &str) -> NaiveDate {
604 NaiveDate::parse_from_str(s, "%Y-%m-%d").unwrap()
605 }
606
607 #[test]
610 fn test_emission_record_serde_roundtrip() {
611 let record = EmissionRecord {
612 id: "EM-001".to_string(),
613 entity_id: "C001".to_string(),
614 scope: EmissionScope::Scope1,
615 scope3_category: None,
616 facility_id: Some("F-001".to_string()),
617 period: d("2025-01-01"),
618 activity_data: Some("100000 kWh".to_string()),
619 activity_unit: Some("kWh".to_string()),
620 emission_factor: Some(dec!(0.18)),
621 co2e_tonnes: dec!(18),
622 estimation_method: EstimationMethod::ActivityBased,
623 source: Some("Natural gas combustion".to_string()),
624 };
625
626 let json = serde_json::to_string(&record).unwrap();
627 let deserialized: EmissionRecord = serde_json::from_str(&json).unwrap();
628 assert_eq!(deserialized.co2e_tonnes, dec!(18));
629 assert_eq!(deserialized.scope, EmissionScope::Scope1);
630 }
631
632 #[test]
633 fn test_emission_factor_calculation() {
634 let consumption_kwh = dec!(100000);
636 let factor = dec!(0.18); let co2e_kg = consumption_kwh * factor;
638 let co2e_tonnes = co2e_kg / dec!(1000);
639 assert_eq!(co2e_tonnes, dec!(18));
640 }
641
642 #[test]
645 fn test_energy_source_renewable() {
646 assert!(EnergySourceType::SolarPv.is_renewable());
647 assert!(EnergySourceType::WindOnshore.is_renewable());
648 assert!(!EnergySourceType::NaturalGas.is_renewable());
649 assert!(!EnergySourceType::Electricity.is_renewable());
650 }
651
652 #[test]
655 fn test_water_consumption_formula() {
656 let usage = WaterUsage {
657 id: "W-001".to_string(),
658 entity_id: "C001".to_string(),
659 facility_id: "F-001".to_string(),
660 period: d("2025-01-01"),
661 source: WaterSource::Municipal,
662 withdrawal_m3: dec!(5000),
663 discharge_m3: dec!(3500),
664 consumption_m3: dec!(1500),
665 is_water_stressed_area: false,
666 };
667
668 assert_eq!(usage.computed_consumption(), dec!(1500));
669 }
670
671 #[test]
674 fn test_waste_diversion() {
675 let recycled = WasteRecord {
676 id: "WS-001".to_string(),
677 entity_id: "C001".to_string(),
678 facility_id: "F-001".to_string(),
679 period: d("2025-01-01"),
680 waste_type: WasteType::General,
681 disposal_method: DisposalMethod::Recycled,
682 quantity_tonnes: dec!(100),
683 is_diverted_from_landfill: true,
684 };
685 assert!(recycled.computed_diversion());
686
687 let landfill = WasteRecord {
688 disposal_method: DisposalMethod::Landfill,
689 ..recycled.clone()
690 };
691 assert!(!landfill.computed_diversion());
692 }
693
694 #[test]
697 fn test_trir_formula() {
698 let metric = SafetyMetric {
699 id: "SM-001".to_string(),
700 entity_id: "C001".to_string(),
701 period: d("2025-01-01"),
702 total_hours_worked: 1_000_000,
703 recordable_incidents: 5,
704 lost_time_incidents: 2,
705 days_away: 30,
706 near_misses: 15,
707 fatalities: 0,
708 trir: dec!(1.0000),
709 ltir: dec!(0.4000),
710 dart_rate: dec!(6.0000),
711 };
712
713 assert_eq!(metric.computed_trir(), dec!(1.0000));
715 assert_eq!(metric.computed_ltir(), dec!(0.4000));
717 assert_eq!(metric.computed_dart_rate(), dec!(6.0000));
719 }
720
721 #[test]
722 fn test_trir_zero_hours() {
723 let metric = SafetyMetric {
724 id: "SM-002".to_string(),
725 entity_id: "C001".to_string(),
726 period: d("2025-01-01"),
727 total_hours_worked: 0,
728 recordable_incidents: 0,
729 lost_time_incidents: 0,
730 days_away: 0,
731 near_misses: 0,
732 fatalities: 0,
733 trir: Decimal::ZERO,
734 ltir: Decimal::ZERO,
735 dart_rate: Decimal::ZERO,
736 };
737 assert_eq!(metric.computed_trir(), Decimal::ZERO);
738 }
739
740 #[test]
743 fn test_diversity_percentage() {
744 let metric = WorkforceDiversityMetric {
745 id: "WD-001".to_string(),
746 entity_id: "C001".to_string(),
747 period: d("2025-01-01"),
748 dimension: DiversityDimension::Gender,
749 level: OrganizationLevel::Corporate,
750 category: "Female".to_string(),
751 headcount: 450,
752 total_headcount: 1000,
753 percentage: dec!(0.4500),
754 };
755
756 assert_eq!(metric.computed_percentage(), dec!(0.4500));
757 }
758
759 #[test]
762 fn test_pay_gap_ratio() {
763 let metric = PayEquityMetric {
764 id: "PE-001".to_string(),
765 entity_id: "C001".to_string(),
766 period: d("2025-01-01"),
767 dimension: DiversityDimension::Gender,
768 reference_group: "Male".to_string(),
769 comparison_group: "Female".to_string(),
770 reference_median_salary: dec!(85000),
771 comparison_median_salary: dec!(78000),
772 pay_gap_ratio: dec!(0.9176),
773 sample_size: 500,
774 };
775
776 assert_eq!(metric.computed_pay_gap_ratio(), dec!(0.9176));
778 }
779
780 #[test]
783 fn test_board_independence() {
784 let metric = GovernanceMetric {
785 id: "GOV-001".to_string(),
786 entity_id: "C001".to_string(),
787 period: d("2025-01-01"),
788 board_size: 12,
789 independent_directors: 8,
790 female_directors: 4,
791 board_independence_ratio: dec!(0.6667),
792 board_gender_diversity_ratio: dec!(0.3333),
793 ethics_training_completion_pct: 0.95,
794 whistleblower_reports: 3,
795 anti_corruption_violations: 0,
796 };
797
798 assert_eq!(metric.computed_independence_ratio(), dec!(0.6667));
799 }
800
801 #[test]
804 fn test_supplier_esg_overall_score() {
805 let assessment = SupplierEsgAssessment {
806 id: "SEA-001".to_string(),
807 entity_id: "C001".to_string(),
808 vendor_id: "V-001".to_string(),
809 assessment_date: d("2025-06-15"),
810 method: AssessmentMethod::ThirdPartyAudit,
811 environmental_score: dec!(75),
812 social_score: dec!(80),
813 governance_score: dec!(85),
814 overall_score: dec!(80),
815 risk_flag: EsgRiskFlag::Low,
816 corrective_actions_required: 0,
817 };
818
819 assert_eq!(assessment.computed_overall_score(), dec!(80.00));
820 }
821
822 #[test]
825 fn test_materiality_double_threshold() {
826 let assessment = MaterialityAssessment {
827 id: "MA-001".to_string(),
828 entity_id: "C001".to_string(),
829 period: d("2025-01-01"),
830 topic: "Climate Change".to_string(),
831 impact_score: dec!(8.5),
832 financial_score: dec!(6.0),
833 combined_score: dec!(7.25),
834 is_material: true,
835 };
836
837 assert!(assessment.is_material_at_threshold(dec!(7.0)));
839 assert!(!assessment.is_material_at_threshold(dec!(9.0)));
841 }
842
843 #[test]
846 fn test_safety_metric_serde_roundtrip() {
847 let metric = SafetyMetric {
848 id: "SM-100".to_string(),
849 entity_id: "C001".to_string(),
850 period: d("2025-01-01"),
851 total_hours_worked: 500_000,
852 recordable_incidents: 3,
853 lost_time_incidents: 1,
854 days_away: 10,
855 near_misses: 8,
856 fatalities: 0,
857 trir: dec!(1.2000),
858 ltir: dec!(0.4000),
859 dart_rate: dec!(4.0000),
860 };
861
862 let json = serde_json::to_string(&metric).unwrap();
863 let deserialized: SafetyMetric = serde_json::from_str(&json).unwrap();
864 assert_eq!(deserialized.trir, dec!(1.2000));
865 assert_eq!(deserialized.recordable_incidents, 3);
866 }
867
868 #[test]
869 fn test_climate_scenario_serde() {
870 let scenario = ClimateScenario {
871 id: "CS-001".to_string(),
872 entity_id: "C001".to_string(),
873 scenario_type: ScenarioType::WellBelow2C,
874 time_horizon: TimeHorizon::Long,
875 description: "Paris-aligned scenario".to_string(),
876 temperature_rise_c: dec!(1.5),
877 transition_risk_impact: dec!(-50000),
878 physical_risk_impact: dec!(-10000),
879 financial_impact: dec!(-60000),
880 };
881
882 let json = serde_json::to_string(&scenario).unwrap();
883 let deserialized: ClimateScenario = serde_json::from_str(&json).unwrap();
884 assert_eq!(deserialized.scenario_type, ScenarioType::WellBelow2C);
885 assert_eq!(deserialized.temperature_rise_c, dec!(1.5));
886 }
887}