use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use crate::client::PaymentProvider;
use crate::error::{MppError, ResultExt};
use crate::protocol::core::{PaymentChallenge, PaymentCredential};
use crate::protocol::intents::ChargeRequest;
use crate::protocol::methods::stripe::types::CreateTokenResult;
use crate::protocol::methods::stripe::{
StripeCredentialPayload, StripeMethodDetails, INTENT_CHARGE, METHOD_NAME,
};
#[derive(Debug, Clone, serde::Serialize)]
pub struct CreateTokenParams {
pub amount: String,
pub currency: String,
pub network_id: String,
pub expires_at: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<std::collections::HashMap<String, String>>,
#[serde(skip)]
pub challenge: serde_json::Value,
}
type CreateTokenFn = dyn Fn(
CreateTokenParams,
) -> Pin<Box<dyn Future<Output = Result<CreateTokenResult, MppError>> + Send>>
+ Send
+ Sync;
#[derive(Clone)]
pub struct StripeProvider {
create_token: Arc<CreateTokenFn>,
}
impl StripeProvider {
pub fn new<F>(create_token: F) -> Self
where
F: Fn(
CreateTokenParams,
)
-> Pin<Box<dyn Future<Output = Result<CreateTokenResult, MppError>> + Send>>
+ Send
+ Sync
+ 'static,
{
Self {
create_token: Arc::new(create_token),
}
}
}
impl PaymentProvider for StripeProvider {
fn supports(&self, method: &str, intent: &str) -> bool {
method == METHOD_NAME && intent == INTENT_CHARGE
}
async fn pay(&self, challenge: &PaymentChallenge) -> Result<PaymentCredential, MppError> {
let request: ChargeRequest = challenge
.request
.decode()
.mpp_config("failed to decode challenge request")?;
let details: StripeMethodDetails = request
.method_details
.as_ref()
.map(|v| serde_json::from_value(v.clone()))
.transpose()
.mpp_config("invalid methodDetails")?
.unwrap_or_default();
let expires_at = challenge
.expires
.as_ref()
.and_then(|e| {
time::OffsetDateTime::parse(e, &time::format_description::well_known::Rfc3339).ok()
})
.map(|dt| dt.unix_timestamp() as u64)
.unwrap_or_else(|| {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
+ 3600
});
let params = CreateTokenParams {
amount: request.amount,
currency: request.currency,
network_id: details.network_id,
expires_at,
metadata: details.metadata,
challenge: serde_json::to_value(challenge).unwrap_or_default(),
};
let result = (self.create_token)(params).await?;
let payload = StripeCredentialPayload {
spt: result.spt,
external_id: result.external_id,
};
Ok(PaymentCredential::new(challenge.to_echo(), payload))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_supports() {
let provider = StripeProvider::new(|_| {
Box::pin(async { Ok(CreateTokenResult::from("spt_test".to_string())) })
});
assert!(provider.supports("stripe", "charge"));
assert!(!provider.supports("tempo", "charge"));
assert!(!provider.supports("stripe", "session"));
}
}