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}