Skip to main content

domain/payment/
mod.rs

1//! Payment-domain values for deposits, references, and review-safe money movement state.
2//!
3//! These types record payment readiness without performing provider writes. That keeps deposit
4//! exceptions and front-desk collection work reviewable:
5//!
6//! ```
7//! use domain::{money, payment};
8//!
9//! let amount = money::Money::new(
10//!     money::MinorUnits::try_new(5_000).unwrap(),
11//!     money::Currency::Usd,
12//! );
13//! let deposit = payment::Deposit::paid(
14//!     amount,
15//!     payment::Reference::try_new("sandbox-receipt-42").unwrap(),
16//! );
17//!
18//! assert_eq!(deposit.status(), payment::DepositStatus::Paid);
19//! assert!(!deposit.requires_collection());
20//! assert!(deposit.payment_reference().is_some());
21//! ```
22
23mod error;
24
25use chrono::{DateTime, Utc};
26use serde::{Deserialize, Deserializer, Serialize};
27
28use crate::money::Money;
29
30pub use error::{Error, Result};
31
32#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
33/// External payment or POS reference used to reconcile pet-resort charges.
34pub struct Reference(String);
35
36impl Reference {
37    /// Promotes an external payment/POS reference after trimming and length validation.
38    pub fn try_new(value: impl Into<String>) -> Result<Self> {
39        let value = value.into().trim().to_string();
40        if value.is_empty() {
41            return Err(Error::EmptyReference);
42        }
43        if value.chars().count() > 160 {
44            return Err(Error::ReferenceTooLong);
45        }
46        Ok(Self(value))
47    }
48
49    /// Returns the owned inner string for storage or outbound mapping.
50    pub fn into_inner(self) -> String {
51        self.0
52    }
53}
54
55impl<'de> Deserialize<'de> for Reference {
56    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
57    where
58        D: Deserializer<'de>,
59    {
60        Self::try_new(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
61    }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
65/// Lifecycle states for collecting, waiving, refunding, or failing a reservation deposit.
66pub enum DepositStatus {
67    /// No deposit or review is needed for this reservation path.
68    NotRequired,
69    /// A deposit must be collected before the booking is secure.
70    Required,
71    /// The required deposit has been collected and reconciled.
72    Paid,
73    /// The collected deposit has been returned to the customer.
74    Refunded,
75    /// Deposit collection was attempted but did not succeed.
76    Failed,
77    /// A manager waived the deposit requirement for the booking.
78    WaivedByManager,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
82/// Deposit amount, status, refund window, and payment reference for a reservation.
83pub struct Deposit {
84    amount: Money,
85    refundable_until: Option<DateTime<Utc>>,
86    status: DepositStatus,
87    payment_reference: Option<Reference>,
88}
89
90impl Deposit {
91    /// Starts a deposit record that still needs front-desk collection.
92    pub const fn required(amount: Money) -> Self {
93        Self {
94            amount,
95            refundable_until: None,
96            status: DepositStatus::Required,
97            payment_reference: None,
98        }
99    }
100
101    /// Creates a deposit already reconciled to a payment reference.
102    pub fn paid(amount: Money, payment_reference: Reference) -> Self {
103        Self::required(amount).mark_paid(payment_reference)
104    }
105
106    /// Starts a deposit record waived by manager exception.
107    pub const fn waived(amount: Money) -> Self {
108        Self {
109            amount,
110            refundable_until: None,
111            status: DepositStatus::WaivedByManager,
112            payment_reference: None,
113        }
114    }
115
116    /// Sets the deadline through which the deposit remains refundable.
117    pub fn with_refundable_until(mut self, refundable_until: DateTime<Utc>) -> Self {
118        self.refundable_until = Some(refundable_until);
119        self
120    }
121
122    /// Marks the deposit as collected and stores the payment reference.
123    pub fn mark_paid(mut self, payment_reference: Reference) -> Self {
124        self.status = DepositStatus::Paid;
125        self.payment_reference = Some(payment_reference);
126        self
127    }
128
129    /// Returns the deposit amount recorded for payment review and reconciliation.
130    pub const fn amount(&self) -> &Money {
131        &self.amount
132    }
133
134    /// Returns the deposit refundable until recorded for payment review and reconciliation.
135    pub const fn refundable_until(&self) -> Option<DateTime<Utc>> {
136        self.refundable_until
137    }
138
139    /// Returns the deposit status recorded for payment review and reconciliation.
140    pub const fn status(&self) -> DepositStatus {
141        self.status
142    }
143
144    /// Returns the deposit payment reference recorded for payment review and reconciliation.
145    pub const fn payment_reference(&self) -> Option<&Reference> {
146        self.payment_reference.as_ref()
147    }
148
149    /// Reports whether this deposit still requires collection from the customer.
150    pub const fn requires_collection(&self) -> bool {
151        matches!(self.status, DepositStatus::Required | DepositStatus::Failed)
152    }
153}