use crate::Error;
#[derive(Debug, Clone)]
pub struct StripeConfig {
pub api_key: String,
pub webhook_secret: String,
pub connect_webhook_secret: Option<String>,
pub application_fee_percent: Option<f64>,
}
impl StripeConfig {
pub fn from_env() -> Result<Self, Error> {
let api_key = std::env::var("STRIPE_SECRET_KEY")
.map_err(|_| Error::Config("STRIPE_SECRET_KEY not set".to_string()))?;
let webhook_secret = std::env::var("STRIPE_WEBHOOK_SECRET")
.map_err(|_| Error::Config("STRIPE_WEBHOOK_SECRET not set".to_string()))?;
let connect_webhook_secret = std::env::var("STRIPE_CONNECT_WEBHOOK_SECRET").ok();
let application_fee_percent = std::env::var("STRIPE_APPLICATION_FEE_PERCENT")
.ok()
.and_then(|v| v.parse::<f64>().ok());
Ok(Self {
api_key,
webhook_secret,
connect_webhook_secret,
application_fee_percent,
})
}
pub fn application_fee_for(&self, amount_cents: i64) -> Option<i64> {
let percent = self.application_fee_percent?;
if percent <= 0.0 {
return None;
}
let fee = (amount_cents as f64 * percent / 100.0).round() as i64;
Some(fee.clamp(0, amount_cents.max(0)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_env_returns_config_error_when_key_missing() {
let old_key = std::env::var("STRIPE_SECRET_KEY").ok();
let old_secret = std::env::var("STRIPE_WEBHOOK_SECRET").ok();
std::env::remove_var("STRIPE_SECRET_KEY");
std::env::remove_var("STRIPE_WEBHOOK_SECRET");
let result = StripeConfig::from_env();
assert!(matches!(result, Err(Error::Config(_))));
if let Some(k) = old_key {
std::env::set_var("STRIPE_SECRET_KEY", k);
}
if let Some(s) = old_secret {
std::env::set_var("STRIPE_WEBHOOK_SECRET", s);
}
}
#[test]
fn config_loads_from_provided_values() {
let config = StripeConfig {
api_key: "sk_test_xxx".to_string(),
webhook_secret: "whsec_xxx".to_string(),
connect_webhook_secret: Some("whsec_connect_xxx".to_string()),
application_fee_percent: Some(2.5),
};
assert_eq!(config.api_key, "sk_test_xxx");
assert_eq!(config.webhook_secret, "whsec_xxx");
assert_eq!(
config.connect_webhook_secret.as_deref(),
Some("whsec_connect_xxx")
);
assert_eq!(config.application_fee_percent, Some(2.5));
}
fn config_with_percent(percent: Option<f64>) -> StripeConfig {
StripeConfig {
api_key: "sk_test_xxx".to_string(),
webhook_secret: "whsec_xxx".to_string(),
connect_webhook_secret: None,
application_fee_percent: percent,
}
}
#[test]
fn application_fee_for_returns_none_when_unset() {
let config = config_with_percent(None);
assert_eq!(config.application_fee_for(2000), None);
}
#[test]
fn application_fee_for_returns_none_when_zero_percent() {
let config = config_with_percent(Some(0.0));
assert_eq!(config.application_fee_for(2000), None);
}
#[test]
fn application_fee_for_returns_none_when_negative_percent() {
let config = config_with_percent(Some(-1.0));
assert_eq!(config.application_fee_for(2000), None);
}
#[test]
fn application_fee_for_computes_normal_case() {
let config = config_with_percent(Some(5.0));
assert_eq!(config.application_fee_for(2000), Some(100));
}
#[test]
fn application_fee_for_rounds_to_nearest_cent() {
let config = config_with_percent(Some(2.5));
assert_eq!(config.application_fee_for(1999), Some(50));
}
#[test]
fn application_fee_for_clamps_to_amount_upper_bound() {
let config = config_with_percent(Some(150.0));
assert_eq!(config.application_fee_for(2000), Some(2000));
}
#[test]
fn application_fee_for_clamps_negative_amount_to_zero() {
let config = config_with_percent(Some(5.0));
assert_eq!(config.application_fee_for(0), Some(0));
assert_eq!(config.application_fee_for(-100), Some(0));
}
}