Skip to main content

dpp_domain/instrument/
binding.rs

1//! [`InstrumentBinding`] — one act's reach into one product group.
2
3use serde::{Deserialize, Serialize};
4
5use super::obligation::PassportObligation;
6use crate::catalog::granularity::Granularity;
7use crate::catalog::retention::RetentionBasis;
8use crate::catalog::status::RegulatoryStatus;
9
10/// One instrument's reach into one product group, and the terms on which it
11/// reaches it.
12///
13/// # Why the law lives here and not on the product group
14///
15/// A product group's `status`, `legalBasis`, passport date and retention period
16/// used to be scalars on its own record, which asserts that exactly one act
17/// governs it. ESPR Art. 5(7) says otherwise: a group-specific delegated act may
18/// *supplement* a horizontal one, and no precedence rule exists anywhere in the
19/// Regulation, so overlapping acts accumulate. Every one of those scalars is
20/// therefore a property of an *(instrument, product group)* pair.
21///
22/// Concretely: a group is routinely in force under one instrument and
23/// provisional under another, and a single boolean must then either assert
24/// against an unadopted act or stay silent about an adopted one.
25///
26/// # Why bindings are held instrument-side
27///
28/// A binding is stored in the manifest of the **instrument**, listing the groups
29/// it reaches — not in each group's manifest listing its instruments. Two
30/// reasons, the second decisive:
31///
32/// 1. A horizontal act's coverage is a property of that act and moves as one
33///    unit. Group-side, widening one act's scope means editing every group's
34///    manifest.
35/// 2. A horizontal act can reach a product group that has **no manifest of its
36///    own** — the Commission's preparatory analysis says so explicitly, naming
37///    light means of transport as a set covered by a horizontal requirement
38///    while never being shortlisted as a product group. Group-side, that case
39///    cannot be written down at all.
40///
41/// So [`product_group`](Self::product_group) is a free-form key and **is not
42/// required to exist** in the product-group catalog. Resolving it is the
43/// caller's business; the record of what an act reaches must not be lost merely
44/// because we hold no schema for the thing reached.
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46#[serde(rename_all = "camelCase")]
47pub struct InstrumentBinding {
48    /// Product-group key this instrument reaches. Not validated against the
49    /// product-group catalog — see the type docs.
50    pub product_group: String,
51    /// Whether this instrument's obligations bind this group **now**, gating
52    /// binding determinations made under this instrument.
53    pub status: RegulatoryStatus,
54    /// The specific provisions this binding rests on, e.g.
55    /// `["Regulation (EU) 2023/1542 Art. 77(1)"]`. Article-level, because the
56    /// binding is article-level: the act as a whole is
57    /// [`Instrument::celex`](super::Instrument::celex).
58    #[serde(default)]
59    pub legal_basis: Vec<String>,
60    /// Passport obligation scoped to this group, overriding the instrument's
61    /// own where an act treats its groups differently. `None` inherits.
62    #[serde(default, skip_serializing_if = "Option::is_none")]
63    pub passport: Option<PassportObligation>,
64    /// Retention period this instrument imposes on this group, in years.
65    /// `None` inherits the instrument's.
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub retention_years: Option<u32>,
68    /// Whether [`Self::retention_years`] traces to an adopted text.
69    #[serde(default, skip_serializing_if = "Option::is_none")]
70    pub retention_years_basis: Option<RetentionBasis>,
71    /// The level this instrument fixes for this group. `None` inherits the
72    /// instrument's, which is itself `None` where no act has fixed one.
73    #[serde(default, skip_serializing_if = "Option::is_none")]
74    pub granularity: Option<Granularity>,
75    /// Scope, caveats, and the citation behind any date or figure above.
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub notes: Option<String>,
78}
79
80impl InstrumentBinding {
81    /// Whether a **binding compliance determination** may be made under this
82    /// instrument for this product group.
83    ///
84    /// This is the same question [`RegulatoryStatus::allows_determination`] has
85    /// always answered, moved to the altitude where it is answerable. It is
86    /// **not** the question of whether a passport is required — see
87    /// [`Self::requires_passport`]. Keeping them apart matters in both
88    /// directions:
89    ///
90    /// - ESPR's unsold-goods duties bind today and are determinable, yet impose
91    ///   no passport; and
92    /// - an adjacent act's ecodesign duties bind today while its passport is
93    ///   discharged through another system entirely.
94    ///
95    /// Reading one as the other in the second case is how a passport obligation
96    /// that does not exist became assertable.
97    #[must_use]
98    pub fn allows_determination(&self) -> bool {
99        self.status.allows_determination()
100    }
101
102    /// Whether this instrument requires a passport for this group, resolving the
103    /// binding's override against the instrument's own obligation.
104    #[must_use]
105    pub fn requires_passport(&self, instrument_default: &PassportObligation) -> bool {
106        self.passport
107            .as_ref()
108            .unwrap_or(instrument_default)
109            .is_required()
110    }
111}