use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::passport::PassportId;
#[cfg(test)]
mod tests;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResponsibleOperator {
pub did: String,
pub name: String,
pub role: OperatorRole,
pub eu_operator_id: Option<String>,
pub country: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub enum OperatorRole {
Manufacturer,
Importer,
Distributor,
AuthorisedRepresentative,
Remanufacturer,
Repurposer,
PreparerForReuse,
Repairer,
Recycler,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub enum TransferReason {
Sale,
Return,
Remanufacturing,
Repurposing,
PreparationForReuse,
Import,
InsolvencySuccession,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TransferRecord {
pub transfer_id: Uuid,
pub passport_id: PassportId,
pub from_operator: ResponsibleOperator,
pub to_operator: ResponsibleOperator,
pub reason: TransferReason,
pub from_signature: Option<String>,
pub to_signature: Option<String>,
pub initiated_at: DateTime<Utc>,
pub completed_at: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rejected_at: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cancelled_at: Option<DateTime<Utc>>,
pub notes: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub enum TransferStatus {
Initiated,
Accepted,
Rejected,
Cancelled,
Completed,
}
impl TransferRecord {
#[must_use]
pub fn signing_payload(&self) -> serde_json::Value {
serde_json::json!({
"transferId": self.transfer_id,
"passportId": self.passport_id,
"fromOperator": self.from_operator,
"toOperator": self.to_operator,
"reason": self.reason,
"initiatedAt": self.initiated_at,
})
}
pub fn status(&self) -> TransferStatus {
if self.rejected_at.is_some() {
return TransferStatus::Rejected;
}
if self.cancelled_at.is_some() {
return TransferStatus::Cancelled;
}
match (&self.from_signature, &self.to_signature, &self.completed_at) {
(Some(_), Some(_), Some(_)) => TransferStatus::Completed,
(Some(_), Some(_), None) => TransferStatus::Accepted,
_ => TransferStatus::Initiated,
}
}
pub fn is_complete(&self) -> bool {
self.from_signature.is_some() && self.to_signature.is_some() && self.completed_at.is_some()
}
pub fn reject(&mut self) -> Result<(), TransferError> {
let s = self.status();
if s != TransferStatus::Initiated {
return Err(TransferError::InvalidState {
current: s,
action: "reject".into(),
});
}
self.rejected_at = Some(Utc::now());
Ok(())
}
pub fn cancel(&mut self) -> Result<(), TransferError> {
match self.status() {
TransferStatus::Initiated | TransferStatus::Accepted => {
self.cancelled_at = Some(Utc::now());
Ok(())
}
s => Err(TransferError::InvalidState {
current: s,
action: "cancel".into(),
}),
}
}
pub fn complete(&mut self) -> Result<(), TransferError> {
let s = self.status();
if s != TransferStatus::Accepted {
return Err(TransferError::InvalidState {
current: s,
action: "complete".into(),
});
}
self.completed_at = Some(Utc::now());
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TransferChain {
pub passport_id: PassportId,
pub original_operator: ResponsibleOperator,
pub transfers: Vec<TransferRecord>,
}
impl TransferChain {
#[must_use]
pub fn new(passport_id: PassportId, original_operator: ResponsibleOperator) -> Self {
Self {
passport_id,
original_operator,
transfers: Vec::new(),
}
}
pub fn current_operator(&self) -> &ResponsibleOperator {
self.transfers
.iter()
.rev()
.find(|t| t.is_complete())
.map(|t| &t.to_operator)
.unwrap_or(&self.original_operator)
}
pub fn transfer_count(&self) -> usize {
self.transfers.iter().filter(|t| t.is_complete()).count()
}
pub fn initiate_transfer(&mut self, record: TransferRecord) -> Result<(), TransferError> {
let current = self.current_operator();
if record.from_operator.did != current.did {
return Err(TransferError::OperatorMismatch {
expected: current.did.clone(),
got: record.from_operator.did.clone(),
});
}
let has_pending = self.transfers.iter().any(|t| {
t.status() == TransferStatus::Initiated || t.status() == TransferStatus::Accepted
});
if has_pending {
return Err(TransferError::TransferAlreadyPending);
}
self.transfers.push(record);
Ok(())
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum TransferError {
OperatorMismatch { expected: String, got: String },
TransferAlreadyPending,
InvalidState {
current: TransferStatus,
action: String,
},
}
impl std::fmt::Display for TransferError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TransferError::OperatorMismatch { expected, got } => {
write!(f, "operator mismatch: expected {expected}, got {got}")
}
TransferError::TransferAlreadyPending => {
write!(f, "a transfer is already pending for this passport")
}
TransferError::InvalidState { current, action } => {
write!(f, "cannot {action}: transfer is in {current:?} state")
}
}
}
}
impl std::error::Error for TransferError {}