Skip to main content

rustlavel_payment/
transfer.rs

1//! Money out: to a bank account or an e-wallet.
2
3use crate::channel::{Bank, Wallet};
4use crate::money::Money;
5use rustlavel_core::{Error, Json, Result};
6
7/// Where the money goes.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum Account {
10    Bank { bank: Bank, number: String, holder: String },
11    Wallet { wallet: Wallet, phone: String, holder: String },
12}
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct TransferRequest {
16    /// The application's id, for matching the result — and for the gateway to
17    /// refuse a second transfer with the same one, which is the protection
18    /// against a retry paying somebody twice.
19    pub reference: String,
20    pub amount: Money,
21    pub to: Account,
22    pub description: String,
23}
24
25impl TransferRequest {
26    pub fn new(reference: impl Into<String>, amount: Money, to: Account) -> TransferRequest {
27        TransferRequest { reference: reference.into(), amount, to, description: String::new() }
28    }
29
30    pub fn description(mut self, description: impl Into<String>) -> TransferRequest {
31        self.description = description.into();
32        self
33    }
34
35    pub fn validate(&self) -> Result<()> {
36        self.amount.positive()?;
37        if self.reference.trim().is_empty() {
38            return Err(Error::msg("a transfer needs a reference: it is what stops a retry paying twice"));
39        }
40        let number = match &self.to {
41            Account::Bank { number, .. } => number,
42            Account::Wallet { phone, .. } => phone,
43        };
44        if number.trim().is_empty() {
45            return Err(Error::msg("a transfer needs a destination account or number"));
46        }
47        Ok(())
48    }
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
52pub enum TransferStatus {
53    Pending,
54    Completed,
55    Failed,
56}
57
58impl TransferStatus {
59    pub fn as_str(self) -> &'static str {
60        match self {
61            TransferStatus::Pending => "pending",
62            TransferStatus::Completed => "completed",
63            TransferStatus::Failed => "failed",
64        }
65    }
66
67    pub fn parse(text: &str) -> Option<TransferStatus> {
68        Some(match text.trim().to_ascii_lowercase().as_str() {
69            "pending" | "processing" => TransferStatus::Pending,
70            "completed" | "success" | "settled" => TransferStatus::Completed,
71            "failed" => TransferStatus::Failed,
72            _ => return None,
73        })
74    }
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct Transfer {
79    /// The gateway's id.
80    pub id: String,
81    pub reference: String,
82    pub status: TransferStatus,
83    pub amount: Money,
84    /// Why it failed, when it did, in the gateway's words.
85    pub failure: Option<String>,
86}
87
88impl Transfer {
89    pub fn to_json(&self) -> Json {
90        Json::object([
91            ("id", Json::from(self.id.as_str())),
92            ("reference", Json::from(self.reference.as_str())),
93            ("status", Json::from(self.status.as_str())),
94            ("amount", self.amount.to_json()),
95            ("failure", self.failure.as_deref().map_or(Json::Null, Json::from)),
96        ])
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn a_transfer_needs_a_reference_and_a_destination() {
106        let to = Account::Bank { bank: Bank::Bca, number: "1234567890".into(), holder: "Ada".into() };
107        assert!(TransferRequest::new("payout-1", Money::idr(50_000), to.clone()).validate().is_ok());
108        assert!(TransferRequest::new("", Money::idr(50_000), to.clone()).validate().is_err());
109
110        let nowhere = Account::Wallet { wallet: Wallet::Dana, phone: " ".into(), holder: "Ada".into() };
111        assert!(TransferRequest::new("payout-2", Money::idr(50_000), nowhere).validate().is_err());
112    }
113}