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