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 {}