Skip to main content

dpp_calc/kernel/
clock.rs

1//! The two dates a compliance computation needs.
2
3use chrono::{DateTime, NaiveDate, Utc};
4use serde::{Deserialize, Serialize};
5
6/// The two dates a compliance computation needs. They are never the same field.
7///
8/// EU staged obligations attach at a regulated triggering event — for
9/// Regulation (EU) 2023/1542 Articles 7, 8 and 10 that is placing on the market
10/// or putting into service. Which law governs a given product is therefore fixed
11/// at that moment, and does not change when the product is reassessed years
12/// later. A battery lawfully placed on the market in 2030 does not acquire the
13/// 18 August 2031 minimums by being audited in 2033.
14///
15/// There is deliberately **no `AssessmentClock::now()`**. A wall-clock
16/// constructor is precisely the mistake this type exists to prevent: it would
17/// let every determination silently re-derive the governing law from whichever
18/// day the calculation happened to run.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20pub struct AssessmentClock {
21    /// Which law applies. Derived from the regulated triggering event — never
22    /// from today's date.
23    pub law_in_force_on: NaiveDate,
24    /// When this computation ran. Receipt provenance only; this never selects a
25    /// ruleset.
26    pub computed_at: DateTime<Utc>,
27}
28
29impl AssessmentClock {
30    /// Build a clock for a product whose governing law was fixed on
31    /// `law_in_force_on`, computed now.
32    ///
33    /// The wall clock supplies `computed_at` and nothing else.
34    /// `law_in_force_on` must come from the product's own record — the
35    /// passport's placing-on-market date — and never from `Utc::now()`.
36    #[must_use]
37    pub fn placed_on(law_in_force_on: NaiveDate) -> Self {
38        Self {
39            law_in_force_on,
40            computed_at: Utc::now(),
41        }
42    }
43
44    /// Fully explicit constructor, pinning both dates.
45    ///
46    /// Re-verifying a stored receipt needs this: reproducing a historical
47    /// calculation means reproducing the computation timestamp too, not just
48    /// the governing law.
49    #[must_use]
50    pub const fn new(law_in_force_on: NaiveDate, computed_at: DateTime<Utc>) -> Self {
51        Self {
52            law_in_force_on,
53            computed_at,
54        }
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn placed_on_takes_the_law_date_from_the_caller_not_the_clock() {
64        let placed = NaiveDate::from_ymd_opt(2030, 6, 1).unwrap();
65        let clock = AssessmentClock::placed_on(placed);
66        assert_eq!(clock.law_in_force_on, placed);
67        // The wall clock only ever supplies `computed_at`.
68        assert!(clock.computed_at > DateTime::UNIX_EPOCH);
69        assert_ne!(clock.law_in_force_on, clock.computed_at.date_naive());
70    }
71
72    #[test]
73    fn new_pins_both_dates_for_receipt_replay() {
74        let law = NaiveDate::from_ymd_opt(2031, 8, 18).unwrap();
75        let computed = DateTime::parse_from_rfc3339("2033-04-01T12:00:00Z")
76            .unwrap()
77            .with_timezone(&Utc);
78        let clock = AssessmentClock::new(law, computed);
79        assert_eq!(clock.law_in_force_on, law);
80        assert_eq!(clock.computed_at, computed);
81    }
82}