use async_trait::async_trait;
use crate::error::Error;
use crate::money::Money;
use crate::payment::{CreatePayment, Payment, Refund, RefundRequest, VerifiedPayment};
#[async_trait]
pub trait PaymentProvider: Send + Sync {
async fn create_payment(&self, req: CreatePayment) -> Result<Payment, Error>;
async fn get_payment(&self, id: &str) -> Result<Payment, Error>;
async fn cancel_payment(&self, id: &str) -> Result<Payment, Error> {
let _ = id;
Err(Error::Unsupported)
}
async fn refund(&self, id: &str, req: RefundRequest) -> Result<Refund, Error> {
let _ = (id, req);
Err(Error::Unsupported)
}
async fn fetch_verified(&self, id: &str, expected: Money) -> Result<VerifiedPayment, Error> {
let payment = self.get_payment(id).await?;
if payment.amount != expected {
return Err(Error::AmountMismatch {
expected,
actual: payment.amount,
});
}
if !payment.status.is_paid() {
return Err(Error::not_paid(payment.status));
}
Ok(VerifiedPayment::new(payment))
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use crate::money::{Currency, Money};
use crate::payment::PaymentStatus;
struct FakeProvider;
#[async_trait]
impl PaymentProvider for FakeProvider {
async fn create_payment(&self, req: CreatePayment) -> Result<Payment, Error> {
Ok(Payment::new("fake_1", PaymentStatus::Open, req.amount()))
}
async fn get_payment(&self, id: &str) -> Result<Payment, Error> {
Ok(Payment::new(
id,
PaymentStatus::Open,
Money::from_minor(0, Currency::EUR),
))
}
}
fn provider() -> Arc<dyn PaymentProvider> {
Arc::new(FakeProvider)
}
#[tokio::test]
async fn arc_dyn_payment_provider_compiles_and_creates_a_payment() {
let req = CreatePayment::new(
Money::from_minor(1000, Currency::EUR),
"order #1",
"https://shop.example/return",
);
let payment = provider().create_payment(req).await.unwrap();
assert_eq!(payment.status, PaymentStatus::Open);
}
#[tokio::test]
async fn arc_dyn_payment_provider_gets_a_payment() {
let payment = provider().get_payment("fake_1").await.unwrap();
assert_eq!(payment.id, "fake_1");
}
#[tokio::test]
async fn default_cancel_payment_is_unsupported_and_not_retriable() {
let err = provider().cancel_payment("fake_1").await.unwrap_err();
assert!(matches!(err, Error::Unsupported));
assert!(!err.is_retriable());
}
#[tokio::test]
async fn default_refund_is_unsupported_and_not_retriable() {
let err = provider()
.refund(
"fake_1",
RefundRequest::new(Money::from_minor(500, Currency::EUR)),
)
.await
.unwrap_err();
assert!(matches!(err, Error::Unsupported));
assert!(!err.is_retriable());
}
struct FixedPaymentProvider(Payment);
#[async_trait]
impl PaymentProvider for FixedPaymentProvider {
async fn create_payment(&self, req: CreatePayment) -> Result<Payment, Error> {
Ok(Payment::new("fixed", PaymentStatus::Open, req.amount()))
}
async fn get_payment(&self, _id: &str) -> Result<Payment, Error> {
Ok(self.0.clone())
}
}
fn fixed_provider(payment: Payment) -> Arc<dyn PaymentProvider> {
Arc::new(FixedPaymentProvider(payment))
}
#[tokio::test]
async fn fetch_verified_through_arc_dyn_succeeds_when_paid_and_amount_matches() {
let amount = Money::from_minor(1000, Currency::EUR);
let provider = fixed_provider(Payment::new("pay_1", PaymentStatus::Paid, amount));
let verified = provider.fetch_verified("pay_1", amount).await.unwrap();
assert_eq!(verified.payment().status, PaymentStatus::Paid);
assert_eq!(verified.payment().amount, amount);
}
#[tokio::test]
async fn fetch_verified_rejects_amount_mismatch() {
let provider = fixed_provider(Payment::new(
"pay_1",
PaymentStatus::Paid,
Money::from_minor(999, Currency::EUR),
));
let err = provider
.fetch_verified("pay_1", Money::from_minor(1000, Currency::EUR))
.await
.unwrap_err();
match err {
Error::AmountMismatch { expected, actual } => {
assert_eq!(expected, Money::from_minor(1000, Currency::EUR));
assert_eq!(actual, Money::from_minor(999, Currency::EUR));
}
other => panic!("expected AmountMismatch, got {other:?}"),
}
assert!(!err.is_retriable());
}
#[tokio::test]
async fn fetch_verified_rejects_matching_amount_that_is_not_yet_paid() {
let amount = Money::from_minor(1000, Currency::EUR);
let provider = fixed_provider(Payment::new("pay_1", PaymentStatus::Open, amount));
let err = provider.fetch_verified("pay_1", amount).await.unwrap_err();
assert!(!err.is_retriable());
match err {
Error::NotPaid { status } => assert_eq!(status, PaymentStatus::Open),
other => panic!("expected NotPaid, got {other:?}"),
}
}
}