Skip to main content

dpp_domain/catalog/
granularity.rs

1//! [`Granularity`] — the level a passport describes, as fixed by an act.
2
3use serde::{Deserialize, Serialize};
4
5/// The level at which a passport describes a product: a model, a production
6/// batch, or one physical unit.
7///
8/// # Why this lives in the catalog and not in a port
9///
10/// ESPR Art. 9(2)(d) makes the level a **delegated-act decision** — "whether the
11/// digital product passport is to be established at model, batch or item level".
12/// It is therefore a property of the instrument that imposes the passport, and
13/// every consumer downstream is reading that decision rather than making one.
14/// The EU registry is one such consumer, which is why
15/// [`RegistrationGranularity`](crate::ports::registry_sync::RegistrationGranularity)
16/// exists and converts from this type rather than the other way round.
17///
18/// No `Default`, deliberately: an act that has not fixed a level is
19/// [`Option::None`] on the instrument, not a guess at item level. Defaulting
20/// here would silently answer a question only a delegated act can answer.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
22#[serde(rename_all = "lowercase")]
23#[non_exhaustive]
24pub enum Granularity {
25    /// One passport covering every unit sharing a model's specifications.
26    Model,
27    /// One passport covering every unit made in one production run.
28    Batch,
29    /// One passport per physical unit.
30    Item,
31}
32
33impl Granularity {
34    /// The more granular of two levels.
35    ///
36    /// The ordering `Model < Batch < Item` is the derived `Ord`, and matches the
37    /// rule the EU registry applies: where several levels are indicated for one
38    /// product, the most granular wins (IR (EU) 2026/1778 Art. 8(3), and Art.
39    /// 8(4)–(5) linking an item registration back up to batch and model).
40    ///
41    /// This is why applicable instruments fold to a **maximum** rather than
42    /// picking one: two acts naming different levels do not conflict, they
43    /// compound.
44    #[must_use]
45    pub fn most_granular(self, other: Self) -> Self {
46        self.max(other)
47    }
48
49    /// Whether a passport at this level describes a **specification** rather
50    /// than manufactured things.
51    ///
52    /// [`Model`](Self::Model) covers every unit sharing a model's
53    /// specifications, so it describes a type. [`Batch`](Self::Batch) and
54    /// [`Item`](Self::Item) both describe things that were made — one run, or
55    /// one unit.
56    ///
57    /// # Why this is answered here
58    ///
59    /// A projection that has to classify the asset it is describing needs it,
60    /// and this enum is `#[non_exhaustive]`, so a match from another crate must
61    /// carry a wildcard. A wildcard would quietly file a future level as one
62    /// answer or the other — and "not stated" is a distinction this type takes
63    /// care to preserve elsewhere, so it should not be thrown away here.
64    /// Authored beside the variants, the match is exhaustive.
65    #[must_use]
66    pub const fn describes_a_type(self) -> bool {
67        match self {
68            Self::Model => true,
69            Self::Batch | Self::Item => false,
70        }
71    }
72}