rustlavel_payment/
transfer.rs1use crate::channel::{Bank, Wallet};
4use crate::money::Money;
5use rustlavel_core::{Error, Json, Result};
6
7#[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 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 pub id: String,
81 pub reference: String,
82 pub status: TransferStatus,
83 pub amount: Money,
84 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}