Skip to main content

stateset_embedded/
returns.rs

1//! Return operations
2
3use stateset_core::{
4    CreateReturn, CustomerId, OrderId, Result, Return, ReturnFilter, ReturnId, ReturnStatus,
5    UpdateReturn, Validate,
6};
7use stateset_db::Database;
8use stateset_observability::Metrics;
9use std::sync::Arc;
10
11#[cfg(feature = "events")]
12use crate::events::EventSystem;
13#[cfg(feature = "events")]
14use rust_decimal::Decimal;
15#[cfg(feature = "events")]
16use stateset_core::CommerceEvent;
17
18/// Return operations interface.
19pub struct Returns {
20    db: Arc<dyn Database>,
21    metrics: Metrics,
22    #[cfg(feature = "events")]
23    event_system: Arc<EventSystem>,
24}
25
26impl std::fmt::Debug for Returns {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        f.debug_struct("Returns").finish_non_exhaustive()
29    }
30}
31
32impl Returns {
33    #[cfg(feature = "events")]
34    pub(crate) fn new(
35        db: Arc<dyn Database>,
36        event_system: Arc<EventSystem>,
37        metrics: Metrics,
38    ) -> Self {
39        Self { db, metrics, event_system }
40    }
41
42    #[cfg(not(feature = "events"))]
43    pub(crate) fn new(db: Arc<dyn Database>, metrics: Metrics) -> Self {
44        Self { db, metrics }
45    }
46
47    #[cfg(feature = "events")]
48    fn emit(&self, event: CommerceEvent) {
49        self.event_system.emit(event);
50    }
51
52    #[cfg(feature = "events")]
53    fn emit_status_change(&self, previous: &Return, updated: &Return) {
54        if previous.status != updated.status {
55            self.emit(CommerceEvent::ReturnStatusChanged {
56                return_id: updated.id,
57                from_status: previous.status,
58                to_status: updated.status,
59                timestamp: updated.updated_at,
60            });
61        }
62    }
63
64    /// Create a new return request.
65    ///
66    /// # Example
67    ///
68    /// ```rust,no_run
69    /// # use stateset_embedded::*;
70    /// # let commerce = Commerce::new(":memory:")?;
71    /// let ret = commerce.returns().create(CreateReturn {
72    ///     order_id: uuid::Uuid::new_v4().into(),
73    ///     reason: ReturnReason::Defective,
74    ///     items: vec![CreateReturnItem {
75    ///         order_item_id: uuid::Uuid::new_v4().into(),
76    ///         quantity: 1,
77    ///         ..Default::default()
78    ///     }],
79    ///     ..Default::default()
80    /// })?;
81    /// # Ok::<(), CommerceError>(())
82    /// ```
83    pub fn create(&self, input: CreateReturn) -> Result<Return> {
84        // Reject an empty/no-items return or a non-positive item quantity before
85        // persisting.
86        input.validate()?;
87        let ret = self.db.returns().create(input)?;
88        self.metrics.record_return_requested(&ret.id.to_string());
89        #[cfg(feature = "events")]
90        {
91            self.emit(CommerceEvent::ReturnRequested {
92                return_id: ret.id,
93                order_id: ret.order_id,
94                customer_id: ret.customer_id,
95                reason: ret.reason,
96                item_count: ret.items.len(),
97                timestamp: ret.created_at,
98            });
99        }
100        Ok(ret)
101    }
102
103    /// Get a return by ID.
104    pub fn get(&self, id: ReturnId) -> Result<Option<Return>> {
105        self.db.returns().get(id)
106    }
107
108    /// Update a return.
109    pub fn update(&self, id: ReturnId, input: UpdateReturn) -> Result<Return> {
110        #[cfg(feature = "events")]
111        let previous = self.db.returns().get(id)?;
112
113        let updated = self.db.returns().update(id, input)?;
114
115        #[cfg(feature = "events")]
116        if let Some(previous) = previous {
117            if previous.status != updated.status {
118                self.emit_status_change(&previous, &updated);
119                match updated.status {
120                    ReturnStatus::Approved => {
121                        self.emit(CommerceEvent::ReturnApproved {
122                            return_id: updated.id,
123                            order_id: updated.order_id,
124                            timestamp: updated.updated_at,
125                        });
126                    }
127                    ReturnStatus::Rejected => {
128                        self.emit(CommerceEvent::ReturnRejected {
129                            return_id: updated.id,
130                            order_id: updated.order_id,
131                            reason: updated.notes.clone().unwrap_or_default(),
132                            timestamp: updated.updated_at,
133                        });
134                    }
135                    ReturnStatus::Completed => {
136                        let refund_amount = updated.refund_amount.unwrap_or(Decimal::ZERO);
137                        self.emit(CommerceEvent::ReturnCompleted {
138                            return_id: updated.id,
139                            order_id: updated.order_id,
140                            refund_amount,
141                            timestamp: updated.updated_at,
142                        });
143                        if let (Some(amount), Some(method)) =
144                            (updated.refund_amount, updated.refund_method.clone())
145                        {
146                            self.emit(CommerceEvent::RefundIssued {
147                                return_id: updated.id,
148                                order_id: updated.order_id,
149                                amount,
150                                method,
151                                timestamp: updated.updated_at,
152                            });
153                        }
154                    }
155                    _ => {}
156                }
157            }
158        }
159
160        Ok(updated)
161    }
162
163    /// List returns with optional filtering.
164    pub fn list(&self, filter: ReturnFilter) -> Result<Vec<Return>> {
165        self.db.returns().list(filter)
166    }
167
168    /// List returns for a specific order.
169    pub fn list_for_order(&self, order_id: OrderId) -> Result<Vec<Return>> {
170        self.db.returns().list(ReturnFilter { order_id: Some(order_id), ..Default::default() })
171    }
172
173    /// List returns for a specific customer.
174    pub fn list_for_customer(&self, customer_id: CustomerId) -> Result<Vec<Return>> {
175        self.db
176            .returns()
177            .list(ReturnFilter { customer_id: Some(customer_id), ..Default::default() })
178    }
179
180    /// Approve a return request.
181    pub fn approve(&self, id: ReturnId) -> Result<Return> {
182        #[cfg(feature = "events")]
183        let previous = self.db.returns().get(id)?;
184
185        let ret = self.db.returns().approve(id)?;
186
187        #[cfg(feature = "events")]
188        {
189            if let Some(previous) = previous {
190                self.emit_status_change(&previous, &ret);
191            }
192            self.emit(CommerceEvent::ReturnApproved {
193                return_id: ret.id,
194                order_id: ret.order_id,
195                timestamp: ret.updated_at,
196            });
197        }
198
199        Ok(ret)
200    }
201
202    /// Reject a return request.
203    pub fn reject(&self, id: ReturnId, reason: &str) -> Result<Return> {
204        #[cfg(feature = "events")]
205        let previous = self.db.returns().get(id)?;
206
207        let ret = self.db.returns().reject(id, reason)?;
208
209        #[cfg(feature = "events")]
210        {
211            if let Some(previous) = previous {
212                self.emit_status_change(&previous, &ret);
213            }
214            self.emit(CommerceEvent::ReturnRejected {
215                return_id: ret.id,
216                order_id: ret.order_id,
217                reason: reason.to_string(),
218                timestamp: ret.updated_at,
219            });
220        }
221
222        Ok(ret)
223    }
224
225    /// Mark a return as received.
226    pub fn mark_received(&self, id: ReturnId) -> Result<Return> {
227        self.update(id, UpdateReturn { status: Some(ReturnStatus::Received), ..Default::default() })
228    }
229
230    /// Complete a return (process refund).
231    pub fn complete(&self, id: ReturnId) -> Result<Return> {
232        #[cfg(feature = "events")]
233        let previous = self.db.returns().get(id)?;
234
235        let ret = self.db.returns().complete(id)?;
236
237        #[cfg(feature = "events")]
238        {
239            if let Some(previous) = previous {
240                self.emit_status_change(&previous, &ret);
241            }
242            let refund_amount = ret.refund_amount.unwrap_or(Decimal::ZERO);
243            self.emit(CommerceEvent::ReturnCompleted {
244                return_id: ret.id,
245                order_id: ret.order_id,
246                refund_amount,
247                timestamp: ret.updated_at,
248            });
249            if let (Some(amount), Some(method)) = (ret.refund_amount, ret.refund_method.clone()) {
250                self.emit(CommerceEvent::RefundIssued {
251                    return_id: ret.id,
252                    order_id: ret.order_id,
253                    amount,
254                    method,
255                    timestamp: ret.updated_at,
256                });
257            }
258        }
259
260        Ok(ret)
261    }
262
263    /// Cancel a return.
264    pub fn cancel(&self, id: ReturnId) -> Result<Return> {
265        #[cfg(feature = "events")]
266        let previous = self.db.returns().get(id)?;
267
268        let ret = self.db.returns().cancel(id)?;
269
270        #[cfg(feature = "events")]
271        if let Some(previous) = previous {
272            self.emit_status_change(&previous, &ret);
273        }
274
275        Ok(ret)
276    }
277
278    /// Count returns matching a filter.
279    pub fn count(&self, filter: ReturnFilter) -> Result<u64> {
280        self.db.returns().count(filter)
281    }
282
283    /// Add tracking number to a return.
284    pub fn add_tracking(&self, id: ReturnId, tracking_number: &str) -> Result<Return> {
285        self.update(
286            id,
287            UpdateReturn {
288                tracking_number: Some(tracking_number.to_string()),
289                status: Some(ReturnStatus::InTransit),
290                ..Default::default()
291            },
292        )
293    }
294
295    /// List pending returns (awaiting approval).
296    pub fn list_pending(&self) -> Result<Vec<Return>> {
297        self.db
298            .returns()
299            .list(ReturnFilter { status: Some(ReturnStatus::Requested), ..Default::default() })
300    }
301}