use serde::{Deserialize, Deserializer, Serialize, Serializer};
macro_rules! extensible_status {
($name:ident { $($variant:ident => $wire:literal),+ $(,)? }) => {
#[doc = concat!("A forward-compatible `", stringify!($name), "` value.")]
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum $name {
$(#[doc = concat!("Wire value `", $wire, "`.")] $variant,)+
Unknown(String),
}
impl $name {
#[must_use]
pub fn as_str(&self) -> &str {
match self {
$(Self::$variant => $wire,)+
Self::Unknown(value) => value,
}
}
}
impl std::fmt::Display for $name {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.as_str())
}
}
impl Serialize for $name {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer {
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for $name {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where D: Deserializer<'de> {
let value = String::deserialize(deserializer)?;
Ok(match value.as_str() {
$($wire => Self::$variant,)+
_ => Self::Unknown(value),
})
}
}
};
}
extensible_status!(InvoiceStatus {
AwaitingClient => "awaiting_client",
AwaitingPayment => "awaiting_payment",
Confirming => "confirming",
UnderpaidWaiting => "underpaid_waiting",
Paid => "paid",
PaidOver => "paid_over",
Underpaid => "underpaid",
Expired => "expired",
Cancelled => "cancelled",
});
extensible_status!(WithdrawalStatus {
Created => "created",
PendingReview => "pending_review",
Signed => "signed",
Cancelling => "cancelling",
Completed => "completed",
Failed => "failed",
Cancelled => "cancelled",
});
#[derive(Clone, Debug, Serialize)]
pub struct CreateInvoiceRequest {
pub project_id: String,
pub amount: String,
pub currency: String,
pub external_order_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub network: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_multiple_payments: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub customer_fee_percent: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_id: Option<String>,
}
#[derive(Clone, Debug, Default)]
pub struct InvoiceListParams {
pub limit: Option<u32>,
pub cursor: Option<String>,
pub status: Option<Vec<InvoiceStatus>>,
pub external_order_id: Option<String>,
pub project_id: Option<String>,
pub created_from: Option<i64>,
pub created_to: Option<i64>,
}
impl InvoiceListParams {
pub(crate) fn query_pairs(&self) -> Result<Vec<(String, String)>, crate::Error> {
let mut values = Vec::new();
push_option(&mut values, "limit", self.limit);
push_string(&mut values, "cursor", self.cursor.as_deref());
if let Some(statuses) = &self.status {
if statuses.is_empty() {
return Err(crate::Error::InvalidArgument(
"invoice status filter cannot be empty".to_owned(),
));
}
values.extend(
statuses
.iter()
.map(|status| ("status".to_owned(), status.as_str().to_owned())),
);
}
push_string(
&mut values,
"external_order_id",
self.external_order_id.as_deref(),
);
push_string(&mut values, "project_id", self.project_id.as_deref());
push_option(&mut values, "created_from", self.created_from);
push_option(&mut values, "created_to", self.created_to);
Ok(values)
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Order {
pub external_id: String,
pub client_id: Option<String>,
pub amount: String,
pub currency: String,
pub network: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Transfer {
pub tx_hash: String,
pub amount: String,
pub status: String,
pub created_at: i64,
pub confirmed_at: Option<i64>,
pub required_confirmations: Option<u32>,
pub estimated_confirmation_at: Option<i64>,
pub explorer_url: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Payment {
pub currency: String,
pub network: String,
pub chain_id: i64,
pub contract_address: Option<String>,
pub expected: String,
pub address: Option<String>,
pub exchange_rate: Option<String>,
pub paid: Option<String>,
pub remaining: Option<String>,
pub fee: Option<String>,
pub net: Option<String>,
pub transfers: Option<Vec<Transfer>>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Invoice {
pub invoice_id: String,
pub project_id: String,
pub status: InvoiceStatus,
pub is_final: bool,
pub is_test: bool,
pub payment_url: String,
pub order: Order,
pub payment: Option<Payment>,
pub created_at: i64,
pub updated_at: i64,
pub expires_at: Option<i64>,
pub completed_at: Option<i64>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct InvoiceListItem {
pub invoice_id: String,
pub project_id: String,
pub external_order_id: String,
pub client_id: Option<String>,
pub status: InvoiceStatus,
pub is_final: bool,
pub is_test: bool,
pub amount: String,
pub currency: String,
pub network: Option<String>,
pub created_at: i64,
pub expires_at: Option<i64>,
pub completed_at: Option<i64>,
}
#[derive(Clone, Debug, Serialize)]
pub struct CreateWithdrawalRequest {
pub destination_address: String,
pub network: String,
pub currency: String,
pub amount: String,
pub external_order_id: String,
}
#[derive(Clone, Debug, Default)]
pub struct WithdrawalListParams {
pub limit: Option<u32>,
pub cursor: Option<String>,
pub status: Option<Vec<WithdrawalStatus>>,
pub external_order_id: Option<String>,
pub created_from: Option<i64>,
pub created_to: Option<i64>,
}
impl WithdrawalListParams {
pub(crate) fn query_pairs(&self) -> Result<Vec<(String, String)>, crate::Error> {
let mut values = Vec::new();
push_option(&mut values, "limit", self.limit);
push_string(&mut values, "cursor", self.cursor.as_deref());
if let Some(statuses) = &self.status {
if statuses.is_empty() {
return Err(crate::Error::InvalidArgument(
"withdrawal status filter cannot be empty".to_owned(),
));
}
values.extend(
statuses
.iter()
.map(|status| ("status".to_owned(), status.as_str().to_owned())),
);
}
push_string(
&mut values,
"external_order_id",
self.external_order_id.as_deref(),
);
push_option(&mut values, "created_from", self.created_from);
push_option(&mut values, "created_to", self.created_to);
Ok(values)
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Withdrawal {
pub withdrawal_id: String,
pub external_order_id: String,
pub status: WithdrawalStatus,
pub is_final: bool,
pub is_test: bool,
pub amount: String,
pub fee: Option<String>,
pub currency: String,
pub network: String,
pub destination_address: String,
pub tx_hash: Option<String>,
pub explorer_url: Option<String>,
pub created_at: i64,
pub completed_at: Option<i64>,
pub failed_at: Option<i64>,
pub cancelled_at: Option<i64>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct CursorPage<T> {
pub items: Vec<T>,
pub next_cursor: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Balance {
pub currency: String,
pub available: String,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ServerTime {
pub server_time: i64,
}
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum InvoiceSimulationStage {
Paid,
Overpaid,
Underpay,
Cancel,
}
fn push_string(values: &mut Vec<(String, String)>, key: &str, value: Option<&str>) {
if let Some(value) = value {
values.push((key.to_owned(), value.to_owned()));
}
}
fn push_option<T: ToString>(values: &mut Vec<(String, String)>, key: &str, value: Option<T>) {
if let Some(value) = value {
values.push((key.to_owned(), value.to_string()));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn preserves_unknown_statuses() {
let status: InvoiceStatus = serde_json::from_str("\"future_status\"").unwrap();
assert_eq!(status, InvoiceStatus::Unknown("future_status".to_owned()));
assert_eq!(serde_json::to_string(&status).unwrap(), "\"future_status\"");
}
}