use std::collections::BTreeMap;
use std::fmt;
use url::Url;
use crate::id::PaymentId;
use crate::money::{Money, MoneyError};
use crate::provider::ProviderId;
use crate::raw::Raw;
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct OrderRef(Box<str>);
impl OrderRef {
pub fn new(value: impl Into<Box<str>>) -> Self {
Self(value.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for OrderRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct IdempotencyKey(Box<str>);
impl IdempotencyKey {
pub fn new(value: impl Into<Box<str>>) -> Self {
Self(value.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Status {
Pending,
RequiresAction,
Authorized,
Captured,
Failed,
Canceled,
}
impl Status {
#[must_use]
pub const fn is_open(self) -> bool {
matches!(
self,
Self::Pending | Self::RequiresAction | Self::Authorized
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum NextAction {
Redirect {
url: Url,
continuation: Option<Box<str>>,
},
ConfirmOnClient {
client_secret: Box<str>,
},
}
#[derive(Debug, Clone)]
pub struct Charge {
pub id: Option<PaymentId>,
pub order: Option<OrderRef>,
pub amount: Money,
pub order_amount: Option<Money>,
pub status: Status,
pub next_action: Option<NextAction>,
pub provider: ProviderId,
pub raw: Raw,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ChargeRequest {
pub order: OrderRef,
pub amount: Money,
pub customer: Option<Box<str>>,
pub description: Option<Box<str>>,
pub return_url: Option<Url>,
pub idempotency_key: Option<IdempotencyKey>,
pub metadata: BTreeMap<String, String>,
}
impl ChargeRequest {
#[must_use]
pub fn builder(order: OrderRef, amount: Money) -> ChargeRequestBuilder {
ChargeRequestBuilder {
order,
amount,
customer: None,
description: None,
return_url: None,
idempotency_key: None,
metadata: BTreeMap::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct ChargeRequestBuilder {
order: OrderRef,
amount: Money,
customer: Option<Box<str>>,
description: Option<Box<str>>,
return_url: Option<Url>,
idempotency_key: Option<IdempotencyKey>,
metadata: BTreeMap<String, String>,
}
impl ChargeRequestBuilder {
#[must_use]
pub fn customer(mut self, customer: impl Into<Box<str>>) -> Self {
self.customer = Some(customer.into());
self
}
#[must_use]
pub fn description(mut self, description: impl Into<Box<str>>) -> Self {
self.description = Some(description.into());
self
}
#[must_use]
pub fn return_url(mut self, url: Url) -> Self {
self.return_url = Some(url);
self
}
#[must_use]
pub fn idempotency_key(mut self, key: IdempotencyKey) -> Self {
self.idempotency_key = Some(key);
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<ChargeRequest, ChargeRequestError> {
if self.order.as_str().is_empty() {
return Err(ChargeRequestError::EmptyOrderRef);
}
self.amount.require_positive()?;
Ok(ChargeRequest {
order: self.order,
amount: self.amount,
customer: self.customer,
description: self.description,
return_url: self.return_url,
idempotency_key: self.idempotency_key,
metadata: self.metadata,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum ChargeRequestError {
#[error("order reference is empty")]
EmptyOrderRef,
#[error(transparent)]
Amount(#[from] MoneyError),
}
#[cfg(test)]
mod tests {
use super::{ChargeRequest, ChargeRequestError, OrderRef};
use crate::money::{Currency, Money};
fn ten_lira() -> Money {
Money::from_minor_units(1000, Currency::Try)
}
#[test]
fn a_built_request_keeps_what_was_set() {
let request = ChargeRequest::builder(OrderRef::new("ord-1"), ten_lira())
.description("bir kahve")
.metadata("site", "vucod")
.build()
.expect("valid request");
assert_eq!(request.order.as_str(), "ord-1");
assert_eq!(request.description.as_deref(), Some("bir kahve"));
assert_eq!(
request.metadata.get("site").map(String::as_str),
Some("vucod")
);
assert!(request.customer.is_none());
}
#[test]
fn build_rejects_a_zero_amount() {
let err = ChargeRequest::builder(
OrderRef::new("ord-1"),
Money::from_minor_units(0, Currency::Try),
)
.build()
.expect_err("zero is not chargeable");
assert!(matches!(err, ChargeRequestError::Amount(_)));
}
#[test]
fn build_rejects_an_empty_order_reference() {
let err = ChargeRequest::builder(OrderRef::new(""), ten_lira())
.build()
.expect_err("an empty reference is not usable");
assert_eq!(err, ChargeRequestError::EmptyOrderRef);
}
}