mostro_core/
dispute.rs

1use crate::{order::Order, user::User, user::UserInfo};
2use chrono::Utc;
3use nostr_sdk::Timestamp;
4use serde::{Deserialize, Serialize};
5#[cfg(feature = "sqlx")]
6use sqlx::{FromRow, Type};
7#[cfg(feature = "sqlx")]
8use sqlx_crud::SqlxCrud;
9use std::{fmt::Display, str::FromStr};
10use uuid::Uuid;
11
12/// Each status that a dispute can have
13#[cfg_attr(feature = "sqlx", derive(Type))]
14#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)]
15#[serde(rename_all = "kebab-case")]
16pub enum Status {
17    /// Dispute initiated and waiting to be taken by a solver
18    #[default]
19    Initiated,
20    /// Taken by a solver
21    InProgress,
22    /// Canceled by admin/solver and refunded to seller
23    SellerRefunded,
24    /// Settled seller's invoice by admin/solver and started to pay sats to buyer
25    Settled,
26    /// Released by the seller
27    Released,
28}
29
30impl Display for Status {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        match self {
33            Status::Initiated => write!(f, "initiated"),
34            Status::InProgress => write!(f, "in-progress"),
35            Status::SellerRefunded => write!(f, "seller-refunded"),
36            Status::Settled => write!(f, "settled"),
37            Status::Released => write!(f, "released"),
38        }
39    }
40}
41
42impl FromStr for Status {
43    type Err = ();
44
45    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
46        match s {
47            "initiated" => std::result::Result::Ok(Self::Initiated),
48            "in-progress" => std::result::Result::Ok(Self::InProgress),
49            "seller-refunded" => std::result::Result::Ok(Self::SellerRefunded),
50            "settled" => std::result::Result::Ok(Self::Settled),
51            "released" => std::result::Result::Ok(Self::Released),
52            _ => Err(()),
53        }
54    }
55}
56
57/// Database representation of a dispute
58#[cfg_attr(feature = "sqlx", derive(FromRow, SqlxCrud), external_id)]
59#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)]
60pub struct Dispute {
61    /// Unique identifier for the dispute
62    pub id: Uuid,
63    /// Order ID that the dispute is related to
64    pub order_id: Uuid,
65    /// Status of the dispute
66    pub status: String,
67    /// Previous status of the order before the dispute was created
68    pub order_previous_status: String,
69    /// Public key of the solver
70    pub solver_pubkey: Option<String>,
71    /// Timestamp when the dispute was created
72    pub created_at: i64,
73    /// Timestamp when the dispute was taken by a solver
74    pub taken_at: i64,
75}
76
77#[derive(Debug, Default, Deserialize, Serialize, Clone)]
78pub struct SolverDisputeInfo {
79    pub id: Uuid,
80    pub kind: String,
81    pub status: String,
82    pub hash: Option<String>,
83    pub preimage: Option<String>,
84    pub order_previous_status: String,
85    pub initiator_pubkey: String,
86    pub buyer_pubkey: Option<String>,
87    pub seller_pubkey: Option<String>,
88    pub initiator_full_privacy: bool,
89    pub counterpart_full_privacy: bool,
90    pub initiator_info: Option<UserInfo>,
91    pub counterpart_info: Option<UserInfo>,
92    pub premium: i64,
93    pub payment_method: String,
94    pub amount: i64,
95    pub fiat_amount: i64,
96    pub fee: i64,
97    pub routing_fee: i64,
98    pub buyer_invoice: Option<String>,
99    pub invoice_held_at: i64,
100    pub taken_at: i64,
101    pub created_at: i64,
102}
103
104impl SolverDisputeInfo {
105    pub fn new(
106        order: &Order,
107        dispute: &Dispute,
108        initiator_tradekey: String,
109        counterpart: Option<User>,
110        initiator: Option<User>,
111    ) -> Self {
112        // Get initiator and counterpart info if not in full privacy mode
113        let mut initiator_info = None;
114        let mut counterpart_info = None;
115        let mut initiator_full_privacy = true;
116        let mut counterpart_full_privacy = true;
117
118        if let Some(initiator) = initiator {
119            let now = Timestamp::now();
120            let initiator_operating_days = (now.as_u64() - initiator.created_at as u64) / 86400;
121            initiator_info = Some(UserInfo {
122                rating: initiator.total_rating,
123                reviews: initiator.total_reviews,
124                operating_days: initiator_operating_days,
125            });
126            initiator_full_privacy = false;
127        }
128        if let Some(counterpart) = counterpart {
129            let now = Timestamp::now();
130            let couterpart_operating_days = (now.as_u64() - counterpart.created_at as u64) / 86400;
131            counterpart_info = Some(UserInfo {
132                rating: counterpart.total_rating,
133                reviews: counterpart.total_reviews,
134                operating_days: couterpart_operating_days,
135            });
136            counterpart_full_privacy = false;
137        }
138
139        Self {
140            id: order.id,
141            kind: order.kind.clone(),
142            status: order.status.clone(),
143            hash: order.hash.clone(),
144            preimage: order.preimage.clone(),
145            order_previous_status: dispute.order_previous_status.clone(),
146            initiator_pubkey: initiator_tradekey,
147            buyer_pubkey: order.buyer_pubkey.clone(),
148            seller_pubkey: order.seller_pubkey.clone(),
149            initiator_full_privacy,
150            counterpart_full_privacy,
151            counterpart_info,
152            initiator_info,
153            premium: order.premium,
154            payment_method: order.payment_method.clone(),
155            amount: order.amount,
156            fiat_amount: order.fiat_amount,
157            fee: order.fee,
158            routing_fee: order.routing_fee,
159            buyer_invoice: order.buyer_invoice.clone(),
160            invoice_held_at: order.invoice_held_at,
161            taken_at: order.taken_at,
162            created_at: order.created_at,
163        }
164    }
165}
166
167impl Dispute {
168    pub fn new(order_id: Uuid, order_status: String) -> Self {
169        Self {
170            id: Uuid::new_v4(),
171            order_id,
172            status: Status::Initiated.to_string(),
173            order_previous_status: order_status,
174            solver_pubkey: None,
175            created_at: Utc::now().timestamp(),
176            taken_at: 0,
177        }
178    }
179}