use crate::error::MppError;
use crate::protocol::core::{PaymentChallenge, PaymentCredential};
use reqwest::header::HeaderMap;
use reqwest::Url;
use std::future::Future;
#[derive(Clone, Debug)]
pub struct PaymentContext {
pub url: Url,
pub headers: HeaderMap,
}
pub trait PaymentProvider: Clone + Send + Sync {
fn supports(&self, method: &str, intent: &str) -> bool;
fn select_challenge<'a>(
&self,
challenges: &[&'a PaymentChallenge],
) -> Option<&'a PaymentChallenge> {
challenges.first().copied()
}
fn pay(
&self,
challenge: &PaymentChallenge,
) -> impl Future<Output = Result<PaymentCredential, MppError>> + Send;
fn pay_with_context(
&self,
challenge: &PaymentChallenge,
context: PaymentContext,
) -> impl Future<Output = Result<PaymentCredential, MppError>> + Send {
let _ = context;
self.pay(challenge)
}
fn prepare_http_payment_challenge(
&self,
challenge: &PaymentChallenge,
context: PaymentContext,
) -> impl Future<Output = Result<Option<PaymentChallenge>, MppError>> + Send {
let challenge = challenge.clone();
async move {
let _ = context;
Ok(Some(challenge))
}
}
fn prepare_application_websocket_challenge(
&self,
challenge: &PaymentChallenge,
context: PaymentContext,
) -> impl Future<Output = Result<PaymentChallenge, MppError>> + Send {
let challenge = challenge.clone();
async move {
let _ = context;
Ok(challenge)
}
}
fn commit_payment(
&self,
challenge: &PaymentChallenge,
credential: &PaymentCredential,
) -> impl Future<Output = Result<(), MppError>> + Send {
let _ = (challenge, credential);
async { Ok(()) }
}
fn rollback_payment(
&self,
challenge: &PaymentChallenge,
credential: &PaymentCredential,
) -> impl Future<Output = Result<(), MppError>> + Send {
let _ = (challenge, credential);
async { Ok(()) }
}
fn abandon_payment(&self, challenge: &PaymentChallenge, credential: &PaymentCredential) {
let _ = (challenge, credential);
}
fn accept_payment_header(&self) -> Option<String> {
None
}
}
pub(crate) async fn commit_payments<P: PaymentProvider>(
provider: &P,
payments: &[(PaymentChallenge, PaymentCredential)],
) -> Result<(), MppError> {
for (challenge, credential) in payments {
provider.commit_payment(challenge, credential).await?;
}
Ok(())
}
pub(crate) async fn rollback_payments<P: PaymentProvider>(
provider: &P,
payments: &[(PaymentChallenge, PaymentCredential)],
) -> Result<(), MppError> {
for (challenge, credential) in payments {
provider.rollback_payment(challenge, credential).await?;
}
Ok(())
}
pub(crate) struct PendingPayments<P: PaymentProvider> {
provider: P,
payments: Vec<(PaymentChallenge, PaymentCredential)>,
}
impl<P: PaymentProvider> PendingPayments<P> {
pub(crate) fn new(provider: P) -> Self {
Self {
provider,
payments: Vec::new(),
}
}
pub(crate) async fn commit(&mut self) -> Result<(), MppError> {
commit_payments(&self.provider, &self.payments).await?;
self.payments.clear();
Ok(())
}
pub(crate) async fn rollback(&mut self) -> Result<(), MppError> {
rollback_payments(&self.provider, &self.payments).await?;
self.payments.clear();
Ok(())
}
}
impl<P: PaymentProvider> std::ops::Deref for PendingPayments<P> {
type Target = Vec<(PaymentChallenge, PaymentCredential)>;
fn deref(&self) -> &Self::Target {
&self.payments
}
}
impl<P: PaymentProvider> std::ops::DerefMut for PendingPayments<P> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.payments
}
}
impl<P: PaymentProvider> Drop for PendingPayments<P> {
fn drop(&mut self) {
for (challenge, credential) in &self.payments {
self.provider.abandon_payment(challenge, credential);
}
}
}
#[derive(Clone)]
pub struct MultiProvider {
providers: Vec<Box<dyn DynPaymentProvider>>,
}
impl MultiProvider {
pub fn new() -> Self {
Self {
providers: Vec::new(),
}
}
pub fn with<P: PaymentProvider + 'static>(mut self, provider: P) -> Self {
self.providers.push(Box::new(provider));
self
}
pub fn add<P: PaymentProvider + 'static>(&mut self, provider: P) -> &mut Self {
self.providers.push(Box::new(provider));
self
}
pub fn has_support(&self, method: &str, intent: &str) -> bool {
self.providers
.iter()
.any(|p| p.dyn_supports(method, intent))
}
}
impl Default for MultiProvider {
fn default() -> Self {
Self::new()
}
}
impl PaymentProvider for MultiProvider {
fn supports(&self, method: &str, intent: &str) -> bool {
self.has_support(method, intent)
}
fn select_challenge<'a>(
&self,
challenges: &[&'a PaymentChallenge],
) -> Option<&'a PaymentChallenge> {
let first = challenges.first()?;
let provider = self
.providers
.iter()
.find(|provider| provider.dyn_supports(first.method.as_str(), first.intent.as_str()))?;
let candidates = challenges
.iter()
.copied()
.filter(|challenge| {
provider.dyn_supports(challenge.method.as_str(), challenge.intent.as_str())
})
.collect::<Vec<_>>();
provider.dyn_select_challenge(&candidates)
}
async fn pay(&self, challenge: &PaymentChallenge) -> Result<PaymentCredential, MppError> {
let method = challenge.method.as_str();
let intent = challenge.intent.as_str();
for provider in &self.providers {
if provider.dyn_supports(method, intent) {
return provider.dyn_pay(challenge).await;
}
}
Err(MppError::UnsupportedPaymentMethod(format!(
"no provider supports method={}, intent={}",
method, intent
)))
}
async fn pay_with_context(
&self,
challenge: &PaymentChallenge,
context: PaymentContext,
) -> Result<PaymentCredential, MppError> {
let method = challenge.method.as_str();
let intent = challenge.intent.as_str();
for provider in &self.providers {
if provider.dyn_supports(method, intent) {
return provider.dyn_pay_with_context(challenge, context).await;
}
}
Err(MppError::UnsupportedPaymentMethod(format!(
"no provider supports method={}, intent={}",
method, intent
)))
}
async fn prepare_http_payment_challenge(
&self,
challenge: &PaymentChallenge,
context: PaymentContext,
) -> Result<Option<PaymentChallenge>, MppError> {
let method = challenge.method.as_str();
let intent = challenge.intent.as_str();
for provider in &self.providers {
if provider.dyn_supports(method, intent) {
return provider
.dyn_prepare_http_payment_challenge(challenge, context)
.await;
}
}
Err(MppError::UnsupportedPaymentMethod(format!(
"no provider supports method={}, intent={}",
method, intent
)))
}
async fn prepare_application_websocket_challenge(
&self,
challenge: &PaymentChallenge,
context: PaymentContext,
) -> Result<PaymentChallenge, MppError> {
let method = challenge.method.as_str();
let intent = challenge.intent.as_str();
for provider in &self.providers {
if provider.dyn_supports(method, intent) {
return provider
.dyn_prepare_application_websocket_challenge(challenge, context)
.await;
}
}
Err(MppError::UnsupportedPaymentMethod(format!(
"no provider supports method={}, intent={}",
method, intent
)))
}
async fn commit_payment(
&self,
challenge: &PaymentChallenge,
credential: &PaymentCredential,
) -> Result<(), MppError> {
let method = challenge.method.as_str();
let intent = challenge.intent.as_str();
for provider in &self.providers {
if provider.dyn_supports(method, intent) {
return provider.dyn_commit_payment(challenge, credential).await;
}
}
Err(MppError::UnsupportedPaymentMethod(format!(
"no provider supports method={}, intent={}",
method, intent
)))
}
async fn rollback_payment(
&self,
challenge: &PaymentChallenge,
credential: &PaymentCredential,
) -> Result<(), MppError> {
let method = challenge.method.as_str();
let intent = challenge.intent.as_str();
for provider in &self.providers {
if provider.dyn_supports(method, intent) {
return provider.dyn_rollback_payment(challenge, credential).await;
}
}
Err(MppError::UnsupportedPaymentMethod(format!(
"no provider supports method={}, intent={}",
method, intent
)))
}
fn abandon_payment(&self, challenge: &PaymentChallenge, credential: &PaymentCredential) {
let method = challenge.method.as_str();
let intent = challenge.intent.as_str();
for provider in &self.providers {
if provider.dyn_supports(method, intent) {
provider.dyn_abandon_payment(challenge, credential);
return;
}
}
}
fn accept_payment_header(&self) -> Option<String> {
let headers: Vec<String> = self
.providers
.iter()
.filter_map(|p| p.dyn_accept_payment_header())
.collect();
if headers.is_empty() {
None
} else {
Some(headers.join(", "))
}
}
}
trait DynPaymentProvider: Send + Sync {
fn dyn_supports(&self, method: &str, intent: &str) -> bool;
fn dyn_select_challenge<'a>(
&self,
challenges: &[&'a PaymentChallenge],
) -> Option<&'a PaymentChallenge>;
fn dyn_pay<'a>(
&'a self,
challenge: &'a PaymentChallenge,
) -> std::pin::Pin<Box<dyn Future<Output = Result<PaymentCredential, MppError>> + Send + 'a>>;
fn dyn_pay_with_context<'a>(
&'a self,
challenge: &'a PaymentChallenge,
context: PaymentContext,
) -> std::pin::Pin<Box<dyn Future<Output = Result<PaymentCredential, MppError>> + Send + 'a>>;
fn dyn_prepare_http_payment_challenge<'a>(
&'a self,
challenge: &'a PaymentChallenge,
context: PaymentContext,
) -> std::pin::Pin<
Box<dyn Future<Output = Result<Option<PaymentChallenge>, MppError>> + Send + 'a>,
>;
fn dyn_prepare_application_websocket_challenge<'a>(
&'a self,
challenge: &'a PaymentChallenge,
context: PaymentContext,
) -> std::pin::Pin<Box<dyn Future<Output = Result<PaymentChallenge, MppError>> + Send + 'a>>;
fn dyn_commit_payment<'a>(
&'a self,
challenge: &'a PaymentChallenge,
credential: &'a PaymentCredential,
) -> std::pin::Pin<Box<dyn Future<Output = Result<(), MppError>> + Send + 'a>>;
fn dyn_rollback_payment<'a>(
&'a self,
challenge: &'a PaymentChallenge,
credential: &'a PaymentCredential,
) -> std::pin::Pin<Box<dyn Future<Output = Result<(), MppError>> + Send + 'a>>;
fn dyn_abandon_payment(&self, challenge: &PaymentChallenge, credential: &PaymentCredential);
fn dyn_accept_payment_header(&self) -> Option<String>;
fn clone_box(&self) -> Box<dyn DynPaymentProvider>;
}
impl<P: PaymentProvider + 'static> DynPaymentProvider for P {
fn dyn_supports(&self, method: &str, intent: &str) -> bool {
PaymentProvider::supports(self, method, intent)
}
fn dyn_select_challenge<'a>(
&self,
challenges: &[&'a PaymentChallenge],
) -> Option<&'a PaymentChallenge> {
PaymentProvider::select_challenge(self, challenges)
}
fn dyn_pay<'a>(
&'a self,
challenge: &'a PaymentChallenge,
) -> std::pin::Pin<Box<dyn Future<Output = Result<PaymentCredential, MppError>> + Send + 'a>>
{
Box::pin(PaymentProvider::pay(self, challenge))
}
fn dyn_pay_with_context<'a>(
&'a self,
challenge: &'a PaymentChallenge,
context: PaymentContext,
) -> std::pin::Pin<Box<dyn Future<Output = Result<PaymentCredential, MppError>> + Send + 'a>>
{
Box::pin(PaymentProvider::pay_with_context(self, challenge, context))
}
fn dyn_prepare_http_payment_challenge<'a>(
&'a self,
challenge: &'a PaymentChallenge,
context: PaymentContext,
) -> std::pin::Pin<
Box<dyn Future<Output = Result<Option<PaymentChallenge>, MppError>> + Send + 'a>,
> {
Box::pin(PaymentProvider::prepare_http_payment_challenge(
self, challenge, context,
))
}
fn dyn_prepare_application_websocket_challenge<'a>(
&'a self,
challenge: &'a PaymentChallenge,
context: PaymentContext,
) -> std::pin::Pin<Box<dyn Future<Output = Result<PaymentChallenge, MppError>> + Send + 'a>>
{
Box::pin(PaymentProvider::prepare_application_websocket_challenge(
self, challenge, context,
))
}
fn dyn_commit_payment<'a>(
&'a self,
challenge: &'a PaymentChallenge,
credential: &'a PaymentCredential,
) -> std::pin::Pin<Box<dyn Future<Output = Result<(), MppError>> + Send + 'a>> {
Box::pin(PaymentProvider::commit_payment(self, challenge, credential))
}
fn dyn_rollback_payment<'a>(
&'a self,
challenge: &'a PaymentChallenge,
credential: &'a PaymentCredential,
) -> std::pin::Pin<Box<dyn Future<Output = Result<(), MppError>> + Send + 'a>> {
Box::pin(PaymentProvider::rollback_payment(
self, challenge, credential,
))
}
fn dyn_abandon_payment(&self, challenge: &PaymentChallenge, credential: &PaymentCredential) {
PaymentProvider::abandon_payment(self, challenge, credential);
}
fn dyn_accept_payment_header(&self) -> Option<String> {
PaymentProvider::accept_payment_header(self)
}
fn clone_box(&self) -> Box<dyn DynPaymentProvider> {
Box::new(self.clone())
}
}
impl Clone for Box<dyn DynPaymentProvider> {
fn clone(&self) -> Self {
self.clone_box()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Clone)]
struct MockProvider {
method: &'static str,
intent: &'static str,
}
impl PaymentProvider for MockProvider {
fn supports(&self, method: &str, intent: &str) -> bool {
self.method == method && self.intent == intent
}
async fn pay(&self, challenge: &PaymentChallenge) -> Result<PaymentCredential, MppError> {
use crate::protocol::core::PaymentPayload;
Ok(PaymentCredential::new(
challenge.to_echo(),
PaymentPayload::hash(format!("mock-{}", self.method)),
))
}
}
#[test]
fn test_multi_provider_supports() {
let multi = MultiProvider::new()
.with(MockProvider {
method: "tempo",
intent: "charge",
})
.with(MockProvider {
method: "stripe",
intent: "charge",
});
assert!(multi.has_support("tempo", "charge"));
assert!(multi.has_support("stripe", "charge"));
assert!(!multi.has_support("bitcoin", "charge"));
assert!(!multi.has_support("tempo", "authorize"));
}
#[test]
fn test_multi_provider_empty() {
let multi = MultiProvider::new();
assert!(!multi.has_support("tempo", "charge"));
}
#[test]
fn test_multi_provider_clone() {
let multi = MultiProvider::new().with(MockProvider {
method: "tempo",
intent: "charge",
});
let cloned = multi.clone();
assert!(cloned.has_support("tempo", "charge"));
}
}