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}