use serde::{Deserialize, Serialize};
use super::bank::BankCode;
use super::common::{Channel, Currency, Metadata, Money};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PayoutStatus {
Pending,
Completed,
Failed,
Reversed,
Other(String),
}
impl PayoutStatus {
pub fn as_str(&self) -> &str {
match self {
Self::Pending => "pending",
Self::Completed => "completed",
Self::Failed => "failed",
Self::Reversed => "reversed",
Self::Other(s) => s.as_str(),
}
}
pub fn is_terminal(&self) -> bool {
matches!(self, Self::Completed | Self::Failed | Self::Reversed)
}
}
impl<'de> Deserialize<'de> for PayoutStatus {
fn deserialize<D>(d: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(d)?;
Ok(match s.as_str() {
"pending" => Self::Pending,
"completed" => Self::Completed,
"failed" => Self::Failed,
"reversed" => Self::Reversed,
_ => Self::Other(s),
})
}
}
impl Serialize for PayoutStatus {
fn serialize<S>(&self, ser: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
ser.serialize_str(self.as_str())
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "channel", rename_all = "snake_case")]
pub enum SendPayoutRequest {
Mobile(MobilePayout),
Bank(BankPayout),
}
#[derive(Debug, Clone, Serialize)]
pub struct MobilePayout {
pub amount: u64,
pub recipient_phone: String,
pub recipient_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub narration: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub webhook_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<Metadata>,
}
impl MobilePayout {
pub fn new(
amount: u64,
recipient_phone: impl Into<String>,
recipient_name: impl Into<String>,
) -> Self {
Self {
amount,
recipient_phone: recipient_phone.into(),
recipient_name: recipient_name.into(),
narration: None,
webhook_url: None,
metadata: None,
}
}
pub fn with_narration(mut self, narration: impl Into<String>) -> Self {
self.narration = Some(narration.into());
self
}
pub fn with_webhook_url(mut self, url: impl Into<String>) -> Self {
self.webhook_url = Some(url.into());
self
}
pub fn with_metadata(mut self, metadata: Metadata) -> Self {
self.metadata = Some(metadata);
self
}
}
#[derive(Debug, Clone, Serialize)]
pub struct BankPayout {
pub amount: u64,
pub recipient_bank: BankCode,
pub recipient_account: String,
pub recipient_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub narration: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub webhook_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<Metadata>,
}
impl BankPayout {
pub fn new(
amount: u64,
recipient_bank: BankCode,
recipient_account: impl Into<String>,
recipient_name: impl Into<String>,
) -> Self {
Self {
amount,
recipient_bank,
recipient_account: recipient_account.into(),
recipient_name: recipient_name.into(),
narration: None,
webhook_url: None,
metadata: None,
}
}
pub fn with_narration(mut self, narration: impl Into<String>) -> Self {
self.narration = Some(narration.into());
self
}
pub fn with_webhook_url(mut self, url: impl Into<String>) -> Self {
self.webhook_url = Some(url.into());
self
}
pub fn with_metadata(mut self, metadata: Metadata) -> Self {
self.metadata = Some(metadata);
self
}
}
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct Payout {
pub reference: String,
pub status: PayoutStatus,
pub amount: Money,
pub fees: Money,
pub total: Money,
pub channel: Channel,
pub recipient: Recipient,
#[serde(default)]
pub external_reference: Option<String>,
#[serde(default)]
pub narration: Option<String>,
#[serde(default)]
pub failure_reason: Option<String>,
#[serde(default)]
pub metadata: Option<Metadata>,
}
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct Recipient {
pub name: String,
#[serde(default)]
pub phone: Option<String>,
#[serde(default)]
pub account: Option<String>,
#[serde(default)]
pub bank: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub struct PayoutFee {
pub amount: u64,
pub fee_amount: u64,
pub total_amount: u64,
pub currency: Currency,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mobile_payout_serialises_with_channel_tag() {
let req = SendPayoutRequest::Mobile(
MobilePayout::new(5000, "255781000000", "Recipient")
.with_narration("Salary"),
);
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["channel"], "mobile");
assert_eq!(json["amount"], 5000);
assert_eq!(json["recipient_phone"], "255781000000");
assert_eq!(json["narration"], "Salary");
}
#[test]
fn bank_payout_serialises_with_bank_code() {
let req = SendPayoutRequest::Bank(BankPayout::new(
5000,
BankCode::Crdb,
"0150000000",
"Recipient",
));
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["channel"], "bank");
assert_eq!(json["recipient_bank"], "CRDB");
}
#[test]
fn payout_response_deserialises() {
let body = serde_json::json!({
"reference": "667c9279-846f-4001-b046-fdecab204f4f",
"status": "pending",
"amount": {"value": 5000, "currency": "TZS"},
"fees": {"value": 1500, "currency": "TZS"},
"total": {"value": 6500, "currency": "TZS"},
"channel": {"type": "mobile_money", "provider": "airtel"},
"recipient": {"name": "Recipient", "phone": "255781000000"},
"external_reference": "fVJQRPGYbtN3"
});
let payout: Payout = serde_json::from_value(body).unwrap();
assert_eq!(payout.status, PayoutStatus::Pending);
assert_eq!(payout.total.value, 6500);
assert_eq!(payout.channel.provider.as_deref(), Some("airtel"));
}
}