dpp_calc/ruleset_registry/status.rs
1//! Machine-readable calculator status map.
2//!
3//! The authoritative answer to "which sector–methodology metrics can be computed
4//! today vs. which are awaiting a delegated act?" — consumed by the CLI
5//! `odal calc status` view and by integrations doing compliance-readiness checks.
6//!
7//! **Status is derived, never declared.** Each entry names the ruleset that
8//! implements it; whether that ruleset is in force comes from the ruleset's own
9//! [`Effectivity`]. An earlier version of this file restated "pending" in a
10//! second place, hand-maintained alongside the rulesets themselves — two
11//! sources for one fact, free to drift apart.
12
13use chrono::NaiveDate;
14
15use super::resolve::all_rulesets;
16use crate::ruleset::Effectivity;
17
18/// What implements a sector–methodology pair, if anything.
19#[derive(Debug, Clone, PartialEq)]
20pub enum CalculatorImpl {
21 /// Implemented by the ruleset with this id. Whether it is in force is read
22 /// from that ruleset and never restated here.
23 Ruleset(&'static str),
24 /// The regulation imposes reporting or prohibition duties but mandates no
25 /// quantitative methodology — there is nothing to calculate.
26 ReportingOnly,
27 /// A methodology exists in law but no ruleset is implemented yet. Distinct
28 /// from `ReportingOnly`: this one is work outstanding, not work absent.
29 NotImplemented,
30}
31
32/// Whether a calculator can be used for a product whose law was fixed on a
33/// given date.
34#[derive(Debug, Clone, PartialEq)]
35pub enum CalculatorStatus {
36 /// Implemented and in force on the queried date.
37 Active { ruleset_id: &'static str },
38 /// Implemented, in force, but only from a later date than the one queried.
39 NotYetEffective {
40 ruleset_id: &'static str,
41 from: NaiveDate,
42 },
43 /// Implemented, but the instrument that would date it has not entered into
44 /// force, so it has no application date at all.
45 AwaitingInstrument {
46 ruleset_id: &'static str,
47 empowerment: &'static str,
48 },
49 /// Implemented, but its effective period has ended.
50 Expired {
51 ruleset_id: &'static str,
52 until: NaiveDate,
53 },
54 /// No ruleset is implemented for this methodology.
55 NotImplemented,
56 /// No quantitative methodology is mandated.
57 ReportingOnly,
58 /// The entry names a ruleset id that no longer exists in `all_rulesets()`.
59 /// Surfaced rather than panicked so a status view degrades loudly instead of
60 /// dying; the drift guard in the test module fails on it.
61 UnknownRuleset { ruleset_id: &'static str },
62}
63
64pub struct SectorCalculatorEntry {
65 /// Sector key from the `SectorCatalog` (e.g. `"electronics"`, `"battery"`).
66 pub sector_key: &'static str,
67 /// Product category within the sector (e.g. `"smartphone-tablet"`).
68 pub product_category: &'static str,
69 /// Methodology identifier (e.g. `"repairability-heuristic"`, `"co2e-pef"`).
70 pub methodology: &'static str,
71 /// What implements this pair. Status is derived from it.
72 pub implementation: CalculatorImpl,
73}
74
75impl SectorCalculatorEntry {
76 /// Status for a product whose governing law was fixed on `law_in_force_on`.
77 ///
78 /// Takes the date rather than reading the clock: a readiness view for
79 /// "today" and a readiness view for a product placed on the market in 2030
80 /// are different questions, and the caller knows which it is asking.
81 #[must_use]
82 pub fn status_on(&self, law_in_force_on: NaiveDate) -> CalculatorStatus {
83 let ruleset_id = match self.implementation {
84 CalculatorImpl::ReportingOnly => return CalculatorStatus::ReportingOnly,
85 CalculatorImpl::NotImplemented => return CalculatorStatus::NotImplemented,
86 CalculatorImpl::Ruleset(id) => id,
87 };
88
89 let Some(ruleset) = all_rulesets().iter().find(|r| r.id().0 == ruleset_id) else {
90 return CalculatorStatus::UnknownRuleset { ruleset_id };
91 };
92
93 match ruleset.effectivity() {
94 Effectivity::Pending { empowerment, .. } => CalculatorStatus::AwaitingInstrument {
95 ruleset_id,
96 empowerment,
97 },
98 Effectivity::InForce { from, until } => {
99 if law_in_force_on < *from {
100 CalculatorStatus::NotYetEffective {
101 ruleset_id,
102 from: *from,
103 }
104 } else if let Some(until) = until
105 && law_in_force_on > *until
106 {
107 CalculatorStatus::Expired {
108 ruleset_id,
109 until: *until,
110 }
111 } else {
112 CalculatorStatus::Active { ruleset_id }
113 }
114 }
115 }
116 }
117}
118
119/// Complete map of all sector–methodology–implementation triples known to this
120/// build.
121///
122/// Suitable for CLI status displays, API responses, and automated
123/// compliance-readiness checks. Call [`SectorCalculatorEntry::status_on`] to
124/// resolve an entry against a date.
125pub fn sector_calculator_map() -> &'static [SectorCalculatorEntry] {
126 &[
127 // ── Electronics ──────────────────────────────────────────────────────
128 SectorCalculatorEntry {
129 sector_key: "electronics",
130 product_category: "smartphone-tablet",
131 // Non-regulatory: a simplified repairability heuristic is available,
132 // NOT the enacted EU 2023/1669 Annex IV index, which is not
133 // implemented. Output is a heuristic band, not a class.
134 methodology: "repairability-heuristic",
135 implementation: CalculatorImpl::Ruleset("repairability-heuristic-v1"),
136 },
137 SectorCalculatorEntry {
138 sector_key: "electronics",
139 product_category: "laptop",
140 methodology: "repairability-heuristic",
141 implementation: CalculatorImpl::Ruleset("laptop-repairability"),
142 },
143 SectorCalculatorEntry {
144 sector_key: "electronics",
145 product_category: "displays",
146 methodology: "repairability-heuristic",
147 implementation: CalculatorImpl::Ruleset("displays-repairability"),
148 },
149 SectorCalculatorEntry {
150 sector_key: "electronics",
151 product_category: "washing-machine",
152 methodology: "repairability-heuristic",
153 implementation: CalculatorImpl::Ruleset("washing-machine-repairability"),
154 },
155 // ── Battery ──────────────────────────────────────────────────────────
156 SectorCalculatorEntry {
157 sector_key: "battery",
158 product_category: "all",
159 methodology: "co2e-pef",
160 implementation: CalculatorImpl::Ruleset("co2e-cradle-to-gate"),
161 },
162 SectorCalculatorEntry {
163 sector_key: "battery",
164 product_category: "all",
165 methodology: "co2e-battery-regulation-art7",
166 // No CfbRuleset impl exists — gated on the Art. 7(1) methodology
167 // delegated act and a licensed factor dataset. See co2e::cfb.
168 implementation: CalculatorImpl::NotImplemented,
169 },
170 // ── Unsold goods ─────────────────────────────────────────────────────
171 SectorCalculatorEntry {
172 sector_key: "unsoldGoods",
173 product_category: "all",
174 methodology: "unsold-goods-reporting",
175 // ESPR Art. 25 imposes reporting/prohibition obligations only.
176 implementation: CalculatorImpl::ReportingOnly,
177 },
178 ]
179}