use async_trait::async_trait;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use crate::{
Amount, Error, Gateway, Result, StartRequest, StartResponse, VerifyRequest, VerifyResponse,
};
#[derive(Debug, Clone)]
pub enum Behavior {
Succeed,
FailGateway {
code: i64,
message: String,
},
}
pub struct MockGateway {
inner: Arc<MockInner>,
}
struct MockInner {
next_id: AtomicU64,
start_calls: AtomicU64,
verify_calls: AtomicU64,
refund_calls: AtomicU64,
start_behavior: Mutex<Behavior>,
verify_behavior: Mutex<Behavior>,
}
impl Default for MockGateway {
fn default() -> Self {
Self::new()
}
}
impl MockGateway {
#[must_use]
pub fn new() -> Self {
Self {
inner: Arc::new(MockInner {
next_id: AtomicU64::new(1),
start_calls: AtomicU64::new(0),
verify_calls: AtomicU64::new(0),
refund_calls: AtomicU64::new(0),
start_behavior: Mutex::new(Behavior::Succeed),
verify_behavior: Mutex::new(Behavior::Succeed),
}),
}
}
pub fn set_start_behavior(&self, b: Behavior) {
*self.inner.start_behavior.lock().unwrap() = b;
}
pub fn set_verify_behavior(&self, b: Behavior) {
*self.inner.verify_behavior.lock().unwrap() = b;
}
#[must_use]
pub fn start_call_count(&self) -> u64 {
self.inner.start_calls.load(Ordering::SeqCst)
}
#[must_use]
pub fn verify_call_count(&self) -> u64 {
self.inner.verify_calls.load(Ordering::SeqCst)
}
#[must_use]
pub fn refund_call_count(&self) -> u64 {
self.inner.refund_calls.load(Ordering::SeqCst)
}
fn next_id(&self) -> u64 {
self.inner.next_id.fetch_add(1, Ordering::SeqCst)
}
}
#[async_trait]
impl Gateway for MockGateway {
fn name(&self) -> &'static str {
"mock"
}
async fn start_payment(&self, req: &StartRequest) -> Result<StartResponse> {
self.inner.start_calls.fetch_add(1, Ordering::SeqCst);
let behavior = self.inner.start_behavior.lock().unwrap().clone();
if let Behavior::FailGateway { code, message } = behavior {
return Err(Error::Gateway {
provider: "mock",
code,
message,
});
}
let id = self.next_id();
let authority = format!("MOCK-AUTH-{id:020}");
Ok(StartResponse {
authority: authority.clone(),
payment_url: format!("https://mock.invalid/pay/{}", authority),
provider: "mock",
raw: serde_json::json!({
"amount": req.amount.as_rials(),
"description": req.description,
"callback_url": req.callback_url,
}),
})
}
async fn verify_payment(&self, req: &VerifyRequest) -> Result<VerifyResponse> {
self.inner.verify_calls.fetch_add(1, Ordering::SeqCst);
let behavior = self.inner.verify_behavior.lock().unwrap().clone();
if let Behavior::FailGateway { code, message } = behavior {
return Err(Error::Gateway {
provider: "mock",
code,
message,
});
}
let id = self.next_id();
Ok(VerifyResponse {
transaction_id: format!("MOCK-TX-{id:020}"),
authority: req.authority.clone(),
amount: req.amount,
card_pan: Some("6037-99**-****-0006".into()),
card_hash: Some(format!("mock-hash-{id}")),
fee: Some(Amount::rial(0)),
provider: "mock",
raw: serde_json::json!({"mock": true}),
})
}
async fn refund_payment(&self, req: &crate::RefundRequest) -> Result<crate::RefundResponse> {
self.inner.refund_calls.fetch_add(1, Ordering::SeqCst);
let id = self.next_id();
Ok(crate::RefundResponse {
refund_id: format!("MOCK-REFUND-{id:020}"),
transaction_id: req.transaction_id.clone(),
amount: req.amount.unwrap_or_else(|| Amount::rial(0)),
provider: "mock",
raw: serde_json::json!({"mock_refund": true}),
})
}
}