Skip to main content

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}