Skip to main content

dpp_domain/domain/transfer/
mod.rs

1//! Transfer of Responsibility model for EU ESPR DPP.
2//!
3//! When a product undergoes preparation for reuse, repurposing, or
4//! remanufacturing, the new economic operator assumes complete responsibility
5//! for providing up-to-date DPP information. The infrastructure must track
6//! these transfers with full provenance.
7//!
8//! This module provides:
9//! - `ResponsibleOperator` — identifies the current economic operator
10//! - `TransferRecord` — a single ownership transfer event
11//! - `TransferChain` — the complete history of responsibility transfers
12//! - State machine validation for transfer flows
13
14use chrono::{DateTime, Utc};
15use serde::{Deserialize, Serialize};
16use uuid::Uuid;
17
18use super::passport::PassportId;
19
20#[cfg(test)]
21mod tests;
22
23// ─── Operator identity ───────────────────────────────────────────────────
24
25/// Identifies an economic operator responsible for a DPP.
26///
27/// Under ESPR, the "responsible economic operator" is whoever places or
28/// makes the product available on the EU market. This can be the original
29/// manufacturer, an importer, a distributor, or a remanufacturer.
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31#[serde(rename_all = "camelCase")]
32pub struct ResponsibleOperator {
33    /// The operator's DID (e.g. `did:web:acme.example.com`).
34    pub did: String,
35    /// Human-readable name of the economic operator.
36    pub name: String,
37    /// The operator's role in the supply chain.
38    pub role: OperatorRole,
39    /// EU-assigned economic operator identifier, if available.
40    pub eu_operator_id: Option<String>,
41    /// ISO 3166-1 alpha-2 country code of the operator's establishment.
42    pub country: String,
43}
44
45/// The role of an economic operator in the DPP supply chain.
46///
47/// Determines what DPP fields the operator may introduce or update,
48/// as specified by the applicable delegated act.
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(rename_all = "camelCase")]
51#[non_exhaustive]
52pub enum OperatorRole {
53    /// Original equipment manufacturer.
54    Manufacturer,
55    /// Imports the product into the EU market.
56    Importer,
57    /// Makes the product available on the market without altering it.
58    Distributor,
59    /// An EU-established entity authorised to act on behalf of a
60    /// non-EU manufacturer.
61    AuthorisedRepresentative,
62    /// Performs remanufacturing — restores the product to original
63    /// or improved specifications.
64    Remanufacturer,
65    /// Adapts the product for a different purpose than originally intended.
66    Repurposer,
67    /// Prepares a used product for resale (testing, cleaning, repair).
68    PreparerForReuse,
69    /// Professional repairer with authorised DPP update rights.
70    Repairer,
71    /// Processes end-of-life products for material recovery.
72    Recycler,
73}
74
75// ─── Transfer record ─────────────────────────────────────────────────────
76
77/// The reason for a transfer of DPP responsibility.
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79#[serde(rename_all = "camelCase")]
80#[non_exhaustive]
81pub enum TransferReason {
82    /// Product sold to a new economic operator for market placement.
83    Sale,
84    /// Product returned to the supply chain (e.g. customer return).
85    Return,
86    /// Product sent for remanufacturing.
87    Remanufacturing,
88    /// Product adapted for a different purpose.
89    Repurposing,
90    /// Product prepared for resale as second-hand.
91    PreparationForReuse,
92    /// Product imported into the EU by a new importer.
93    Import,
94    /// Original operator became insolvent; responsibilities assumed by successor.
95    InsolvencySuccession,
96}
97
98/// A single transfer-of-responsibility event in the DPP lifecycle.
99///
100/// Each transfer is cryptographically signed by both the outgoing and
101/// incoming operators to create an auditable provenance chain.
102#[derive(Debug, Clone, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct TransferRecord {
105    /// Unique identifier for this transfer event.
106    pub transfer_id: Uuid,
107    /// The passport being transferred.
108    pub passport_id: PassportId,
109    /// The outgoing (previous) responsible operator.
110    pub from_operator: ResponsibleOperator,
111    /// The incoming (new) responsible operator.
112    pub to_operator: ResponsibleOperator,
113    /// The reason for this transfer.
114    pub reason: TransferReason,
115    /// Compact JWS signature from the outgoing operator, signing over the
116    /// transfer payload to authorise the handover.
117    pub from_signature: Option<String>,
118    /// Compact JWS signature from the incoming operator, accepting
119    /// responsibility for the DPP.
120    pub to_signature: Option<String>,
121    /// Timestamp when the transfer was initiated.
122    pub initiated_at: DateTime<Utc>,
123    /// Timestamp when the transfer was completed (both parties signed).
124    /// `None` if the transfer is still pending acceptance.
125    pub completed_at: Option<DateTime<Utc>>,
126    /// Timestamp when the incoming operator explicitly rejected the transfer.
127    /// Set by [`TransferRecord::reject`]; makes the record terminal.
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    pub rejected_at: Option<DateTime<Utc>>,
130    /// Timestamp when the outgoing operator cancelled the transfer.
131    /// Set by [`TransferRecord::cancel`]; makes the record terminal.
132    #[serde(default, skip_serializing_if = "Option::is_none")]
133    pub cancelled_at: Option<DateTime<Utc>>,
134    /// Free-text notes (e.g. conditions, regulatory references).
135    pub notes: Option<String>,
136}
137
138/// Status of a transfer-of-responsibility flow.
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140#[serde(rename_all = "camelCase")]
141#[non_exhaustive]
142pub enum TransferStatus {
143    /// Transfer initiated by the outgoing operator, awaiting acceptance.
144    Initiated,
145    /// Incoming operator has accepted responsibility.
146    Accepted,
147    /// Transfer rejected by the incoming operator.
148    Rejected,
149    /// Transfer cancelled by the outgoing operator before acceptance.
150    Cancelled,
151    /// Transfer completed — DPP now under the new operator's control.
152    Completed,
153}
154
155impl TransferRecord {
156    /// Determine the current status of this transfer.
157    ///
158    /// Terminal states (`Rejected`, `Cancelled`) take priority over signatures,
159    /// so a cancelled transfer that already had the from_signature still reports
160    /// `Cancelled` rather than `Initiated`.
161    pub fn status(&self) -> TransferStatus {
162        if self.rejected_at.is_some() {
163            return TransferStatus::Rejected;
164        }
165        if self.cancelled_at.is_some() {
166            return TransferStatus::Cancelled;
167        }
168        match (&self.from_signature, &self.to_signature, &self.completed_at) {
169            (Some(_), Some(_), Some(_)) => TransferStatus::Completed,
170            (Some(_), Some(_), None) => TransferStatus::Accepted,
171            _ => TransferStatus::Initiated,
172        }
173    }
174
175    /// Returns `true` if both parties have signed and the transfer is finalised.
176    pub fn is_complete(&self) -> bool {
177        self.from_signature.is_some() && self.to_signature.is_some() && self.completed_at.is_some()
178    }
179
180    /// The incoming operator explicitly rejects the transfer.
181    ///
182    /// Only valid from `Initiated` state. After rejection the record is terminal;
183    /// a new transfer may be initiated on the chain.
184    pub fn reject(&mut self) -> Result<(), TransferError> {
185        let s = self.status();
186        if s != TransferStatus::Initiated {
187            return Err(TransferError::InvalidState {
188                current: s,
189                action: "reject".into(),
190            });
191        }
192        self.rejected_at = Some(Utc::now());
193        Ok(())
194    }
195
196    /// The outgoing operator cancels the transfer before it completes.
197    ///
198    /// Valid from `Initiated` or `Accepted` state. After cancellation the
199    /// record is terminal; a new transfer may be initiated on the chain.
200    pub fn cancel(&mut self) -> Result<(), TransferError> {
201        match self.status() {
202            TransferStatus::Initiated | TransferStatus::Accepted => {
203                self.cancelled_at = Some(Utc::now());
204                Ok(())
205            }
206            s => Err(TransferError::InvalidState {
207                current: s,
208                action: "cancel".into(),
209            }),
210        }
211    }
212
213    /// Mark the transfer as completed once both parties have signed.
214    ///
215    /// Only valid from `Accepted` state (both signatures present, no
216    /// `completed_at` yet). This is the final step before the incoming
217    /// operator becomes the current responsible operator in the chain.
218    pub fn complete(&mut self) -> Result<(), TransferError> {
219        let s = self.status();
220        if s != TransferStatus::Accepted {
221            return Err(TransferError::InvalidState {
222                current: s,
223                action: "complete".into(),
224            });
225        }
226        self.completed_at = Some(Utc::now());
227        Ok(())
228    }
229}
230
231// ─── Transfer chain ──────────────────────────────────────────────────────
232
233/// The complete history of responsibility transfers for a DPP.
234///
235/// Maintained as an append-only log. Once a transfer is completed,
236/// it cannot be modified or removed.
237#[derive(Debug, Clone, Serialize, Deserialize)]
238#[serde(rename_all = "camelCase")]
239pub struct TransferChain {
240    /// The passport this chain belongs to.
241    pub passport_id: PassportId,
242    /// The original responsible operator (at first publication).
243    pub original_operator: ResponsibleOperator,
244    /// Ordered list of transfer events (oldest first).
245    pub transfers: Vec<TransferRecord>,
246}
247
248impl TransferChain {
249    /// Create a new chain with an initial operator and no transfers.
250    #[must_use]
251    pub fn new(passport_id: PassportId, original_operator: ResponsibleOperator) -> Self {
252        Self {
253            passport_id,
254            original_operator,
255            transfers: Vec::new(),
256        }
257    }
258
259    /// Returns the current responsible operator.
260    ///
261    /// If no completed transfers exist, returns the original operator.
262    /// Otherwise, returns the `to_operator` of the most recent completed transfer.
263    pub fn current_operator(&self) -> &ResponsibleOperator {
264        self.transfers
265            .iter()
266            .rev()
267            .find(|t| t.is_complete())
268            .map(|t| &t.to_operator)
269            .unwrap_or(&self.original_operator)
270    }
271
272    /// Returns the total number of completed transfers.
273    pub fn transfer_count(&self) -> usize {
274        self.transfers.iter().filter(|t| t.is_complete()).count()
275    }
276
277    /// Append a new transfer record to the chain.
278    ///
279    /// Validates that:
280    /// - The `from_operator` matches the current responsible operator.
281    /// - No other transfer is currently pending (initiated but not completed/cancelled).
282    pub fn initiate_transfer(&mut self, record: TransferRecord) -> Result<(), TransferError> {
283        // Check that from_operator matches current
284        let current = self.current_operator();
285        if record.from_operator.did != current.did {
286            return Err(TransferError::OperatorMismatch {
287                expected: current.did.clone(),
288                got: record.from_operator.did.clone(),
289            });
290        }
291
292        // Check no pending transfer exists
293        let has_pending = self.transfers.iter().any(|t| {
294            t.status() == TransferStatus::Initiated || t.status() == TransferStatus::Accepted
295        });
296        if has_pending {
297            return Err(TransferError::TransferAlreadyPending);
298        }
299
300        self.transfers.push(record);
301        Ok(())
302    }
303}
304
305// ─── Errors ──────────────────────────────────────────────────────────────
306
307/// Errors specific to transfer-of-responsibility operations.
308#[derive(Debug, Clone, PartialEq)]
309#[non_exhaustive]
310pub enum TransferError {
311    /// The `from_operator` on the transfer record doesn't match the
312    /// current responsible operator on the chain.
313    OperatorMismatch { expected: String, got: String },
314    /// A transfer is already pending for this passport.
315    TransferAlreadyPending,
316    /// The transfer record is not in a state that allows this operation.
317    InvalidState {
318        current: TransferStatus,
319        action: String,
320    },
321}
322
323impl std::fmt::Display for TransferError {
324    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
325        match self {
326            TransferError::OperatorMismatch { expected, got } => {
327                write!(f, "operator mismatch: expected {expected}, got {got}")
328            }
329            TransferError::TransferAlreadyPending => {
330                write!(f, "a transfer is already pending for this passport")
331            }
332            TransferError::InvalidState { current, action } => {
333                write!(f, "cannot {action}: transfer is in {current:?} state")
334            }
335        }
336    }
337}
338
339impl std::error::Error for TransferError {}