use std::collections::BTreeMap;
use url::Url;
use crate::charge::{IdempotencyKey, NextAction};
use crate::id::{PaymentId, RefundId};
use crate::money::{Money, MoneyError};
use crate::provider::ProviderId;
use crate::raw::Raw;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum RefundReason {
Duplicate,
Fraudulent,
RequestedByCustomer,
Other(Box<str>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum RefundStatus {
Pending,
RequiresAction,
Succeeded,
Failed,
Canceled,
}
impl RefundStatus {
#[must_use]
pub const fn is_open(self) -> bool {
matches!(self, Self::Pending | Self::RequiresAction)
}
}
#[derive(Debug, Clone)]
pub struct Refund {
pub id: Option<RefundId>,
pub payment: PaymentId,
pub amount: Money,
pub status: RefundStatus,
pub next_action: Option<NextAction>,
pub provider: ProviderId,
pub raw: Raw,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct RefundRequest {
pub payment: PaymentId,
pub amount: Option<Money>,
pub reason: Option<RefundReason>,
pub idempotency_key: Option<IdempotencyKey>,
pub customer: Option<Box<str>>,
pub return_url: Option<Url>,
pub metadata: BTreeMap<String, String>,
}
impl RefundRequest {
#[must_use]
pub fn builder(payment: PaymentId) -> RefundRequestBuilder {
RefundRequestBuilder {
payment,
amount: None,
reason: None,
idempotency_key: None,
customer: None,
return_url: None,
metadata: BTreeMap::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct RefundRequestBuilder {
payment: PaymentId,
amount: Option<Money>,
reason: Option<RefundReason>,
idempotency_key: Option<IdempotencyKey>,
customer: Option<Box<str>>,
return_url: Option<Url>,
metadata: BTreeMap<String, String>,
}
impl RefundRequestBuilder {
#[must_use]
pub fn amount(mut self, amount: Money) -> Self {
self.amount = Some(amount);
self
}
#[must_use]
pub fn reason(mut self, reason: RefundReason) -> Self {
self.reason = Some(reason);
self
}
#[must_use]
pub fn idempotency_key(mut self, key: IdempotencyKey) -> Self {
self.idempotency_key = Some(key);
self
}
#[must_use]
pub fn customer(mut self, customer: impl Into<Box<str>>) -> Self {
self.customer = Some(customer.into());
self
}
#[must_use]
pub fn return_url(mut self, url: Url) -> Self {
self.return_url = Some(url);
self
}
#[must_use]
pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn build(self) -> Result<RefundRequest, RefundRequestError> {
if self.payment.as_str().is_empty() {
return Err(RefundRequestError::EmptyPaymentId);
}
if let Some(amount) = self.amount {
amount.require_positive()?;
}
Ok(RefundRequest {
payment: self.payment,
amount: self.amount,
reason: self.reason,
idempotency_key: self.idempotency_key,
customer: self.customer,
return_url: self.return_url,
metadata: self.metadata,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum RefundRequestError {
#[error("payment identifier is empty")]
EmptyPaymentId,
#[error(transparent)]
Amount(#[from] MoneyError),
}
#[cfg(test)]
mod tests {
use super::{RefundRequest, RefundRequestError, RefundStatus};
use crate::id::PaymentId;
use crate::money::{Currency, Money};
#[test]
fn a_refund_with_no_amount_asks_for_all_of_it() {
let request = RefundRequest::builder(PaymentId::issued("pi_1"))
.build()
.expect("valid request");
assert!(request.amount.is_none());
}
#[test]
fn build_rejects_a_zero_amount() {
let err = RefundRequest::builder(PaymentId::issued("pi_1"))
.amount(Money::from_minor_units(0, Currency::Try))
.build()
.expect_err("zero is not refundable");
assert!(matches!(err, RefundRequestError::Amount(_)));
}
#[test]
fn build_rejects_a_payment_nothing_names() {
let err = RefundRequest::builder(PaymentId::issued(""))
.build()
.expect_err("an empty identifier is not usable");
assert_eq!(err, RefundRequestError::EmptyPaymentId);
}
#[test]
fn only_a_settled_refund_is_closed() {
assert!(RefundStatus::Pending.is_open());
assert!(RefundStatus::RequiresAction.is_open());
assert!(!RefundStatus::Succeeded.is_open());
assert!(!RefundStatus::Failed.is_open());
assert!(!RefundStatus::Canceled.is_open());
}
}