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    /// The canonical content both operators sign over: the immutable core
157    /// of the transfer, excluding the signatures themselves and the lifecycle
158    /// timestamps set *after* signing (`completed_at`/`rejected_at`/`cancelled_at`).
159    ///
160    /// Both `from_operator` and `to_operator` sign a JWS over the JCS
161    /// canonicalisation of this value, so the two signatures bind the same
162    /// immutable handover terms. Tampering any bound field invalidates both.
163    #[must_use]
164    pub fn signing_payload(&self) -> serde_json::Value {
165        serde_json::json!({
166            "transferId": self.transfer_id,
167            "passportId": self.passport_id,
168            "fromOperator": self.from_operator,
169            "toOperator": self.to_operator,
170            "reason": self.reason,
171            "initiatedAt": self.initiated_at,
172        })
173    }
174
175    /// Determine the current status of this transfer.
176    ///
177    /// Terminal states (`Rejected`, `Cancelled`) take priority over signatures,
178    /// so a cancelled transfer that already had the from_signature still reports
179    /// `Cancelled` rather than `Initiated`.
180    pub fn status(&self) -> TransferStatus {
181        if self.rejected_at.is_some() {
182            return TransferStatus::Rejected;
183        }
184        if self.cancelled_at.is_some() {
185            return TransferStatus::Cancelled;
186        }
187        match (&self.from_signature, &self.to_signature, &self.completed_at) {
188            (Some(_), Some(_), Some(_)) => TransferStatus::Completed,
189            (Some(_), Some(_), None) => TransferStatus::Accepted,
190            _ => TransferStatus::Initiated,
191        }
192    }
193
194    /// Returns `true` if both parties have signed and the transfer is finalised.
195    pub fn is_complete(&self) -> bool {
196        self.from_signature.is_some() && self.to_signature.is_some() && self.completed_at.is_some()
197    }
198
199    /// The incoming operator explicitly rejects the transfer.
200    ///
201    /// Only valid from `Initiated` state. After rejection the record is terminal;
202    /// a new transfer may be initiated on the chain.
203    pub fn reject(&mut self) -> Result<(), TransferError> {
204        let s = self.status();
205        if s != TransferStatus::Initiated {
206            return Err(TransferError::InvalidState {
207                current: s,
208                action: "reject".into(),
209            });
210        }
211        self.rejected_at = Some(Utc::now());
212        Ok(())
213    }
214
215    /// The outgoing operator cancels the transfer before it completes.
216    ///
217    /// Valid from `Initiated` or `Accepted` state. After cancellation the
218    /// record is terminal; a new transfer may be initiated on the chain.
219    pub fn cancel(&mut self) -> Result<(), TransferError> {
220        match self.status() {
221            TransferStatus::Initiated | TransferStatus::Accepted => {
222                self.cancelled_at = Some(Utc::now());
223                Ok(())
224            }
225            s => Err(TransferError::InvalidState {
226                current: s,
227                action: "cancel".into(),
228            }),
229        }
230    }
231
232    /// Mark the transfer as completed once both parties have signed.
233    ///
234    /// Only valid from `Accepted` state (both signatures present, no
235    /// `completed_at` yet). This is the final step before the incoming
236    /// operator becomes the current responsible operator in the chain.
237    pub fn complete(&mut self) -> Result<(), TransferError> {
238        let s = self.status();
239        if s != TransferStatus::Accepted {
240            return Err(TransferError::InvalidState {
241                current: s,
242                action: "complete".into(),
243            });
244        }
245        self.completed_at = Some(Utc::now());
246        Ok(())
247    }
248}
249
250// ─── Transfer chain ──────────────────────────────────────────────────────
251
252/// The complete history of responsibility transfers for a DPP.
253///
254/// Maintained as an append-only log. Once a transfer is completed,
255/// it cannot be modified or removed.
256#[derive(Debug, Clone, Serialize, Deserialize)]
257#[serde(rename_all = "camelCase")]
258pub struct TransferChain {
259    /// The passport this chain belongs to.
260    pub passport_id: PassportId,
261    /// The original responsible operator (at first publication).
262    pub original_operator: ResponsibleOperator,
263    /// Ordered list of transfer events (oldest first).
264    pub transfers: Vec<TransferRecord>,
265}
266
267impl TransferChain {
268    /// Create a new chain with an initial operator and no transfers.
269    #[must_use]
270    pub fn new(passport_id: PassportId, original_operator: ResponsibleOperator) -> Self {
271        Self {
272            passport_id,
273            original_operator,
274            transfers: Vec::new(),
275        }
276    }
277
278    /// Returns the current responsible operator.
279    ///
280    /// If no completed transfers exist, returns the original operator.
281    /// Otherwise, returns the `to_operator` of the most recent completed transfer.
282    pub fn current_operator(&self) -> &ResponsibleOperator {
283        self.transfers
284            .iter()
285            .rev()
286            .find(|t| t.is_complete())
287            .map(|t| &t.to_operator)
288            .unwrap_or(&self.original_operator)
289    }
290
291    /// Returns the total number of completed transfers.
292    pub fn transfer_count(&self) -> usize {
293        self.transfers.iter().filter(|t| t.is_complete()).count()
294    }
295
296    /// Append a new transfer record to the chain.
297    ///
298    /// Validates that:
299    /// - The `from_operator` matches the current responsible operator.
300    /// - No other transfer is currently pending (initiated but not completed/cancelled).
301    pub fn initiate_transfer(&mut self, record: TransferRecord) -> Result<(), TransferError> {
302        // Check that from_operator matches current
303        let current = self.current_operator();
304        if record.from_operator.did != current.did {
305            return Err(TransferError::OperatorMismatch {
306                expected: current.did.clone(),
307                got: record.from_operator.did.clone(),
308            });
309        }
310
311        // Check no pending transfer exists
312        let has_pending = self.transfers.iter().any(|t| {
313            t.status() == TransferStatus::Initiated || t.status() == TransferStatus::Accepted
314        });
315        if has_pending {
316            return Err(TransferError::TransferAlreadyPending);
317        }
318
319        self.transfers.push(record);
320        Ok(())
321    }
322}
323
324// ─── Errors ──────────────────────────────────────────────────────────────
325
326/// Errors specific to transfer-of-responsibility operations.
327#[derive(Debug, Clone, PartialEq)]
328#[non_exhaustive]
329pub enum TransferError {
330    /// The `from_operator` on the transfer record doesn't match the
331    /// current responsible operator on the chain.
332    OperatorMismatch { expected: String, got: String },
333    /// A transfer is already pending for this passport.
334    TransferAlreadyPending,
335    /// The transfer record is not in a state that allows this operation.
336    InvalidState {
337        current: TransferStatus,
338        action: String,
339    },
340}
341
342impl std::fmt::Display for TransferError {
343    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
344        match self {
345            TransferError::OperatorMismatch { expected, got } => {
346                write!(f, "operator mismatch: expected {expected}, got {got}")
347            }
348            TransferError::TransferAlreadyPending => {
349                write!(f, "a transfer is already pending for this passport")
350            }
351            TransferError::InvalidState { current, action } => {
352                write!(f, "cannot {action}: transfer is in {current:?} state")
353            }
354        }
355    }
356}
357
358impl std::error::Error for TransferError {}