Skip to main content

stateset_embedded/
payments.rs

1//! Payment operations for processing transactions and refunds
2//!
3//! # Example
4//!
5//! ```ignore
6//! use stateset_embedded::{Commerce, CreatePayment, PaymentMethodType, OrderId};
7//! use rust_decimal_macros::dec;
8//!
9//! let commerce = Commerce::new("./store.db")?;
10//!
11//! // Create a payment for an order
12//! let payment = commerce.payments().create(CreatePayment {
13//!     order_id: Some(OrderId::new()),
14//!     payment_method: PaymentMethodType::CreditCard,
15//!     amount: dec!(99.99),
16//!     card_brand: Some(stateset_embedded::CardBrand::Visa),
17//!     card_last4: Some("4242".into()),
18//!     ..Default::default()
19//! })?;
20//!
21//! // Mark payment as completed
22//! let payment = commerce.payments().mark_completed(payment.id)?;
23//!
24//! // Process a refund
25//! let refund = commerce.payments().create_refund(stateset_embedded::CreateRefund {
26//!     payment_id: payment.id,
27//!     amount: Some(dec!(25.00)),
28//!     reason: Some("Partial refund - damaged item".into()),
29//!     ..Default::default()
30//! })?;
31//! # Ok::<(), stateset_embedded::CommerceError>(())
32//! ```
33
34use crate::Database;
35use rust_decimal::prelude::ToPrimitive;
36use stateset_core::{
37    CommerceError, CreatePayment, CreatePaymentMethod, CreateRefund, CustomerId, OrderId, Payment,
38    PaymentFilter, PaymentId, PaymentMethod, Refund, Result, Validate,
39};
40use stateset_observability::Metrics;
41use std::sync::Arc;
42use uuid::Uuid;
43
44/// Payment operations for transaction processing and refunds
45pub struct Payments {
46    db: Arc<dyn Database>,
47    metrics: Metrics,
48}
49
50impl std::fmt::Debug for Payments {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        f.debug_struct("Payments").finish_non_exhaustive()
53    }
54}
55
56impl Payments {
57    pub(crate) fn new(db: Arc<dyn Database>, metrics: Metrics) -> Self {
58        Self { db, metrics }
59    }
60
61    /// Create a new payment
62    ///
63    /// # Example
64    ///
65    /// ```rust,no_run
66    /// use stateset_embedded::{Commerce, CreatePayment, PaymentMethodType, CardBrand, OrderId, CurrencyCode};
67    /// use rust_decimal_macros::dec;
68    ///
69    /// let commerce = Commerce::new("./store.db")?;
70    ///
71    /// let payment = commerce.payments().create(CreatePayment {
72    ///     order_id: Some(OrderId::new()),
73    ///     payment_method: PaymentMethodType::CreditCard,
74    ///     amount: dec!(149.99),
75    ///     currency: Some(CurrencyCode::USD),
76    ///     card_brand: Some(CardBrand::Visa),
77    ///     card_last4: Some("4242".into()),
78    ///     billing_email: Some("customer@example.com".into()),
79    ///     ..Default::default()
80    /// })?;
81    /// # Ok::<(), stateset_embedded::CommerceError>(())
82    /// ```
83    #[tracing::instrument(skip(self, input), fields(amount = %input.amount, method = ?input.payment_method))]
84    pub fn create(&self, input: CreatePayment) -> Result<Payment> {
85        tracing::info!("creating payment");
86        // Reject a negative amount or malformed billing email before persisting.
87        input.validate()?;
88        self.db.payments().create(input)
89    }
90
91    /// Get a payment by ID
92    pub fn get(&self, id: PaymentId) -> Result<Option<Payment>> {
93        self.db.payments().get(id)
94    }
95
96    /// Get a payment by payment number (e.g., "PAY-20231215123456")
97    pub fn get_by_number(&self, payment_number: &str) -> Result<Option<Payment>> {
98        self.db.payments().get_by_number(payment_number)
99    }
100
101    /// Get a payment by external ID (e.g., Stripe payment intent ID)
102    pub fn get_by_external_id(&self, external_id: &str) -> Result<Option<Payment>> {
103        self.db.payments().get_by_external_id(external_id)
104    }
105
106    /// Update a payment
107    pub fn update(&self, id: PaymentId, input: stateset_core::UpdatePayment) -> Result<Payment> {
108        self.db.payments().update(id, input)
109    }
110
111    /// List payments with optional filtering
112    pub fn list(&self, filter: PaymentFilter) -> Result<Vec<Payment>> {
113        self.db.payments().list(filter)
114    }
115
116    /// Get all payments for an order
117    pub fn for_order(&self, order_id: OrderId) -> Result<Vec<Payment>> {
118        self.db.payments().for_order(order_id)
119    }
120
121    /// Get all payments for an invoice
122    pub fn for_invoice(&self, invoice_id: Uuid) -> Result<Vec<Payment>> {
123        self.db.payments().for_invoice(invoice_id.into())
124    }
125
126    /// Mark payment as processing
127    pub fn mark_processing(&self, id: PaymentId) -> Result<Payment> {
128        self.db.payments().mark_processing(id)
129    }
130
131    /// Mark payment as completed
132    ///
133    /// This records the payment timestamp and marks the transaction as successful.
134    #[tracing::instrument(skip(self), fields(payment_id = %id))]
135    pub fn mark_completed(&self, id: PaymentId) -> Result<Payment> {
136        tracing::info!("marking payment as completed");
137        let payment = self.db.payments().mark_completed(id)?;
138        self.metrics.record_payment_completed(
139            &payment.id.to_string(),
140            payment.amount.to_f64().unwrap_or(0.0),
141        );
142        Ok(payment)
143    }
144
145    /// Mark payment as failed
146    ///
147    /// # Arguments
148    ///
149    /// * `id` - Payment ID
150    /// * `reason` - Human-readable failure reason
151    /// * `code` - Optional error code from payment processor
152    pub fn mark_failed(&self, id: PaymentId, reason: &str, code: Option<&str>) -> Result<Payment> {
153        self.db.payments().mark_failed(id, reason, code)
154    }
155
156    /// Cancel a payment
157    pub fn cancel(&self, id: PaymentId) -> Result<Payment> {
158        self.db.payments().cancel(id)
159    }
160
161    /// Create a refund for a payment
162    ///
163    /// # Example
164    ///
165    /// ```rust,no_run
166    /// use stateset_embedded::{Commerce, CreateRefund, PaymentId};
167    /// use rust_decimal_macros::dec;
168    ///
169    /// let commerce = Commerce::new("./store.db")?;
170    ///
171    /// // Full refund (omit amount for full refund)
172    /// let refund = commerce.payments().create_refund(CreateRefund {
173    ///     payment_id: PaymentId::new(),
174    ///     reason: Some("Customer request".into()),
175    ///     ..Default::default()
176    /// })?;
177    ///
178    /// // Partial refund
179    /// let refund = commerce.payments().create_refund(CreateRefund {
180    ///     payment_id: PaymentId::new(),
181    ///     amount: Some(dec!(50.00)),
182    ///     reason: Some("Partial refund for damaged item".into()),
183    ///     ..Default::default()
184    /// })?;
185    /// # Ok::<(), stateset_embedded::CommerceError>(())
186    /// ```
187    #[tracing::instrument(skip(self, input), fields(payment_id = %input.payment_id))]
188    pub fn create_refund(&self, input: CreateRefund) -> Result<Refund> {
189        tracing::info!("creating refund");
190        // Reject a nil payment reference or non-positive requested amount before
191        // touching the database at all.
192        input.validate()?;
193        // Defense-in-depth: validate the refund against the payment's current
194        // status and remaining refundable balance before delegating. The DB
195        // backends enforce this too, but rejecting early keeps invalid refunds
196        // out of the persistence layer entirely.
197        let payment = self.db.payments().get(input.payment_id)?.ok_or(CommerceError::NotFound)?;
198        payment.validate_refund(input.amount)?;
199        self.db.payments().create_refund(input)
200    }
201
202    /// Get a refund by ID
203    pub fn get_refund(&self, id: Uuid) -> Result<Option<Refund>> {
204        self.db.payments().get_refund(id)
205    }
206
207    /// Get all refunds for a payment
208    pub fn get_refunds(&self, payment_id: PaymentId) -> Result<Vec<Refund>> {
209        self.db.payments().get_refunds(payment_id)
210    }
211
212    /// Complete a refund
213    ///
214    /// This marks the refund as processed and updates the payment's refunded amount.
215    pub fn complete_refund(&self, id: Uuid) -> Result<Refund> {
216        self.db.payments().complete_refund(id)
217    }
218
219    /// Mark a refund as failed
220    pub fn fail_refund(&self, id: Uuid, reason: &str) -> Result<Refund> {
221        self.db.payments().fail_refund(id, reason)
222    }
223
224    /// Create a stored payment method for a customer
225    ///
226    /// # Example
227    ///
228    /// ```rust,no_run
229    /// use stateset_embedded::{Commerce, CreatePaymentMethod, PaymentMethodType, CardBrand, CustomerId};
230    ///
231    /// let commerce = Commerce::new("./store.db")?;
232    ///
233    /// let method = commerce.payments().create_payment_method(CreatePaymentMethod {
234    ///     customer_id: CustomerId::new(),
235    ///     method_type: PaymentMethodType::CreditCard,
236    ///     is_default: Some(true),
237    ///     card_brand: Some(CardBrand::Visa),
238    ///     card_last4: Some("4242".into()),
239    ///     card_exp_month: Some(12),
240    ///     card_exp_year: Some(2025),
241    ///     cardholder_name: Some("Alice Smith".into()),
242    ///     ..Default::default()
243    /// })?;
244    /// # Ok::<(), stateset_embedded::CommerceError>(())
245    /// ```
246    pub fn create_payment_method(&self, input: CreatePaymentMethod) -> Result<PaymentMethod> {
247        self.db.payments().create_payment_method(input)
248    }
249
250    /// Get all payment methods for a customer
251    pub fn get_payment_methods(&self, customer_id: CustomerId) -> Result<Vec<PaymentMethod>> {
252        self.db.payments().get_payment_methods(customer_id)
253    }
254
255    /// Delete a payment method
256    pub fn delete_payment_method(&self, id: Uuid) -> Result<()> {
257        self.db.payments().delete_payment_method(id)
258    }
259
260    /// Set a payment method as the default for a customer
261    pub fn set_default_payment_method(
262        &self,
263        customer_id: CustomerId,
264        method_id: Uuid,
265    ) -> Result<()> {
266        self.db.payments().set_default_payment_method(customer_id, method_id)
267    }
268
269    /// Count payments matching a filter
270    pub fn count(&self, filter: PaymentFilter) -> Result<u64> {
271        self.db.payments().count(filter)
272    }
273}