use crate::{order::Order, user::User, user::UserInfo};
use chrono::Utc;
use nostr::types::Timestamp;
use serde::{Deserialize, Serialize};
#[cfg(feature = "sqlx")]
use sqlx::{FromRow, Type};
use std::{fmt::Display, str::FromStr};
use uuid::Uuid;
#[cfg_attr(feature = "sqlx", derive(Type))]
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum Status {
#[default]
Initiated,
InProgress,
SellerRefunded,
Settled,
Released,
CooperativelyCanceled,
}
impl Display for Status {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Status::Initiated => write!(f, "initiated"),
Status::InProgress => write!(f, "in-progress"),
Status::SellerRefunded => write!(f, "seller-refunded"),
Status::Settled => write!(f, "settled"),
Status::Released => write!(f, "released"),
Status::CooperativelyCanceled => write!(f, "cooperatively-canceled"),
}
}
}
impl FromStr for Status {
type Err = ();
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"initiated" => std::result::Result::Ok(Self::Initiated),
"in-progress" => std::result::Result::Ok(Self::InProgress),
"seller-refunded" => std::result::Result::Ok(Self::SellerRefunded),
"settled" => std::result::Result::Ok(Self::Settled),
"released" => std::result::Result::Ok(Self::Released),
"cooperatively-canceled" => std::result::Result::Ok(Self::CooperativelyCanceled),
_ => Err(()),
}
}
}
#[cfg_attr(feature = "sqlx", derive(FromRow))]
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)]
pub struct Dispute {
pub id: Uuid,
pub order_id: Uuid,
pub status: String,
pub order_previous_status: String,
pub solver_pubkey: Option<String>,
pub created_at: i64,
pub taken_at: i64,
}
#[derive(Debug, Default, Deserialize, Serialize, Clone)]
pub struct SolverDisputeInfo {
pub id: Uuid,
pub kind: String,
pub status: String,
pub hash: Option<String>,
pub preimage: Option<String>,
pub order_previous_status: String,
pub initiator_pubkey: String,
pub buyer_pubkey: Option<String>,
pub seller_pubkey: Option<String>,
pub initiator_full_privacy: bool,
pub counterpart_full_privacy: bool,
pub initiator_info: Option<UserInfo>,
pub counterpart_info: Option<UserInfo>,
pub premium: i64,
pub payment_method: String,
pub amount: i64,
pub fiat_amount: i64,
pub fee: i64,
pub routing_fee: i64,
pub buyer_invoice: Option<String>,
pub invoice_held_at: i64,
pub taken_at: i64,
pub created_at: i64,
}
impl SolverDisputeInfo {
pub fn new(
order: &Order,
dispute: &Dispute,
initiator_tradekey: String,
counterpart: Option<User>,
initiator: Option<User>,
) -> Self {
let mut initiator_info = None;
let mut counterpart_info = None;
let mut initiator_full_privacy = true;
let mut counterpart_full_privacy = true;
if let Some(initiator) = initiator {
let now = Timestamp::now();
let initiator_operating_days = (now.as_secs() - initiator.created_at as u64) / 86400;
initiator_info = Some(UserInfo {
rating: initiator.total_rating,
reviews: initiator.total_reviews,
operating_days: initiator_operating_days,
since: Some(crate::user::day_truncate(initiator.created_at)),
});
initiator_full_privacy = false;
}
if let Some(counterpart) = counterpart {
let now = Timestamp::now();
let couterpart_operating_days = (now.as_secs() - counterpart.created_at as u64) / 86400;
counterpart_info = Some(UserInfo {
rating: counterpart.total_rating,
reviews: counterpart.total_reviews,
operating_days: couterpart_operating_days,
since: Some(crate::user::day_truncate(counterpart.created_at)),
});
counterpart_full_privacy = false;
}
Self {
id: order.id,
kind: order.kind.clone(),
status: order.status.clone(),
hash: order.hash.clone(),
preimage: order.preimage.clone(),
order_previous_status: dispute.order_previous_status.clone(),
initiator_pubkey: initiator_tradekey,
buyer_pubkey: order.buyer_pubkey.clone(),
seller_pubkey: order.seller_pubkey.clone(),
initiator_full_privacy,
counterpart_full_privacy,
counterpart_info,
initiator_info,
premium: order.premium,
payment_method: order.payment_method.clone(),
amount: order.amount,
fiat_amount: order.fiat_amount,
fee: order.fee,
routing_fee: order.routing_fee,
buyer_invoice: order.buyer_invoice.clone(),
invoice_held_at: order.invoice_held_at,
taken_at: order.taken_at,
created_at: order.created_at,
}
}
}
impl Dispute {
pub fn new(order_id: Uuid, order_status: String) -> Self {
Self {
id: Uuid::new_v4(),
order_id,
status: Status::Initiated.to_string(),
order_previous_status: order_status,
solver_pubkey: None,
created_at: Utc::now().timestamp(),
taken_at: 0,
}
}
}
#[cfg(test)]
mod tests {
use super::Status;
use std::str::FromStr;
const ALL: [Status; 6] = [
Status::Initiated,
Status::InProgress,
Status::SellerRefunded,
Status::Settled,
Status::Released,
Status::CooperativelyCanceled,
];
#[test]
fn every_status_round_trips_through_display_fromstr_and_serde() {
for status in ALL {
let text = status.to_string();
assert_eq!(Status::from_str(&text), Ok(status.clone()), "{text}");
assert_eq!(
serde_json::to_string(&status).unwrap(),
format!("\"{text}\""),
"serde and Display disagree on {text}"
);
}
}
#[test]
fn cooperatively_canceled_is_kebab_case() {
assert_eq!(
Status::CooperativelyCanceled.to_string(),
"cooperatively-canceled"
);
}
#[test]
fn unknown_status_does_not_parse() {
assert!(Status::from_str("cooperative-cancel").is_err());
}
}