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}