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> {
challenge.validate_for_charge(METHOD_NAME)?;
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::*;
use crate::protocol::core::Base64UrlJson;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
#[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"));
}
#[tokio::test]
async fn test_pay_rejects_expired_challenge_before_creating_token() {
let called = Arc::new(AtomicBool::new(false));
let provider = StripeProvider::new({
let called = called.clone();
move |_| {
called.store(true, Ordering::SeqCst);
Box::pin(async { Ok(CreateTokenResult::from("spt_test".to_string())) })
}
});
let request = ChargeRequest {
amount: "1000".to_string(),
currency: "usd".to_string(),
method_details: Some(serde_json::json!({
"networkId": "internal",
"paymentMethodTypes": ["card"]
})),
..Default::default()
};
let challenge = PaymentChallenge::new(
"challenge-123",
"api.example.com",
"stripe",
"charge",
Base64UrlJson::from_typed(&request).unwrap(),
)
.with_expires("2020-01-01T00:00:00Z");
let err = provider.pay(&challenge).await.unwrap_err();
assert!(matches!(err, MppError::PaymentExpired(_)));
assert!(!called.load(Ordering::SeqCst));
}
}