use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(transparent)]
pub struct Money(String);
impl Money {
#[must_use]
pub fn new(amount: impl Into<String>) -> Self {
Self(amount.into())
}
#[must_use]
pub fn bdt(amount: &str) -> Self {
Self(amount.to_string())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<&str> for Money {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
impl From<String> for Money {
fn from(s: String) -> Self {
Self(s)
}
}
impl AsRef<str> for Money {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for Money {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Currency {
#[default]
#[serde(rename = "BDT")]
Bdt,
}
impl fmt::Display for Currency {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Bdt => "BDT",
})
}
}
impl FromStr for Currency {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"BDT" => Ok(Self::Bdt),
_ => Err(()),
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Intent {
#[default]
Sale,
Authorization,
}
impl fmt::Display for Intent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Sale => "sale",
Self::Authorization => "authorization",
})
}
}
impl FromStr for Intent {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"sale" => Ok(Self::Sale),
"authorization" => Ok(Self::Authorization),
_ => Err(()),
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum TransactionStatus {
#[default]
Initiated,
Completed,
PendingAuthorized,
Authorized,
Expired,
Cancelled,
Declined,
}
impl fmt::Display for TransactionStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Initiated => "Initiated",
Self::Completed => "Completed",
Self::PendingAuthorized => "PendingAuthorized",
Self::Authorized => "Authorized",
Self::Expired => "Expired",
Self::Cancelled => "Cancelled",
Self::Declined => "Declined",
})
}
}
impl FromStr for TransactionStatus {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"Initiated" => Ok(Self::Initiated),
"Completed" => Ok(Self::Completed),
"PendingAuthorized" => Ok(Self::PendingAuthorized),
"Authorized" => Ok(Self::Authorized),
"Expired" => Ok(Self::Expired),
"Cancelled" => Ok(Self::Cancelled),
"Declined" => Ok(Self::Declined),
_ => Err(()),
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum PayerType {
#[default]
#[serde(rename = "Customer")]
Customer,
#[serde(rename = "Merchant")]
Merchant,
}
impl fmt::Display for PayerType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Customer => "Customer",
Self::Merchant => "Merchant",
})
}
}
impl FromStr for PayerType {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"Customer" => Ok(Self::Customer),
"Merchant" => Ok(Self::Merchant),
_ => Err(()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn money_round_trip_with_decimals() {
let m = Money::bdt("100.00");
let json = serde_json::to_string(&m).unwrap();
assert_eq!(json, "\"100.00\"");
let back: Money = serde_json::from_str(&json).unwrap();
assert_eq!(back, m);
}
#[test]
fn money_round_trip_without_decimals() {
let m = Money::bdt("15");
let json = serde_json::to_string(&m).unwrap();
assert_eq!(json, "\"15\"");
let back: Money = serde_json::from_str(&json).unwrap();
assert_eq!(back, m);
}
#[test]
fn money_round_trip_with_one_decimal() {
let m = Money::bdt("1234.5");
let json = serde_json::to_string(&m).unwrap();
assert_eq!(json, "\"1234.5\"");
let back: Money = serde_json::from_str(&json).unwrap();
assert_eq!(back, m);
}
#[test]
fn money_display_and_as_str() {
let m = Money::bdt("42");
assert_eq!(m.as_str(), "42");
assert_eq!(m.to_string(), "42");
}
#[test]
fn money_from_conversions() {
let m: Money = "10".into();
assert_eq!(m.as_str(), "10");
let m: Money = String::from("20").into();
assert_eq!(m.as_str(), "20");
}
#[test]
fn currency_serialises_as_bdt() {
let j = serde_json::to_string(&Currency::Bdt).unwrap();
assert_eq!(j, "\"BDT\"");
}
#[test]
fn intent_default_is_sale() {
assert_eq!(Intent::default(), Intent::Sale);
}
#[test]
fn intent_serialisation() {
assert_eq!(serde_json::to_string(&Intent::Sale).unwrap(), "\"sale\"");
assert_eq!(
serde_json::to_string(&Intent::Authorization).unwrap(),
"\"authorization\""
);
}
#[test]
fn transaction_status_serialisation() {
let j = serde_json::to_string(&TransactionStatus::PendingAuthorized).unwrap();
assert_eq!(j, "\"PendingAuthorized\"");
let j = serde_json::to_string(&TransactionStatus::Authorized).unwrap();
assert_eq!(j, "\"Authorized\"");
}
#[test]
fn intent_from_str_parses_known() {
assert_eq!("sale".parse::<Intent>().unwrap(), Intent::Sale);
assert_eq!(
"authorization".parse::<Intent>().unwrap(),
Intent::Authorization
);
assert!("unknown".parse::<Intent>().is_err());
}
#[test]
fn transaction_status_from_str_parses_all_six() {
assert_eq!(
"Initiated".parse::<TransactionStatus>().unwrap(),
TransactionStatus::Initiated
);
assert_eq!(
"Completed".parse::<TransactionStatus>().unwrap(),
TransactionStatus::Completed
);
assert_eq!(
"PendingAuthorized".parse::<TransactionStatus>().unwrap(),
TransactionStatus::PendingAuthorized
);
assert_eq!(
"Authorized".parse::<TransactionStatus>().unwrap(),
TransactionStatus::Authorized
);
assert_eq!(
"Expired".parse::<TransactionStatus>().unwrap(),
TransactionStatus::Expired
);
assert_eq!(
"Cancelled".parse::<TransactionStatus>().unwrap(),
TransactionStatus::Cancelled
);
assert_eq!(
"Declined".parse::<TransactionStatus>().unwrap(),
TransactionStatus::Declined
);
assert!("Foo".parse::<TransactionStatus>().is_err());
}
#[test]
fn payer_type_display_and_from_str() {
assert_eq!(PayerType::Customer.to_string(), "Customer");
assert_eq!(PayerType::Merchant.to_string(), "Merchant");
assert_eq!(
"Customer".parse::<PayerType>().unwrap(),
PayerType::Customer
);
}
use proptest::prelude::*;
proptest! {
#[test]
fn money_round_trip_arbitrary(s in "[0-9.]{0,32}") {
let m = Money::new(s.clone());
let json = serde_json::to_string(&m).unwrap();
prop_assert_eq!(json.clone(), format!("\"{}\"", s));
let back: Money = serde_json::from_str(&json).unwrap();
prop_assert_eq!(back, m);
}
}
}