dpp_domain/lint/
finding.rs1use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7
8use crate::product_group::{self, DisclosureScope, ProductGroupData};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
15pub enum LintSeverity {
16 Warning,
17 Notice,
18}
19
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23#[serde(rename_all = "camelCase")]
24pub struct LintFinding {
25 pub code: String,
26 pub field: String,
27 pub severity: LintSeverity,
28 pub message: String,
29}
30
31fn convert(f: dpp_rules::lint::LintFinding) -> LintFinding {
32 LintFinding {
33 code: f.code.to_owned(),
34 field: f.field.to_owned(),
35 severity: match f.severity {
36 dpp_rules::lint::LintSeverity::Warning => LintSeverity::Warning,
37 dpp_rules::lint::LintSeverity::Notice => LintSeverity::Notice,
38 },
39 message: f.message,
40 }
41}
42
43#[must_use]
46pub fn lint_product_group_data(data: &ProductGroupData, as_of: DateTime<Utc>) -> Vec<LintFinding> {
47 let mut findings = product_group_lints(data, as_of);
48 findings.extend(svhc_lints(data));
49 findings
50}
51
52fn svhc_lints(data: &ProductGroupData) -> Vec<LintFinding> {
64 let Some(substances) = data.svhc_substances() else {
65 return Vec::new();
66 };
67 let inputs: Vec<dpp_rules::SvhcInput<'_>> = substances
68 .iter()
69 .map(|s| dpp_rules::SvhcInput {
70 cas_number: &s.cas_number,
71 substance_name: &s.substance_name,
72 concentration_pct: s.concentration_pct,
73 })
74 .collect();
75 dpp_rules::lint::svhc::lint_svhc_declarations(&inputs, &dpp_rules::CandidateList::embedded())
76 .into_iter()
77 .map(convert)
78 .collect()
79}
80
81fn product_group_lints(data: &ProductGroupData, as_of: DateTime<Utc>) -> Vec<LintFinding> {
85 match data {
86 ProductGroupData::Battery(b) => {
87 let cathode: Vec<f64> = b
88 .cathode_material
89 .as_deref()
90 .unwrap_or(&[])
91 .iter()
92 .map(|m| m.weight_pct)
93 .collect();
94 let anode: Vec<f64> = b
95 .anode_material
96 .as_deref()
97 .unwrap_or(&[])
98 .iter()
99 .map(|m| m.weight_pct)
100 .collect();
101 let electrolyte: Vec<f64> = b
102 .electrolyte_material
103 .as_deref()
104 .unwrap_or(&[])
105 .iter()
106 .map(|m| m.weight_pct)
107 .collect();
108 let input = dpp_rules::lint::battery::BatteryLintInput {
109 nominal_voltage_v: b.nominal_voltage_v,
110 nominal_capacity_ah: b.nominal_capacity_ah,
111 rated_energy_wh: b.rated_energy_wh,
112 rated_capacity_kwh: b.rated_capacity_kwh,
113 operating_temp_min_c: b.operating_temp_min_c,
114 operating_temp_max_c: b.operating_temp_max_c,
115 manufacturing_date_unix: b.manufacturing_date.map(|d| d.timestamp()),
116 as_of_unix: as_of.timestamp(),
117 cathode_material_pct: &cathode,
118 anode_material_pct: &anode,
119 electrolyte_material_pct: &electrolyte,
120 };
121 dpp_rules::lint::battery::lint_battery(&input)
122 .into_iter()
123 .map(convert)
124 .collect()
125 }
126 ProductGroupData::Textile(t) => {
127 let fibres: Vec<&str> = t
128 .fibre_composition
129 .iter()
130 .map(|f| f.fibre.as_str())
131 .collect();
132 let input = dpp_rules::lint::textile::TextileLintInput {
133 durability_score: t.durability_score,
134 expected_wash_cycles: t.expected_wash_cycles,
135 repair_count: t.repair_count,
136 repair_history_url: t.repair_history_url.as_deref(),
137 prior_use_cycles: t.prior_use_cycles,
138 reuse_condition: t.reuse_condition.as_deref(),
139 repair_score: t.repair_score,
140 disassembly_instructions: t.disassembly_instructions.as_deref(),
141 spare_parts_available: t.spare_parts_available,
142 microplastic_shedding_mg_per_wash: t.microplastic_shedding_mg_per_wash,
143 fibres: &fibres,
144 };
145 dpp_rules::lint::textile::lint_textile(&input)
146 .into_iter()
147 .map(convert)
148 .collect()
149 }
150 ProductGroupData::UnsoldGoods(u) => {
151 let lines: Vec<dpp_rules::lint::unsold_goods::DisclosureLineInput<'_>> = u
152 .lines
153 .iter()
154 .map(|l| dpp_rules::lint::unsold_goods::DisclosureLineInput {
155 cn_category: l
159 .cn_categories
160 .first()
161 .map_or("", product_group::CnCategory::as_str),
162 reason_point: l.reason.article_2_point(),
163 units: l.units_discarded.value,
164 weight_kg: l.weight_kg.value,
165 preparing_for_reuse_pct: l.treatment.preparing_for_reuse_pct,
166 recycling_pct: l.treatment.recycling_pct,
167 other_recovery_pct: l.treatment.other_recovery_pct,
168 disposal_pct: l.treatment.disposal_pct,
169 unknown_pct: l.treatment.unknown_pct,
170 })
171 .collect();
172 let input = dpp_rules::lint::unsold_goods::UnsoldGoodsLintInput {
173 lines: &lines,
174 consolidated_undertaking_count: match &u.entity.scope {
175 DisclosureScope::Consolidated { undertakings } => Some(undertakings.len()),
176 DisclosureScope::Standalone => None,
177 },
178 measures_taken_len: u.measures_taken.trim().chars().count(),
179 measures_planned_len: u.measures_planned.trim().chars().count(),
180 };
181 dpp_rules::lint::unsold_goods::lint_unsold_goods(&input)
182 .into_iter()
183 .map(convert)
184 .collect()
185 }
186 _ => Vec::new(),
187 }
188}