use std::str::FromStr;
use kasapay_core::{Currency, Error, ErrorKind, Money, ProviderId, Status};
pub(crate) const PROVIDER: ProviderId = ProviderId::STRIPE;
pub(crate) fn currency(currency: Currency) -> Result<stripe_types::Currency, Error> {
let refuse = || {
Error::new(
ErrorKind::Unsupported,
PROVIDER,
format!("Stripe does not settle in {currency}"),
)
};
let Ok(mapped) = stripe_types::Currency::from_str(¤cy.code().to_ascii_lowercase());
if matches!(mapped, stripe_types::Currency::Unknown(_)) {
return Err(refuse());
}
Ok(mapped)
}
pub(crate) fn currency_back(currency: &stripe_types::Currency) -> Option<Currency> {
currency.to_string().to_ascii_uppercase().parse().ok()
}
pub(crate) fn amount(minor_units: i64, from: &stripe_types::Currency) -> Result<Money, Error> {
let currency = currency_back(from).ok_or_else(|| {
Error::new(
ErrorKind::Unsupported,
PROVIDER,
format!("kasapay has no Currency for Stripe's {from:?}"),
)
})?;
Ok(Money::from_minor_units(minor_units, currency))
}
#[expect(
clippy::match_same_arms,
reason = "naming Processing is worth more than folding it into the wildcard"
)]
pub(crate) fn status(status: &stripe_shared::PaymentIntentStatus) -> Status {
use stripe_shared::PaymentIntentStatus as S;
match status {
S::Canceled => Status::Canceled,
S::RequiresCapture => Status::Authorized,
S::Succeeded => Status::Captured,
S::RequiresAction | S::RequiresConfirmation | S::RequiresPaymentMethod => {
Status::RequiresAction
}
S::Processing => Status::Pending,
_ => Status::Pending,
}
}
pub(crate) fn error(error: &stripe::StripeError) -> Error {
let stripe::StripeError::Stripe(api, status) = error else {
let kind = match error {
stripe::StripeError::JSONDeserialize(_) => ErrorKind::Malformed,
stripe::StripeError::ClientError(_) | stripe::StripeError::Timeout => {
ErrorKind::Transport
}
stripe::StripeError::ConfigError(_) => ErrorKind::InvalidRequest,
stripe::StripeError::Stripe(..) => unreachable!("matched by the let-else"),
};
return Error::new(kind, PROVIDER, error.to_string());
};
let kind = match &api.type_ {
stripe_shared::ApiErrorsType::CardError => ErrorKind::Declined,
stripe_shared::ApiErrorsType::ApiError => ErrorKind::Provider,
_ => kind_for_status(*status),
};
let message = api
.message
.clone()
.unwrap_or_else(|| format!("Stripe answered {status} with no message"));
let error = Error::new(kind, PROVIDER, message);
match api
.decline_code
.as_deref()
.or_else(|| api.code.as_ref().map(stripe_shared::ApiErrorsCode::as_str))
{
Some(code) => error.with_code(code),
None => error,
}
}
const fn kind_for_status(code: u16) -> ErrorKind {
match code {
401 | 403 => ErrorKind::Auth,
402 => ErrorKind::Declined,
404 => ErrorKind::NotFound,
429 => ErrorKind::RateLimited,
400 | 422 => ErrorKind::InvalidRequest,
_ => ErrorKind::Provider,
}
}
#[cfg(test)]
mod tests {
use kasapay_core::{Currency, ErrorKind, Status};
#[test]
fn a_refused_card_is_a_decline_and_a_bad_request_is_not() {
assert_eq!(super::kind_for_status(402), ErrorKind::Declined);
assert!(!ErrorKind::Declined.is_retryable());
assert_eq!(super::kind_for_status(400), ErrorKind::InvalidRequest);
assert_eq!(super::kind_for_status(401), ErrorKind::Auth);
}
#[test]
fn an_authentication_failure_is_not_a_bad_request() {
assert_eq!(super::kind_for_status(401), ErrorKind::Auth);
assert_eq!(super::kind_for_status(403), ErrorKind::Auth);
assert!(!ErrorKind::Auth.is_retryable());
}
#[test]
fn a_timeout_is_worth_retrying() {
assert!(super::error(&stripe::StripeError::Timeout).is_retryable());
}
#[test]
fn every_currency_stripe_settles_survives_the_round_trip() {
for currency in Currency::KNOWN.iter().copied() {
if let Ok(there) = super::currency(currency) {
assert_eq!(
super::currency_back(&there),
Some(currency),
"{currency} is sent to Stripe and cannot be read back"
);
}
}
}
#[test]
fn a_currency_stripe_cannot_settle_is_refused_rather_than_rounded() {
let error = super::currency(Currency::Kwd).expect_err("Stripe has no three-place currency");
assert_eq!(error.kind(), ErrorKind::Unsupported);
}
#[test]
fn the_three_stalled_statuses_collapse_to_one() {
use stripe_shared::PaymentIntentStatus as S;
for stalled in [
S::RequiresAction,
S::RequiresConfirmation,
S::RequiresPaymentMethod,
] {
assert_eq!(super::status(&stalled), Status::RequiresAction);
}
assert_eq!(super::status(&S::Succeeded), Status::Captured);
assert_eq!(super::status(&S::RequiresCapture), Status::Authorized);
}
}