use serde_json::json;
use snippe::models::bank::BankCode;
use snippe::models::payout::{BankPayout, MobilePayout, PayoutStatus, SendPayoutRequest};
use snippe::{Client, IdempotencyKey};
use wiremock::matchers::{body_json, header, method, path, query_param};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn client_for(server: &MockServer) -> Client {
Client::builder()
.api_key("snp_test_key")
.base_url(server.uri())
.build()
.unwrap()
}
#[tokio::test]
async fn fee_endpoint() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/payouts/fee"))
.and(query_param("amount", "50000"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"data": {
"amount": 50000,
"fee_amount": 1000,
"total_amount": 51000,
"currency": "TZS"
}
})))
.mount(&server)
.await;
let client = client_for(&server);
let fee = client.payouts().fee(50_000).await.unwrap();
assert_eq!(fee.fee_amount, 1000);
assert_eq!(fee.total_amount, 51_000);
}
#[tokio::test]
async fn send_mobile_payout() {
let server = MockServer::start().await;
let expected = json!({
"channel": "mobile",
"amount": 5000,
"recipient_phone": "255781000000",
"recipient_name": "Recipient Name",
"narration": "Salary January 2026"
});
let response = json!({
"data": {
"reference": "667c9279",
"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 Name", "phone": "255781000000"},
"external_reference": "fVJQRPGYbtN3"
}
});
Mock::given(method("POST"))
.and(path("/v1/payouts/send"))
.and(header("Idempotency-Key", "po-jan26-1"))
.and(body_json(expected))
.respond_with(ResponseTemplate::new(201).set_body_json(response))
.mount(&server)
.await;
let client = client_for(&server);
let req = SendPayoutRequest::Mobile(
MobilePayout::new(5000, "255781000000", "Recipient Name")
.with_narration("Salary January 2026"),
);
let key = IdempotencyKey::new("po-jan26-1").unwrap();
let payout = client.payouts().send(&req, Some(&key)).await.unwrap();
assert_eq!(payout.reference, "667c9279");
assert_eq!(payout.status, PayoutStatus::Pending);
assert_eq!(payout.total.value, 6500);
assert_eq!(payout.channel.r#type, "mobile_money");
assert_eq!(payout.channel.provider.as_deref(), Some("airtel"));
}
#[tokio::test]
async fn send_bank_payout_uses_bank_code() {
let server = MockServer::start().await;
let expected = json!({
"channel": "bank",
"amount": 10000,
"recipient_bank": "CRDB",
"recipient_account": "0150000000",
"recipient_name": "Vendor LLC"
});
let response = json!({
"data": {
"reference": "bo_1",
"status": "pending",
"amount": {"value": 10000, "currency": "TZS"},
"fees": {"value": 2000, "currency": "TZS"},
"total": {"value": 12000, "currency": "TZS"},
"channel": {"type": "bank"},
"recipient": {"name": "Vendor LLC", "account": "0150000000", "bank": "CRDB"}
}
});
Mock::given(method("POST"))
.and(path("/v1/payouts/send"))
.and(body_json(expected))
.respond_with(ResponseTemplate::new(201).set_body_json(response))
.mount(&server)
.await;
let client = client_for(&server);
let req = SendPayoutRequest::Bank(BankPayout::new(
10_000,
BankCode::Crdb,
"0150000000",
"Vendor LLC",
));
let payout = client.payouts().send(&req, None).await.unwrap();
assert_eq!(payout.recipient.bank.as_deref(), Some("CRDB"));
}
#[tokio::test]
async fn insufficient_balance_error() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/payouts/send"))
.respond_with(ResponseTemplate::new(500).set_body_json(json!({
"status": "error",
"code": 500,
"error_code": "payment_failed",
"message": "insufficient balance: available 5000, required 6500"
})))
.mount(&server)
.await;
let client = client_for(&server);
let req = SendPayoutRequest::Mobile(MobilePayout::new(5000, "255781000000", "X"));
let err = client.payouts().send(&req, None).await.unwrap_err();
if let snippe::Error::Api(e) = err {
assert_eq!(e.error_code, snippe::ErrorCode::PaymentFailed);
assert_eq!(e.status, 500);
} else {
panic!("expected Api error");
}
}