Skip to main content

stateset_embedded/
lib.rs

1//! Python bindings for StateSet Embedded Commerce
2//!
3//! Provides a local-first commerce library with SQLite storage.
4//!
5//! ```python
6//! from stateset_embedded import Commerce
7//!
8//! commerce = Commerce("./store.db")
9//! customer = commerce.customers.create(
10//!     email="alice@example.com",
11//!     first_name="Alice",
12//!     last_name="Smith"
13//! )
14//! ```
15
16use pyo3::exceptions::{PyRuntimeError, PyValueError};
17use pyo3::prelude::*;
18use rust_decimal::Decimal;
19use serde_json;
20// Use :: prefix to refer to the external crate, not the pymodule
21use ::stateset_embedded::Commerce as RustCommerce;
22use std::sync::{Arc, Mutex};
23
24fn to_f64_or_nan<T>(value: T) -> f64
25where
26    T: TryInto<f64>,
27    <T as TryInto<f64>>::Error: std::fmt::Display,
28{
29    match value.try_into() {
30        Ok(converted) => converted,
31        Err(err) => {
32            eprintln!("stateset-embedded: failed to convert to f64: {}", err);
33            f64::NAN
34        }
35    }
36}
37
38// ============================================================================
39// Commerce
40// ============================================================================
41
42/// Main Commerce instance for local commerce operations.
43///
44/// Example:
45///     commerce = Commerce("./store.db")
46///     commerce = Commerce(":memory:")  # In-memory database
47#[pyclass]
48pub struct Commerce {
49    inner: Arc<Mutex<RustCommerce>>,
50}
51
52#[pymethods]
53impl Commerce {
54    /// Create a new Commerce instance with a database path.
55    ///
56    /// Args:
57    ///     db_path: Path to SQLite database file, or ":memory:" for in-memory.
58    #[new]
59    fn new(db_path: String) -> PyResult<Self> {
60        let commerce = RustCommerce::new(&db_path).map_err(|e| {
61            PyRuntimeError::new_err(format!("Failed to initialize commerce: {}", e))
62        })?;
63
64        Ok(Self { inner: Arc::new(Mutex::new(commerce)) })
65    }
66
67    /// Get the customers API.
68    #[getter]
69    fn customers(&self) -> Customers {
70        Customers { commerce: self.inner.clone() }
71    }
72
73    /// Get the orders API.
74    #[getter]
75    fn orders(&self) -> Orders {
76        Orders { commerce: self.inner.clone() }
77    }
78
79    /// Get the products API.
80    #[getter]
81    fn products(&self) -> Products {
82        Products { commerce: self.inner.clone() }
83    }
84
85    /// Get the custom objects API (custom states / metaobjects).
86    #[getter]
87    fn custom_objects(&self) -> CustomObjectsApi {
88        CustomObjectsApi { commerce: self.inner.clone() }
89    }
90
91    /// Alias for `custom_objects` (for users who prefer the "custom states" name).
92    #[getter]
93    fn custom_states(&self) -> CustomObjectsApi {
94        self.custom_objects()
95    }
96
97    /// Get the inventory API.
98    #[getter]
99    fn inventory(&self) -> Inventory {
100        Inventory { commerce: self.inner.clone() }
101    }
102
103    /// Get the returns API.
104    #[getter]
105    fn returns(&self) -> Returns {
106        Returns { commerce: self.inner.clone() }
107    }
108
109    /// Get the payments API.
110    #[getter]
111    fn payments(&self) -> Payments {
112        Payments { commerce: self.inner.clone() }
113    }
114
115    /// Get the shipments API.
116    #[getter]
117    fn shipments(&self) -> Shipments {
118        Shipments { commerce: self.inner.clone() }
119    }
120
121    /// Get the warranties API.
122    #[getter]
123    fn warranties(&self) -> Warranties {
124        Warranties { commerce: self.inner.clone() }
125    }
126
127    /// Get the purchase orders API.
128    #[getter]
129    fn purchase_orders(&self) -> PurchaseOrders {
130        PurchaseOrders { commerce: self.inner.clone() }
131    }
132
133    /// Get the invoices API.
134    #[getter]
135    fn invoices(&self) -> Invoices {
136        Invoices { commerce: self.inner.clone() }
137    }
138
139    /// Get the bill of materials API.
140    #[getter]
141    fn bom(&self) -> BomApi {
142        BomApi { commerce: self.inner.clone() }
143    }
144
145    /// Get the work orders API.
146    #[getter]
147    fn work_orders(&self) -> WorkOrders {
148        WorkOrders { commerce: self.inner.clone() }
149    }
150
151    /// Get the carts API.
152    #[getter]
153    fn carts(&self) -> Carts {
154        Carts { commerce: self.inner.clone() }
155    }
156
157    /// Get the analytics API.
158    #[getter]
159    fn analytics(&self) -> Analytics {
160        Analytics { commerce: self.inner.clone() }
161    }
162
163    /// Get the currency API.
164    #[getter]
165    fn currency(&self) -> CurrencyOperations {
166        CurrencyOperations { commerce: self.inner.clone() }
167    }
168
169    /// Get the subscriptions API.
170    #[getter]
171    fn subscriptions(&self) -> Subscriptions {
172        Subscriptions { commerce: self.inner.clone() }
173    }
174
175    /// Get the promotions API.
176    #[getter]
177    fn promotions(&self) -> PromotionsApi {
178        PromotionsApi { commerce: self.inner.clone() }
179    }
180
181    /// Get the tax API.
182    #[getter]
183    fn tax(&self) -> TaxApi {
184        TaxApi { commerce: self.inner.clone() }
185    }
186
187    /// Get the quality control API.
188    #[getter]
189    fn quality(&self) -> QualityApi {
190        QualityApi { commerce: self.inner.clone() }
191    }
192
193    /// Get the lots/batch tracking API.
194    #[getter]
195    fn lots(&self) -> LotsApi {
196        LotsApi { commerce: self.inner.clone() }
197    }
198
199    /// Get the serial numbers API.
200    #[getter]
201    fn serials(&self) -> SerialsApi {
202        SerialsApi { commerce: self.inner.clone() }
203    }
204
205    /// Get the warehouse API.
206    #[getter]
207    fn warehouse(&self) -> WarehouseApi {
208        WarehouseApi { commerce: self.inner.clone() }
209    }
210
211    /// Get the receiving API.
212    #[getter]
213    fn receiving(&self) -> ReceivingApi {
214        ReceivingApi { commerce: self.inner.clone() }
215    }
216
217    /// Get the fulfillment API.
218    #[getter]
219    fn fulfillment(&self) -> FulfillmentApi {
220        FulfillmentApi { commerce: self.inner.clone() }
221    }
222
223    /// Get the accounts payable API.
224    #[getter]
225    fn accounts_payable(&self) -> AccountsPayableApi {
226        AccountsPayableApi { commerce: self.inner.clone() }
227    }
228
229    /// Get the accounts receivable API.
230    #[getter]
231    fn accounts_receivable(&self) -> AccountsReceivableApi {
232        AccountsReceivableApi { commerce: self.inner.clone() }
233    }
234
235    /// Get the cost accounting API.
236    #[getter]
237    fn cost_accounting(&self) -> CostAccountingApi {
238        CostAccountingApi { commerce: self.inner.clone() }
239    }
240
241    /// Get the credit management API.
242    #[getter]
243    fn credit(&self) -> CreditApi {
244        CreditApi { commerce: self.inner.clone() }
245    }
246
247    /// Get the backorder management API.
248    #[getter]
249    fn backorder(&self) -> BackorderApi {
250        BackorderApi { commerce: self.inner.clone() }
251    }
252
253    /// Get the general ledger API.
254    #[getter]
255    fn general_ledger(&self) -> GeneralLedgerApi {
256        GeneralLedgerApi { commerce: self.inner.clone() }
257    }
258
259    /// Get the vector search API for semantic search operations.
260    ///
261    /// Requires OPENAI_API_KEY environment variable to be set.
262    ///
263    /// Example:
264    ///     vector = commerce.vector("sk-...")
265    ///     results = vector.search_products("wireless bluetooth headphones", limit=10)
266    fn vector(&self, openai_api_key: String) -> PyResult<VectorSearch> {
267        Ok(VectorSearch { commerce: self.inner.clone(), api_key: openai_api_key })
268    }
269}
270
271// ============================================================================
272// Customer Types
273// ============================================================================
274
275/// Customer data returned from operations.
276#[pyclass]
277#[derive(Clone)]
278pub struct Customer {
279    #[pyo3(get)]
280    id: String,
281    #[pyo3(get)]
282    email: String,
283    #[pyo3(get)]
284    first_name: String,
285    #[pyo3(get)]
286    last_name: String,
287    #[pyo3(get)]
288    phone: Option<String>,
289    #[pyo3(get)]
290    status: String,
291    #[pyo3(get)]
292    accepts_marketing: bool,
293    #[pyo3(get)]
294    created_at: String,
295    #[pyo3(get)]
296    updated_at: String,
297}
298
299#[pymethods]
300impl Customer {
301    fn __repr__(&self) -> String {
302        format!(
303            "Customer(id='{}', email='{}', name='{} {}')",
304            self.id, self.email, self.first_name, self.last_name
305        )
306    }
307
308    /// Get the full name.
309    #[getter]
310    fn full_name(&self) -> String {
311        format!("{} {}", self.first_name, self.last_name)
312    }
313}
314
315impl From<stateset_core::Customer> for Customer {
316    fn from(c: stateset_core::Customer) -> Self {
317        Self {
318            id: c.id.to_string(),
319            email: c.email,
320            first_name: c.first_name,
321            last_name: c.last_name,
322            phone: c.phone,
323            status: format!("{}", c.status),
324            accepts_marketing: c.accepts_marketing,
325            created_at: c.created_at.to_rfc3339(),
326            updated_at: c.updated_at.to_rfc3339(),
327        }
328    }
329}
330
331// ============================================================================
332// Customers API
333// ============================================================================
334
335/// Customer management operations.
336#[pyclass]
337pub struct Customers {
338    commerce: Arc<Mutex<RustCommerce>>,
339}
340
341#[pymethods]
342impl Customers {
343    /// Create a new customer.
344    ///
345    /// Args:
346    ///     email: Customer email address (required)
347    ///     first_name: First name (required)
348    ///     last_name: Last name (required)
349    ///     phone: Phone number (optional)
350    ///     accepts_marketing: Marketing opt-in (optional, default False)
351    ///
352    /// Returns:
353    ///     Customer: The created customer
354    #[pyo3(signature = (email, first_name, last_name, phone=None, accepts_marketing=None))]
355    fn create(
356        &self,
357        email: String,
358        first_name: String,
359        last_name: String,
360        phone: Option<String>,
361        accepts_marketing: Option<bool>,
362    ) -> PyResult<Customer> {
363        let commerce = self
364            .commerce
365            .lock()
366            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
367
368        let customer = commerce
369            .customers()
370            .create(stateset_core::CreateCustomer {
371                email,
372                first_name,
373                last_name,
374                phone,
375                accepts_marketing,
376                ..Default::default()
377            })
378            .map_err(|e| PyRuntimeError::new_err(format!("Failed to create customer: {}", e)))?;
379
380        Ok(customer.into())
381    }
382
383    /// Get a customer by ID.
384    ///
385    /// Args:
386    ///     id: Customer UUID
387    ///
388    /// Returns:
389    ///     Customer or None if not found
390    fn get(&self, id: String) -> PyResult<Option<Customer>> {
391        let commerce = self
392            .commerce
393            .lock()
394            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
395
396        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
397
398        let customer = commerce
399            .customers()
400            .get(uuid)
401            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get customer: {}", e)))?;
402
403        Ok(customer.map(|c| c.into()))
404    }
405
406    /// Get a customer by email.
407    ///
408    /// Args:
409    ///     email: Customer email address
410    ///
411    /// Returns:
412    ///     Customer or None if not found
413    fn get_by_email(&self, email: String) -> PyResult<Option<Customer>> {
414        let commerce = self
415            .commerce
416            .lock()
417            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
418
419        let customer = commerce
420            .customers()
421            .get_by_email(&email)
422            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get customer: {}", e)))?;
423
424        Ok(customer.map(|c| c.into()))
425    }
426
427    /// List all customers.
428    ///
429    /// Returns:
430    ///     List[Customer]: All customers
431    fn list(&self) -> PyResult<Vec<Customer>> {
432        let commerce = self
433            .commerce
434            .lock()
435            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
436
437        let customers = commerce
438            .customers()
439            .list(Default::default())
440            .map_err(|e| PyRuntimeError::new_err(format!("Failed to list customers: {}", e)))?;
441
442        Ok(customers.into_iter().map(|c| c.into()).collect())
443    }
444
445    /// Count customers.
446    ///
447    /// Returns:
448    ///     int: Number of customers
449    fn count(&self) -> PyResult<u32> {
450        let commerce = self
451            .commerce
452            .lock()
453            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
454
455        let count = commerce
456            .customers()
457            .count(Default::default())
458            .map_err(|e| PyRuntimeError::new_err(format!("Failed to count customers: {}", e)))?;
459
460        Ok(count as u32)
461    }
462}
463
464// ============================================================================
465// Order Types
466// ============================================================================
467
468/// Order line item.
469#[pyclass]
470#[derive(Clone)]
471pub struct OrderItem {
472    #[pyo3(get)]
473    id: String,
474    #[pyo3(get)]
475    sku: String,
476    #[pyo3(get)]
477    name: String,
478    #[pyo3(get)]
479    quantity: i32,
480    #[pyo3(get)]
481    unit_price: f64,
482    #[pyo3(get)]
483    total: f64,
484}
485
486#[pymethods]
487impl OrderItem {
488    fn __repr__(&self) -> String {
489        format!("OrderItem(sku='{}', qty={}, price={})", self.sku, self.quantity, self.unit_price)
490    }
491}
492
493/// Order data returned from operations.
494#[pyclass]
495#[derive(Clone)]
496pub struct Order {
497    #[pyo3(get)]
498    id: String,
499    #[pyo3(get)]
500    order_number: String,
501    #[pyo3(get)]
502    customer_id: String,
503    #[pyo3(get)]
504    status: String,
505    #[pyo3(get)]
506    total_amount: f64,
507    #[pyo3(get)]
508    currency: String,
509    #[pyo3(get)]
510    payment_status: String,
511    #[pyo3(get)]
512    fulfillment_status: String,
513    #[pyo3(get)]
514    tracking_number: Option<String>,
515    #[pyo3(get)]
516    items: Vec<OrderItem>,
517    #[pyo3(get)]
518    version: i32,
519    #[pyo3(get)]
520    created_at: String,
521    #[pyo3(get)]
522    updated_at: String,
523}
524
525#[pymethods]
526impl Order {
527    fn __repr__(&self) -> String {
528        format!(
529            "Order(number='{}', status='{}', total={} {})",
530            self.order_number, self.status, self.total_amount, self.currency
531        )
532    }
533
534    /// Get the number of items in the order.
535    #[getter]
536    fn item_count(&self) -> usize {
537        self.items.len()
538    }
539}
540
541impl From<stateset_core::Order> for Order {
542    fn from(o: stateset_core::Order) -> Self {
543        Self {
544            id: o.id.to_string(),
545            order_number: o.order_number,
546            customer_id: o.customer_id.to_string(),
547            status: format!("{}", o.status),
548            total_amount: to_f64_or_nan(o.total_amount),
549            currency: o.currency,
550            payment_status: format!("{}", o.payment_status),
551            fulfillment_status: format!("{}", o.fulfillment_status),
552            tracking_number: o.tracking_number,
553            items: o
554                .items
555                .into_iter()
556                .map(|i| OrderItem {
557                    id: i.id.to_string(),
558                    sku: i.sku,
559                    name: i.name,
560                    quantity: i.quantity,
561                    unit_price: to_f64_or_nan(i.unit_price),
562                    total: to_f64_or_nan(i.total),
563                })
564                .collect(),
565            version: o.version,
566            created_at: o.created_at.to_rfc3339(),
567            updated_at: o.updated_at.to_rfc3339(),
568        }
569    }
570}
571
572/// Input for creating an order item.
573#[pyclass]
574#[derive(Clone)]
575pub struct CreateOrderItemInput {
576    #[pyo3(get, set)]
577    sku: String,
578    #[pyo3(get, set)]
579    name: String,
580    #[pyo3(get, set)]
581    quantity: i32,
582    #[pyo3(get, set)]
583    unit_price: f64,
584    #[pyo3(get, set)]
585    product_id: Option<String>,
586    #[pyo3(get, set)]
587    variant_id: Option<String>,
588}
589
590#[pymethods]
591impl CreateOrderItemInput {
592    #[new]
593    #[pyo3(signature = (sku, name, quantity, unit_price, product_id=None, variant_id=None))]
594    fn new(
595        sku: String,
596        name: String,
597        quantity: i32,
598        unit_price: f64,
599        product_id: Option<String>,
600        variant_id: Option<String>,
601    ) -> Self {
602        Self { sku, name, quantity, unit_price, product_id, variant_id }
603    }
604}
605
606// ============================================================================
607// Orders API
608// ============================================================================
609
610/// Order management operations.
611#[pyclass]
612pub struct Orders {
613    commerce: Arc<Mutex<RustCommerce>>,
614}
615
616#[pymethods]
617impl Orders {
618    /// Create a new order.
619    ///
620    /// Args:
621    ///     customer_id: Customer UUID
622    ///     items: List of CreateOrderItemInput
623    ///     currency: Currency code (default "USD")
624    ///     notes: Order notes (optional)
625    ///
626    /// Returns:
627    ///     Order: The created order
628    #[pyo3(signature = (customer_id, items, currency=None, notes=None))]
629    fn create(
630        &self,
631        customer_id: String,
632        items: Vec<CreateOrderItemInput>,
633        currency: Option<String>,
634        notes: Option<String>,
635    ) -> PyResult<Order> {
636        let commerce = self
637            .commerce
638            .lock()
639            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
640
641        let cust_uuid =
642            customer_id.parse().map_err(|_| PyValueError::new_err("Invalid customer UUID"))?;
643
644        let order_items: Vec<stateset_core::CreateOrderItem> = items
645            .into_iter()
646            .map(|i| {
647                let product_id = i.product_id.and_then(|s| s.parse().ok()).unwrap_or_default();
648                let variant_id = i.variant_id.and_then(|s| s.parse().ok());
649
650                stateset_core::CreateOrderItem {
651                    product_id,
652                    variant_id,
653                    sku: i.sku,
654                    name: i.name,
655                    quantity: i.quantity,
656                    unit_price: Decimal::from_f64_retain(i.unit_price).unwrap_or_default(),
657                    ..Default::default()
658                }
659            })
660            .collect();
661
662        let order = commerce
663            .orders()
664            .create(stateset_core::CreateOrder {
665                customer_id: cust_uuid,
666                items: order_items,
667                currency,
668                notes,
669                ..Default::default()
670            })
671            .map_err(|e| PyRuntimeError::new_err(format!("Failed to create order: {}", e)))?;
672
673        Ok(order.into())
674    }
675
676    /// Get an order by ID.
677    ///
678    /// Args:
679    ///     id: Order UUID
680    ///
681    /// Returns:
682    ///     Order or None if not found
683    fn get(&self, id: String) -> PyResult<Option<Order>> {
684        let commerce = self
685            .commerce
686            .lock()
687            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
688
689        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
690
691        let order = commerce
692            .orders()
693            .get(uuid)
694            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get order: {}", e)))?;
695
696        Ok(order.map(|o| o.into()))
697    }
698
699    /// List all orders.
700    ///
701    /// Returns:
702    ///     List[Order]: All orders
703    fn list(&self) -> PyResult<Vec<Order>> {
704        let commerce = self
705            .commerce
706            .lock()
707            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
708
709        let orders = commerce
710            .orders()
711            .list(Default::default())
712            .map_err(|e| PyRuntimeError::new_err(format!("Failed to list orders: {}", e)))?;
713
714        Ok(orders.into_iter().map(|o| o.into()).collect())
715    }
716
717    /// Update order status.
718    ///
719    /// Args:
720    ///     id: Order UUID
721    ///     status: New status (pending, confirmed, processing, shipped, delivered, cancelled, refunded)
722    ///
723    /// Returns:
724    ///     Order: The updated order
725    fn update_status(&self, id: String, status: String) -> PyResult<Order> {
726        let commerce = self
727            .commerce
728            .lock()
729            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
730
731        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
732
733        let order_status = match status.to_lowercase().as_str() {
734            "pending" => stateset_core::OrderStatus::Pending,
735            "confirmed" => stateset_core::OrderStatus::Confirmed,
736            "processing" => stateset_core::OrderStatus::Processing,
737            "shipped" => stateset_core::OrderStatus::Shipped,
738            "delivered" => stateset_core::OrderStatus::Delivered,
739            "cancelled" => stateset_core::OrderStatus::Cancelled,
740            "refunded" => stateset_core::OrderStatus::Refunded,
741            _ => return Err(PyValueError::new_err(format!("Invalid status: {}", status))),
742        };
743
744        let order = commerce
745            .orders()
746            .update_status(uuid, order_status)
747            .map_err(|e| PyRuntimeError::new_err(format!("Failed to update order: {}", e)))?;
748
749        Ok(order.into())
750    }
751
752    /// Ship an order.
753    ///
754    /// Args:
755    ///     id: Order UUID
756    ///     tracking_number: Tracking number (optional)
757    ///
758    /// Returns:
759    ///     Order: The shipped order
760    #[pyo3(signature = (id, tracking_number=None))]
761    fn ship(&self, id: String, tracking_number: Option<String>) -> PyResult<Order> {
762        let commerce = self
763            .commerce
764            .lock()
765            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
766
767        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
768
769        let order = commerce
770            .orders()
771            .ship(uuid, tracking_number.as_deref())
772            .map_err(|e| PyRuntimeError::new_err(format!("Failed to ship order: {}", e)))?;
773
774        Ok(order.into())
775    }
776
777    /// Cancel an order.
778    ///
779    /// Args:
780    ///     id: Order UUID
781    ///
782    /// Returns:
783    ///     Order: The cancelled order
784    fn cancel(&self, id: String) -> PyResult<Order> {
785        let commerce = self
786            .commerce
787            .lock()
788            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
789
790        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
791
792        let order = commerce
793            .orders()
794            .cancel(uuid)
795            .map_err(|e| PyRuntimeError::new_err(format!("Failed to cancel order: {}", e)))?;
796
797        Ok(order.into())
798    }
799
800    /// Count orders.
801    ///
802    /// Returns:
803    ///     int: Number of orders
804    fn count(&self) -> PyResult<u32> {
805        let commerce = self
806            .commerce
807            .lock()
808            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
809
810        let count = commerce
811            .orders()
812            .count(Default::default())
813            .map_err(|e| PyRuntimeError::new_err(format!("Failed to count orders: {}", e)))?;
814
815        Ok(count as u32)
816    }
817}
818
819// ============================================================================
820// Product Types
821// ============================================================================
822
823/// Product data returned from operations.
824#[pyclass]
825#[derive(Clone)]
826pub struct Product {
827    #[pyo3(get)]
828    id: String,
829    #[pyo3(get)]
830    name: String,
831    #[pyo3(get)]
832    slug: String,
833    #[pyo3(get)]
834    description: String,
835    #[pyo3(get)]
836    status: String,
837    #[pyo3(get)]
838    created_at: String,
839    #[pyo3(get)]
840    updated_at: String,
841}
842
843#[pymethods]
844impl Product {
845    fn __repr__(&self) -> String {
846        format!("Product(name='{}', slug='{}', status='{}')", self.name, self.slug, self.status)
847    }
848}
849
850impl From<stateset_core::Product> for Product {
851    fn from(p: stateset_core::Product) -> Self {
852        Self {
853            id: p.id.to_string(),
854            name: p.name,
855            slug: p.slug,
856            description: p.description,
857            status: format!("{}", p.status),
858            created_at: p.created_at.to_rfc3339(),
859            updated_at: p.updated_at.to_rfc3339(),
860        }
861    }
862}
863
864/// Product variant data.
865#[pyclass]
866#[derive(Clone)]
867pub struct ProductVariant {
868    #[pyo3(get)]
869    id: String,
870    #[pyo3(get)]
871    product_id: String,
872    #[pyo3(get)]
873    sku: String,
874    #[pyo3(get)]
875    name: String,
876    #[pyo3(get)]
877    price: f64,
878    #[pyo3(get)]
879    compare_at_price: Option<f64>,
880    #[pyo3(get)]
881    is_default: bool,
882}
883
884#[pymethods]
885impl ProductVariant {
886    fn __repr__(&self) -> String {
887        format!("ProductVariant(sku='{}', price={})", self.sku, self.price)
888    }
889}
890
891impl From<stateset_core::ProductVariant> for ProductVariant {
892    fn from(v: stateset_core::ProductVariant) -> Self {
893        Self {
894            id: v.id.to_string(),
895            product_id: v.product_id.to_string(),
896            sku: v.sku,
897            name: v.name,
898            price: to_f64_or_nan(v.price),
899            compare_at_price: v.compare_at_price.map(|d| to_f64_or_nan(d)),
900            is_default: v.is_default,
901        }
902    }
903}
904
905/// Input for creating a product variant.
906#[pyclass]
907#[derive(Clone)]
908pub struct CreateProductVariantInput {
909    #[pyo3(get, set)]
910    sku: String,
911    #[pyo3(get, set)]
912    name: Option<String>,
913    #[pyo3(get, set)]
914    price: f64,
915    #[pyo3(get, set)]
916    compare_at_price: Option<f64>,
917}
918
919#[pymethods]
920impl CreateProductVariantInput {
921    #[new]
922    #[pyo3(signature = (sku, price, name=None, compare_at_price=None))]
923    fn new(sku: String, price: f64, name: Option<String>, compare_at_price: Option<f64>) -> Self {
924        Self { sku, name, price, compare_at_price }
925    }
926}
927
928// ============================================================================
929// Products API
930// ============================================================================
931
932/// Product catalog operations.
933#[pyclass]
934pub struct Products {
935    commerce: Arc<Mutex<RustCommerce>>,
936}
937
938#[pymethods]
939impl Products {
940    /// Create a new product.
941    ///
942    /// Args:
943    ///     name: Product name
944    ///     description: Product description (optional)
945    ///     variants: List of CreateProductVariantInput (optional)
946    ///
947    /// Returns:
948    ///     Product: The created product
949    #[pyo3(signature = (name, description=None, variants=None))]
950    fn create(
951        &self,
952        name: String,
953        description: Option<String>,
954        variants: Option<Vec<CreateProductVariantInput>>,
955    ) -> PyResult<Product> {
956        let commerce = self
957            .commerce
958            .lock()
959            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
960
961        let variant_inputs = variants.map(|vs| {
962            vs.into_iter()
963                .map(|v| stateset_core::CreateProductVariant {
964                    sku: v.sku,
965                    name: v.name,
966                    price: Decimal::from_f64_retain(v.price).unwrap_or_default(),
967                    compare_at_price: v.compare_at_price.and_then(|p| Decimal::from_f64_retain(p)),
968                    ..Default::default()
969                })
970                .collect()
971        });
972
973        let product = commerce
974            .products()
975            .create(stateset_core::CreateProduct {
976                name,
977                description,
978                variants: variant_inputs,
979                ..Default::default()
980            })
981            .map_err(|e| PyRuntimeError::new_err(format!("Failed to create product: {}", e)))?;
982
983        Ok(product.into())
984    }
985
986    /// Get a product by ID.
987    ///
988    /// Args:
989    ///     id: Product UUID
990    ///
991    /// Returns:
992    ///     Product or None if not found
993    fn get(&self, id: String) -> PyResult<Option<Product>> {
994        let commerce = self
995            .commerce
996            .lock()
997            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
998
999        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
1000
1001        let product = commerce
1002            .products()
1003            .get(uuid)
1004            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get product: {}", e)))?;
1005
1006        Ok(product.map(|p| p.into()))
1007    }
1008
1009    /// Get a product variant by SKU.
1010    ///
1011    /// Args:
1012    ///     sku: Product variant SKU
1013    ///
1014    /// Returns:
1015    ///     ProductVariant or None if not found
1016    fn get_variant_by_sku(&self, sku: String) -> PyResult<Option<ProductVariant>> {
1017        let commerce = self
1018            .commerce
1019            .lock()
1020            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
1021
1022        let variant = commerce
1023            .products()
1024            .get_variant_by_sku(&sku)
1025            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get variant: {}", e)))?;
1026
1027        Ok(variant.map(|v| v.into()))
1028    }
1029
1030    /// List all products.
1031    ///
1032    /// Returns:
1033    ///     List[Product]: All products
1034    fn list(&self) -> PyResult<Vec<Product>> {
1035        let commerce = self
1036            .commerce
1037            .lock()
1038            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
1039
1040        let products = commerce
1041            .products()
1042            .list(Default::default())
1043            .map_err(|e| PyRuntimeError::new_err(format!("Failed to list products: {}", e)))?;
1044
1045        Ok(products.into_iter().map(|p| p.into()).collect())
1046    }
1047
1048    /// Count products.
1049    ///
1050    /// Returns:
1051    ///     int: Number of products
1052    fn count(&self) -> PyResult<u32> {
1053        let commerce = self
1054            .commerce
1055            .lock()
1056            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
1057
1058        let count = commerce
1059            .products()
1060            .count(Default::default())
1061            .map_err(|e| PyRuntimeError::new_err(format!("Failed to count products: {}", e)))?;
1062
1063        Ok(count as u32)
1064    }
1065}
1066
1067// ============================================================================
1068// Custom Objects (Custom States / Metaobjects)
1069// ============================================================================
1070
1071/// Custom field definition (output).
1072#[pyclass]
1073#[derive(Clone)]
1074pub struct CustomFieldDefinition {
1075    #[pyo3(get)]
1076    key: String,
1077    #[pyo3(get)]
1078    field_type: String,
1079    #[pyo3(get)]
1080    required: bool,
1081    #[pyo3(get)]
1082    list: bool,
1083    #[pyo3(get)]
1084    description: Option<String>,
1085}
1086
1087impl From<stateset_core::CustomFieldDefinition> for CustomFieldDefinition {
1088    fn from(f: stateset_core::CustomFieldDefinition) -> Self {
1089        Self {
1090            key: f.key,
1091            field_type: f.field_type.to_string(),
1092            required: f.required,
1093            list: f.list,
1094            description: f.description,
1095        }
1096    }
1097}
1098
1099/// Input for defining a custom field in a type schema.
1100#[pyclass]
1101#[derive(Clone)]
1102pub struct CustomFieldDefinitionInput {
1103    #[pyo3(get, set)]
1104    key: String,
1105    #[pyo3(get, set)]
1106    field_type: String,
1107    #[pyo3(get, set)]
1108    required: bool,
1109    #[pyo3(get, set)]
1110    list: bool,
1111    #[pyo3(get, set)]
1112    description: Option<String>,
1113}
1114
1115#[pymethods]
1116impl CustomFieldDefinitionInput {
1117    #[new]
1118    #[pyo3(signature = (key, field_type, required=false, list=false, description=None))]
1119    fn new(
1120        key: String,
1121        field_type: String,
1122        required: bool,
1123        list: bool,
1124        description: Option<String>,
1125    ) -> Self {
1126        Self { key, field_type, required, list, description }
1127    }
1128}
1129
1130/// Custom object type (schema) output.
1131#[pyclass]
1132#[derive(Clone)]
1133pub struct CustomObjectType {
1134    #[pyo3(get)]
1135    id: String,
1136    #[pyo3(get)]
1137    handle: String,
1138    #[pyo3(get)]
1139    display_name: String,
1140    #[pyo3(get)]
1141    description: String,
1142    #[pyo3(get)]
1143    fields: Vec<CustomFieldDefinition>,
1144    #[pyo3(get)]
1145    created_at: String,
1146    #[pyo3(get)]
1147    updated_at: String,
1148    #[pyo3(get)]
1149    version: i32,
1150}
1151
1152impl From<stateset_core::CustomObjectType> for CustomObjectType {
1153    fn from(t: stateset_core::CustomObjectType) -> Self {
1154        Self {
1155            id: t.id.to_string(),
1156            handle: t.handle,
1157            display_name: t.display_name,
1158            description: t.description,
1159            fields: t.fields.into_iter().map(|f| f.into()).collect(),
1160            created_at: t.created_at.to_rfc3339(),
1161            updated_at: t.updated_at.to_rfc3339(),
1162            version: t.version,
1163        }
1164    }
1165}
1166
1167/// Custom object record output.
1168#[pyclass]
1169#[derive(Clone)]
1170pub struct CustomObject {
1171    #[pyo3(get)]
1172    id: String,
1173    #[pyo3(get)]
1174    type_id: String,
1175    #[pyo3(get)]
1176    type_handle: String,
1177    #[pyo3(get)]
1178    handle: Option<String>,
1179    #[pyo3(get)]
1180    owner_type: Option<String>,
1181    #[pyo3(get)]
1182    owner_id: Option<String>,
1183    /// Values JSON string (always an object).
1184    #[pyo3(get)]
1185    values_json: String,
1186    #[pyo3(get)]
1187    created_at: String,
1188    #[pyo3(get)]
1189    updated_at: String,
1190    #[pyo3(get)]
1191    version: i32,
1192}
1193
1194impl From<stateset_core::CustomObject> for CustomObject {
1195    fn from(o: stateset_core::CustomObject) -> Self {
1196        let values_json = serde_json::to_string(&o.values).unwrap_or_else(|_| "{}".to_string());
1197        Self {
1198            id: o.id.to_string(),
1199            type_id: o.type_id.to_string(),
1200            type_handle: o.type_handle,
1201            handle: o.handle,
1202            owner_type: o.owner_type,
1203            owner_id: o.owner_id,
1204            values_json,
1205            created_at: o.created_at.to_rfc3339(),
1206            updated_at: o.updated_at.to_rfc3339(),
1207            version: o.version,
1208        }
1209    }
1210}
1211
1212fn parse_custom_field_type(s: &str) -> PyResult<stateset_core::CustomFieldType> {
1213    s.parse::<stateset_core::CustomFieldType>()
1214        .map_err(|e| PyValueError::new_err(format!("Invalid custom field type '{}': {}", s, e)))
1215}
1216
1217/// Custom objects API for defining schemas and storing typed records.
1218#[pyclass]
1219pub struct CustomObjectsApi {
1220    commerce: Arc<Mutex<RustCommerce>>,
1221}
1222
1223#[pymethods]
1224impl CustomObjectsApi {
1225    // ------------------------------------------------------------------------
1226    // Types
1227    // ------------------------------------------------------------------------
1228
1229    /// Create a new custom object type (schema).
1230    #[pyo3(signature = (handle, display_name, description=None, fields=None))]
1231    fn create_type(
1232        &self,
1233        handle: String,
1234        display_name: String,
1235        description: Option<String>,
1236        fields: Option<Vec<CustomFieldDefinitionInput>>,
1237    ) -> PyResult<CustomObjectType> {
1238        let commerce = self
1239            .commerce
1240            .lock()
1241            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
1242
1243        let mut out_fields = Vec::new();
1244        if let Some(fields) = fields {
1245            out_fields.reserve(fields.len());
1246            for f in fields {
1247                out_fields.push(stateset_core::CustomFieldDefinition {
1248                    key: f.key,
1249                    field_type: parse_custom_field_type(&f.field_type)?,
1250                    required: f.required,
1251                    list: f.list,
1252                    description: f.description,
1253                });
1254            }
1255        }
1256
1257        let ty = commerce
1258            .custom_objects()
1259            .create_type(stateset_core::CreateCustomObjectType {
1260                handle,
1261                display_name,
1262                description,
1263                fields: out_fields,
1264            })
1265            .map_err(|e| {
1266                PyRuntimeError::new_err(format!("Failed to create custom object type: {}", e))
1267            })?;
1268
1269        Ok(ty.into())
1270    }
1271
1272    /// Get a custom object type by ID.
1273    fn get_type(&self, id: String) -> PyResult<Option<CustomObjectType>> {
1274        let commerce = self
1275            .commerce
1276            .lock()
1277            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
1278
1279        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
1280
1281        let ty = commerce.custom_objects().get_type(uuid).map_err(|e| {
1282            PyRuntimeError::new_err(format!("Failed to get custom object type: {}", e))
1283        })?;
1284
1285        Ok(ty.map(|t| t.into()))
1286    }
1287
1288    /// Get a custom object type by handle.
1289    fn get_type_by_handle(&self, handle: String) -> PyResult<Option<CustomObjectType>> {
1290        let commerce = self
1291            .commerce
1292            .lock()
1293            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
1294
1295        let ty = commerce.custom_objects().get_type_by_handle(&handle).map_err(|e| {
1296            PyRuntimeError::new_err(format!("Failed to get custom object type: {}", e))
1297        })?;
1298
1299        Ok(ty.map(|t| t.into()))
1300    }
1301
1302    /// Update a custom object type.
1303    #[pyo3(signature = (id, display_name=None, description=None, fields=None))]
1304    fn update_type(
1305        &self,
1306        id: String,
1307        display_name: Option<String>,
1308        description: Option<String>,
1309        fields: Option<Vec<CustomFieldDefinitionInput>>,
1310    ) -> PyResult<CustomObjectType> {
1311        let commerce = self
1312            .commerce
1313            .lock()
1314            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
1315
1316        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
1317
1318        let fields = if let Some(fields) = fields {
1319            let mut out = Vec::with_capacity(fields.len());
1320            for f in fields {
1321                out.push(stateset_core::CustomFieldDefinition {
1322                    key: f.key,
1323                    field_type: parse_custom_field_type(&f.field_type)?,
1324                    required: f.required,
1325                    list: f.list,
1326                    description: f.description,
1327                });
1328            }
1329            Some(out)
1330        } else {
1331            None
1332        };
1333
1334        let updated = commerce
1335            .custom_objects()
1336            .update_type(
1337                uuid,
1338                stateset_core::UpdateCustomObjectType { display_name, description, fields },
1339            )
1340            .map_err(|e| {
1341                PyRuntimeError::new_err(format!("Failed to update custom object type: {}", e))
1342            })?;
1343
1344        Ok(updated.into())
1345    }
1346
1347    /// List custom object types.
1348    #[pyo3(signature = (search=None, limit=None, offset=None))]
1349    fn list_types(
1350        &self,
1351        search: Option<String>,
1352        limit: Option<u32>,
1353        offset: Option<u32>,
1354    ) -> PyResult<Vec<CustomObjectType>> {
1355        let commerce = self
1356            .commerce
1357            .lock()
1358            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
1359
1360        let list = commerce
1361            .custom_objects()
1362            .list_types(stateset_core::CustomObjectTypeFilter { search, limit, offset })
1363            .map_err(|e| {
1364                PyRuntimeError::new_err(format!("Failed to list custom object types: {}", e))
1365            })?;
1366
1367        Ok(list.into_iter().map(|t| t.into()).collect())
1368    }
1369
1370    /// Delete a custom object type.
1371    fn delete_type(&self, id: String) -> PyResult<()> {
1372        let commerce = self
1373            .commerce
1374            .lock()
1375            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
1376
1377        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
1378
1379        commerce.custom_objects().delete_type(uuid).map_err(|e| {
1380            PyRuntimeError::new_err(format!("Failed to delete custom object type: {}", e))
1381        })?;
1382
1383        Ok(())
1384    }
1385
1386    // ------------------------------------------------------------------------
1387    // Records
1388    // ------------------------------------------------------------------------
1389
1390    /// Create a new custom object record.
1391    #[pyo3(signature = (type_handle, values_json, handle=None, owner_type=None, owner_id=None))]
1392    fn create_object(
1393        &self,
1394        type_handle: String,
1395        values_json: String,
1396        handle: Option<String>,
1397        owner_type: Option<String>,
1398        owner_id: Option<String>,
1399    ) -> PyResult<CustomObject> {
1400        let commerce = self
1401            .commerce
1402            .lock()
1403            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
1404
1405        let values: serde_json::Value = serde_json::from_str(&values_json)
1406            .map_err(|e| PyValueError::new_err(format!("Invalid values_json: {}", e)))?;
1407
1408        let obj = commerce
1409            .custom_objects()
1410            .create_object(stateset_core::CreateCustomObject {
1411                type_handle,
1412                handle,
1413                owner_type,
1414                owner_id,
1415                values,
1416            })
1417            .map_err(|e| {
1418                PyRuntimeError::new_err(format!("Failed to create custom object: {}", e))
1419            })?;
1420
1421        Ok(obj.into())
1422    }
1423
1424    /// Get a custom object record by ID.
1425    fn get_object(&self, id: String) -> PyResult<Option<CustomObject>> {
1426        let commerce = self
1427            .commerce
1428            .lock()
1429            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
1430
1431        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
1432
1433        let obj = commerce
1434            .custom_objects()
1435            .get_object(uuid)
1436            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get custom object: {}", e)))?;
1437
1438        Ok(obj.map(|o| o.into()))
1439    }
1440
1441    /// Get a custom object record by type handle and object handle.
1442    fn get_object_by_handle(
1443        &self,
1444        type_handle: String,
1445        object_handle: String,
1446    ) -> PyResult<Option<CustomObject>> {
1447        let commerce = self
1448            .commerce
1449            .lock()
1450            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
1451
1452        let obj = commerce
1453            .custom_objects()
1454            .get_object_by_handle(&type_handle, &object_handle)
1455            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get custom object: {}", e)))?;
1456
1457        Ok(obj.map(|o| o.into()))
1458    }
1459
1460    /// Update a custom object record.
1461    #[pyo3(signature = (id, handle=None, owner_type=None, owner_id=None, values_json=None))]
1462    fn update_object(
1463        &self,
1464        id: String,
1465        handle: Option<String>,
1466        owner_type: Option<String>,
1467        owner_id: Option<String>,
1468        values_json: Option<String>,
1469    ) -> PyResult<CustomObject> {
1470        let commerce = self
1471            .commerce
1472            .lock()
1473            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
1474
1475        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
1476
1477        let values = if let Some(values_json) = values_json {
1478            Some(
1479                serde_json::from_str(&values_json)
1480                    .map_err(|e| PyValueError::new_err(format!("Invalid values_json: {}", e)))?,
1481            )
1482        } else {
1483            None
1484        };
1485
1486        let updated = commerce
1487            .custom_objects()
1488            .update_object(
1489                uuid,
1490                stateset_core::UpdateCustomObject { handle, owner_type, owner_id, values },
1491            )
1492            .map_err(|e| {
1493                PyRuntimeError::new_err(format!("Failed to update custom object: {}", e))
1494            })?;
1495
1496        Ok(updated.into())
1497    }
1498
1499    /// List custom object records.
1500    #[pyo3(signature = (type_handle=None, owner_type=None, owner_id=None, handle=None, limit=None, offset=None))]
1501    fn list_objects(
1502        &self,
1503        type_handle: Option<String>,
1504        owner_type: Option<String>,
1505        owner_id: Option<String>,
1506        handle: Option<String>,
1507        limit: Option<u32>,
1508        offset: Option<u32>,
1509    ) -> PyResult<Vec<CustomObject>> {
1510        let commerce = self
1511            .commerce
1512            .lock()
1513            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
1514
1515        let list = commerce
1516            .custom_objects()
1517            .list_objects(stateset_core::CustomObjectFilter {
1518                type_handle,
1519                owner_type,
1520                owner_id,
1521                handle,
1522                limit,
1523                offset,
1524            })
1525            .map_err(|e| {
1526                PyRuntimeError::new_err(format!("Failed to list custom objects: {}", e))
1527            })?;
1528
1529        Ok(list.into_iter().map(|o| o.into()).collect())
1530    }
1531
1532    /// Delete a custom object record.
1533    fn delete_object(&self, id: String) -> PyResult<()> {
1534        let commerce = self
1535            .commerce
1536            .lock()
1537            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
1538
1539        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
1540
1541        commerce.custom_objects().delete_object(uuid).map_err(|e| {
1542            PyRuntimeError::new_err(format!("Failed to delete custom object: {}", e))
1543        })?;
1544
1545        Ok(())
1546    }
1547}
1548
1549// ============================================================================
1550// Inventory Types
1551// ============================================================================
1552
1553/// Inventory item data.
1554#[pyclass]
1555#[derive(Clone)]
1556pub struct InventoryItem {
1557    #[pyo3(get)]
1558    id: i64,
1559    #[pyo3(get)]
1560    sku: String,
1561    #[pyo3(get)]
1562    name: String,
1563    #[pyo3(get)]
1564    description: Option<String>,
1565    #[pyo3(get)]
1566    unit_of_measure: String,
1567    #[pyo3(get)]
1568    is_active: bool,
1569}
1570
1571#[pymethods]
1572impl InventoryItem {
1573    fn __repr__(&self) -> String {
1574        format!("InventoryItem(sku='{}', name='{}')", self.sku, self.name)
1575    }
1576}
1577
1578impl From<stateset_core::InventoryItem> for InventoryItem {
1579    fn from(i: stateset_core::InventoryItem) -> Self {
1580        Self {
1581            id: i.id,
1582            sku: i.sku,
1583            name: i.name,
1584            description: i.description,
1585            unit_of_measure: i.unit_of_measure,
1586            is_active: i.is_active,
1587        }
1588    }
1589}
1590
1591/// Stock level information.
1592#[pyclass]
1593#[derive(Clone)]
1594pub struct StockLevel {
1595    #[pyo3(get)]
1596    sku: String,
1597    #[pyo3(get)]
1598    name: String,
1599    #[pyo3(get)]
1600    total_on_hand: f64,
1601    #[pyo3(get)]
1602    total_allocated: f64,
1603    #[pyo3(get)]
1604    total_available: f64,
1605}
1606
1607#[pymethods]
1608impl StockLevel {
1609    fn __repr__(&self) -> String {
1610        format!("StockLevel(sku='{}', available={})", self.sku, self.total_available)
1611    }
1612}
1613
1614impl From<stateset_core::StockLevel> for StockLevel {
1615    fn from(s: stateset_core::StockLevel) -> Self {
1616        Self {
1617            sku: s.sku,
1618            name: s.name,
1619            total_on_hand: to_f64_or_nan(s.total_on_hand),
1620            total_allocated: to_f64_or_nan(s.total_allocated),
1621            total_available: to_f64_or_nan(s.total_available),
1622        }
1623    }
1624}
1625
1626/// Inventory reservation.
1627#[pyclass]
1628#[derive(Clone)]
1629pub struct Reservation {
1630    #[pyo3(get)]
1631    id: String,
1632    #[pyo3(get)]
1633    item_id: i64,
1634    #[pyo3(get)]
1635    quantity: f64,
1636    #[pyo3(get)]
1637    status: String,
1638}
1639
1640#[pymethods]
1641impl Reservation {
1642    fn __repr__(&self) -> String {
1643        format!("Reservation(id='{}', qty={}, status='{}')", self.id, self.quantity, self.status)
1644    }
1645}
1646
1647impl From<stateset_core::InventoryReservation> for Reservation {
1648    fn from(r: stateset_core::InventoryReservation) -> Self {
1649        Self {
1650            id: r.id.to_string(),
1651            item_id: r.item_id,
1652            quantity: to_f64_or_nan(r.quantity),
1653            status: format!("{}", r.status),
1654        }
1655    }
1656}
1657
1658// ============================================================================
1659// Inventory API
1660// ============================================================================
1661
1662/// Inventory management operations.
1663#[pyclass]
1664pub struct Inventory {
1665    commerce: Arc<Mutex<RustCommerce>>,
1666}
1667
1668#[pymethods]
1669impl Inventory {
1670    /// Create a new inventory item.
1671    ///
1672    /// Args:
1673    ///     sku: Stock keeping unit
1674    ///     name: Item name
1675    ///     description: Item description (optional)
1676    ///     initial_quantity: Starting quantity (optional, default 0)
1677    ///     reorder_point: Reorder alert threshold (optional)
1678    ///
1679    /// Returns:
1680    ///     InventoryItem: The created item
1681    #[pyo3(signature = (sku, name, description=None, initial_quantity=None, reorder_point=None))]
1682    fn create_item(
1683        &self,
1684        sku: String,
1685        name: String,
1686        description: Option<String>,
1687        initial_quantity: Option<f64>,
1688        reorder_point: Option<f64>,
1689    ) -> PyResult<InventoryItem> {
1690        let commerce = self
1691            .commerce
1692            .lock()
1693            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
1694
1695        let item = commerce
1696            .inventory()
1697            .create_item(stateset_core::CreateInventoryItem {
1698                sku,
1699                name,
1700                description,
1701                initial_quantity: initial_quantity.and_then(|q| Decimal::from_f64_retain(q)),
1702                reorder_point: reorder_point.and_then(|r| Decimal::from_f64_retain(r)),
1703                ..Default::default()
1704            })
1705            .map_err(|e| {
1706                PyRuntimeError::new_err(format!("Failed to create inventory item: {}", e))
1707            })?;
1708
1709        Ok(item.into())
1710    }
1711
1712    /// Get stock level for a SKU.
1713    ///
1714    /// Args:
1715    ///     sku: Stock keeping unit
1716    ///
1717    /// Returns:
1718    ///     StockLevel or None if not found
1719    fn get_stock(&self, sku: String) -> PyResult<Option<StockLevel>> {
1720        let commerce = self
1721            .commerce
1722            .lock()
1723            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
1724
1725        let stock = commerce
1726            .inventory()
1727            .get_stock(&sku)
1728            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get stock: {}", e)))?;
1729
1730        Ok(stock.map(|s| s.into()))
1731    }
1732
1733    /// Adjust inventory quantity.
1734    ///
1735    /// Args:
1736    ///     sku: Stock keeping unit
1737    ///     quantity: Quantity to add (positive) or remove (negative)
1738    ///     reason: Reason for adjustment
1739    fn adjust(&self, sku: String, quantity: f64, reason: String) -> PyResult<()> {
1740        let commerce = self
1741            .commerce
1742            .lock()
1743            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
1744
1745        let qty = Decimal::from_f64_retain(quantity)
1746            .ok_or_else(|| PyValueError::new_err("Invalid quantity"))?;
1747
1748        commerce
1749            .inventory()
1750            .adjust(&sku, qty, &reason)
1751            .map_err(|e| PyRuntimeError::new_err(format!("Failed to adjust inventory: {}", e)))?;
1752
1753        Ok(())
1754    }
1755
1756    /// Reserve inventory for an order.
1757    ///
1758    /// Args:
1759    ///     sku: Stock keeping unit
1760    ///     quantity: Quantity to reserve
1761    ///     reference_type: Type of reference (e.g., "order")
1762    ///     reference_id: Reference identifier
1763    ///     expires_in_seconds: Reservation expiry time (optional)
1764    ///
1765    /// Returns:
1766    ///     Reservation: The created reservation
1767    #[pyo3(signature = (sku, quantity, reference_type, reference_id, expires_in_seconds=None))]
1768    fn reserve(
1769        &self,
1770        sku: String,
1771        quantity: f64,
1772        reference_type: String,
1773        reference_id: String,
1774        expires_in_seconds: Option<i64>,
1775    ) -> PyResult<Reservation> {
1776        let commerce = self
1777            .commerce
1778            .lock()
1779            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
1780
1781        let qty = Decimal::from_f64_retain(quantity)
1782            .ok_or_else(|| PyValueError::new_err("Invalid quantity"))?;
1783
1784        let reservation = commerce
1785            .inventory()
1786            .reserve(&sku, qty, &reference_type, &reference_id, expires_in_seconds)
1787            .map_err(|e| PyRuntimeError::new_err(format!("Failed to reserve inventory: {}", e)))?;
1788
1789        Ok(reservation.into())
1790    }
1791
1792    /// Confirm a reservation (deducts from on-hand).
1793    ///
1794    /// Args:
1795    ///     reservation_id: Reservation UUID
1796    fn confirm_reservation(&self, reservation_id: String) -> PyResult<()> {
1797        let commerce = self
1798            .commerce
1799            .lock()
1800            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
1801
1802        let uuid = reservation_id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
1803
1804        commerce.inventory().confirm_reservation(uuid).map_err(|e| {
1805            PyRuntimeError::new_err(format!("Failed to confirm reservation: {}", e))
1806        })?;
1807
1808        Ok(())
1809    }
1810
1811    /// Release a reservation (returns to available).
1812    ///
1813    /// Args:
1814    ///     reservation_id: Reservation UUID
1815    fn release_reservation(&self, reservation_id: String) -> PyResult<()> {
1816        let commerce = self
1817            .commerce
1818            .lock()
1819            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
1820
1821        let uuid = reservation_id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
1822
1823        commerce.inventory().release_reservation(uuid).map_err(|e| {
1824            PyRuntimeError::new_err(format!("Failed to release reservation: {}", e))
1825        })?;
1826
1827        Ok(())
1828    }
1829}
1830
1831// ============================================================================
1832// Return Types
1833// ============================================================================
1834
1835/// Return request data.
1836#[pyclass]
1837#[derive(Clone)]
1838pub struct Return {
1839    #[pyo3(get)]
1840    id: String,
1841    #[pyo3(get)]
1842    order_id: String,
1843    #[pyo3(get)]
1844    status: String,
1845    #[pyo3(get)]
1846    reason: String,
1847    #[pyo3(get)]
1848    idempotency_key: Option<String>,
1849    #[pyo3(get)]
1850    version: i32,
1851    #[pyo3(get)]
1852    created_at: String,
1853}
1854
1855#[pymethods]
1856impl Return {
1857    fn __repr__(&self) -> String {
1858        format!("Return(id='{}', status='{}', reason='{}')", self.id, self.status, self.reason)
1859    }
1860}
1861
1862impl From<stateset_core::Return> for Return {
1863    fn from(r: stateset_core::Return) -> Self {
1864        Self {
1865            id: r.id.to_string(),
1866            order_id: r.order_id.to_string(),
1867            status: format!("{}", r.status),
1868            reason: format!("{}", r.reason),
1869            idempotency_key: r.idempotency_key,
1870            version: r.version,
1871            created_at: r.created_at.to_rfc3339(),
1872        }
1873    }
1874}
1875
1876/// Input for creating a return item.
1877#[pyclass]
1878#[derive(Clone)]
1879pub struct CreateReturnItemInput {
1880    #[pyo3(get, set)]
1881    order_item_id: String,
1882    #[pyo3(get, set)]
1883    quantity: i32,
1884}
1885
1886#[pymethods]
1887impl CreateReturnItemInput {
1888    #[new]
1889    fn new(order_item_id: String, quantity: i32) -> Self {
1890        Self { order_item_id, quantity }
1891    }
1892}
1893
1894// ============================================================================
1895// Returns API
1896// ============================================================================
1897
1898/// Return processing operations.
1899#[pyclass]
1900pub struct Returns {
1901    commerce: Arc<Mutex<RustCommerce>>,
1902}
1903
1904#[pymethods]
1905impl Returns {
1906    /// Create a new return request.
1907    ///
1908    /// Args:
1909    ///     order_id: Order UUID
1910    ///     reason: Return reason (defective, not_as_described, wrong_item, etc.)
1911    ///     items: List of CreateReturnItemInput
1912    ///     reason_details: Additional details (optional)
1913    ///
1914    /// Returns:
1915    ///     Return: The created return
1916    #[pyo3(signature = (order_id, reason, items, reason_details=None, idempotency_key=None))]
1917    fn create(
1918        &self,
1919        order_id: String,
1920        reason: String,
1921        items: Vec<CreateReturnItemInput>,
1922        reason_details: Option<String>,
1923        idempotency_key: Option<String>,
1924    ) -> PyResult<Return> {
1925        let commerce = self
1926            .commerce
1927            .lock()
1928            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
1929
1930        let ord_uuid = order_id.parse().map_err(|_| PyValueError::new_err("Invalid order UUID"))?;
1931
1932        let return_reason = match reason.to_lowercase().as_str() {
1933            "defective" => stateset_core::ReturnReason::Defective,
1934            "not_as_described" => stateset_core::ReturnReason::NotAsDescribed,
1935            "wrong_item" => stateset_core::ReturnReason::WrongItem,
1936            "no_longer_needed" => stateset_core::ReturnReason::NoLongerNeeded,
1937            "changed_mind" => stateset_core::ReturnReason::ChangedMind,
1938            "better_price_found" => stateset_core::ReturnReason::BetterPriceFound,
1939            "damaged" => stateset_core::ReturnReason::Damaged,
1940            _ => stateset_core::ReturnReason::Other,
1941        };
1942
1943        let return_items: Vec<stateset_core::CreateReturnItem> = items
1944            .into_iter()
1945            .map(|i| {
1946                let order_item_id = i.order_item_id.parse().unwrap_or_default();
1947                stateset_core::CreateReturnItem {
1948                    order_item_id,
1949                    quantity: i.quantity,
1950                    ..Default::default()
1951                }
1952            })
1953            .collect();
1954
1955        let ret = commerce
1956            .returns()
1957            .create(stateset_core::CreateReturn {
1958                order_id: ord_uuid,
1959                reason: return_reason,
1960                reason_details,
1961                idempotency_key,
1962                items: return_items,
1963                ..Default::default()
1964            })
1965            .map_err(|e| PyRuntimeError::new_err(format!("Failed to create return: {}", e)))?;
1966
1967        Ok(ret.into())
1968    }
1969
1970    /// Get a return by ID.
1971    ///
1972    /// Args:
1973    ///     id: Return UUID
1974    ///
1975    /// Returns:
1976    ///     Return or None if not found
1977    fn get(&self, id: String) -> PyResult<Option<Return>> {
1978        let commerce = self
1979            .commerce
1980            .lock()
1981            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
1982
1983        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
1984
1985        let ret = commerce
1986            .returns()
1987            .get(uuid)
1988            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get return: {}", e)))?;
1989
1990        Ok(ret.map(|r| r.into()))
1991    }
1992
1993    /// Approve a return request.
1994    ///
1995    /// Args:
1996    ///     id: Return UUID
1997    ///
1998    /// Returns:
1999    ///     Return: The approved return
2000    fn approve(&self, id: String) -> PyResult<Return> {
2001        let commerce = self
2002            .commerce
2003            .lock()
2004            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
2005
2006        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
2007
2008        let ret = commerce
2009            .returns()
2010            .approve(uuid)
2011            .map_err(|e| PyRuntimeError::new_err(format!("Failed to approve return: {}", e)))?;
2012
2013        Ok(ret.into())
2014    }
2015
2016    /// Reject a return request.
2017    ///
2018    /// Args:
2019    ///     id: Return UUID
2020    ///     reason: Rejection reason
2021    ///
2022    /// Returns:
2023    ///     Return: The rejected return
2024    fn reject(&self, id: String, reason: String) -> PyResult<Return> {
2025        let commerce = self
2026            .commerce
2027            .lock()
2028            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
2029
2030        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
2031
2032        let ret = commerce
2033            .returns()
2034            .reject(uuid, &reason)
2035            .map_err(|e| PyRuntimeError::new_err(format!("Failed to reject return: {}", e)))?;
2036
2037        Ok(ret.into())
2038    }
2039
2040    /// List all returns.
2041    ///
2042    /// Returns:
2043    ///     List[Return]: All returns
2044    fn list(&self) -> PyResult<Vec<Return>> {
2045        let commerce = self
2046            .commerce
2047            .lock()
2048            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
2049
2050        let returns = commerce
2051            .returns()
2052            .list(Default::default())
2053            .map_err(|e| PyRuntimeError::new_err(format!("Failed to list returns: {}", e)))?;
2054
2055        Ok(returns.into_iter().map(|r| r.into()).collect())
2056    }
2057
2058    /// Count returns.
2059    ///
2060    /// Returns:
2061    ///     int: Number of returns
2062    fn count(&self) -> PyResult<u32> {
2063        let commerce = self
2064            .commerce
2065            .lock()
2066            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
2067
2068        let count = commerce
2069            .returns()
2070            .count(Default::default())
2071            .map_err(|e| PyRuntimeError::new_err(format!("Failed to count returns: {}", e)))?;
2072
2073        Ok(count as u32)
2074    }
2075}
2076
2077// ============================================================================
2078// Payment Types
2079// ============================================================================
2080
2081/// Payment data returned from operations.
2082#[pyclass]
2083#[derive(Clone)]
2084pub struct Payment {
2085    #[pyo3(get)]
2086    id: String,
2087    #[pyo3(get)]
2088    payment_number: String,
2089    #[pyo3(get)]
2090    order_id: Option<String>,
2091    #[pyo3(get)]
2092    invoice_id: Option<String>,
2093    #[pyo3(get)]
2094    customer_id: Option<String>,
2095    #[pyo3(get)]
2096    idempotency_key: Option<String>,
2097    #[pyo3(get)]
2098    amount: f64,
2099    #[pyo3(get)]
2100    currency: String,
2101    #[pyo3(get)]
2102    status: String,
2103    #[pyo3(get)]
2104    payment_method: String,
2105    #[pyo3(get)]
2106    version: i32,
2107    #[pyo3(get)]
2108    created_at: String,
2109    #[pyo3(get)]
2110    updated_at: String,
2111}
2112
2113#[pymethods]
2114impl Payment {
2115    fn __repr__(&self) -> String {
2116        format!(
2117            "Payment(number='{}', amount={} {}, status='{}')",
2118            self.payment_number, self.amount, self.currency, self.status
2119        )
2120    }
2121}
2122
2123impl From<stateset_core::Payment> for Payment {
2124    fn from(p: stateset_core::Payment) -> Self {
2125        Self {
2126            id: p.id.to_string(),
2127            payment_number: p.payment_number,
2128            order_id: p.order_id.map(|id| id.to_string()),
2129            invoice_id: p.invoice_id.map(|id| id.to_string()),
2130            customer_id: p.customer_id.map(|id| id.to_string()),
2131            idempotency_key: p.idempotency_key,
2132            amount: to_f64_or_nan(p.amount),
2133            currency: p.currency,
2134            status: format!("{}", p.status),
2135            payment_method: format!("{}", p.payment_method),
2136            version: p.version,
2137            created_at: p.created_at.to_rfc3339(),
2138            updated_at: p.updated_at.to_rfc3339(),
2139        }
2140    }
2141}
2142
2143/// Refund data returned from operations.
2144#[pyclass]
2145#[derive(Clone)]
2146pub struct Refund {
2147    #[pyo3(get)]
2148    id: String,
2149    #[pyo3(get)]
2150    payment_id: String,
2151    #[pyo3(get)]
2152    idempotency_key: Option<String>,
2153    #[pyo3(get)]
2154    amount: f64,
2155    #[pyo3(get)]
2156    status: String,
2157    #[pyo3(get)]
2158    reason: Option<String>,
2159    #[pyo3(get)]
2160    created_at: String,
2161}
2162
2163#[pymethods]
2164impl Refund {
2165    fn __repr__(&self) -> String {
2166        format!("Refund(id='{}', amount={}, status='{}')", self.id, self.amount, self.status)
2167    }
2168}
2169
2170impl From<stateset_core::Refund> for Refund {
2171    fn from(r: stateset_core::Refund) -> Self {
2172        Self {
2173            id: r.id.to_string(),
2174            payment_id: r.payment_id.to_string(),
2175            idempotency_key: r.idempotency_key,
2176            amount: to_f64_or_nan(r.amount),
2177            status: format!("{}", r.status),
2178            reason: r.reason,
2179            created_at: r.created_at.to_rfc3339(),
2180        }
2181    }
2182}
2183
2184// ============================================================================
2185// Payments API
2186// ============================================================================
2187
2188/// Payment processing operations.
2189#[pyclass]
2190pub struct Payments {
2191    commerce: Arc<Mutex<RustCommerce>>,
2192}
2193
2194#[pymethods]
2195impl Payments {
2196    /// Create a new payment.
2197    ///
2198    /// Args:
2199    ///     amount: Payment amount
2200    ///     currency: Currency code (default "USD")
2201    ///     order_id: Associated order UUID (optional)
2202    ///     customer_id: Customer UUID (optional)
2203    ///     payment_method: Payment method type (optional)
2204    ///
2205    /// Returns:
2206    ///     Payment: The created payment
2207    #[pyo3(signature = (amount, currency=None, order_id=None, customer_id=None, payment_method=None, idempotency_key=None))]
2208    fn create(
2209        &self,
2210        amount: f64,
2211        currency: Option<String>,
2212        order_id: Option<String>,
2213        customer_id: Option<String>,
2214        payment_method: Option<String>,
2215        idempotency_key: Option<String>,
2216    ) -> PyResult<Payment> {
2217        let commerce = self
2218            .commerce
2219            .lock()
2220            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
2221
2222        let order_uuid = order_id
2223            .map(|id| id.parse())
2224            .transpose()
2225            .map_err(|_| PyValueError::new_err("Invalid order UUID"))?;
2226
2227        let customer_uuid = customer_id
2228            .map(|id| id.parse())
2229            .transpose()
2230            .map_err(|_| PyValueError::new_err("Invalid customer UUID"))?;
2231
2232        let method = payment_method
2233            .map(|m| match m.to_lowercase().as_str() {
2234                "credit_card" => stateset_core::PaymentMethodType::CreditCard,
2235                "debit_card" => stateset_core::PaymentMethodType::DebitCard,
2236                "bank_transfer" => stateset_core::PaymentMethodType::BankTransfer,
2237                "paypal" => stateset_core::PaymentMethodType::PayPal,
2238                "crypto" => stateset_core::PaymentMethodType::Crypto,
2239                _ => stateset_core::PaymentMethodType::CreditCard,
2240            })
2241            .unwrap_or(stateset_core::PaymentMethodType::CreditCard);
2242
2243        let payment = commerce
2244            .payments()
2245            .create(stateset_core::CreatePayment {
2246                order_id: order_uuid,
2247                customer_id: customer_uuid,
2248                idempotency_key,
2249                amount: Decimal::from_f64_retain(amount).unwrap_or_default(),
2250                currency,
2251                payment_method: method,
2252                ..Default::default()
2253            })
2254            .map_err(|e| PyRuntimeError::new_err(format!("Failed to create payment: {}", e)))?;
2255
2256        Ok(payment.into())
2257    }
2258
2259    /// Get a payment by ID.
2260    fn get(&self, id: String) -> PyResult<Option<Payment>> {
2261        let commerce = self
2262            .commerce
2263            .lock()
2264            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
2265
2266        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
2267
2268        let payment = commerce
2269            .payments()
2270            .get(uuid)
2271            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get payment: {}", e)))?;
2272
2273        Ok(payment.map(|p| p.into()))
2274    }
2275
2276    /// List all payments.
2277    fn list(&self) -> PyResult<Vec<Payment>> {
2278        let commerce = self
2279            .commerce
2280            .lock()
2281            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
2282
2283        let payments = commerce
2284            .payments()
2285            .list(Default::default())
2286            .map_err(|e| PyRuntimeError::new_err(format!("Failed to list payments: {}", e)))?;
2287
2288        Ok(payments.into_iter().map(|p| p.into()).collect())
2289    }
2290
2291    /// Mark payment as completed.
2292    fn complete(&self, id: String) -> PyResult<Payment> {
2293        let commerce = self
2294            .commerce
2295            .lock()
2296            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
2297
2298        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
2299
2300        let payment = commerce
2301            .payments()
2302            .mark_completed(uuid)
2303            .map_err(|e| PyRuntimeError::new_err(format!("Failed to complete payment: {}", e)))?;
2304
2305        Ok(payment.into())
2306    }
2307
2308    /// Mark payment as failed.
2309    #[pyo3(signature = (id, reason, code=None))]
2310    fn mark_failed(&self, id: String, reason: String, code: Option<String>) -> PyResult<Payment> {
2311        let commerce = self
2312            .commerce
2313            .lock()
2314            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
2315
2316        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
2317
2318        let payment = commerce
2319            .payments()
2320            .mark_failed(uuid, &reason, code.as_deref())
2321            .map_err(|e| PyRuntimeError::new_err(format!("Failed to fail payment: {}", e)))?;
2322
2323        Ok(payment.into())
2324    }
2325
2326    /// Create a refund for a payment.
2327    #[pyo3(signature = (payment_id, amount, reason=None, idempotency_key=None))]
2328    fn create_refund(
2329        &self,
2330        payment_id: String,
2331        amount: f64,
2332        reason: Option<String>,
2333        idempotency_key: Option<String>,
2334    ) -> PyResult<Refund> {
2335        let commerce = self
2336            .commerce
2337            .lock()
2338            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
2339
2340        let uuid = payment_id.parse().map_err(|_| PyValueError::new_err("Invalid payment UUID"))?;
2341
2342        let refund = commerce
2343            .payments()
2344            .create_refund(stateset_core::CreateRefund {
2345                payment_id: uuid,
2346                amount: Some(Decimal::from_f64_retain(amount).unwrap_or_default()),
2347                reason,
2348                idempotency_key,
2349                ..Default::default()
2350            })
2351            .map_err(|e| PyRuntimeError::new_err(format!("Failed to create refund: {}", e)))?;
2352
2353        Ok(refund.into())
2354    }
2355
2356    /// Count payments.
2357    fn count(&self) -> PyResult<u32> {
2358        let commerce = self
2359            .commerce
2360            .lock()
2361            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
2362
2363        let count = commerce
2364            .payments()
2365            .count(Default::default())
2366            .map_err(|e| PyRuntimeError::new_err(format!("Failed to count payments: {}", e)))?;
2367
2368        Ok(count as u32)
2369    }
2370}
2371
2372// ============================================================================
2373// Shipment Types
2374// ============================================================================
2375
2376/// Shipment data returned from operations.
2377#[pyclass]
2378#[derive(Clone)]
2379pub struct Shipment {
2380    #[pyo3(get)]
2381    id: String,
2382    #[pyo3(get)]
2383    shipment_number: String,
2384    #[pyo3(get)]
2385    order_id: String,
2386    #[pyo3(get)]
2387    status: String,
2388    #[pyo3(get)]
2389    carrier: String,
2390    #[pyo3(get)]
2391    shipping_method: String,
2392    #[pyo3(get)]
2393    tracking_number: Option<String>,
2394    #[pyo3(get)]
2395    tracking_url: Option<String>,
2396    #[pyo3(get)]
2397    recipient_name: String,
2398    #[pyo3(get)]
2399    shipping_address: String,
2400    #[pyo3(get)]
2401    version: i32,
2402    #[pyo3(get)]
2403    created_at: String,
2404    #[pyo3(get)]
2405    updated_at: String,
2406}
2407
2408#[pymethods]
2409impl Shipment {
2410    fn __repr__(&self) -> String {
2411        format!(
2412            "Shipment(number='{}', status='{}', carrier='{}')",
2413            self.shipment_number, self.status, self.carrier
2414        )
2415    }
2416}
2417
2418impl From<stateset_core::Shipment> for Shipment {
2419    fn from(s: stateset_core::Shipment) -> Self {
2420        Self {
2421            id: s.id.to_string(),
2422            shipment_number: s.shipment_number,
2423            order_id: s.order_id.to_string(),
2424            status: format!("{}", s.status),
2425            carrier: format!("{}", s.carrier),
2426            shipping_method: format!("{}", s.shipping_method),
2427            tracking_number: s.tracking_number,
2428            tracking_url: s.tracking_url,
2429            recipient_name: s.recipient_name,
2430            shipping_address: s.shipping_address,
2431            version: s.version,
2432            created_at: s.created_at.to_rfc3339(),
2433            updated_at: s.updated_at.to_rfc3339(),
2434        }
2435    }
2436}
2437
2438// ============================================================================
2439// Shipments API
2440// ============================================================================
2441
2442/// Shipment management operations.
2443#[pyclass]
2444pub struct Shipments {
2445    commerce: Arc<Mutex<RustCommerce>>,
2446}
2447
2448#[pymethods]
2449impl Shipments {
2450    /// Create a new shipment.
2451    #[pyo3(signature = (order_id, recipient_name, shipping_address, carrier=None, shipping_method=None, tracking_number=None))]
2452    fn create(
2453        &self,
2454        order_id: String,
2455        recipient_name: String,
2456        shipping_address: String,
2457        carrier: Option<String>,
2458        shipping_method: Option<String>,
2459        tracking_number: Option<String>,
2460    ) -> PyResult<Shipment> {
2461        let commerce = self
2462            .commerce
2463            .lock()
2464            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
2465
2466        let order_uuid =
2467            order_id.parse().map_err(|_| PyValueError::new_err("Invalid order UUID"))?;
2468
2469        let carrier_type = carrier.and_then(|c| match c.to_lowercase().as_str() {
2470            "ups" => Some(stateset_core::ShippingCarrier::Ups),
2471            "fedex" => Some(stateset_core::ShippingCarrier::FedEx),
2472            "usps" => Some(stateset_core::ShippingCarrier::Usps),
2473            "dhl" => Some(stateset_core::ShippingCarrier::Dhl),
2474            _ => Some(stateset_core::ShippingCarrier::Other),
2475        });
2476
2477        let method = shipping_method.and_then(|m| match m.to_lowercase().as_str() {
2478            "standard" => Some(stateset_core::ShippingMethod::Standard),
2479            "express" => Some(stateset_core::ShippingMethod::Express),
2480            "overnight" => Some(stateset_core::ShippingMethod::Overnight),
2481            "ground" => Some(stateset_core::ShippingMethod::Ground),
2482            _ => Some(stateset_core::ShippingMethod::Standard),
2483        });
2484
2485        let shipment = commerce
2486            .shipments()
2487            .create(stateset_core::CreateShipment {
2488                order_id: order_uuid,
2489                recipient_name,
2490                shipping_address,
2491                carrier: carrier_type,
2492                shipping_method: method,
2493                tracking_number,
2494                ..Default::default()
2495            })
2496            .map_err(|e| PyRuntimeError::new_err(format!("Failed to create shipment: {}", e)))?;
2497
2498        Ok(shipment.into())
2499    }
2500
2501    /// Get a shipment by ID.
2502    fn get(&self, id: String) -> PyResult<Option<Shipment>> {
2503        let commerce = self
2504            .commerce
2505            .lock()
2506            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
2507
2508        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
2509
2510        let shipment = commerce
2511            .shipments()
2512            .get(uuid)
2513            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get shipment: {}", e)))?;
2514
2515        Ok(shipment.map(|s| s.into()))
2516    }
2517
2518    /// List all shipments.
2519    fn list(&self) -> PyResult<Vec<Shipment>> {
2520        let commerce = self
2521            .commerce
2522            .lock()
2523            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
2524
2525        let shipments = commerce
2526            .shipments()
2527            .list(Default::default())
2528            .map_err(|e| PyRuntimeError::new_err(format!("Failed to list shipments: {}", e)))?;
2529
2530        Ok(shipments.into_iter().map(|s| s.into()).collect())
2531    }
2532
2533    /// Ship a shipment with optional tracking number.
2534    #[pyo3(signature = (id, tracking_number=None))]
2535    fn ship(&self, id: String, tracking_number: Option<String>) -> PyResult<Shipment> {
2536        let commerce = self
2537            .commerce
2538            .lock()
2539            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
2540
2541        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
2542
2543        let shipment = commerce
2544            .shipments()
2545            .ship(uuid, tracking_number)
2546            .map_err(|e| PyRuntimeError::new_err(format!("Failed to ship: {}", e)))?;
2547
2548        Ok(shipment.into())
2549    }
2550
2551    /// Mark shipment as delivered.
2552    fn mark_delivered(&self, id: String) -> PyResult<Shipment> {
2553        let commerce = self
2554            .commerce
2555            .lock()
2556            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
2557
2558        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
2559
2560        let shipment = commerce
2561            .shipments()
2562            .mark_delivered(uuid)
2563            .map_err(|e| PyRuntimeError::new_err(format!("Failed to deliver: {}", e)))?;
2564
2565        Ok(shipment.into())
2566    }
2567
2568    /// Cancel a shipment.
2569    fn cancel(&self, id: String) -> PyResult<Shipment> {
2570        let commerce = self
2571            .commerce
2572            .lock()
2573            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
2574
2575        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
2576
2577        let shipment = commerce
2578            .shipments()
2579            .cancel(uuid)
2580            .map_err(|e| PyRuntimeError::new_err(format!("Failed to cancel shipment: {}", e)))?;
2581
2582        Ok(shipment.into())
2583    }
2584
2585    /// Count shipments.
2586    fn count(&self) -> PyResult<u32> {
2587        let commerce = self
2588            .commerce
2589            .lock()
2590            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
2591
2592        let count = commerce
2593            .shipments()
2594            .count(Default::default())
2595            .map_err(|e| PyRuntimeError::new_err(format!("Failed to count shipments: {}", e)))?;
2596
2597        Ok(count as u32)
2598    }
2599}
2600
2601// ============================================================================
2602// Warranty Types
2603// ============================================================================
2604
2605/// Warranty data returned from operations.
2606#[pyclass]
2607#[derive(Clone)]
2608pub struct Warranty {
2609    #[pyo3(get)]
2610    id: String,
2611    #[pyo3(get)]
2612    warranty_number: String,
2613    #[pyo3(get)]
2614    customer_id: String,
2615    #[pyo3(get)]
2616    product_id: Option<String>,
2617    #[pyo3(get)]
2618    order_id: Option<String>,
2619    #[pyo3(get)]
2620    status: String,
2621    #[pyo3(get)]
2622    warranty_type: String,
2623    #[pyo3(get)]
2624    start_date: String,
2625    #[pyo3(get)]
2626    end_date: String,
2627    #[pyo3(get)]
2628    created_at: String,
2629}
2630
2631#[pymethods]
2632impl Warranty {
2633    fn __repr__(&self) -> String {
2634        format!(
2635            "Warranty(number='{}', status='{}', type='{}')",
2636            self.warranty_number, self.status, self.warranty_type
2637        )
2638    }
2639}
2640
2641impl From<stateset_core::Warranty> for Warranty {
2642    fn from(w: stateset_core::Warranty) -> Self {
2643        Self {
2644            id: w.id.to_string(),
2645            warranty_number: w.warranty_number,
2646            customer_id: w.customer_id.to_string(),
2647            product_id: w.product_id.map(|id| id.to_string()),
2648            order_id: w.order_id.map(|id| id.to_string()),
2649            status: format!("{}", w.status),
2650            warranty_type: format!("{}", w.warranty_type),
2651            start_date: w.start_date.to_rfc3339(),
2652            end_date: w.end_date.map(|d| d.to_rfc3339()).unwrap_or_default(),
2653            created_at: w.created_at.to_rfc3339(),
2654        }
2655    }
2656}
2657
2658/// Warranty claim data.
2659#[pyclass]
2660#[derive(Clone)]
2661pub struct WarrantyClaim {
2662    #[pyo3(get)]
2663    id: String,
2664    #[pyo3(get)]
2665    claim_number: String,
2666    #[pyo3(get)]
2667    warranty_id: String,
2668    #[pyo3(get)]
2669    status: String,
2670    #[pyo3(get)]
2671    issue_description: String,
2672    #[pyo3(get)]
2673    resolution: String,
2674    #[pyo3(get)]
2675    created_at: String,
2676}
2677
2678#[pymethods]
2679impl WarrantyClaim {
2680    fn __repr__(&self) -> String {
2681        format!("WarrantyClaim(number='{}', status='{}')", self.claim_number, self.status)
2682    }
2683}
2684
2685impl From<stateset_core::WarrantyClaim> for WarrantyClaim {
2686    fn from(c: stateset_core::WarrantyClaim) -> Self {
2687        Self {
2688            id: c.id.to_string(),
2689            claim_number: c.claim_number,
2690            warranty_id: c.warranty_id.to_string(),
2691            status: format!("{}", c.status),
2692            issue_description: c.issue_description,
2693            resolution: format!("{}", c.resolution),
2694            created_at: c.created_at.to_rfc3339(),
2695        }
2696    }
2697}
2698
2699// ============================================================================
2700// Warranties API
2701// ============================================================================
2702
2703/// Warranty management operations.
2704#[pyclass]
2705pub struct Warranties {
2706    commerce: Arc<Mutex<RustCommerce>>,
2707}
2708
2709#[pymethods]
2710impl Warranties {
2711    /// Register a new warranty.
2712    #[pyo3(signature = (customer_id, product_id=None, order_id=None, duration_months=None))]
2713    fn create(
2714        &self,
2715        customer_id: String,
2716        product_id: Option<String>,
2717        order_id: Option<String>,
2718        duration_months: Option<i32>,
2719    ) -> PyResult<Warranty> {
2720        let commerce = self
2721            .commerce
2722            .lock()
2723            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
2724
2725        let cust_uuid =
2726            customer_id.parse().map_err(|_| PyValueError::new_err("Invalid customer UUID"))?;
2727
2728        let prod_uuid = product_id
2729            .map(|id| id.parse())
2730            .transpose()
2731            .map_err(|_| PyValueError::new_err("Invalid product UUID"))?;
2732
2733        let order_uuid = order_id
2734            .map(|id| id.parse())
2735            .transpose()
2736            .map_err(|_| PyValueError::new_err("Invalid order UUID"))?;
2737
2738        let warranty = commerce
2739            .warranties()
2740            .create(stateset_core::CreateWarranty {
2741                customer_id: cust_uuid,
2742                product_id: prod_uuid,
2743                order_id: order_uuid,
2744                duration_months,
2745                ..Default::default()
2746            })
2747            .map_err(|e| PyRuntimeError::new_err(format!("Failed to create warranty: {}", e)))?;
2748
2749        Ok(warranty.into())
2750    }
2751
2752    /// Get a warranty by ID.
2753    fn get(&self, id: String) -> PyResult<Option<Warranty>> {
2754        let commerce = self
2755            .commerce
2756            .lock()
2757            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
2758
2759        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
2760
2761        let warranty = commerce
2762            .warranties()
2763            .get(uuid)
2764            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get warranty: {}", e)))?;
2765
2766        Ok(warranty.map(|w| w.into()))
2767    }
2768
2769    /// List all warranties.
2770    fn list(&self) -> PyResult<Vec<Warranty>> {
2771        let commerce = self
2772            .commerce
2773            .lock()
2774            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
2775
2776        let warranties = commerce
2777            .warranties()
2778            .list(Default::default())
2779            .map_err(|e| PyRuntimeError::new_err(format!("Failed to list warranties: {}", e)))?;
2780
2781        Ok(warranties.into_iter().map(|w| w.into()).collect())
2782    }
2783
2784    /// File a warranty claim.
2785    #[pyo3(signature = (warranty_id, issue_description, contact_email=None))]
2786    fn create_claim(
2787        &self,
2788        warranty_id: String,
2789        issue_description: String,
2790        contact_email: Option<String>,
2791    ) -> PyResult<WarrantyClaim> {
2792        let commerce = self
2793            .commerce
2794            .lock()
2795            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
2796
2797        let warranty_uuid =
2798            warranty_id.parse().map_err(|_| PyValueError::new_err("Invalid warranty UUID"))?;
2799
2800        let claim = commerce
2801            .warranties()
2802            .create_claim(stateset_core::CreateWarrantyClaim {
2803                warranty_id: warranty_uuid,
2804                issue_description,
2805                contact_email,
2806                ..Default::default()
2807            })
2808            .map_err(|e| PyRuntimeError::new_err(format!("Failed to create claim: {}", e)))?;
2809
2810        Ok(claim.into())
2811    }
2812
2813    /// Approve a warranty claim.
2814    fn approve_claim(&self, id: String) -> PyResult<WarrantyClaim> {
2815        let commerce = self
2816            .commerce
2817            .lock()
2818            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
2819
2820        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
2821
2822        let claim = commerce
2823            .warranties()
2824            .approve_claim(uuid)
2825            .map_err(|e| PyRuntimeError::new_err(format!("Failed to approve claim: {}", e)))?;
2826
2827        Ok(claim.into())
2828    }
2829
2830    /// Deny a warranty claim.
2831    fn deny_claim(&self, id: String, reason: String) -> PyResult<WarrantyClaim> {
2832        let commerce = self
2833            .commerce
2834            .lock()
2835            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
2836
2837        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
2838
2839        let claim = commerce
2840            .warranties()
2841            .deny_claim(uuid, &reason)
2842            .map_err(|e| PyRuntimeError::new_err(format!("Failed to deny claim: {}", e)))?;
2843
2844        Ok(claim.into())
2845    }
2846
2847    /// Complete a warranty claim with resolution.
2848    fn complete_claim(&self, id: String, resolution: String) -> PyResult<WarrantyClaim> {
2849        let commerce = self
2850            .commerce
2851            .lock()
2852            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
2853
2854        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
2855
2856        let res = match resolution.to_lowercase().as_str() {
2857            "repair" => stateset_core::ClaimResolution::Repair,
2858            "replacement" => stateset_core::ClaimResolution::Replacement,
2859            "refund" => stateset_core::ClaimResolution::Refund,
2860            "store_credit" => stateset_core::ClaimResolution::StoreCredit,
2861            "denied" => stateset_core::ClaimResolution::Denied,
2862            _ => stateset_core::ClaimResolution::None,
2863        };
2864
2865        let claim = commerce
2866            .warranties()
2867            .complete_claim(uuid, res)
2868            .map_err(|e| PyRuntimeError::new_err(format!("Failed to complete claim: {}", e)))?;
2869
2870        Ok(claim.into())
2871    }
2872
2873    /// Count warranties.
2874    fn count(&self) -> PyResult<u32> {
2875        let commerce = self
2876            .commerce
2877            .lock()
2878            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
2879
2880        let count = commerce
2881            .warranties()
2882            .count(Default::default())
2883            .map_err(|e| PyRuntimeError::new_err(format!("Failed to count warranties: {}", e)))?;
2884
2885        Ok(count as u32)
2886    }
2887}
2888
2889// ============================================================================
2890// Purchase Order Types
2891// ============================================================================
2892
2893/// Supplier data.
2894#[pyclass]
2895#[derive(Clone)]
2896pub struct Supplier {
2897    #[pyo3(get)]
2898    id: String,
2899    #[pyo3(get)]
2900    name: String,
2901    #[pyo3(get)]
2902    supplier_code: String,
2903    #[pyo3(get)]
2904    email: Option<String>,
2905    #[pyo3(get)]
2906    phone: Option<String>,
2907    #[pyo3(get)]
2908    is_active: bool,
2909    #[pyo3(get)]
2910    created_at: String,
2911}
2912
2913#[pymethods]
2914impl Supplier {
2915    fn __repr__(&self) -> String {
2916        format!("Supplier(name='{}', code='{}')", self.name, self.supplier_code)
2917    }
2918}
2919
2920impl From<stateset_core::Supplier> for Supplier {
2921    fn from(s: stateset_core::Supplier) -> Self {
2922        Self {
2923            id: s.id.to_string(),
2924            name: s.name,
2925            supplier_code: s.supplier_code,
2926            email: s.email,
2927            phone: s.phone,
2928            is_active: s.is_active,
2929            created_at: s.created_at.to_rfc3339(),
2930        }
2931    }
2932}
2933
2934/// Purchase order data.
2935#[pyclass]
2936#[derive(Clone)]
2937pub struct PurchaseOrder {
2938    #[pyo3(get)]
2939    id: String,
2940    #[pyo3(get)]
2941    po_number: String,
2942    #[pyo3(get)]
2943    supplier_id: String,
2944    #[pyo3(get)]
2945    status: String,
2946    #[pyo3(get)]
2947    total_amount: f64,
2948    #[pyo3(get)]
2949    created_at: String,
2950    #[pyo3(get)]
2951    updated_at: String,
2952}
2953
2954#[pymethods]
2955impl PurchaseOrder {
2956    fn __repr__(&self) -> String {
2957        format!(
2958            "PurchaseOrder(number='{}', status='{}', total={})",
2959            self.po_number, self.status, self.total_amount
2960        )
2961    }
2962}
2963
2964impl From<stateset_core::PurchaseOrder> for PurchaseOrder {
2965    fn from(po: stateset_core::PurchaseOrder) -> Self {
2966        Self {
2967            id: po.id.to_string(),
2968            po_number: po.po_number,
2969            supplier_id: po.supplier_id.to_string(),
2970            status: format!("{}", po.status),
2971            total_amount: to_f64_or_nan(po.total),
2972            created_at: po.created_at.to_rfc3339(),
2973            updated_at: po.updated_at.to_rfc3339(),
2974        }
2975    }
2976}
2977
2978// ============================================================================
2979// Purchase Orders API
2980// ============================================================================
2981
2982/// Purchase order management operations.
2983#[pyclass]
2984pub struct PurchaseOrders {
2985    commerce: Arc<Mutex<RustCommerce>>,
2986}
2987
2988#[pymethods]
2989impl PurchaseOrders {
2990    /// Create a new supplier.
2991    #[pyo3(signature = (name, email=None, phone=None))]
2992    fn create_supplier(
2993        &self,
2994        name: String,
2995        email: Option<String>,
2996        phone: Option<String>,
2997    ) -> PyResult<Supplier> {
2998        let commerce = self
2999            .commerce
3000            .lock()
3001            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3002
3003        let supplier = commerce
3004            .purchase_orders()
3005            .create_supplier(stateset_core::CreateSupplier {
3006                name,
3007                email,
3008                phone,
3009                ..Default::default()
3010            })
3011            .map_err(|e| PyRuntimeError::new_err(format!("Failed to create supplier: {}", e)))?;
3012
3013        Ok(supplier.into())
3014    }
3015
3016    /// Get a supplier by ID.
3017    fn get_supplier(&self, id: String) -> PyResult<Option<Supplier>> {
3018        let commerce = self
3019            .commerce
3020            .lock()
3021            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3022
3023        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
3024
3025        let supplier = commerce
3026            .purchase_orders()
3027            .get_supplier(uuid)
3028            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get supplier: {}", e)))?;
3029
3030        Ok(supplier.map(|s| s.into()))
3031    }
3032
3033    /// List all suppliers.
3034    fn list_suppliers(&self) -> PyResult<Vec<Supplier>> {
3035        let commerce = self
3036            .commerce
3037            .lock()
3038            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3039
3040        let suppliers = commerce
3041            .purchase_orders()
3042            .list_suppliers(Default::default())
3043            .map_err(|e| PyRuntimeError::new_err(format!("Failed to list suppliers: {}", e)))?;
3044
3045        Ok(suppliers.into_iter().map(|s| s.into()).collect())
3046    }
3047
3048    /// Create a new purchase order.
3049    fn create(&self, supplier_id: String) -> PyResult<PurchaseOrder> {
3050        let commerce = self
3051            .commerce
3052            .lock()
3053            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3054
3055        let supp_uuid =
3056            supplier_id.parse().map_err(|_| PyValueError::new_err("Invalid supplier UUID"))?;
3057
3058        let po = commerce
3059            .purchase_orders()
3060            .create(stateset_core::CreatePurchaseOrder {
3061                supplier_id: supp_uuid,
3062                items: vec![],
3063                ..Default::default()
3064            })
3065            .map_err(|e| PyRuntimeError::new_err(format!("Failed to create PO: {}", e)))?;
3066
3067        Ok(po.into())
3068    }
3069
3070    /// Get a purchase order by ID.
3071    fn get(&self, id: String) -> PyResult<Option<PurchaseOrder>> {
3072        let commerce = self
3073            .commerce
3074            .lock()
3075            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3076
3077        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
3078
3079        let po = commerce
3080            .purchase_orders()
3081            .get(uuid)
3082            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get PO: {}", e)))?;
3083
3084        Ok(po.map(|p| p.into()))
3085    }
3086
3087    /// List all purchase orders.
3088    fn list(&self) -> PyResult<Vec<PurchaseOrder>> {
3089        let commerce = self
3090            .commerce
3091            .lock()
3092            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3093
3094        let pos = commerce
3095            .purchase_orders()
3096            .list(Default::default())
3097            .map_err(|e| PyRuntimeError::new_err(format!("Failed to list POs: {}", e)))?;
3098
3099        Ok(pos.into_iter().map(|p| p.into()).collect())
3100    }
3101
3102    /// Submit PO for approval.
3103    fn submit(&self, id: String) -> PyResult<PurchaseOrder> {
3104        let commerce = self
3105            .commerce
3106            .lock()
3107            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3108
3109        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
3110
3111        let po = commerce
3112            .purchase_orders()
3113            .submit(uuid)
3114            .map_err(|e| PyRuntimeError::new_err(format!("Failed to submit PO: {}", e)))?;
3115
3116        Ok(po.into())
3117    }
3118
3119    /// Approve a purchase order.
3120    fn approve(&self, id: String, approved_by: String) -> PyResult<PurchaseOrder> {
3121        let commerce = self
3122            .commerce
3123            .lock()
3124            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3125
3126        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
3127
3128        let po = commerce
3129            .purchase_orders()
3130            .approve(uuid, &approved_by)
3131            .map_err(|e| PyRuntimeError::new_err(format!("Failed to approve PO: {}", e)))?;
3132
3133        Ok(po.into())
3134    }
3135
3136    /// Send PO to supplier.
3137    fn send(&self, id: String) -> PyResult<PurchaseOrder> {
3138        let commerce = self
3139            .commerce
3140            .lock()
3141            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3142
3143        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
3144
3145        let po = commerce
3146            .purchase_orders()
3147            .send(uuid)
3148            .map_err(|e| PyRuntimeError::new_err(format!("Failed to send PO: {}", e)))?;
3149
3150        Ok(po.into())
3151    }
3152
3153    /// Cancel a purchase order.
3154    fn cancel(&self, id: String) -> PyResult<PurchaseOrder> {
3155        let commerce = self
3156            .commerce
3157            .lock()
3158            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3159
3160        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
3161
3162        let po = commerce
3163            .purchase_orders()
3164            .cancel(uuid)
3165            .map_err(|e| PyRuntimeError::new_err(format!("Failed to cancel PO: {}", e)))?;
3166
3167        Ok(po.into())
3168    }
3169
3170    /// Count purchase orders.
3171    fn count(&self) -> PyResult<u32> {
3172        let commerce = self
3173            .commerce
3174            .lock()
3175            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3176
3177        let count = commerce
3178            .purchase_orders()
3179            .count(Default::default())
3180            .map_err(|e| PyRuntimeError::new_err(format!("Failed to count POs: {}", e)))?;
3181
3182        Ok(count as u32)
3183    }
3184}
3185
3186// ============================================================================
3187// Invoice Types
3188// ============================================================================
3189
3190/// Invoice data.
3191#[pyclass]
3192#[derive(Clone)]
3193pub struct Invoice {
3194    #[pyo3(get)]
3195    id: String,
3196    #[pyo3(get)]
3197    invoice_number: String,
3198    #[pyo3(get)]
3199    customer_id: String,
3200    #[pyo3(get)]
3201    order_id: Option<String>,
3202    #[pyo3(get)]
3203    status: String,
3204    #[pyo3(get)]
3205    subtotal: f64,
3206    #[pyo3(get)]
3207    tax_amount: f64,
3208    #[pyo3(get)]
3209    total: f64,
3210    #[pyo3(get)]
3211    amount_paid: f64,
3212    #[pyo3(get)]
3213    due_date: String,
3214    #[pyo3(get)]
3215    created_at: String,
3216}
3217
3218#[pymethods]
3219impl Invoice {
3220    fn __repr__(&self) -> String {
3221        format!(
3222            "Invoice(number='{}', status='{}', total={})",
3223            self.invoice_number, self.status, self.total
3224        )
3225    }
3226
3227    #[getter]
3228    fn balance_due(&self) -> f64 {
3229        self.total - self.amount_paid
3230    }
3231}
3232
3233impl From<stateset_core::Invoice> for Invoice {
3234    fn from(inv: stateset_core::Invoice) -> Self {
3235        Self {
3236            id: inv.id.to_string(),
3237            invoice_number: inv.invoice_number,
3238            customer_id: inv.customer_id.to_string(),
3239            order_id: inv.order_id.map(|id| id.to_string()),
3240            status: format!("{}", inv.status),
3241            subtotal: to_f64_or_nan(inv.subtotal),
3242            tax_amount: to_f64_or_nan(inv.tax_amount),
3243            total: to_f64_or_nan(inv.total),
3244            amount_paid: to_f64_or_nan(inv.amount_paid),
3245            due_date: inv.due_date.to_rfc3339(),
3246            created_at: inv.created_at.to_rfc3339(),
3247        }
3248    }
3249}
3250
3251// ============================================================================
3252// Invoices API
3253// ============================================================================
3254
3255/// Invoice management operations.
3256#[pyclass]
3257pub struct Invoices {
3258    commerce: Arc<Mutex<RustCommerce>>,
3259}
3260
3261#[pymethods]
3262impl Invoices {
3263    /// Create a new invoice.
3264    #[pyo3(signature = (customer_id, order_id=None, billing_email=None))]
3265    fn create(
3266        &self,
3267        customer_id: String,
3268        order_id: Option<String>,
3269        billing_email: Option<String>,
3270    ) -> PyResult<Invoice> {
3271        let commerce = self
3272            .commerce
3273            .lock()
3274            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3275
3276        let cust_uuid =
3277            customer_id.parse().map_err(|_| PyValueError::new_err("Invalid customer UUID"))?;
3278
3279        let order_uuid = order_id
3280            .map(|id| id.parse())
3281            .transpose()
3282            .map_err(|_| PyValueError::new_err("Invalid order UUID"))?;
3283
3284        let invoice = commerce
3285            .invoices()
3286            .create(stateset_core::CreateInvoice {
3287                customer_id: cust_uuid,
3288                order_id: order_uuid,
3289                billing_email,
3290                items: vec![],
3291                ..Default::default()
3292            })
3293            .map_err(|e| PyRuntimeError::new_err(format!("Failed to create invoice: {}", e)))?;
3294
3295        Ok(invoice.into())
3296    }
3297
3298    /// Get an invoice by ID.
3299    fn get(&self, id: String) -> PyResult<Option<Invoice>> {
3300        let commerce = self
3301            .commerce
3302            .lock()
3303            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3304
3305        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
3306
3307        let invoice = commerce
3308            .invoices()
3309            .get(uuid)
3310            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get invoice: {}", e)))?;
3311
3312        Ok(invoice.map(|i| i.into()))
3313    }
3314
3315    /// List all invoices.
3316    fn list(&self) -> PyResult<Vec<Invoice>> {
3317        let commerce = self
3318            .commerce
3319            .lock()
3320            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3321
3322        let invoices = commerce
3323            .invoices()
3324            .list(Default::default())
3325            .map_err(|e| PyRuntimeError::new_err(format!("Failed to list invoices: {}", e)))?;
3326
3327        Ok(invoices.into_iter().map(|i| i.into()).collect())
3328    }
3329
3330    /// Send an invoice.
3331    fn send(&self, id: String) -> PyResult<Invoice> {
3332        let commerce = self
3333            .commerce
3334            .lock()
3335            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3336
3337        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
3338
3339        let invoice = commerce
3340            .invoices()
3341            .send(uuid)
3342            .map_err(|e| PyRuntimeError::new_err(format!("Failed to send invoice: {}", e)))?;
3343
3344        Ok(invoice.into())
3345    }
3346
3347    /// Record a payment against an invoice.
3348    #[pyo3(signature = (id, amount, payment_method=None, reference=None))]
3349    fn record_payment(
3350        &self,
3351        id: String,
3352        amount: f64,
3353        payment_method: Option<String>,
3354        reference: Option<String>,
3355    ) -> PyResult<Invoice> {
3356        let commerce = self
3357            .commerce
3358            .lock()
3359            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3360
3361        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
3362
3363        let invoice = commerce
3364            .invoices()
3365            .record_payment(
3366                uuid,
3367                stateset_core::RecordInvoicePayment {
3368                    amount: Decimal::from_f64_retain(amount).unwrap_or_default(),
3369                    payment_method,
3370                    reference,
3371                    ..Default::default()
3372                },
3373            )
3374            .map_err(|e| PyRuntimeError::new_err(format!("Failed to record payment: {}", e)))?;
3375
3376        Ok(invoice.into())
3377    }
3378
3379    /// Void an invoice.
3380    fn void(&self, id: String) -> PyResult<Invoice> {
3381        let commerce = self
3382            .commerce
3383            .lock()
3384            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3385
3386        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
3387
3388        let invoice = commerce
3389            .invoices()
3390            .void(uuid)
3391            .map_err(|e| PyRuntimeError::new_err(format!("Failed to void invoice: {}", e)))?;
3392
3393        Ok(invoice.into())
3394    }
3395
3396    /// Get overdue invoices.
3397    fn get_overdue(&self) -> PyResult<Vec<Invoice>> {
3398        let commerce = self
3399            .commerce
3400            .lock()
3401            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3402
3403        let invoices = commerce.invoices().get_overdue().map_err(|e| {
3404            PyRuntimeError::new_err(format!("Failed to get overdue invoices: {}", e))
3405        })?;
3406
3407        Ok(invoices.into_iter().map(|i| i.into()).collect())
3408    }
3409
3410    /// Count invoices.
3411    fn count(&self) -> PyResult<u32> {
3412        let commerce = self
3413            .commerce
3414            .lock()
3415            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3416
3417        let count = commerce
3418            .invoices()
3419            .count(Default::default())
3420            .map_err(|e| PyRuntimeError::new_err(format!("Failed to count invoices: {}", e)))?;
3421
3422        Ok(count as u32)
3423    }
3424}
3425
3426// ============================================================================
3427// BOM Types
3428// ============================================================================
3429
3430/// Bill of Materials data.
3431#[pyclass]
3432#[derive(Clone)]
3433pub struct Bom {
3434    #[pyo3(get)]
3435    id: String,
3436    #[pyo3(get)]
3437    bom_number: String,
3438    #[pyo3(get)]
3439    name: String,
3440    #[pyo3(get)]
3441    product_id: String,
3442    #[pyo3(get)]
3443    status: String,
3444    #[pyo3(get)]
3445    revision: String,
3446    #[pyo3(get)]
3447    created_at: String,
3448}
3449
3450#[pymethods]
3451impl Bom {
3452    fn __repr__(&self) -> String {
3453        format!("Bom(number='{}', name='{}', status='{}')", self.bom_number, self.name, self.status)
3454    }
3455}
3456
3457impl From<stateset_core::BillOfMaterials> for Bom {
3458    fn from(bom: stateset_core::BillOfMaterials) -> Self {
3459        Self {
3460            id: bom.id.to_string(),
3461            bom_number: bom.bom_number,
3462            name: bom.name,
3463            product_id: bom.product_id.to_string(),
3464            status: format!("{}", bom.status),
3465            revision: bom.revision,
3466            created_at: bom.created_at.to_rfc3339(),
3467        }
3468    }
3469}
3470
3471/// BOM component data.
3472#[pyclass]
3473#[derive(Clone)]
3474pub struct BomComponent {
3475    #[pyo3(get)]
3476    id: String,
3477    #[pyo3(get)]
3478    bom_id: String,
3479    #[pyo3(get)]
3480    component_sku: Option<String>,
3481    #[pyo3(get)]
3482    name: String,
3483    #[pyo3(get)]
3484    quantity: f64,
3485    #[pyo3(get)]
3486    unit_of_measure: String,
3487}
3488
3489#[pymethods]
3490impl BomComponent {
3491    fn __repr__(&self) -> String {
3492        format!("BomComponent(name='{}', qty={})", self.name, self.quantity)
3493    }
3494}
3495
3496impl From<stateset_core::BomComponent> for BomComponent {
3497    fn from(c: stateset_core::BomComponent) -> Self {
3498        Self {
3499            id: c.id.to_string(),
3500            bom_id: c.bom_id.to_string(),
3501            component_sku: c.component_sku,
3502            name: c.name,
3503            quantity: to_f64_or_nan(c.quantity),
3504            unit_of_measure: c.unit_of_measure,
3505        }
3506    }
3507}
3508
3509// ============================================================================
3510// BOM API
3511// ============================================================================
3512
3513/// Bill of Materials management operations.
3514#[pyclass]
3515pub struct BomApi {
3516    commerce: Arc<Mutex<RustCommerce>>,
3517}
3518
3519#[pymethods]
3520impl BomApi {
3521    /// Create a new BOM.
3522    #[pyo3(signature = (name, product_id, description=None, revision=None))]
3523    fn create(
3524        &self,
3525        name: String,
3526        product_id: String,
3527        description: Option<String>,
3528        revision: Option<String>,
3529    ) -> PyResult<Bom> {
3530        let commerce = self
3531            .commerce
3532            .lock()
3533            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3534
3535        let prod_uuid =
3536            product_id.parse().map_err(|_| PyValueError::new_err("Invalid product UUID"))?;
3537
3538        let bom = commerce
3539            .bom()
3540            .create(stateset_core::CreateBom {
3541                name,
3542                product_id: prod_uuid,
3543                description,
3544                revision,
3545                ..Default::default()
3546            })
3547            .map_err(|e| PyRuntimeError::new_err(format!("Failed to create BOM: {}", e)))?;
3548
3549        Ok(bom.into())
3550    }
3551
3552    /// Get a BOM by ID.
3553    fn get(&self, id: String) -> PyResult<Option<Bom>> {
3554        let commerce = self
3555            .commerce
3556            .lock()
3557            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3558
3559        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
3560
3561        let bom = commerce
3562            .bom()
3563            .get(uuid)
3564            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get BOM: {}", e)))?;
3565
3566        Ok(bom.map(|b| b.into()))
3567    }
3568
3569    /// List all BOMs.
3570    fn list(&self) -> PyResult<Vec<Bom>> {
3571        let commerce = self
3572            .commerce
3573            .lock()
3574            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3575
3576        let boms = commerce
3577            .bom()
3578            .list(Default::default())
3579            .map_err(|e| PyRuntimeError::new_err(format!("Failed to list BOMs: {}", e)))?;
3580
3581        Ok(boms.into_iter().map(|b| b.into()).collect())
3582    }
3583
3584    /// Add a component to a BOM.
3585    #[pyo3(signature = (bom_id, name, quantity, component_sku=None, unit_of_measure=None))]
3586    fn add_component(
3587        &self,
3588        bom_id: String,
3589        name: String,
3590        quantity: f64,
3591        component_sku: Option<String>,
3592        unit_of_measure: Option<String>,
3593    ) -> PyResult<BomComponent> {
3594        let commerce = self
3595            .commerce
3596            .lock()
3597            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3598
3599        let uuid = bom_id.parse().map_err(|_| PyValueError::new_err("Invalid BOM UUID"))?;
3600
3601        let component = commerce
3602            .bom()
3603            .add_component(
3604                uuid,
3605                stateset_core::CreateBomComponent {
3606                    component_sku,
3607                    name,
3608                    quantity: Decimal::from_f64_retain(quantity).unwrap_or_default(),
3609                    unit_of_measure,
3610                    ..Default::default()
3611                },
3612            )
3613            .map_err(|e| PyRuntimeError::new_err(format!("Failed to add component: {}", e)))?;
3614
3615        Ok(component.into())
3616    }
3617
3618    /// Get components for a BOM.
3619    fn get_components(&self, bom_id: String) -> PyResult<Vec<BomComponent>> {
3620        let commerce = self
3621            .commerce
3622            .lock()
3623            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3624
3625        let uuid = bom_id.parse().map_err(|_| PyValueError::new_err("Invalid BOM UUID"))?;
3626
3627        let components = commerce
3628            .bom()
3629            .get_components(uuid)
3630            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get components: {}", e)))?;
3631
3632        Ok(components.into_iter().map(|c| c.into()).collect())
3633    }
3634
3635    /// Activate a BOM.
3636    fn activate(&self, id: String) -> PyResult<Bom> {
3637        let commerce = self
3638            .commerce
3639            .lock()
3640            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3641
3642        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
3643
3644        let bom = commerce
3645            .bom()
3646            .activate(uuid)
3647            .map_err(|e| PyRuntimeError::new_err(format!("Failed to activate BOM: {}", e)))?;
3648
3649        Ok(bom.into())
3650    }
3651
3652    /// Count BOMs.
3653    fn count(&self) -> PyResult<u32> {
3654        let commerce = self
3655            .commerce
3656            .lock()
3657            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3658
3659        let count = commerce
3660            .bom()
3661            .count(Default::default())
3662            .map_err(|e| PyRuntimeError::new_err(format!("Failed to count BOMs: {}", e)))?;
3663
3664        Ok(count as u32)
3665    }
3666}
3667
3668// ============================================================================
3669// Work Order Types
3670// ============================================================================
3671
3672/// Work order data.
3673#[pyclass]
3674#[derive(Clone)]
3675pub struct WorkOrder {
3676    #[pyo3(get)]
3677    id: String,
3678    #[pyo3(get)]
3679    work_order_number: String,
3680    #[pyo3(get)]
3681    product_id: String,
3682    #[pyo3(get)]
3683    bom_id: Option<String>,
3684    #[pyo3(get)]
3685    status: String,
3686    #[pyo3(get)]
3687    priority: String,
3688    #[pyo3(get)]
3689    quantity_to_build: f64,
3690    #[pyo3(get)]
3691    quantity_completed: f64,
3692    #[pyo3(get)]
3693    version: i32,
3694    #[pyo3(get)]
3695    created_at: String,
3696    #[pyo3(get)]
3697    updated_at: String,
3698}
3699
3700#[pymethods]
3701impl WorkOrder {
3702    fn __repr__(&self) -> String {
3703        format!(
3704            "WorkOrder(number='{}', status='{}', qty={}/{})",
3705            self.work_order_number, self.status, self.quantity_completed, self.quantity_to_build
3706        )
3707    }
3708}
3709
3710impl From<stateset_core::WorkOrder> for WorkOrder {
3711    fn from(wo: stateset_core::WorkOrder) -> Self {
3712        Self {
3713            id: wo.id.to_string(),
3714            work_order_number: wo.work_order_number,
3715            product_id: wo.product_id.to_string(),
3716            bom_id: wo.bom_id.map(|id| id.to_string()),
3717            status: format!("{}", wo.status),
3718            priority: format!("{}", wo.priority),
3719            quantity_to_build: to_f64_or_nan(wo.quantity_to_build),
3720            quantity_completed: to_f64_or_nan(wo.quantity_completed),
3721            version: wo.version,
3722            created_at: wo.created_at.to_rfc3339(),
3723            updated_at: wo.updated_at.to_rfc3339(),
3724        }
3725    }
3726}
3727
3728// ============================================================================
3729// Work Orders API
3730// ============================================================================
3731
3732/// Work order management operations.
3733#[pyclass]
3734pub struct WorkOrders {
3735    commerce: Arc<Mutex<RustCommerce>>,
3736}
3737
3738#[pymethods]
3739impl WorkOrders {
3740    /// Create a new work order.
3741    #[pyo3(signature = (product_id, quantity_to_build, bom_id=None, priority=None, notes=None))]
3742    fn create(
3743        &self,
3744        product_id: String,
3745        quantity_to_build: f64,
3746        bom_id: Option<String>,
3747        priority: Option<String>,
3748        notes: Option<String>,
3749    ) -> PyResult<WorkOrder> {
3750        let commerce = self
3751            .commerce
3752            .lock()
3753            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3754
3755        let prod_uuid =
3756            product_id.parse().map_err(|_| PyValueError::new_err("Invalid product UUID"))?;
3757
3758        let bom_uuid = bom_id
3759            .map(|id| id.parse())
3760            .transpose()
3761            .map_err(|_| PyValueError::new_err("Invalid BOM UUID"))?;
3762
3763        let prio = priority.and_then(|p| match p.to_lowercase().as_str() {
3764            "low" => Some(stateset_core::WorkOrderPriority::Low),
3765            "normal" => Some(stateset_core::WorkOrderPriority::Normal),
3766            "high" => Some(stateset_core::WorkOrderPriority::High),
3767            "urgent" => Some(stateset_core::WorkOrderPriority::Urgent),
3768            _ => None,
3769        });
3770
3771        let wo = commerce
3772            .work_orders()
3773            .create(stateset_core::CreateWorkOrder {
3774                product_id: prod_uuid,
3775                bom_id: bom_uuid,
3776                quantity_to_build: Decimal::from_f64_retain(quantity_to_build).unwrap_or_default(),
3777                priority: prio,
3778                notes,
3779                ..Default::default()
3780            })
3781            .map_err(|e| PyRuntimeError::new_err(format!("Failed to create work order: {}", e)))?;
3782
3783        Ok(wo.into())
3784    }
3785
3786    /// Get a work order by ID.
3787    fn get(&self, id: String) -> PyResult<Option<WorkOrder>> {
3788        let commerce = self
3789            .commerce
3790            .lock()
3791            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3792
3793        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
3794
3795        let wo = commerce
3796            .work_orders()
3797            .get(uuid)
3798            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get work order: {}", e)))?;
3799
3800        Ok(wo.map(|w| w.into()))
3801    }
3802
3803    /// List all work orders.
3804    fn list(&self) -> PyResult<Vec<WorkOrder>> {
3805        let commerce = self
3806            .commerce
3807            .lock()
3808            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3809
3810        let wos = commerce
3811            .work_orders()
3812            .list(Default::default())
3813            .map_err(|e| PyRuntimeError::new_err(format!("Failed to list work orders: {}", e)))?;
3814
3815        Ok(wos.into_iter().map(|w| w.into()).collect())
3816    }
3817
3818    /// Start a work order.
3819    fn start(&self, id: String) -> PyResult<WorkOrder> {
3820        let commerce = self
3821            .commerce
3822            .lock()
3823            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3824
3825        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
3826
3827        let wo = commerce
3828            .work_orders()
3829            .start(uuid)
3830            .map_err(|e| PyRuntimeError::new_err(format!("Failed to start work order: {}", e)))?;
3831
3832        Ok(wo.into())
3833    }
3834
3835    /// Complete a work order.
3836    fn complete(&self, id: String, quantity_completed: f64) -> PyResult<WorkOrder> {
3837        let commerce = self
3838            .commerce
3839            .lock()
3840            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3841
3842        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
3843
3844        let wo = commerce
3845            .work_orders()
3846            .complete(uuid, Decimal::from_f64_retain(quantity_completed).unwrap_or_default())
3847            .map_err(|e| {
3848                PyRuntimeError::new_err(format!("Failed to complete work order: {}", e))
3849            })?;
3850
3851        Ok(wo.into())
3852    }
3853
3854    /// Cancel a work order.
3855    fn cancel(&self, id: String) -> PyResult<WorkOrder> {
3856        let commerce = self
3857            .commerce
3858            .lock()
3859            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3860
3861        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
3862
3863        let wo = commerce
3864            .work_orders()
3865            .cancel(uuid)
3866            .map_err(|e| PyRuntimeError::new_err(format!("Failed to cancel work order: {}", e)))?;
3867
3868        Ok(wo.into())
3869    }
3870
3871    /// Count work orders.
3872    fn count(&self) -> PyResult<u32> {
3873        let commerce = self
3874            .commerce
3875            .lock()
3876            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
3877
3878        let count = commerce
3879            .work_orders()
3880            .count(Default::default())
3881            .map_err(|e| PyRuntimeError::new_err(format!("Failed to count work orders: {}", e)))?;
3882
3883        Ok(count as u32)
3884    }
3885}
3886
3887// ============================================================================
3888// Cart Types
3889// ============================================================================
3890
3891/// Cart address data.
3892#[pyclass]
3893#[derive(Clone)]
3894pub struct CartAddress {
3895    #[pyo3(get)]
3896    first_name: String,
3897    #[pyo3(get)]
3898    last_name: String,
3899    #[pyo3(get)]
3900    company: Option<String>,
3901    #[pyo3(get)]
3902    line1: String,
3903    #[pyo3(get)]
3904    line2: Option<String>,
3905    #[pyo3(get)]
3906    city: String,
3907    #[pyo3(get)]
3908    state: Option<String>,
3909    #[pyo3(get)]
3910    postal_code: String,
3911    #[pyo3(get)]
3912    country: String,
3913    #[pyo3(get)]
3914    phone: Option<String>,
3915    #[pyo3(get)]
3916    email: Option<String>,
3917}
3918
3919#[pymethods]
3920impl CartAddress {
3921    #[new]
3922    #[pyo3(signature = (first_name, last_name, line1, city, postal_code, country, company=None, line2=None, state=None, phone=None, email=None))]
3923    fn new(
3924        first_name: String,
3925        last_name: String,
3926        line1: String,
3927        city: String,
3928        postal_code: String,
3929        country: String,
3930        company: Option<String>,
3931        line2: Option<String>,
3932        state: Option<String>,
3933        phone: Option<String>,
3934        email: Option<String>,
3935    ) -> Self {
3936        Self {
3937            first_name,
3938            last_name,
3939            company,
3940            line1,
3941            line2,
3942            city,
3943            state,
3944            postal_code,
3945            country,
3946            phone,
3947            email,
3948        }
3949    }
3950
3951    fn __repr__(&self) -> String {
3952        format!("CartAddress(name='{} {}', city='{}')", self.first_name, self.last_name, self.city)
3953    }
3954}
3955
3956impl From<stateset_core::CartAddress> for CartAddress {
3957    fn from(a: stateset_core::CartAddress) -> Self {
3958        Self {
3959            first_name: a.first_name,
3960            last_name: a.last_name,
3961            company: a.company,
3962            line1: a.line1,
3963            line2: a.line2,
3964            city: a.city,
3965            state: a.state,
3966            postal_code: a.postal_code,
3967            country: a.country,
3968            phone: a.phone,
3969            email: a.email,
3970        }
3971    }
3972}
3973
3974impl From<&CartAddress> for stateset_core::CartAddress {
3975    fn from(a: &CartAddress) -> Self {
3976        Self {
3977            first_name: a.first_name.clone(),
3978            last_name: a.last_name.clone(),
3979            company: a.company.clone(),
3980            line1: a.line1.clone(),
3981            line2: a.line2.clone(),
3982            city: a.city.clone(),
3983            state: a.state.clone(),
3984            postal_code: a.postal_code.clone(),
3985            country: a.country.clone(),
3986            phone: a.phone.clone(),
3987            email: a.email.clone(),
3988        }
3989    }
3990}
3991
3992/// Cart item data.
3993#[pyclass]
3994#[derive(Clone)]
3995pub struct CartItem {
3996    #[pyo3(get)]
3997    id: String,
3998    #[pyo3(get)]
3999    cart_id: String,
4000    #[pyo3(get)]
4001    product_id: Option<String>,
4002    #[pyo3(get)]
4003    variant_id: Option<String>,
4004    #[pyo3(get)]
4005    sku: String,
4006    #[pyo3(get)]
4007    name: String,
4008    #[pyo3(get)]
4009    description: Option<String>,
4010    #[pyo3(get)]
4011    image_url: Option<String>,
4012    #[pyo3(get)]
4013    quantity: i32,
4014    #[pyo3(get)]
4015    unit_price: f64,
4016    #[pyo3(get)]
4017    original_price: Option<f64>,
4018    #[pyo3(get)]
4019    discount_amount: f64,
4020    #[pyo3(get)]
4021    tax_amount: f64,
4022    #[pyo3(get)]
4023    total: f64,
4024    #[pyo3(get)]
4025    created_at: String,
4026    #[pyo3(get)]
4027    updated_at: String,
4028}
4029
4030#[pymethods]
4031impl CartItem {
4032    fn __repr__(&self) -> String {
4033        format!("CartItem(sku='{}', qty={}, total={})", self.sku, self.quantity, self.total)
4034    }
4035}
4036
4037impl From<stateset_core::CartItem> for CartItem {
4038    fn from(i: stateset_core::CartItem) -> Self {
4039        Self {
4040            id: i.id.to_string(),
4041            cart_id: i.cart_id.to_string(),
4042            product_id: i.product_id.map(|id| id.to_string()),
4043            variant_id: i.variant_id.map(|id| id.to_string()),
4044            sku: i.sku,
4045            name: i.name,
4046            description: i.description,
4047            image_url: i.image_url,
4048            quantity: i.quantity,
4049            unit_price: to_f64_or_nan(i.unit_price),
4050            original_price: i.original_price.map(|p| to_f64_or_nan(p)),
4051            discount_amount: to_f64_or_nan(i.discount_amount),
4052            tax_amount: to_f64_or_nan(i.tax_amount),
4053            total: to_f64_or_nan(i.total),
4054            created_at: i.created_at.to_rfc3339(),
4055            updated_at: i.updated_at.to_rfc3339(),
4056        }
4057    }
4058}
4059
4060/// Shipping rate option.
4061#[pyclass]
4062#[derive(Clone)]
4063pub struct ShippingRate {
4064    #[pyo3(get)]
4065    id: String,
4066    #[pyo3(get)]
4067    carrier: String,
4068    #[pyo3(get)]
4069    service: String,
4070    #[pyo3(get)]
4071    description: Option<String>,
4072    #[pyo3(get)]
4073    price: f64,
4074    #[pyo3(get)]
4075    currency: String,
4076    #[pyo3(get)]
4077    estimated_days: Option<i32>,
4078    #[pyo3(get)]
4079    estimated_delivery: Option<String>,
4080}
4081
4082#[pymethods]
4083impl ShippingRate {
4084    fn __repr__(&self) -> String {
4085        format!(
4086            "ShippingRate(carrier='{}', service='{}', price={})",
4087            self.carrier, self.service, self.price
4088        )
4089    }
4090}
4091
4092impl From<stateset_core::ShippingRate> for ShippingRate {
4093    fn from(r: stateset_core::ShippingRate) -> Self {
4094        Self {
4095            id: r.id,
4096            carrier: r.carrier,
4097            service: r.service,
4098            description: r.description,
4099            price: to_f64_or_nan(r.price),
4100            currency: r.currency,
4101            estimated_days: r.estimated_days,
4102            estimated_delivery: r.estimated_delivery.map(|d| d.to_rfc3339()),
4103        }
4104    }
4105}
4106
4107/// Checkout result returned when completing a cart.
4108#[pyclass]
4109#[derive(Clone)]
4110pub struct CheckoutResult {
4111    #[pyo3(get)]
4112    order_id: String,
4113    #[pyo3(get)]
4114    order_number: String,
4115    #[pyo3(get)]
4116    cart_id: String,
4117    #[pyo3(get)]
4118    payment_id: Option<String>,
4119    #[pyo3(get)]
4120    total_charged: f64,
4121    #[pyo3(get)]
4122    currency: String,
4123}
4124
4125#[pymethods]
4126impl CheckoutResult {
4127    fn __repr__(&self) -> String {
4128        format!(
4129            "CheckoutResult(order='{}', total={} {})",
4130            self.order_number, self.total_charged, self.currency
4131        )
4132    }
4133}
4134
4135impl From<stateset_core::CheckoutResult> for CheckoutResult {
4136    fn from(r: stateset_core::CheckoutResult) -> Self {
4137        Self {
4138            order_id: r.order_id.to_string(),
4139            order_number: r.order_number,
4140            cart_id: r.cart_id.to_string(),
4141            payment_id: r.payment_id.map(|id| id.to_string()),
4142            total_charged: to_f64_or_nan(r.total_charged),
4143            currency: r.currency,
4144        }
4145    }
4146}
4147
4148/// Cart data returned from operations.
4149#[pyclass]
4150#[derive(Clone)]
4151pub struct Cart {
4152    #[pyo3(get)]
4153    id: String,
4154    #[pyo3(get)]
4155    cart_number: String,
4156    #[pyo3(get)]
4157    customer_id: Option<String>,
4158    #[pyo3(get)]
4159    status: String,
4160    #[pyo3(get)]
4161    currency: String,
4162    #[pyo3(get)]
4163    subtotal: f64,
4164    #[pyo3(get)]
4165    tax_amount: f64,
4166    #[pyo3(get)]
4167    shipping_amount: f64,
4168    #[pyo3(get)]
4169    discount_amount: f64,
4170    #[pyo3(get)]
4171    grand_total: f64,
4172    #[pyo3(get)]
4173    customer_email: Option<String>,
4174    #[pyo3(get)]
4175    customer_name: Option<String>,
4176    #[pyo3(get)]
4177    payment_method: Option<String>,
4178    #[pyo3(get)]
4179    payment_status: String,
4180    #[pyo3(get)]
4181    fulfillment_type: String,
4182    #[pyo3(get)]
4183    shipping_method: Option<String>,
4184    #[pyo3(get)]
4185    coupon_code: Option<String>,
4186    #[pyo3(get)]
4187    notes: Option<String>,
4188    #[pyo3(get)]
4189    item_count: i32,
4190    #[pyo3(get)]
4191    created_at: String,
4192    #[pyo3(get)]
4193    updated_at: String,
4194    #[pyo3(get)]
4195    expires_at: Option<String>,
4196    // Store items separately
4197    _items: Vec<CartItem>,
4198    _shipping_address: Option<CartAddress>,
4199    _billing_address: Option<CartAddress>,
4200}
4201
4202#[pymethods]
4203impl Cart {
4204    fn __repr__(&self) -> String {
4205        format!(
4206            "Cart(number='{}', status='{}', total={} {})",
4207            self.cart_number, self.status, self.grand_total, self.currency
4208        )
4209    }
4210
4211    /// Get cart items.
4212    #[getter]
4213    fn items(&self) -> Vec<CartItem> {
4214        self._items.clone()
4215    }
4216
4217    /// Get the shipping address.
4218    #[getter]
4219    fn shipping_address(&self) -> Option<CartAddress> {
4220        self._shipping_address.clone()
4221    }
4222
4223    /// Get the billing address.
4224    #[getter]
4225    fn billing_address(&self) -> Option<CartAddress> {
4226        self._billing_address.clone()
4227    }
4228}
4229
4230impl From<stateset_core::Cart> for Cart {
4231    fn from(c: stateset_core::Cart) -> Self {
4232        let item_count = c.items.len() as i32;
4233        Self {
4234            id: c.id.to_string(),
4235            cart_number: c.cart_number,
4236            customer_id: c.customer_id.map(|id| id.to_string()),
4237            status: format!("{}", c.status),
4238            currency: c.currency,
4239            subtotal: to_f64_or_nan(c.subtotal),
4240            tax_amount: to_f64_or_nan(c.tax_amount),
4241            shipping_amount: to_f64_or_nan(c.shipping_amount),
4242            discount_amount: to_f64_or_nan(c.discount_amount),
4243            grand_total: to_f64_or_nan(c.grand_total),
4244            customer_email: c.customer_email,
4245            customer_name: c.customer_name,
4246            payment_method: c.payment_method,
4247            payment_status: format!("{}", c.payment_status),
4248            fulfillment_type: c
4249                .fulfillment_type
4250                .map(|ft| format!("{}", ft))
4251                .unwrap_or_else(|| "Shipping".to_string()),
4252            shipping_method: c.shipping_method,
4253            coupon_code: c.coupon_code,
4254            notes: c.notes,
4255            item_count,
4256            created_at: c.created_at.to_rfc3339(),
4257            updated_at: c.updated_at.to_rfc3339(),
4258            expires_at: c.expires_at.map(|d| d.to_rfc3339()),
4259            _items: c.items.into_iter().map(|i| i.into()).collect(),
4260            _shipping_address: c.shipping_address.map(|a| a.into()),
4261            _billing_address: c.billing_address.map(|a| a.into()),
4262        }
4263    }
4264}
4265
4266/// Input for adding a cart item.
4267#[pyclass]
4268#[derive(Clone)]
4269pub struct AddCartItemInput {
4270    #[pyo3(get, set)]
4271    sku: String,
4272    #[pyo3(get, set)]
4273    name: String,
4274    #[pyo3(get, set)]
4275    quantity: i32,
4276    #[pyo3(get, set)]
4277    unit_price: f64,
4278    #[pyo3(get, set)]
4279    product_id: Option<String>,
4280    #[pyo3(get, set)]
4281    variant_id: Option<String>,
4282    #[pyo3(get, set)]
4283    description: Option<String>,
4284    #[pyo3(get, set)]
4285    image_url: Option<String>,
4286    #[pyo3(get, set)]
4287    original_price: Option<f64>,
4288    #[pyo3(get, set)]
4289    weight: Option<f64>,
4290    #[pyo3(get, set)]
4291    requires_shipping: Option<bool>,
4292}
4293
4294#[pymethods]
4295impl AddCartItemInput {
4296    #[new]
4297    #[pyo3(signature = (sku, name, quantity, unit_price, product_id=None, variant_id=None, description=None, image_url=None, original_price=None, weight=None, requires_shipping=None))]
4298    fn new(
4299        sku: String,
4300        name: String,
4301        quantity: i32,
4302        unit_price: f64,
4303        product_id: Option<String>,
4304        variant_id: Option<String>,
4305        description: Option<String>,
4306        image_url: Option<String>,
4307        original_price: Option<f64>,
4308        weight: Option<f64>,
4309        requires_shipping: Option<bool>,
4310    ) -> Self {
4311        Self {
4312            sku,
4313            name,
4314            quantity,
4315            unit_price,
4316            product_id,
4317            variant_id,
4318            description,
4319            image_url,
4320            original_price,
4321            weight,
4322            requires_shipping,
4323        }
4324    }
4325}
4326
4327// ============================================================================
4328// Carts API
4329// ============================================================================
4330
4331/// Cart and checkout management operations.
4332#[pyclass]
4333pub struct Carts {
4334    commerce: Arc<Mutex<RustCommerce>>,
4335}
4336
4337#[pymethods]
4338impl Carts {
4339    /// Create a new cart.
4340    ///
4341    /// Args:
4342    ///     customer_id: Customer UUID (optional for guest checkout)
4343    ///     customer_email: Customer email (optional)
4344    ///     customer_name: Customer name (optional)
4345    ///     currency: Currency code (default "USD")
4346    ///     expires_in_minutes: Cart expiration time (optional)
4347    ///
4348    /// Returns:
4349    ///     Cart: The created cart
4350    #[pyo3(signature = (customer_id=None, customer_email=None, customer_name=None, currency=None, expires_in_minutes=None))]
4351    fn create(
4352        &self,
4353        customer_id: Option<String>,
4354        customer_email: Option<String>,
4355        customer_name: Option<String>,
4356        currency: Option<String>,
4357        expires_in_minutes: Option<i64>,
4358    ) -> PyResult<Cart> {
4359        let commerce = self
4360            .commerce
4361            .lock()
4362            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
4363
4364        let cust_uuid = customer_id
4365            .map(|id| id.parse())
4366            .transpose()
4367            .map_err(|_| PyValueError::new_err("Invalid customer UUID"))?;
4368
4369        let cart = commerce
4370            .carts()
4371            .create(stateset_core::CreateCart {
4372                customer_id: cust_uuid,
4373                customer_email,
4374                customer_name,
4375                currency,
4376                expires_in_minutes,
4377                ..Default::default()
4378            })
4379            .map_err(|e| PyRuntimeError::new_err(format!("Failed to create cart: {}", e)))?;
4380
4381        Ok(cart.into())
4382    }
4383
4384    /// Get a cart by ID.
4385    ///
4386    /// Args:
4387    ///     id: Cart UUID
4388    ///
4389    /// Returns:
4390    ///     Cart or None if not found
4391    fn get(&self, id: String) -> PyResult<Option<Cart>> {
4392        let commerce = self
4393            .commerce
4394            .lock()
4395            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
4396
4397        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
4398
4399        let cart = commerce
4400            .carts()
4401            .get(uuid)
4402            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get cart: {}", e)))?;
4403
4404        Ok(cart.map(|c| c.into()))
4405    }
4406
4407    /// Get a cart by cart number.
4408    ///
4409    /// Args:
4410    ///     cart_number: Cart number string
4411    ///
4412    /// Returns:
4413    ///     Cart or None if not found
4414    fn get_by_number(&self, cart_number: String) -> PyResult<Option<Cart>> {
4415        let commerce = self
4416            .commerce
4417            .lock()
4418            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
4419
4420        let cart = commerce
4421            .carts()
4422            .get_by_number(&cart_number)
4423            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get cart: {}", e)))?;
4424
4425        Ok(cart.map(|c| c.into()))
4426    }
4427
4428    /// Update a cart.
4429    ///
4430    /// Args:
4431    ///     id: Cart UUID
4432    ///     customer_email: Customer email (optional)
4433    ///     customer_phone: Customer phone (optional)
4434    ///     customer_name: Customer name (optional)
4435    ///     shipping_method: Shipping method string (optional)
4436    ///     coupon_code: Coupon code (optional)
4437    ///     notes: Notes (optional)
4438    ///
4439    /// Returns:
4440    ///     Cart: Updated cart
4441    #[pyo3(signature = (id, customer_email=None, customer_phone=None, customer_name=None, shipping_method=None, coupon_code=None, notes=None))]
4442    fn update(
4443        &self,
4444        id: String,
4445        customer_email: Option<String>,
4446        customer_phone: Option<String>,
4447        customer_name: Option<String>,
4448        shipping_method: Option<String>,
4449        coupon_code: Option<String>,
4450        notes: Option<String>,
4451    ) -> PyResult<Cart> {
4452        let commerce = self
4453            .commerce
4454            .lock()
4455            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
4456
4457        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
4458
4459        let cart = commerce
4460            .carts()
4461            .update(
4462                uuid,
4463                stateset_core::UpdateCart {
4464                    customer_email,
4465                    customer_phone,
4466                    customer_name,
4467                    shipping_method,
4468                    coupon_code,
4469                    notes,
4470                    ..Default::default()
4471                },
4472            )
4473            .map_err(|e| PyRuntimeError::new_err(format!("Failed to update cart: {}", e)))?;
4474
4475        Ok(cart.into())
4476    }
4477
4478    /// List all carts.
4479    ///
4480    /// Returns:
4481    ///     List[Cart]: All carts
4482    fn list(&self) -> PyResult<Vec<Cart>> {
4483        let commerce = self
4484            .commerce
4485            .lock()
4486            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
4487
4488        let carts = commerce
4489            .carts()
4490            .list(Default::default())
4491            .map_err(|e| PyRuntimeError::new_err(format!("Failed to list carts: {}", e)))?;
4492
4493        Ok(carts.into_iter().map(|c| c.into()).collect())
4494    }
4495
4496    /// Get all carts for a customer.
4497    ///
4498    /// Args:
4499    ///     customer_id: Customer UUID
4500    ///
4501    /// Returns:
4502    ///     List[Cart]: Customer's carts
4503    fn for_customer(&self, customer_id: String) -> PyResult<Vec<Cart>> {
4504        let commerce = self
4505            .commerce
4506            .lock()
4507            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
4508
4509        let uuid = customer_id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
4510
4511        let carts = commerce
4512            .carts()
4513            .for_customer(uuid)
4514            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get customer carts: {}", e)))?;
4515
4516        Ok(carts.into_iter().map(|c| c.into()).collect())
4517    }
4518
4519    /// Delete a cart.
4520    ///
4521    /// Args:
4522    ///     id: Cart UUID
4523    fn delete(&self, id: String) -> PyResult<()> {
4524        let commerce = self
4525            .commerce
4526            .lock()
4527            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
4528
4529        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
4530
4531        commerce
4532            .carts()
4533            .delete(uuid)
4534            .map_err(|e| PyRuntimeError::new_err(format!("Failed to delete cart: {}", e)))?;
4535
4536        Ok(())
4537    }
4538
4539    // === Item Operations ===
4540
4541    /// Add an item to the cart.
4542    ///
4543    /// Args:
4544    ///     cart_id: Cart UUID
4545    ///     item: AddCartItemInput
4546    ///
4547    /// Returns:
4548    ///     CartItem: The added item
4549    fn add_item(&self, cart_id: String, item: AddCartItemInput) -> PyResult<CartItem> {
4550        let commerce = self
4551            .commerce
4552            .lock()
4553            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
4554
4555        let uuid = cart_id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
4556
4557        let prod_uuid = item
4558            .product_id
4559            .map(|id| id.parse())
4560            .transpose()
4561            .map_err(|_| PyValueError::new_err("Invalid product UUID"))?;
4562
4563        let var_uuid = item
4564            .variant_id
4565            .map(|id| id.parse())
4566            .transpose()
4567            .map_err(|_| PyValueError::new_err("Invalid variant UUID"))?;
4568
4569        let cart_item = commerce
4570            .carts()
4571            .add_item(
4572                uuid,
4573                stateset_core::AddCartItem {
4574                    product_id: prod_uuid,
4575                    variant_id: var_uuid,
4576                    sku: item.sku,
4577                    name: item.name,
4578                    description: item.description,
4579                    image_url: item.image_url,
4580                    quantity: item.quantity,
4581                    unit_price: Decimal::from_str_exact(&item.unit_price.to_string())
4582                        .unwrap_or_default(),
4583                    original_price: item
4584                        .original_price
4585                        .map(|p| Decimal::from_str_exact(&p.to_string()).unwrap_or_default()),
4586                    weight: item
4587                        .weight
4588                        .map(|w| Decimal::from_str_exact(&w.to_string()).unwrap_or_default()),
4589                    requires_shipping: item.requires_shipping,
4590                    metadata: None,
4591                },
4592            )
4593            .map_err(|e| PyRuntimeError::new_err(format!("Failed to add item: {}", e)))?;
4594
4595        Ok(cart_item.into())
4596    }
4597
4598    /// Update a cart item.
4599    ///
4600    /// Args:
4601    ///     item_id: Cart item UUID
4602    ///     quantity: New quantity (optional)
4603    ///
4604    /// Returns:
4605    ///     CartItem: The updated item
4606    #[pyo3(signature = (item_id, quantity=None))]
4607    fn update_item(&self, item_id: String, quantity: Option<i32>) -> PyResult<CartItem> {
4608        let commerce = self
4609            .commerce
4610            .lock()
4611            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
4612
4613        let uuid = item_id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
4614
4615        let cart_item = commerce
4616            .carts()
4617            .update_item(uuid, stateset_core::UpdateCartItem { quantity, ..Default::default() })
4618            .map_err(|e| PyRuntimeError::new_err(format!("Failed to update item: {}", e)))?;
4619
4620        Ok(cart_item.into())
4621    }
4622
4623    /// Remove an item from the cart.
4624    ///
4625    /// Args:
4626    ///     item_id: Cart item UUID
4627    fn remove_item(&self, item_id: String) -> PyResult<()> {
4628        let commerce = self
4629            .commerce
4630            .lock()
4631            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
4632
4633        let uuid = item_id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
4634
4635        commerce
4636            .carts()
4637            .remove_item(uuid)
4638            .map_err(|e| PyRuntimeError::new_err(format!("Failed to remove item: {}", e)))?;
4639
4640        Ok(())
4641    }
4642
4643    /// Get all items in a cart.
4644    ///
4645    /// Args:
4646    ///     cart_id: Cart UUID
4647    ///
4648    /// Returns:
4649    ///     List[CartItem]: Cart items
4650    fn get_items(&self, cart_id: String) -> PyResult<Vec<CartItem>> {
4651        let commerce = self
4652            .commerce
4653            .lock()
4654            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
4655
4656        let uuid = cart_id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
4657
4658        let items = commerce
4659            .carts()
4660            .get_items(uuid)
4661            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get items: {}", e)))?;
4662
4663        Ok(items.into_iter().map(|i| i.into()).collect())
4664    }
4665
4666    /// Clear all items from a cart.
4667    ///
4668    /// Args:
4669    ///     cart_id: Cart UUID
4670    fn clear_items(&self, cart_id: String) -> PyResult<()> {
4671        let commerce = self
4672            .commerce
4673            .lock()
4674            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
4675
4676        let uuid = cart_id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
4677
4678        commerce
4679            .carts()
4680            .clear_items(uuid)
4681            .map_err(|e| PyRuntimeError::new_err(format!("Failed to clear items: {}", e)))?;
4682
4683        Ok(())
4684    }
4685
4686    // === Address Operations ===
4687
4688    /// Set the shipping address.
4689    ///
4690    /// Args:
4691    ///     id: Cart UUID
4692    ///     address: CartAddress
4693    ///
4694    /// Returns:
4695    ///     Cart: Updated cart
4696    fn set_shipping_address(&self, id: String, address: CartAddress) -> PyResult<Cart> {
4697        let commerce = self
4698            .commerce
4699            .lock()
4700            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
4701
4702        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
4703
4704        let cart = commerce.carts().set_shipping_address(uuid, (&address).into()).map_err(|e| {
4705            PyRuntimeError::new_err(format!("Failed to set shipping address: {}", e))
4706        })?;
4707
4708        Ok(cart.into())
4709    }
4710
4711    /// Set the billing address.
4712    ///
4713    /// Args:
4714    ///     id: Cart UUID
4715    ///     address: CartAddress
4716    ///
4717    /// Returns:
4718    ///     Cart: Updated cart
4719    fn set_billing_address(&self, id: String, address: CartAddress) -> PyResult<Cart> {
4720        let commerce = self
4721            .commerce
4722            .lock()
4723            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
4724
4725        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
4726
4727        let cart = commerce.carts().set_billing_address(uuid, (&address).into()).map_err(|e| {
4728            PyRuntimeError::new_err(format!("Failed to set billing address: {}", e))
4729        })?;
4730
4731        Ok(cart.into())
4732    }
4733
4734    // === Shipping Operations ===
4735
4736    /// Set shipping selection (address + method/carrier/amount).
4737    ///
4738    /// Args:
4739    ///     id: Cart UUID
4740    ///     address: CartAddress
4741    ///     shipping_method: Shipping method (optional)
4742    ///     shipping_carrier: Shipping carrier (optional)
4743    ///     shipping_amount: Shipping amount (optional)
4744    ///
4745    /// Returns:
4746    ///     Cart: Updated cart
4747    #[pyo3(signature = (id, address, shipping_method=None, shipping_carrier=None, shipping_amount=None))]
4748    fn set_shipping(
4749        &self,
4750        id: String,
4751        address: CartAddress,
4752        shipping_method: Option<String>,
4753        shipping_carrier: Option<String>,
4754        shipping_amount: Option<f64>,
4755    ) -> PyResult<Cart> {
4756        let commerce = self
4757            .commerce
4758            .lock()
4759            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
4760
4761        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
4762
4763        let amount_dec = match shipping_amount {
4764            Some(v) => Some(
4765                Decimal::from_f64_retain(v)
4766                    .ok_or_else(|| PyValueError::new_err("Invalid shipping amount"))?,
4767            ),
4768            None => None,
4769        };
4770
4771        let cart = commerce
4772            .carts()
4773            .set_shipping(
4774                uuid,
4775                stateset_core::SetCartShipping {
4776                    shipping_address: (&address).into(),
4777                    shipping_method,
4778                    shipping_carrier,
4779                    shipping_amount: amount_dec,
4780                },
4781            )
4782            .map_err(|e| PyRuntimeError::new_err(format!("Failed to set shipping: {}", e)))?;
4783
4784        Ok(cart.into())
4785    }
4786
4787    /// Get available shipping rates for the cart.
4788    ///
4789    /// Args:
4790    ///     id: Cart UUID
4791    ///
4792    /// Returns:
4793    ///     List[ShippingRate]: Available shipping options
4794    fn get_shipping_rates(&self, id: String) -> PyResult<Vec<ShippingRate>> {
4795        let commerce = self
4796            .commerce
4797            .lock()
4798            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
4799
4800        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
4801
4802        let rates = commerce
4803            .carts()
4804            .get_shipping_rates(uuid)
4805            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get shipping rates: {}", e)))?;
4806
4807        Ok(rates.into_iter().map(|r| r.into()).collect())
4808    }
4809
4810    // === Payment Operations ===
4811
4812    /// Set the payment method.
4813    ///
4814    /// Args:
4815    ///     id: Cart UUID
4816    ///     payment_method: Payment method string (e.g., "credit_card")
4817    ///     payment_token: Payment token (optional)
4818    ///
4819    /// Returns:
4820    ///     Cart: Updated cart
4821    #[pyo3(signature = (id, payment_method, payment_token=None))]
4822    fn set_payment(
4823        &self,
4824        id: String,
4825        payment_method: String,
4826        payment_token: Option<String>,
4827    ) -> PyResult<Cart> {
4828        let commerce = self
4829            .commerce
4830            .lock()
4831            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
4832
4833        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
4834
4835        let cart = commerce
4836            .carts()
4837            .set_payment(
4838                uuid,
4839                stateset_core::SetCartPayment {
4840                    payment_method,
4841                    payment_token,
4842                    ..Default::default()
4843                },
4844            )
4845            .map_err(|e| PyRuntimeError::new_err(format!("Failed to set payment: {}", e)))?;
4846
4847        Ok(cart.into())
4848    }
4849
4850    // === Discount Operations ===
4851
4852    /// Apply a coupon code to the cart.
4853    ///
4854    /// Args:
4855    ///     id: Cart UUID
4856    ///     coupon_code: Coupon/discount code
4857    ///
4858    /// Returns:
4859    ///     Cart: Updated cart
4860    fn apply_discount(&self, id: String, coupon_code: String) -> PyResult<Cart> {
4861        let commerce = self
4862            .commerce
4863            .lock()
4864            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
4865
4866        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
4867
4868        let cart = commerce
4869            .carts()
4870            .apply_discount(uuid, &coupon_code)
4871            .map_err(|e| PyRuntimeError::new_err(format!("Failed to apply discount: {}", e)))?;
4872
4873        Ok(cart.into())
4874    }
4875
4876    /// Remove the discount from the cart.
4877    ///
4878    /// Args:
4879    ///     id: Cart UUID
4880    ///
4881    /// Returns:
4882    ///     Cart: Updated cart
4883    fn remove_discount(&self, id: String) -> PyResult<Cart> {
4884        let commerce = self
4885            .commerce
4886            .lock()
4887            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
4888
4889        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
4890
4891        let cart = commerce
4892            .carts()
4893            .remove_discount(uuid)
4894            .map_err(|e| PyRuntimeError::new_err(format!("Failed to remove discount: {}", e)))?;
4895
4896        Ok(cart.into())
4897    }
4898
4899    // === Checkout Flow ===
4900
4901    /// Mark the cart as ready for payment.
4902    ///
4903    /// Validates that all required info is present.
4904    ///
4905    /// Args:
4906    ///     id: Cart UUID
4907    ///
4908    /// Returns:
4909    ///     Cart: Updated cart
4910    fn mark_ready_for_payment(&self, id: String) -> PyResult<Cart> {
4911        let commerce = self
4912            .commerce
4913            .lock()
4914            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
4915
4916        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
4917
4918        let cart = commerce
4919            .carts()
4920            .mark_ready_for_payment(uuid)
4921            .map_err(|e| PyRuntimeError::new_err(format!("Failed to mark ready: {}", e)))?;
4922
4923        Ok(cart.into())
4924    }
4925
4926    /// Begin the checkout process.
4927    ///
4928    /// Args:
4929    ///     id: Cart UUID
4930    ///
4931    /// Returns:
4932    ///     Cart: Updated cart
4933    fn begin_checkout(&self, id: String) -> PyResult<Cart> {
4934        let commerce = self
4935            .commerce
4936            .lock()
4937            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
4938
4939        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
4940
4941        let cart = commerce
4942            .carts()
4943            .begin_checkout(uuid)
4944            .map_err(|e| PyRuntimeError::new_err(format!("Failed to begin checkout: {}", e)))?;
4945
4946        Ok(cart.into())
4947    }
4948
4949    /// Complete the checkout and create an order.
4950    ///
4951    /// Args:
4952    ///     id: Cart UUID
4953    ///
4954    /// Returns:
4955    ///     CheckoutResult: Order creation result
4956    fn complete(&self, id: String) -> PyResult<CheckoutResult> {
4957        let commerce = self
4958            .commerce
4959            .lock()
4960            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
4961
4962        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
4963
4964        let result = commerce
4965            .carts()
4966            .complete(uuid)
4967            .map_err(|e| PyRuntimeError::new_err(format!("Failed to complete checkout: {}", e)))?;
4968
4969        Ok(result.into())
4970    }
4971
4972    /// Cancel the cart.
4973    ///
4974    /// Args:
4975    ///     id: Cart UUID
4976    ///
4977    /// Returns:
4978    ///     Cart: Updated cart
4979    fn cancel(&self, id: String) -> PyResult<Cart> {
4980        let commerce = self
4981            .commerce
4982            .lock()
4983            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
4984
4985        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
4986
4987        let cart = commerce
4988            .carts()
4989            .cancel(uuid)
4990            .map_err(|e| PyRuntimeError::new_err(format!("Failed to cancel cart: {}", e)))?;
4991
4992        Ok(cart.into())
4993    }
4994
4995    /// Mark the cart as abandoned.
4996    ///
4997    /// Args:
4998    ///     id: Cart UUID
4999    ///
5000    /// Returns:
5001    ///     Cart: Updated cart
5002    fn abandon(&self, id: String) -> PyResult<Cart> {
5003        let commerce = self
5004            .commerce
5005            .lock()
5006            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
5007
5008        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
5009
5010        let cart = commerce
5011            .carts()
5012            .abandon(uuid)
5013            .map_err(|e| PyRuntimeError::new_err(format!("Failed to abandon cart: {}", e)))?;
5014
5015        Ok(cart.into())
5016    }
5017
5018    /// Expire the cart.
5019    ///
5020    /// Args:
5021    ///     id: Cart UUID
5022    ///
5023    /// Returns:
5024    ///     Cart: Updated cart
5025    fn expire(&self, id: String) -> PyResult<Cart> {
5026        let commerce = self
5027            .commerce
5028            .lock()
5029            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
5030
5031        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
5032
5033        let cart = commerce
5034            .carts()
5035            .expire(uuid)
5036            .map_err(|e| PyRuntimeError::new_err(format!("Failed to expire cart: {}", e)))?;
5037
5038        Ok(cart.into())
5039    }
5040
5041    // === Inventory Operations ===
5042
5043    /// Reserve inventory for cart items.
5044    ///
5045    /// Args:
5046    ///     id: Cart UUID
5047    ///
5048    /// Returns:
5049    ///     Cart: Updated cart
5050    fn reserve_inventory(&self, id: String) -> PyResult<Cart> {
5051        let commerce = self
5052            .commerce
5053            .lock()
5054            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
5055
5056        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
5057
5058        let cart = commerce
5059            .carts()
5060            .reserve_inventory(uuid)
5061            .map_err(|e| PyRuntimeError::new_err(format!("Failed to reserve inventory: {}", e)))?;
5062
5063        Ok(cart.into())
5064    }
5065
5066    /// Release inventory reservations.
5067    ///
5068    /// Args:
5069    ///     id: Cart UUID
5070    ///
5071    /// Returns:
5072    ///     Cart: Updated cart
5073    fn release_inventory(&self, id: String) -> PyResult<Cart> {
5074        let commerce = self
5075            .commerce
5076            .lock()
5077            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
5078
5079        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
5080
5081        let cart = commerce
5082            .carts()
5083            .release_inventory(uuid)
5084            .map_err(|e| PyRuntimeError::new_err(format!("Failed to release inventory: {}", e)))?;
5085
5086        Ok(cart.into())
5087    }
5088
5089    /// Recalculate cart totals.
5090    ///
5091    /// Args:
5092    ///     id: Cart UUID
5093    ///
5094    /// Returns:
5095    ///     Cart: Updated cart
5096    fn recalculate(&self, id: String) -> PyResult<Cart> {
5097        let commerce = self
5098            .commerce
5099            .lock()
5100            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
5101
5102        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
5103
5104        let cart = commerce
5105            .carts()
5106            .recalculate(uuid)
5107            .map_err(|e| PyRuntimeError::new_err(format!("Failed to recalculate: {}", e)))?;
5108
5109        Ok(cart.into())
5110    }
5111
5112    /// Set the tax amount for the cart.
5113    ///
5114    /// Args:
5115    ///     id: Cart UUID
5116    ///     tax_amount: Tax amount
5117    ///
5118    /// Returns:
5119    ///     Cart: Updated cart
5120    fn set_tax(&self, id: String, tax_amount: f64) -> PyResult<Cart> {
5121        let commerce = self
5122            .commerce
5123            .lock()
5124            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
5125
5126        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
5127
5128        let cart = commerce
5129            .carts()
5130            .set_tax(uuid, Decimal::from_str_exact(&tax_amount.to_string()).unwrap_or_default())
5131            .map_err(|e| PyRuntimeError::new_err(format!("Failed to set tax: {}", e)))?;
5132
5133        Ok(cart.into())
5134    }
5135
5136    // === Query Operations ===
5137
5138    /// Get abandoned carts.
5139    ///
5140    /// Returns:
5141    ///     List[Cart]: Abandoned carts
5142    fn get_abandoned(&self) -> PyResult<Vec<Cart>> {
5143        let commerce = self
5144            .commerce
5145            .lock()
5146            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
5147
5148        let carts = commerce.carts().get_abandoned().map_err(|e| {
5149            PyRuntimeError::new_err(format!("Failed to get abandoned carts: {}", e))
5150        })?;
5151
5152        Ok(carts.into_iter().map(|c| c.into()).collect())
5153    }
5154
5155    /// Get expired carts.
5156    ///
5157    /// Returns:
5158    ///     List[Cart]: Expired carts
5159    fn get_expired(&self) -> PyResult<Vec<Cart>> {
5160        let commerce = self
5161            .commerce
5162            .lock()
5163            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
5164
5165        let carts = commerce
5166            .carts()
5167            .get_expired()
5168            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get expired carts: {}", e)))?;
5169
5170        Ok(carts.into_iter().map(|c| c.into()).collect())
5171    }
5172
5173    /// Count carts.
5174    ///
5175    /// Returns:
5176    ///     int: Number of carts
5177    fn count(&self) -> PyResult<u32> {
5178        let commerce = self
5179            .commerce
5180            .lock()
5181            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
5182
5183        let count = commerce
5184            .carts()
5185            .count(Default::default())
5186            .map_err(|e| PyRuntimeError::new_err(format!("Failed to count carts: {}", e)))?;
5187
5188        Ok(count as u32)
5189    }
5190}
5191
5192// ============================================================================
5193// Analytics Types
5194// ============================================================================
5195
5196fn dec_to_f64(d: &Decimal) -> f64 {
5197    to_f64_or_nan(*d)
5198}
5199
5200fn parse_time_period(period: &str) -> stateset_core::TimePeriod {
5201    match period.to_lowercase().as_str() {
5202        "today" => stateset_core::TimePeriod::Today,
5203        "yesterday" => stateset_core::TimePeriod::Yesterday,
5204        "last7days" | "last_7_days" => stateset_core::TimePeriod::Last7Days,
5205        "last30days" | "last_30_days" => stateset_core::TimePeriod::Last30Days,
5206        "this_month" | "thismonth" => stateset_core::TimePeriod::ThisMonth,
5207        "last_month" | "lastmonth" => stateset_core::TimePeriod::LastMonth,
5208        "this_quarter" | "thisquarter" => stateset_core::TimePeriod::ThisQuarter,
5209        "last_quarter" | "lastquarter" => stateset_core::TimePeriod::LastQuarter,
5210        "this_year" | "thisyear" => stateset_core::TimePeriod::ThisYear,
5211        "last_year" | "lastyear" => stateset_core::TimePeriod::LastYear,
5212        "all_time" | "alltime" | "all" => stateset_core::TimePeriod::AllTime,
5213        _ => stateset_core::TimePeriod::Last30Days,
5214    }
5215}
5216
5217fn parse_time_granularity(granularity: &str) -> stateset_core::TimeGranularity {
5218    match granularity.to_lowercase().as_str() {
5219        "hour" | "hourly" => stateset_core::TimeGranularity::Hour,
5220        "day" | "daily" => stateset_core::TimeGranularity::Day,
5221        "week" | "weekly" => stateset_core::TimeGranularity::Week,
5222        "month" | "monthly" => stateset_core::TimeGranularity::Month,
5223        "quarter" | "quarterly" => stateset_core::TimeGranularity::Quarter,
5224        "year" | "yearly" => stateset_core::TimeGranularity::Year,
5225        _ => stateset_core::TimeGranularity::Day,
5226    }
5227}
5228
5229fn build_analytics_query(
5230    period: Option<String>,
5231    granularity: Option<String>,
5232    limit: Option<u32>,
5233) -> stateset_core::AnalyticsQuery {
5234    let mut q = stateset_core::AnalyticsQuery::new();
5235    if let Some(p) = period {
5236        q = q.period(parse_time_period(&p));
5237    }
5238    if let Some(g) = granularity {
5239        q = q.granularity(parse_time_granularity(&g));
5240    }
5241    if let Some(l) = limit {
5242        q = q.limit(l);
5243    }
5244    q
5245}
5246
5247/// Sales summary metrics.
5248#[pyclass]
5249#[derive(Clone)]
5250pub struct SalesSummary {
5251    #[pyo3(get)]
5252    total_revenue: f64,
5253    #[pyo3(get)]
5254    order_count: u32,
5255    #[pyo3(get)]
5256    average_order_value: f64,
5257    #[pyo3(get)]
5258    items_sold: u32,
5259    #[pyo3(get)]
5260    unique_customers: u32,
5261}
5262
5263impl From<stateset_core::SalesSummary> for SalesSummary {
5264    fn from(s: stateset_core::SalesSummary) -> Self {
5265        Self {
5266            total_revenue: dec_to_f64(&s.total_revenue),
5267            order_count: s.order_count as u32,
5268            average_order_value: dec_to_f64(&s.average_order_value),
5269            items_sold: s.items_sold as u32,
5270            unique_customers: s.unique_customers as u32,
5271        }
5272    }
5273}
5274
5275/// Revenue metrics grouped by time period.
5276#[pyclass]
5277#[derive(Clone)]
5278pub struct RevenueByPeriod {
5279    #[pyo3(get)]
5280    period: String,
5281    #[pyo3(get)]
5282    revenue: f64,
5283    #[pyo3(get)]
5284    order_count: u32,
5285    #[pyo3(get)]
5286    period_start: String,
5287}
5288
5289impl From<stateset_core::RevenueByPeriod> for RevenueByPeriod {
5290    fn from(r: stateset_core::RevenueByPeriod) -> Self {
5291        Self {
5292            period: r.period,
5293            revenue: dec_to_f64(&r.revenue),
5294            order_count: r.order_count as u32,
5295            period_start: r.period_start.to_rfc3339(),
5296        }
5297    }
5298}
5299
5300/// Top selling product metrics.
5301#[pyclass]
5302#[derive(Clone)]
5303pub struct TopProduct {
5304    #[pyo3(get)]
5305    product_id: Option<String>,
5306    #[pyo3(get)]
5307    sku: String,
5308    #[pyo3(get)]
5309    name: String,
5310    #[pyo3(get)]
5311    units_sold: u32,
5312    #[pyo3(get)]
5313    revenue: f64,
5314    #[pyo3(get)]
5315    order_count: u32,
5316}
5317
5318impl From<stateset_core::TopProduct> for TopProduct {
5319    fn from(p: stateset_core::TopProduct) -> Self {
5320        Self {
5321            product_id: p.product_id.map(|id| id.to_string()),
5322            sku: p.sku,
5323            name: p.name,
5324            units_sold: p.units_sold as u32,
5325            revenue: dec_to_f64(&p.revenue),
5326            order_count: p.order_count as u32,
5327        }
5328    }
5329}
5330
5331/// Product performance with period comparison.
5332#[pyclass]
5333#[derive(Clone)]
5334pub struct ProductPerformance {
5335    #[pyo3(get)]
5336    product_id: String,
5337    #[pyo3(get)]
5338    sku: String,
5339    #[pyo3(get)]
5340    name: String,
5341    #[pyo3(get)]
5342    units_sold: u32,
5343    #[pyo3(get)]
5344    revenue: f64,
5345    #[pyo3(get)]
5346    previous_units_sold: u32,
5347    #[pyo3(get)]
5348    previous_revenue: f64,
5349    #[pyo3(get)]
5350    units_growth_percent: f64,
5351    #[pyo3(get)]
5352    revenue_growth_percent: f64,
5353}
5354
5355impl From<stateset_core::ProductPerformance> for ProductPerformance {
5356    fn from(p: stateset_core::ProductPerformance) -> Self {
5357        Self {
5358            product_id: p.product_id.to_string(),
5359            sku: p.sku,
5360            name: p.name,
5361            units_sold: p.units_sold as u32,
5362            revenue: dec_to_f64(&p.revenue),
5363            previous_units_sold: p.previous_units_sold as u32,
5364            previous_revenue: dec_to_f64(&p.previous_revenue),
5365            units_growth_percent: dec_to_f64(&p.units_growth_percent),
5366            revenue_growth_percent: dec_to_f64(&p.revenue_growth_percent),
5367        }
5368    }
5369}
5370
5371/// Customer segment metrics.
5372#[pyclass]
5373#[derive(Clone)]
5374pub struct CustomerMetrics {
5375    #[pyo3(get)]
5376    total_customers: u32,
5377    #[pyo3(get)]
5378    new_customers: u32,
5379    #[pyo3(get)]
5380    returning_customers: u32,
5381    #[pyo3(get)]
5382    average_lifetime_value: f64,
5383    #[pyo3(get)]
5384    average_orders_per_customer: f64,
5385}
5386
5387impl From<stateset_core::CustomerMetrics> for CustomerMetrics {
5388    fn from(m: stateset_core::CustomerMetrics) -> Self {
5389        Self {
5390            total_customers: m.total_customers as u32,
5391            new_customers: m.new_customers as u32,
5392            returning_customers: m.returning_customers as u32,
5393            average_lifetime_value: dec_to_f64(&m.average_lifetime_value),
5394            average_orders_per_customer: dec_to_f64(&m.average_orders_per_customer),
5395        }
5396    }
5397}
5398
5399/// Top customer by spend.
5400#[pyclass]
5401#[derive(Clone)]
5402pub struct TopCustomer {
5403    #[pyo3(get)]
5404    customer_id: String,
5405    #[pyo3(get)]
5406    name: String,
5407    #[pyo3(get)]
5408    email: String,
5409    #[pyo3(get)]
5410    order_count: u32,
5411    #[pyo3(get)]
5412    total_spent: f64,
5413    #[pyo3(get)]
5414    average_order_value: f64,
5415}
5416
5417impl From<stateset_core::TopCustomer> for TopCustomer {
5418    fn from(c: stateset_core::TopCustomer) -> Self {
5419        Self {
5420            customer_id: c.customer_id.to_string(),
5421            name: c.name,
5422            email: c.email,
5423            order_count: c.order_count as u32,
5424            total_spent: dec_to_f64(&c.total_spent),
5425            average_order_value: dec_to_f64(&c.average_order_value),
5426        }
5427    }
5428}
5429
5430/// Inventory health summary.
5431#[pyclass]
5432#[derive(Clone)]
5433pub struct InventoryHealth {
5434    #[pyo3(get)]
5435    total_skus: u32,
5436    #[pyo3(get)]
5437    in_stock_skus: u32,
5438    #[pyo3(get)]
5439    low_stock_skus: u32,
5440    #[pyo3(get)]
5441    out_of_stock_skus: u32,
5442    #[pyo3(get)]
5443    total_value: f64,
5444}
5445
5446impl From<stateset_core::InventoryHealth> for InventoryHealth {
5447    fn from(h: stateset_core::InventoryHealth) -> Self {
5448        Self {
5449            total_skus: h.total_skus as u32,
5450            in_stock_skus: h.in_stock_skus as u32,
5451            low_stock_skus: h.low_stock_skus as u32,
5452            out_of_stock_skus: h.out_of_stock_skus as u32,
5453            total_value: dec_to_f64(&h.total_value),
5454        }
5455    }
5456}
5457
5458/// Low stock item.
5459#[pyclass]
5460#[derive(Clone)]
5461pub struct LowStockItem {
5462    #[pyo3(get)]
5463    sku: String,
5464    #[pyo3(get)]
5465    name: String,
5466    #[pyo3(get)]
5467    on_hand: f64,
5468    #[pyo3(get)]
5469    allocated: f64,
5470    #[pyo3(get)]
5471    available: f64,
5472    #[pyo3(get)]
5473    reorder_point: Option<f64>,
5474    #[pyo3(get)]
5475    average_daily_sales: Option<f64>,
5476    #[pyo3(get)]
5477    days_of_stock: Option<f64>,
5478}
5479
5480impl From<stateset_core::LowStockItem> for LowStockItem {
5481    fn from(i: stateset_core::LowStockItem) -> Self {
5482        Self {
5483            sku: i.sku,
5484            name: i.name,
5485            on_hand: dec_to_f64(&i.on_hand),
5486            allocated: dec_to_f64(&i.allocated),
5487            available: dec_to_f64(&i.available),
5488            reorder_point: i.reorder_point.as_ref().map(dec_to_f64),
5489            average_daily_sales: i.average_daily_sales.as_ref().map(dec_to_f64),
5490            days_of_stock: i.days_of_stock.as_ref().map(dec_to_f64),
5491        }
5492    }
5493}
5494
5495/// Inventory movement summary.
5496#[pyclass]
5497#[derive(Clone)]
5498pub struct InventoryMovement {
5499    #[pyo3(get)]
5500    sku: String,
5501    #[pyo3(get)]
5502    name: String,
5503    #[pyo3(get)]
5504    units_sold: u32,
5505    #[pyo3(get)]
5506    units_received: u32,
5507    #[pyo3(get)]
5508    units_returned: u32,
5509    #[pyo3(get)]
5510    units_adjusted: i32,
5511    #[pyo3(get)]
5512    net_change: i32,
5513}
5514
5515impl From<stateset_core::InventoryMovement> for InventoryMovement {
5516    fn from(m: stateset_core::InventoryMovement) -> Self {
5517        Self {
5518            sku: m.sku,
5519            name: m.name,
5520            units_sold: m.units_sold as u32,
5521            units_received: m.units_received as u32,
5522            units_returned: m.units_returned as u32,
5523            units_adjusted: m.units_adjusted as i32,
5524            net_change: m.net_change as i32,
5525        }
5526    }
5527}
5528
5529/// Order status breakdown.
5530#[pyclass]
5531#[derive(Clone)]
5532pub struct OrderStatusBreakdown {
5533    #[pyo3(get)]
5534    pending: u32,
5535    #[pyo3(get)]
5536    confirmed: u32,
5537    #[pyo3(get)]
5538    processing: u32,
5539    #[pyo3(get)]
5540    shipped: u32,
5541    #[pyo3(get)]
5542    delivered: u32,
5543    #[pyo3(get)]
5544    cancelled: u32,
5545    #[pyo3(get)]
5546    refunded: u32,
5547}
5548
5549impl From<stateset_core::OrderStatusBreakdown> for OrderStatusBreakdown {
5550    fn from(b: stateset_core::OrderStatusBreakdown) -> Self {
5551        Self {
5552            pending: b.pending as u32,
5553            confirmed: b.confirmed as u32,
5554            processing: b.processing as u32,
5555            shipped: b.shipped as u32,
5556            delivered: b.delivered as u32,
5557            cancelled: b.cancelled as u32,
5558            refunded: b.refunded as u32,
5559        }
5560    }
5561}
5562
5563/// Order fulfillment metrics.
5564#[pyclass]
5565#[derive(Clone)]
5566pub struct FulfillmentMetrics {
5567    #[pyo3(get)]
5568    avg_time_to_ship_hours: Option<f64>,
5569    #[pyo3(get)]
5570    avg_time_to_deliver_hours: Option<f64>,
5571    #[pyo3(get)]
5572    on_time_shipping_percent: Option<f64>,
5573    #[pyo3(get)]
5574    on_time_delivery_percent: Option<f64>,
5575    #[pyo3(get)]
5576    shipped_today: u32,
5577    #[pyo3(get)]
5578    awaiting_shipment: u32,
5579}
5580
5581impl From<stateset_core::FulfillmentMetrics> for FulfillmentMetrics {
5582    fn from(m: stateset_core::FulfillmentMetrics) -> Self {
5583        Self {
5584            avg_time_to_ship_hours: m.avg_time_to_ship_hours.as_ref().map(dec_to_f64),
5585            avg_time_to_deliver_hours: m.avg_time_to_deliver_hours.as_ref().map(dec_to_f64),
5586            on_time_shipping_percent: m.on_time_shipping_percent.as_ref().map(dec_to_f64),
5587            on_time_delivery_percent: m.on_time_delivery_percent.as_ref().map(dec_to_f64),
5588            shipped_today: m.shipped_today as u32,
5589            awaiting_shipment: m.awaiting_shipment as u32,
5590        }
5591    }
5592}
5593
5594/// Return metrics.
5595#[pyclass]
5596#[derive(Clone)]
5597pub struct ReturnMetrics {
5598    #[pyo3(get)]
5599    total_returns: u32,
5600    #[pyo3(get)]
5601    return_rate_percent: f64,
5602    #[pyo3(get)]
5603    total_refunded: f64,
5604}
5605
5606impl From<stateset_core::ReturnMetrics> for ReturnMetrics {
5607    fn from(m: stateset_core::ReturnMetrics) -> Self {
5608        Self {
5609            total_returns: m.total_returns as u32,
5610            return_rate_percent: dec_to_f64(&m.return_rate_percent),
5611            total_refunded: dec_to_f64(&m.total_refunded),
5612        }
5613    }
5614}
5615
5616fn trend_to_string(t: &stateset_core::Trend) -> String {
5617    match t {
5618        stateset_core::Trend::Rising => "rising".to_string(),
5619        stateset_core::Trend::Stable => "stable".to_string(),
5620        stateset_core::Trend::Falling => "falling".to_string(),
5621    }
5622}
5623
5624/// Demand forecast for a SKU.
5625#[pyclass]
5626#[derive(Clone)]
5627pub struct DemandForecast {
5628    #[pyo3(get)]
5629    sku: String,
5630    #[pyo3(get)]
5631    name: String,
5632    #[pyo3(get)]
5633    average_daily_demand: f64,
5634    #[pyo3(get)]
5635    forecasted_demand: f64,
5636    #[pyo3(get)]
5637    confidence: f64,
5638    #[pyo3(get)]
5639    current_stock: f64,
5640    #[pyo3(get)]
5641    days_until_stockout: Option<i32>,
5642    #[pyo3(get)]
5643    recommended_reorder_qty: Option<f64>,
5644    #[pyo3(get)]
5645    trend: String,
5646}
5647
5648impl From<stateset_core::DemandForecast> for DemandForecast {
5649    fn from(f: stateset_core::DemandForecast) -> Self {
5650        Self {
5651            sku: f.sku,
5652            name: f.name,
5653            average_daily_demand: dec_to_f64(&f.average_daily_demand),
5654            forecasted_demand: dec_to_f64(&f.forecasted_demand),
5655            confidence: dec_to_f64(&f.confidence),
5656            current_stock: dec_to_f64(&f.current_stock),
5657            days_until_stockout: f.days_until_stockout,
5658            recommended_reorder_qty: f.recommended_reorder_qty.as_ref().map(dec_to_f64),
5659            trend: trend_to_string(&f.trend),
5660        }
5661    }
5662}
5663
5664/// Revenue forecast.
5665#[pyclass]
5666#[derive(Clone)]
5667pub struct RevenueForecast {
5668    #[pyo3(get)]
5669    period: String,
5670    #[pyo3(get)]
5671    forecasted_revenue: f64,
5672    #[pyo3(get)]
5673    lower_bound: f64,
5674    #[pyo3(get)]
5675    upper_bound: f64,
5676    #[pyo3(get)]
5677    confidence_level: f64,
5678    #[pyo3(get)]
5679    based_on_periods: u32,
5680}
5681
5682impl From<stateset_core::RevenueForecast> for RevenueForecast {
5683    fn from(f: stateset_core::RevenueForecast) -> Self {
5684        Self {
5685            period: f.period,
5686            forecasted_revenue: dec_to_f64(&f.forecasted_revenue),
5687            lower_bound: dec_to_f64(&f.lower_bound),
5688            upper_bound: dec_to_f64(&f.upper_bound),
5689            confidence_level: dec_to_f64(&f.confidence_level),
5690            based_on_periods: f.based_on_periods,
5691        }
5692    }
5693}
5694
5695// ============================================================================
5696// Analytics API
5697// ============================================================================
5698
5699/// Business intelligence and forecasting operations.
5700#[pyclass]
5701pub struct Analytics {
5702    commerce: Arc<Mutex<RustCommerce>>,
5703}
5704
5705#[pymethods]
5706impl Analytics {
5707    /// Get sales summary.
5708    #[pyo3(signature = (period=None, limit=None))]
5709    fn sales_summary(&self, period: Option<String>, limit: Option<u32>) -> PyResult<SalesSummary> {
5710        let commerce = self
5711            .commerce
5712            .lock()
5713            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
5714
5715        let q = build_analytics_query(period, None, limit);
5716        let summary = commerce
5717            .analytics()
5718            .sales_summary(q)
5719            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get sales summary: {}", e)))?;
5720
5721        Ok(summary.into())
5722    }
5723
5724    /// Get revenue by period.
5725    #[pyo3(signature = (period=None, granularity=None))]
5726    fn revenue_by_period(
5727        &self,
5728        period: Option<String>,
5729        granularity: Option<String>,
5730    ) -> PyResult<Vec<RevenueByPeriod>> {
5731        let commerce = self
5732            .commerce
5733            .lock()
5734            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
5735
5736        let q = build_analytics_query(period, granularity, None);
5737        let rows = commerce
5738            .analytics()
5739            .revenue_by_period(q)
5740            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get revenue: {}", e)))?;
5741
5742        Ok(rows.into_iter().map(|r| r.into()).collect())
5743    }
5744
5745    /// Get top selling products.
5746    #[pyo3(signature = (period=None, limit=None))]
5747    fn top_products(
5748        &self,
5749        period: Option<String>,
5750        limit: Option<u32>,
5751    ) -> PyResult<Vec<TopProduct>> {
5752        let commerce = self
5753            .commerce
5754            .lock()
5755            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
5756
5757        let q = build_analytics_query(period, None, limit);
5758        let rows = commerce
5759            .analytics()
5760            .top_products(q)
5761            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get top products: {}", e)))?;
5762
5763        Ok(rows.into_iter().map(|p| p.into()).collect())
5764    }
5765
5766    /// Get product performance with period comparison.
5767    #[pyo3(signature = (period=None, limit=None))]
5768    fn product_performance(
5769        &self,
5770        period: Option<String>,
5771        limit: Option<u32>,
5772    ) -> PyResult<Vec<ProductPerformance>> {
5773        let commerce = self
5774            .commerce
5775            .lock()
5776            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
5777
5778        let q = build_analytics_query(period, None, limit);
5779        let rows = commerce.analytics().product_performance(q).map_err(|e| {
5780            PyRuntimeError::new_err(format!("Failed to get product performance: {}", e))
5781        })?;
5782
5783        Ok(rows.into_iter().map(|p| p.into()).collect())
5784    }
5785
5786    /// Get customer metrics.
5787    #[pyo3(signature = (period=None))]
5788    fn customer_metrics(&self, period: Option<String>) -> PyResult<CustomerMetrics> {
5789        let commerce = self
5790            .commerce
5791            .lock()
5792            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
5793
5794        let q = build_analytics_query(period, None, None);
5795        let metrics = commerce.analytics().customer_metrics(q).map_err(|e| {
5796            PyRuntimeError::new_err(format!("Failed to get customer metrics: {}", e))
5797        })?;
5798
5799        Ok(metrics.into())
5800    }
5801
5802    /// Get top customers by spend.
5803    #[pyo3(signature = (period=None, limit=None))]
5804    fn top_customers(
5805        &self,
5806        period: Option<String>,
5807        limit: Option<u32>,
5808    ) -> PyResult<Vec<TopCustomer>> {
5809        let commerce = self
5810            .commerce
5811            .lock()
5812            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
5813
5814        let q = build_analytics_query(period, None, limit);
5815        let rows = commerce
5816            .analytics()
5817            .top_customers(q)
5818            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get top customers: {}", e)))?;
5819
5820        Ok(rows.into_iter().map(|c| c.into()).collect())
5821    }
5822
5823    /// Get inventory health summary.
5824    fn inventory_health(&self) -> PyResult<InventoryHealth> {
5825        let commerce = self
5826            .commerce
5827            .lock()
5828            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
5829
5830        let health = commerce.analytics().inventory_health().map_err(|e| {
5831            PyRuntimeError::new_err(format!("Failed to get inventory health: {}", e))
5832        })?;
5833
5834        Ok(health.into())
5835    }
5836
5837    /// Get low stock items.
5838    #[pyo3(signature = (threshold=None))]
5839    fn low_stock_items(&self, threshold: Option<f64>) -> PyResult<Vec<LowStockItem>> {
5840        let commerce = self
5841            .commerce
5842            .lock()
5843            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
5844
5845        let threshold_dec = match threshold {
5846            Some(v) => Some(
5847                Decimal::from_f64_retain(v)
5848                    .ok_or_else(|| PyValueError::new_err("Invalid threshold"))?,
5849            ),
5850            None => None,
5851        };
5852
5853        let rows = commerce.analytics().low_stock_items(threshold_dec).map_err(|e| {
5854            PyRuntimeError::new_err(format!("Failed to get low stock items: {}", e))
5855        })?;
5856
5857        Ok(rows.into_iter().map(|i| i.into()).collect())
5858    }
5859
5860    /// Get inventory movement summary.
5861    #[pyo3(signature = (period=None))]
5862    fn inventory_movement(&self, period: Option<String>) -> PyResult<Vec<InventoryMovement>> {
5863        let commerce = self
5864            .commerce
5865            .lock()
5866            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
5867
5868        let q = build_analytics_query(period, None, None);
5869        let rows = commerce.analytics().inventory_movement(q).map_err(|e| {
5870            PyRuntimeError::new_err(format!("Failed to get inventory movement: {}", e))
5871        })?;
5872
5873        Ok(rows.into_iter().map(|m| m.into()).collect())
5874    }
5875
5876    /// Get order status breakdown.
5877    #[pyo3(signature = (period=None))]
5878    fn order_status_breakdown(&self, period: Option<String>) -> PyResult<OrderStatusBreakdown> {
5879        let commerce = self
5880            .commerce
5881            .lock()
5882            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
5883
5884        let q = build_analytics_query(period, None, None);
5885        let breakdown = commerce.analytics().order_status_breakdown(q).map_err(|e| {
5886            PyRuntimeError::new_err(format!("Failed to get order status breakdown: {}", e))
5887        })?;
5888
5889        Ok(breakdown.into())
5890    }
5891
5892    /// Get fulfillment metrics.
5893    #[pyo3(signature = (period=None))]
5894    fn fulfillment_metrics(&self, period: Option<String>) -> PyResult<FulfillmentMetrics> {
5895        let commerce = self
5896            .commerce
5897            .lock()
5898            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
5899
5900        let q = build_analytics_query(period, None, None);
5901        let metrics = commerce.analytics().fulfillment_metrics(q).map_err(|e| {
5902            PyRuntimeError::new_err(format!("Failed to get fulfillment metrics: {}", e))
5903        })?;
5904
5905        Ok(metrics.into())
5906    }
5907
5908    /// Get return metrics.
5909    #[pyo3(signature = (period=None))]
5910    fn return_metrics(&self, period: Option<String>) -> PyResult<ReturnMetrics> {
5911        let commerce = self
5912            .commerce
5913            .lock()
5914            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
5915
5916        let q = build_analytics_query(period, None, None);
5917        let metrics = commerce
5918            .analytics()
5919            .return_metrics(q)
5920            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get return metrics: {}", e)))?;
5921
5922        Ok(metrics.into())
5923    }
5924
5925    /// Get demand forecast.
5926    #[pyo3(signature = (skus=None, days_ahead=None))]
5927    fn demand_forecast(
5928        &self,
5929        skus: Option<Vec<String>>,
5930        days_ahead: Option<u32>,
5931    ) -> PyResult<Vec<DemandForecast>> {
5932        let commerce = self
5933            .commerce
5934            .lock()
5935            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
5936
5937        let forecasts =
5938            commerce.analytics().demand_forecast(skus, days_ahead.unwrap_or(30)).map_err(|e| {
5939                PyRuntimeError::new_err(format!("Failed to get demand forecast: {}", e))
5940            })?;
5941
5942        Ok(forecasts.into_iter().map(|f| f.into()).collect())
5943    }
5944
5945    /// Get revenue forecast.
5946    #[pyo3(signature = (periods_ahead=None, granularity=None))]
5947    fn revenue_forecast(
5948        &self,
5949        periods_ahead: Option<u32>,
5950        granularity: Option<String>,
5951    ) -> PyResult<Vec<RevenueForecast>> {
5952        let commerce = self
5953            .commerce
5954            .lock()
5955            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
5956
5957        let gran = granularity
5958            .as_deref()
5959            .map(parse_time_granularity)
5960            .unwrap_or(stateset_core::TimeGranularity::Month);
5961
5962        let forecasts =
5963            commerce.analytics().revenue_forecast(periods_ahead.unwrap_or(3), gran).map_err(
5964                |e| PyRuntimeError::new_err(format!("Failed to get revenue forecast: {}", e)),
5965            )?;
5966
5967        Ok(forecasts.into_iter().map(|f| f.into()).collect())
5968    }
5969}
5970
5971// ============================================================================
5972// Currency Types + API
5973// ============================================================================
5974
5975fn parse_currency(code: &str) -> PyResult<stateset_core::Currency> {
5976    use std::str::FromStr;
5977    stateset_core::Currency::from_str(code)
5978        .map_err(|e| PyValueError::new_err(format!("Invalid currency code '{}': {}", code, e)))
5979}
5980
5981fn parse_rounding_mode(mode: &str) -> stateset_core::RoundingMode {
5982    match mode.to_lowercase().as_str() {
5983        "half_down" => stateset_core::RoundingMode::HalfDown,
5984        "up" => stateset_core::RoundingMode::Up,
5985        "down" => stateset_core::RoundingMode::Down,
5986        "half_even" => stateset_core::RoundingMode::HalfEven,
5987        _ => stateset_core::RoundingMode::HalfUp,
5988    }
5989}
5990
5991fn rounding_mode_to_string(mode: &stateset_core::RoundingMode) -> String {
5992    match mode {
5993        stateset_core::RoundingMode::HalfUp => "half_up".to_string(),
5994        stateset_core::RoundingMode::HalfDown => "half_down".to_string(),
5995        stateset_core::RoundingMode::Up => "up".to_string(),
5996        stateset_core::RoundingMode::Down => "down".to_string(),
5997        stateset_core::RoundingMode::HalfEven => "half_even".to_string(),
5998    }
5999}
6000
6001/// Exchange rate between currencies.
6002#[pyclass]
6003#[derive(Clone)]
6004pub struct ExchangeRate {
6005    #[pyo3(get)]
6006    id: String,
6007    #[pyo3(get)]
6008    base_currency: String,
6009    #[pyo3(get)]
6010    quote_currency: String,
6011    #[pyo3(get)]
6012    rate: f64,
6013    #[pyo3(get)]
6014    source: String,
6015    #[pyo3(get)]
6016    rate_at: String,
6017    #[pyo3(get)]
6018    created_at: String,
6019    #[pyo3(get)]
6020    updated_at: String,
6021}
6022
6023impl From<stateset_core::ExchangeRate> for ExchangeRate {
6024    fn from(r: stateset_core::ExchangeRate) -> Self {
6025        Self {
6026            id: r.id.to_string(),
6027            base_currency: r.base_currency.code().to_string(),
6028            quote_currency: r.quote_currency.code().to_string(),
6029            rate: dec_to_f64(&r.rate),
6030            source: r.source,
6031            rate_at: r.rate_at.to_rfc3339(),
6032            created_at: r.created_at.to_rfc3339(),
6033            updated_at: r.updated_at.to_rfc3339(),
6034        }
6035    }
6036}
6037
6038/// Result of a currency conversion.
6039#[pyclass]
6040#[derive(Clone)]
6041pub struct ConversionResult {
6042    #[pyo3(get)]
6043    original_amount: f64,
6044    #[pyo3(get)]
6045    original_currency: String,
6046    #[pyo3(get)]
6047    converted_amount: f64,
6048    #[pyo3(get)]
6049    target_currency: String,
6050    #[pyo3(get)]
6051    rate: f64,
6052    #[pyo3(get)]
6053    inverse_rate: f64,
6054    #[pyo3(get)]
6055    rate_at: String,
6056}
6057
6058impl From<stateset_core::ConversionResult> for ConversionResult {
6059    fn from(r: stateset_core::ConversionResult) -> Self {
6060        Self {
6061            original_amount: dec_to_f64(&r.original_amount),
6062            original_currency: r.original_currency.code().to_string(),
6063            converted_amount: dec_to_f64(&r.converted_amount),
6064            target_currency: r.target_currency.code().to_string(),
6065            rate: dec_to_f64(&r.rate),
6066            inverse_rate: dec_to_f64(&r.inverse_rate),
6067            rate_at: r.rate_at.to_rfc3339(),
6068        }
6069    }
6070}
6071
6072/// Store currency settings.
6073#[pyclass]
6074#[derive(Clone)]
6075pub struct StoreCurrencySettings {
6076    #[pyo3(get)]
6077    base_currency: String,
6078    #[pyo3(get)]
6079    enabled_currencies: Vec<String>,
6080    #[pyo3(get)]
6081    auto_convert: bool,
6082    #[pyo3(get)]
6083    rounding_mode: String,
6084}
6085
6086impl From<stateset_core::StoreCurrencySettings> for StoreCurrencySettings {
6087    fn from(s: stateset_core::StoreCurrencySettings) -> Self {
6088        Self {
6089            base_currency: s.base_currency.code().to_string(),
6090            enabled_currencies: s.enabled_currencies.iter().map(|c| c.code().to_string()).collect(),
6091            auto_convert: s.auto_convert,
6092            rounding_mode: rounding_mode_to_string(&s.rounding_mode),
6093        }
6094    }
6095}
6096
6097/// Input for setting an exchange rate.
6098#[pyclass]
6099#[derive(Clone)]
6100pub struct SetExchangeRateInput {
6101    #[pyo3(get, set)]
6102    base_currency: String,
6103    #[pyo3(get, set)]
6104    quote_currency: String,
6105    #[pyo3(get, set)]
6106    rate: f64,
6107    #[pyo3(get, set)]
6108    source: Option<String>,
6109}
6110
6111#[pymethods]
6112impl SetExchangeRateInput {
6113    #[new]
6114    #[pyo3(signature = (base_currency, quote_currency, rate, source=None))]
6115    fn new(
6116        base_currency: String,
6117        quote_currency: String,
6118        rate: f64,
6119        source: Option<String>,
6120    ) -> Self {
6121        Self { base_currency, quote_currency, rate, source }
6122    }
6123}
6124
6125/// Currency and exchange rate operations.
6126#[pyclass]
6127pub struct CurrencyOperations {
6128    commerce: Arc<Mutex<RustCommerce>>,
6129}
6130
6131#[pymethods]
6132impl CurrencyOperations {
6133    /// Get exchange rate between two currencies.
6134    fn get_rate(
6135        &self,
6136        from_currency: String,
6137        to_currency: String,
6138    ) -> PyResult<Option<ExchangeRate>> {
6139        let commerce = self
6140            .commerce
6141            .lock()
6142            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
6143
6144        let rate = commerce
6145            .currency()
6146            .get_rate(parse_currency(&from_currency)?, parse_currency(&to_currency)?)
6147            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get rate: {}", e)))?;
6148
6149        Ok(rate.map(|r| r.into()))
6150    }
6151
6152    /// Get all exchange rates for a base currency.
6153    fn get_rates_for(&self, base_currency: String) -> PyResult<Vec<ExchangeRate>> {
6154        let commerce = self
6155            .commerce
6156            .lock()
6157            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
6158
6159        let rates = commerce
6160            .currency()
6161            .get_rates_for(parse_currency(&base_currency)?)
6162            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get rates: {}", e)))?;
6163
6164        Ok(rates.into_iter().map(|r| r.into()).collect())
6165    }
6166
6167    /// List exchange rates with optional filtering.
6168    #[pyo3(signature = (base_currency=None, quote_currency=None))]
6169    fn list_rates(
6170        &self,
6171        base_currency: Option<String>,
6172        quote_currency: Option<String>,
6173    ) -> PyResult<Vec<ExchangeRate>> {
6174        let commerce = self
6175            .commerce
6176            .lock()
6177            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
6178
6179        let base = match base_currency {
6180            Some(c) => Some(parse_currency(&c)?),
6181            None => None,
6182        };
6183        let quote = match quote_currency {
6184            Some(c) => Some(parse_currency(&c)?),
6185            None => None,
6186        };
6187
6188        let rates = commerce
6189            .currency()
6190            .list_rates(stateset_core::ExchangeRateFilter {
6191                base_currency: base,
6192                quote_currency: quote,
6193                ..Default::default()
6194            })
6195            .map_err(|e| PyRuntimeError::new_err(format!("Failed to list rates: {}", e)))?;
6196
6197        Ok(rates.into_iter().map(|r| r.into()).collect())
6198    }
6199
6200    /// Set an exchange rate.
6201    #[pyo3(signature = (base_currency, quote_currency, rate, source=None))]
6202    fn set_rate(
6203        &self,
6204        base_currency: String,
6205        quote_currency: String,
6206        rate: f64,
6207        source: Option<String>,
6208    ) -> PyResult<ExchangeRate> {
6209        let commerce = self
6210            .commerce
6211            .lock()
6212            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
6213
6214        let rate_dec = Decimal::from_f64_retain(rate)
6215            .ok_or_else(|| PyValueError::new_err("Invalid exchange rate"))?;
6216
6217        let rate = commerce
6218            .currency()
6219            .set_rate(stateset_core::SetExchangeRate {
6220                base_currency: parse_currency(&base_currency)?,
6221                quote_currency: parse_currency(&quote_currency)?,
6222                rate: rate_dec,
6223                source,
6224            })
6225            .map_err(|e| PyRuntimeError::new_err(format!("Failed to set rate: {}", e)))?;
6226
6227        Ok(rate.into())
6228    }
6229
6230    /// Set multiple exchange rates.
6231    fn set_rates(&self, rates: Vec<SetExchangeRateInput>) -> PyResult<Vec<ExchangeRate>> {
6232        let commerce = self
6233            .commerce
6234            .lock()
6235            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
6236
6237        let mut inputs = Vec::with_capacity(rates.len());
6238        for r in rates {
6239            let rate_dec = Decimal::from_f64_retain(r.rate)
6240                .ok_or_else(|| PyValueError::new_err("Invalid exchange rate"))?;
6241
6242            inputs.push(stateset_core::SetExchangeRate {
6243                base_currency: parse_currency(&r.base_currency)?,
6244                quote_currency: parse_currency(&r.quote_currency)?,
6245                rate: rate_dec,
6246                source: r.source,
6247            });
6248        }
6249
6250        let results = commerce
6251            .currency()
6252            .set_rates(inputs)
6253            .map_err(|e| PyRuntimeError::new_err(format!("Failed to set rates: {}", e)))?;
6254
6255        Ok(results.into_iter().map(|r| r.into()).collect())
6256    }
6257
6258    /// Delete an exchange rate by ID.
6259    fn delete_rate(&self, id: String) -> PyResult<()> {
6260        let commerce = self
6261            .commerce
6262            .lock()
6263            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
6264
6265        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
6266
6267        commerce
6268            .currency()
6269            .delete_rate(uuid)
6270            .map_err(|e| PyRuntimeError::new_err(format!("Failed to delete rate: {}", e)))?;
6271
6272        Ok(())
6273    }
6274
6275    /// Convert an amount from one currency to another.
6276    fn convert(
6277        &self,
6278        from_currency: String,
6279        to_currency: String,
6280        amount: f64,
6281    ) -> PyResult<ConversionResult> {
6282        let commerce = self
6283            .commerce
6284            .lock()
6285            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
6286
6287        let amount_dec = Decimal::from_f64_retain(amount)
6288            .ok_or_else(|| PyValueError::new_err("Invalid amount"))?;
6289
6290        let result = commerce
6291            .currency()
6292            .convert(stateset_core::ConvertCurrency {
6293                from: parse_currency(&from_currency)?,
6294                to: parse_currency(&to_currency)?,
6295                amount: amount_dec,
6296            })
6297            .map_err(|e| PyRuntimeError::new_err(format!("Failed to convert currency: {}", e)))?;
6298
6299        Ok(result.into())
6300    }
6301
6302    /// Get store currency settings.
6303    fn get_settings(&self) -> PyResult<StoreCurrencySettings> {
6304        let commerce = self
6305            .commerce
6306            .lock()
6307            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
6308
6309        let settings = commerce
6310            .currency()
6311            .get_settings()
6312            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get settings: {}", e)))?;
6313
6314        Ok(settings.into())
6315    }
6316
6317    /// Update store currency settings.
6318    #[pyo3(signature = (base_currency, enabled_currencies, auto_convert=None, rounding_mode=None))]
6319    fn update_settings(
6320        &self,
6321        base_currency: String,
6322        enabled_currencies: Vec<String>,
6323        auto_convert: Option<bool>,
6324        rounding_mode: Option<String>,
6325    ) -> PyResult<StoreCurrencySettings> {
6326        let commerce = self
6327            .commerce
6328            .lock()
6329            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
6330
6331        let mut enabled = Vec::with_capacity(enabled_currencies.len());
6332        for c in &enabled_currencies {
6333            enabled.push(parse_currency(c)?);
6334        }
6335
6336        let settings = commerce
6337            .currency()
6338            .update_settings(stateset_core::StoreCurrencySettings {
6339                base_currency: parse_currency(&base_currency)?,
6340                enabled_currencies: enabled,
6341                auto_convert: auto_convert.unwrap_or(true),
6342                rounding_mode: rounding_mode
6343                    .as_deref()
6344                    .map(parse_rounding_mode)
6345                    .unwrap_or_default(),
6346            })
6347            .map_err(|e| PyRuntimeError::new_err(format!("Failed to update settings: {}", e)))?;
6348
6349        Ok(settings.into())
6350    }
6351
6352    /// Set the store's base currency.
6353    fn set_base_currency(&self, currency_code: String) -> PyResult<StoreCurrencySettings> {
6354        let commerce = self
6355            .commerce
6356            .lock()
6357            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
6358
6359        let settings = commerce
6360            .currency()
6361            .set_base_currency(parse_currency(&currency_code)?)
6362            .map_err(|e| PyRuntimeError::new_err(format!("Failed to set base currency: {}", e)))?;
6363
6364        Ok(settings.into())
6365    }
6366
6367    /// Enable currencies for the store.
6368    fn enable_currencies(&self, currency_codes: Vec<String>) -> PyResult<StoreCurrencySettings> {
6369        let commerce = self
6370            .commerce
6371            .lock()
6372            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
6373
6374        let mut currencies = Vec::with_capacity(currency_codes.len());
6375        for c in &currency_codes {
6376            currencies.push(parse_currency(c)?);
6377        }
6378
6379        let settings = commerce
6380            .currency()
6381            .enable_currencies(currencies)
6382            .map_err(|e| PyRuntimeError::new_err(format!("Failed to enable currencies: {}", e)))?;
6383
6384        Ok(settings.into())
6385    }
6386
6387    /// Check if a currency is enabled for the store.
6388    fn is_enabled(&self, currency_code: String) -> PyResult<bool> {
6389        let commerce = self
6390            .commerce
6391            .lock()
6392            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
6393
6394        commerce
6395            .currency()
6396            .is_enabled(parse_currency(&currency_code)?)
6397            .map_err(|e| PyRuntimeError::new_err(format!("Failed to check currency: {}", e)))
6398    }
6399
6400    /// Get the store's base currency code.
6401    fn base_currency(&self) -> PyResult<String> {
6402        let commerce = self
6403            .commerce
6404            .lock()
6405            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
6406
6407        let currency = commerce
6408            .currency()
6409            .base_currency()
6410            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get base currency: {}", e)))?;
6411
6412        Ok(currency.code().to_string())
6413    }
6414
6415    /// Get enabled currency codes.
6416    fn enabled_currencies(&self) -> PyResult<Vec<String>> {
6417        let commerce = self
6418            .commerce
6419            .lock()
6420            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
6421
6422        let currencies = commerce.currency().enabled_currencies().map_err(|e| {
6423            PyRuntimeError::new_err(format!("Failed to get enabled currencies: {}", e))
6424        })?;
6425
6426        Ok(currencies.iter().map(|c| c.code().to_string()).collect())
6427    }
6428
6429    /// Format an amount with currency symbol.
6430    fn format(&self, amount: f64, currency_code: String) -> PyResult<String> {
6431        let commerce = self
6432            .commerce
6433            .lock()
6434            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
6435
6436        let amount_dec = Decimal::from_f64_retain(amount)
6437            .ok_or_else(|| PyValueError::new_err("Invalid amount"))?;
6438
6439        Ok(commerce.currency().format(amount_dec, parse_currency(&currency_code)?))
6440    }
6441}
6442
6443// ============================================================================
6444// Subscription Types
6445// ============================================================================
6446
6447/// Subscription plan data.
6448#[pyclass]
6449#[derive(Clone)]
6450pub struct SubscriptionPlan {
6451    #[pyo3(get)]
6452    id: String,
6453    #[pyo3(get)]
6454    code: String,
6455    #[pyo3(get)]
6456    name: String,
6457    #[pyo3(get)]
6458    description: Option<String>,
6459    #[pyo3(get)]
6460    billing_interval: String,
6461    #[pyo3(get)]
6462    billing_interval_count: i32,
6463    #[pyo3(get)]
6464    price: f64,
6465    #[pyo3(get)]
6466    currency: String,
6467    #[pyo3(get)]
6468    setup_fee: f64,
6469    #[pyo3(get)]
6470    trial_days: i32,
6471    #[pyo3(get)]
6472    status: String,
6473    #[pyo3(get)]
6474    created_at: String,
6475    #[pyo3(get)]
6476    updated_at: String,
6477}
6478
6479impl From<stateset_core::SubscriptionPlan> for SubscriptionPlan {
6480    fn from(p: stateset_core::SubscriptionPlan) -> Self {
6481        Self {
6482            id: p.id.to_string(),
6483            code: p.code,
6484            name: p.name,
6485            description: p.description,
6486            billing_interval: format!("{:?}", p.billing_interval).to_lowercase(),
6487            billing_interval_count: 1, // Default to 1 since core doesn't have this field
6488            price: to_f64_or_nan(p.price),
6489            currency: p.currency,
6490            setup_fee: p.setup_fee.map(|d| to_f64_or_nan(d)).unwrap_or(0.0),
6491            trial_days: p.trial_days,
6492            status: format!("{:?}", p.status).to_lowercase(),
6493            created_at: p.created_at.to_rfc3339(),
6494            updated_at: p.updated_at.to_rfc3339(),
6495        }
6496    }
6497}
6498
6499/// Subscription data.
6500#[pyclass]
6501#[derive(Clone)]
6502pub struct Subscription {
6503    #[pyo3(get)]
6504    id: String,
6505    #[pyo3(get)]
6506    subscription_number: String,
6507    #[pyo3(get)]
6508    customer_id: String,
6509    #[pyo3(get)]
6510    plan_id: String,
6511    #[pyo3(get)]
6512    status: String,
6513    #[pyo3(get)]
6514    current_period_start: String,
6515    #[pyo3(get)]
6516    current_period_end: String,
6517    #[pyo3(get)]
6518    trial_ends_at: Option<String>,
6519    #[pyo3(get)]
6520    cancelled_at: Option<String>,
6521    #[pyo3(get)]
6522    ends_at: Option<String>,
6523    #[pyo3(get)]
6524    price: f64,
6525    #[pyo3(get)]
6526    currency: String,
6527    #[pyo3(get)]
6528    created_at: String,
6529    #[pyo3(get)]
6530    updated_at: String,
6531}
6532
6533impl From<stateset_core::Subscription> for Subscription {
6534    fn from(s: stateset_core::Subscription) -> Self {
6535        Self {
6536            id: s.id.to_string(),
6537            subscription_number: s.subscription_number,
6538            customer_id: s.customer_id.to_string(),
6539            plan_id: s.plan_id.to_string(),
6540            status: format!("{:?}", s.status).to_lowercase(),
6541            current_period_start: s.current_period_start.to_rfc3339(),
6542            current_period_end: s.current_period_end.to_rfc3339(),
6543            trial_ends_at: s.trial_ends_at.map(|d| d.to_rfc3339()),
6544            cancelled_at: s.cancelled_at.map(|d| d.to_rfc3339()),
6545            ends_at: s.ends_at.map(|d| d.to_rfc3339()),
6546            price: to_f64_or_nan(s.price),
6547            currency: s.currency,
6548            created_at: s.created_at.to_rfc3339(),
6549            updated_at: s.updated_at.to_rfc3339(),
6550        }
6551    }
6552}
6553
6554/// Billing cycle data.
6555#[pyclass]
6556#[derive(Clone)]
6557pub struct BillingCycle {
6558    #[pyo3(get)]
6559    id: String,
6560    #[pyo3(get)]
6561    cycle_number: i32,
6562    #[pyo3(get)]
6563    subscription_id: String,
6564    #[pyo3(get)]
6565    status: String,
6566    #[pyo3(get)]
6567    period_start: String,
6568    #[pyo3(get)]
6569    period_end: String,
6570    #[pyo3(get)]
6571    total: f64,
6572    #[pyo3(get)]
6573    currency: String,
6574    #[pyo3(get)]
6575    payment_id: Option<String>,
6576    #[pyo3(get)]
6577    invoice_id: Option<String>,
6578    #[pyo3(get)]
6579    created_at: String,
6580    #[pyo3(get)]
6581    updated_at: String,
6582}
6583
6584impl From<stateset_core::BillingCycle> for BillingCycle {
6585    fn from(c: stateset_core::BillingCycle) -> Self {
6586        Self {
6587            id: c.id.to_string(),
6588            cycle_number: c.cycle_number,
6589            subscription_id: c.subscription_id.to_string(),
6590            status: format!("{:?}", c.status).to_lowercase(),
6591            period_start: c.period_start.to_rfc3339(),
6592            period_end: c.period_end.to_rfc3339(),
6593            total: to_f64_or_nan(c.total),
6594            currency: c.currency,
6595            payment_id: c.payment_id,
6596            invoice_id: c.invoice_id.map(|id| id.to_string()),
6597            created_at: c.created_at.to_rfc3339(),
6598            updated_at: c.updated_at.to_rfc3339(),
6599        }
6600    }
6601}
6602
6603/// Subscription event data.
6604#[pyclass]
6605#[derive(Clone)]
6606pub struct SubscriptionEvent {
6607    #[pyo3(get)]
6608    id: String,
6609    #[pyo3(get)]
6610    subscription_id: String,
6611    #[pyo3(get)]
6612    event_type: String,
6613    #[pyo3(get)]
6614    description: String,
6615    #[pyo3(get)]
6616    created_at: String,
6617}
6618
6619impl From<stateset_core::SubscriptionEvent> for SubscriptionEvent {
6620    fn from(e: stateset_core::SubscriptionEvent) -> Self {
6621        Self {
6622            id: e.id.to_string(),
6623            subscription_id: e.subscription_id.to_string(),
6624            event_type: format!("{:?}", e.event_type).to_lowercase(),
6625            description: e.description,
6626            created_at: e.created_at.to_rfc3339(),
6627        }
6628    }
6629}
6630
6631// ============================================================================
6632// Subscriptions API
6633// ============================================================================
6634
6635/// Subscriptions API for subscription management.
6636#[pyclass]
6637pub struct Subscriptions {
6638    commerce: Arc<Mutex<RustCommerce>>,
6639}
6640
6641fn parse_billing_interval(s: &str) -> PyResult<stateset_core::BillingInterval> {
6642    match s.to_lowercase().as_str() {
6643        "weekly" => Ok(stateset_core::BillingInterval::Weekly),
6644        "biweekly" => Ok(stateset_core::BillingInterval::Biweekly),
6645        "monthly" => Ok(stateset_core::BillingInterval::Monthly),
6646        "quarterly" => Ok(stateset_core::BillingInterval::Quarterly),
6647        "annual" | "yearly" => Ok(stateset_core::BillingInterval::Annual),
6648        _ => Err(PyValueError::new_err(format!("Invalid billing interval: {}", s))),
6649    }
6650}
6651
6652#[pymethods]
6653impl Subscriptions {
6654    // ========================================================================
6655    // Subscription Plans
6656    // ========================================================================
6657
6658    /// Create a subscription plan.
6659    #[pyo3(signature = (code, name, price, billing_interval=None, billing_interval_count=None, description=None, currency=None, setup_fee=None, trial_days=None))]
6660    fn create_plan(
6661        &self,
6662        code: String,
6663        name: String,
6664        price: f64,
6665        billing_interval: Option<String>,
6666        billing_interval_count: Option<i32>,
6667        description: Option<String>,
6668        currency: Option<String>,
6669        setup_fee: Option<f64>,
6670        trial_days: Option<i32>,
6671    ) -> PyResult<SubscriptionPlan> {
6672        let commerce = self
6673            .commerce
6674            .lock()
6675            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
6676
6677        let interval = billing_interval
6678            .as_deref()
6679            .map(parse_billing_interval)
6680            .transpose()?
6681            .unwrap_or(stateset_core::BillingInterval::Monthly);
6682
6683        let plan = commerce
6684            .subscriptions()
6685            .create_plan(stateset_core::CreateSubscriptionPlan {
6686                code: Some(code),
6687                name,
6688                description,
6689                billing_interval: interval,
6690                custom_interval_days: billing_interval_count,
6691                price: Decimal::from_f64_retain(price)
6692                    .ok_or_else(|| PyValueError::new_err("Invalid price"))?,
6693                currency: Some(currency.unwrap_or_else(|| "USD".to_string())),
6694                setup_fee: setup_fee.map(|f| Decimal::from_f64_retain(f).unwrap_or_default()),
6695                trial_days,
6696                ..Default::default()
6697            })
6698            .map_err(|e| PyRuntimeError::new_err(format!("Failed to create plan: {}", e)))?;
6699
6700        Ok(plan.into())
6701    }
6702
6703    /// Get a subscription plan by ID.
6704    fn get_plan(&self, id: String) -> PyResult<Option<SubscriptionPlan>> {
6705        let commerce = self
6706            .commerce
6707            .lock()
6708            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
6709
6710        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
6711
6712        let plan = commerce
6713            .subscriptions()
6714            .get_plan(uuid)
6715            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get plan: {}", e)))?;
6716
6717        Ok(plan.map(|p| p.into()))
6718    }
6719
6720    /// List all subscription plans.
6721    #[pyo3(signature = (status=None, billing_interval=None, limit=None, offset=None))]
6722    fn list_plans(
6723        &self,
6724        status: Option<String>,
6725        billing_interval: Option<String>,
6726        limit: Option<u32>,
6727        offset: Option<u32>,
6728    ) -> PyResult<Vec<SubscriptionPlan>> {
6729        let commerce = self
6730            .commerce
6731            .lock()
6732            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
6733
6734        let interval = billing_interval.as_deref().map(parse_billing_interval).transpose()?;
6735
6736        let plan_status = status.as_deref().map(|s| match s.to_lowercase().as_str() {
6737            "draft" => stateset_core::PlanStatus::Draft,
6738            "active" => stateset_core::PlanStatus::Active,
6739            "archived" => stateset_core::PlanStatus::Archived,
6740            _ => stateset_core::PlanStatus::Draft,
6741        });
6742
6743        let plans = commerce
6744            .subscriptions()
6745            .list_plans(stateset_core::SubscriptionPlanFilter {
6746                status: plan_status,
6747                billing_interval: interval,
6748                search: None,
6749                limit,
6750                offset,
6751            })
6752            .map_err(|e| PyRuntimeError::new_err(format!("Failed to list plans: {}", e)))?;
6753
6754        Ok(plans.into_iter().map(|p| p.into()).collect())
6755    }
6756
6757    /// Activate a subscription plan.
6758    fn activate_plan(&self, id: String) -> PyResult<SubscriptionPlan> {
6759        let commerce = self
6760            .commerce
6761            .lock()
6762            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
6763
6764        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
6765
6766        let plan = commerce
6767            .subscriptions()
6768            .activate_plan(uuid)
6769            .map_err(|e| PyRuntimeError::new_err(format!("Failed to activate plan: {}", e)))?;
6770
6771        Ok(plan.into())
6772    }
6773
6774    /// Archive a subscription plan.
6775    fn archive_plan(&self, id: String) -> PyResult<SubscriptionPlan> {
6776        let commerce = self
6777            .commerce
6778            .lock()
6779            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
6780
6781        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
6782
6783        let plan = commerce
6784            .subscriptions()
6785            .archive_plan(uuid)
6786            .map_err(|e| PyRuntimeError::new_err(format!("Failed to archive plan: {}", e)))?;
6787
6788        Ok(plan.into())
6789    }
6790
6791    // ========================================================================
6792    // Subscriptions
6793    // ========================================================================
6794
6795    /// Subscribe a customer to a plan.
6796    #[pyo3(signature = (customer_id, plan_id, skip_trial=None, price=None))]
6797    fn subscribe(
6798        &self,
6799        customer_id: String,
6800        plan_id: String,
6801        skip_trial: Option<bool>,
6802        price: Option<f64>,
6803    ) -> PyResult<Subscription> {
6804        let commerce = self
6805            .commerce
6806            .lock()
6807            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
6808
6809        let cust_uuid =
6810            customer_id.parse().map_err(|_| PyValueError::new_err("Invalid customer UUID"))?;
6811        let plan_uuid = plan_id.parse().map_err(|_| PyValueError::new_err("Invalid plan UUID"))?;
6812
6813        let subscription = commerce
6814            .subscriptions()
6815            .subscribe(stateset_core::CreateSubscription {
6816                customer_id: cust_uuid,
6817                plan_id: plan_uuid,
6818                skip_trial,
6819                price: price.and_then(|p| Decimal::from_f64_retain(p)),
6820                ..Default::default()
6821            })
6822            .map_err(|e| {
6823                PyRuntimeError::new_err(format!("Failed to create subscription: {}", e))
6824            })?;
6825
6826        Ok(subscription.into())
6827    }
6828
6829    /// Get a subscription by ID.
6830    fn get(&self, id: String) -> PyResult<Option<Subscription>> {
6831        let commerce = self
6832            .commerce
6833            .lock()
6834            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
6835
6836        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
6837
6838        let subscription = commerce
6839            .subscriptions()
6840            .get(uuid)
6841            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get subscription: {}", e)))?;
6842
6843        Ok(subscription.map(|s| s.into()))
6844    }
6845
6846    /// Get a subscription by number.
6847    fn get_by_number(&self, number: String) -> PyResult<Option<Subscription>> {
6848        let commerce = self
6849            .commerce
6850            .lock()
6851            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
6852
6853        let subscription = commerce
6854            .subscriptions()
6855            .get_by_number(&number)
6856            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get subscription: {}", e)))?;
6857
6858        Ok(subscription.map(|s| s.into()))
6859    }
6860
6861    /// List subscriptions.
6862    #[pyo3(signature = (customer_id=None, plan_id=None, status=None, limit=None, offset=None))]
6863    fn list(
6864        &self,
6865        customer_id: Option<String>,
6866        plan_id: Option<String>,
6867        status: Option<String>,
6868        limit: Option<u32>,
6869        offset: Option<u32>,
6870    ) -> PyResult<Vec<Subscription>> {
6871        let commerce = self
6872            .commerce
6873            .lock()
6874            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
6875
6876        let cust_uuid = customer_id
6877            .map(|id| id.parse())
6878            .transpose()
6879            .map_err(|_| PyValueError::new_err("Invalid customer UUID"))?;
6880        let p_uuid = plan_id
6881            .map(|id| id.parse())
6882            .transpose()
6883            .map_err(|_| PyValueError::new_err("Invalid plan UUID"))?;
6884
6885        let sub_status = status.as_deref().map(|s| match s.to_lowercase().as_str() {
6886            "pending" => stateset_core::SubscriptionStatus::Pending,
6887            "trial" | "trialing" => stateset_core::SubscriptionStatus::Trial,
6888            "active" => stateset_core::SubscriptionStatus::Active,
6889            "paused" => stateset_core::SubscriptionStatus::Paused,
6890            "past_due" | "pastdue" => stateset_core::SubscriptionStatus::PastDue,
6891            "cancelled" | "canceled" => stateset_core::SubscriptionStatus::Cancelled,
6892            "expired" => stateset_core::SubscriptionStatus::Expired,
6893            _ => stateset_core::SubscriptionStatus::Active,
6894        });
6895
6896        let subscriptions = commerce
6897            .subscriptions()
6898            .list(stateset_core::SubscriptionFilter {
6899                customer_id: cust_uuid,
6900                plan_id: p_uuid,
6901                status: sub_status,
6902                from_date: None,
6903                to_date: None,
6904                search: None,
6905                limit,
6906                offset,
6907            })
6908            .map_err(|e| PyRuntimeError::new_err(format!("Failed to list subscriptions: {}", e)))?;
6909
6910        Ok(subscriptions.into_iter().map(|s| s.into()).collect())
6911    }
6912
6913    /// Pause a subscription.
6914    #[pyo3(signature = (id, reason=None))]
6915    fn pause(&self, id: String, reason: Option<String>) -> PyResult<Subscription> {
6916        let commerce = self
6917            .commerce
6918            .lock()
6919            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
6920
6921        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
6922
6923        let subscription = commerce
6924            .subscriptions()
6925            .pause(uuid, stateset_core::PauseSubscription { reason, resume_at: None })
6926            .map_err(|e| PyRuntimeError::new_err(format!("Failed to pause subscription: {}", e)))?;
6927
6928        Ok(subscription.into())
6929    }
6930
6931    /// Resume a paused subscription.
6932    fn resume(&self, id: String) -> PyResult<Subscription> {
6933        let commerce = self
6934            .commerce
6935            .lock()
6936            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
6937
6938        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
6939
6940        let subscription = commerce.subscriptions().resume(uuid).map_err(|e| {
6941            PyRuntimeError::new_err(format!("Failed to resume subscription: {}", e))
6942        })?;
6943
6944        Ok(subscription.into())
6945    }
6946
6947    /// Cancel a subscription.
6948    #[pyo3(signature = (id, immediate=None, reason=None))]
6949    fn cancel(
6950        &self,
6951        id: String,
6952        immediate: Option<bool>,
6953        reason: Option<String>,
6954    ) -> PyResult<Subscription> {
6955        let commerce = self
6956            .commerce
6957            .lock()
6958            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
6959
6960        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
6961
6962        let subscription = commerce
6963            .subscriptions()
6964            .cancel(
6965                uuid,
6966                stateset_core::CancelSubscription {
6967                    immediate,
6968                    reason: reason.clone(),
6969                    feedback: None,
6970                },
6971            )
6972            .map_err(|e| {
6973                PyRuntimeError::new_err(format!("Failed to cancel subscription: {}", e))
6974            })?;
6975
6976        Ok(subscription.into())
6977    }
6978
6979    /// Skip the next billing cycle.
6980    #[pyo3(signature = (id, reason=None))]
6981    fn skip_next_cycle(&self, id: String, reason: Option<String>) -> PyResult<Subscription> {
6982        let commerce = self
6983            .commerce
6984            .lock()
6985            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
6986
6987        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
6988
6989        let subscription = commerce
6990            .subscriptions()
6991            .skip_next_cycle(uuid, stateset_core::SkipBillingCycle { reason })
6992            .map_err(|e| PyRuntimeError::new_err(format!("Failed to skip billing cycle: {}", e)))?;
6993
6994        Ok(subscription.into())
6995    }
6996
6997    // ========================================================================
6998    // Billing Cycles
6999    // ========================================================================
7000
7001    /// List billing cycles.
7002    #[pyo3(signature = (subscription_id=None, status=None, limit=None, offset=None))]
7003    fn list_billing_cycles(
7004        &self,
7005        subscription_id: Option<String>,
7006        status: Option<String>,
7007        limit: Option<u32>,
7008        offset: Option<u32>,
7009    ) -> PyResult<Vec<BillingCycle>> {
7010        let commerce = self
7011            .commerce
7012            .lock()
7013            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
7014
7015        let sub_uuid = subscription_id
7016            .map(|id| id.parse())
7017            .transpose()
7018            .map_err(|_| PyValueError::new_err("Invalid subscription UUID"))?;
7019
7020        let cycle_status = status.as_deref().map(|s| match s.to_lowercase().as_str() {
7021            "scheduled" | "pending" => stateset_core::BillingCycleStatus::Scheduled,
7022            "processing" => stateset_core::BillingCycleStatus::Processing,
7023            "paid" => stateset_core::BillingCycleStatus::Paid,
7024            "failed" => stateset_core::BillingCycleStatus::Failed,
7025            "skipped" => stateset_core::BillingCycleStatus::Skipped,
7026            "refunded" => stateset_core::BillingCycleStatus::Refunded,
7027            "voided" => stateset_core::BillingCycleStatus::Voided,
7028            _ => stateset_core::BillingCycleStatus::Scheduled,
7029        });
7030
7031        let cycles = commerce
7032            .subscriptions()
7033            .list_billing_cycles(stateset_core::BillingCycleFilter {
7034                subscription_id: sub_uuid,
7035                status: cycle_status,
7036                from_date: None,
7037                to_date: None,
7038                limit,
7039                offset,
7040            })
7041            .map_err(|e| {
7042                PyRuntimeError::new_err(format!("Failed to list billing cycles: {}", e))
7043            })?;
7044
7045        Ok(cycles.into_iter().map(|c| c.into()).collect())
7046    }
7047
7048    /// Get a billing cycle by ID.
7049    fn get_billing_cycle(&self, id: String) -> PyResult<Option<BillingCycle>> {
7050        let commerce = self
7051            .commerce
7052            .lock()
7053            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
7054
7055        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
7056
7057        let cycle = commerce
7058            .subscriptions()
7059            .get_billing_cycle(uuid)
7060            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get billing cycle: {}", e)))?;
7061
7062        Ok(cycle.map(|c| c.into()))
7063    }
7064
7065    // ========================================================================
7066    // Events
7067    // ========================================================================
7068
7069    /// Get events for a subscription.
7070    fn get_events(&self, subscription_id: String) -> PyResult<Vec<SubscriptionEvent>> {
7071        let commerce = self
7072            .commerce
7073            .lock()
7074            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
7075
7076        let uuid = subscription_id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
7077
7078        let events = commerce
7079            .subscriptions()
7080            .get_events(uuid)
7081            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get events: {}", e)))?;
7082
7083        Ok(events.into_iter().map(|e| e.into()).collect())
7084    }
7085}
7086
7087// ============================================================================
7088// Promotions Types
7089// ============================================================================
7090
7091/// Promotion output
7092#[pyclass]
7093#[derive(Clone)]
7094pub struct Promotion {
7095    #[pyo3(get)]
7096    id: String,
7097    #[pyo3(get)]
7098    code: String,
7099    #[pyo3(get)]
7100    name: String,
7101    #[pyo3(get)]
7102    description: Option<String>,
7103    #[pyo3(get)]
7104    promotion_type: String,
7105    #[pyo3(get)]
7106    trigger: String,
7107    #[pyo3(get)]
7108    target: String,
7109    #[pyo3(get)]
7110    stacking: String,
7111    #[pyo3(get)]
7112    status: String,
7113    #[pyo3(get)]
7114    percentage_off: Option<f64>,
7115    #[pyo3(get)]
7116    fixed_amount_off: Option<f64>,
7117    #[pyo3(get)]
7118    max_discount_amount: Option<f64>,
7119    #[pyo3(get)]
7120    buy_quantity: Option<i32>,
7121    #[pyo3(get)]
7122    get_quantity: Option<i32>,
7123    #[pyo3(get)]
7124    starts_at: String,
7125    #[pyo3(get)]
7126    ends_at: Option<String>,
7127    #[pyo3(get)]
7128    total_usage_limit: Option<i32>,
7129    #[pyo3(get)]
7130    per_customer_limit: Option<i32>,
7131    #[pyo3(get)]
7132    usage_count: i32,
7133    #[pyo3(get)]
7134    currency: String,
7135    #[pyo3(get)]
7136    priority: i32,
7137    #[pyo3(get)]
7138    created_at: String,
7139    #[pyo3(get)]
7140    updated_at: String,
7141}
7142
7143impl From<stateset_core::Promotion> for Promotion {
7144    fn from(p: stateset_core::Promotion) -> Self {
7145        Self {
7146            id: p.id.to_string(),
7147            code: p.code,
7148            name: p.name,
7149            description: p.description,
7150            promotion_type: format!("{:?}", p.promotion_type).to_lowercase(),
7151            trigger: format!("{:?}", p.trigger).to_lowercase(),
7152            target: format!("{:?}", p.target).to_lowercase(),
7153            stacking: format!("{:?}", p.stacking).to_lowercase(),
7154            status: format!("{:?}", p.status).to_lowercase(),
7155            percentage_off: p.percentage_off.map(|d| to_f64_or_nan(d)),
7156            fixed_amount_off: p.fixed_amount_off.map(|d| to_f64_or_nan(d)),
7157            max_discount_amount: p.max_discount_amount.map(|d| to_f64_or_nan(d)),
7158            buy_quantity: p.buy_quantity,
7159            get_quantity: p.get_quantity,
7160            starts_at: p.starts_at.to_rfc3339(),
7161            ends_at: p.ends_at.map(|d| d.to_rfc3339()),
7162            total_usage_limit: p.total_usage_limit,
7163            per_customer_limit: p.per_customer_limit,
7164            usage_count: p.usage_count,
7165            currency: p.currency,
7166            priority: p.priority,
7167            created_at: p.created_at.to_rfc3339(),
7168            updated_at: p.updated_at.to_rfc3339(),
7169        }
7170    }
7171}
7172
7173/// Coupon code output
7174#[pyclass]
7175#[derive(Clone)]
7176pub struct Coupon {
7177    #[pyo3(get)]
7178    id: String,
7179    #[pyo3(get)]
7180    promotion_id: String,
7181    #[pyo3(get)]
7182    code: String,
7183    #[pyo3(get)]
7184    status: String,
7185    #[pyo3(get)]
7186    usage_limit: Option<i32>,
7187    #[pyo3(get)]
7188    per_customer_limit: Option<i32>,
7189    #[pyo3(get)]
7190    usage_count: i32,
7191    #[pyo3(get)]
7192    starts_at: Option<String>,
7193    #[pyo3(get)]
7194    ends_at: Option<String>,
7195    #[pyo3(get)]
7196    created_at: String,
7197    #[pyo3(get)]
7198    updated_at: String,
7199}
7200
7201impl From<stateset_core::CouponCode> for Coupon {
7202    fn from(c: stateset_core::CouponCode) -> Self {
7203        Self {
7204            id: c.id.to_string(),
7205            promotion_id: c.promotion_id.to_string(),
7206            code: c.code,
7207            status: format!("{:?}", c.status).to_lowercase(),
7208            usage_limit: c.usage_limit,
7209            per_customer_limit: c.per_customer_limit,
7210            usage_count: c.usage_count,
7211            starts_at: c.starts_at.map(|d| d.to_rfc3339()),
7212            ends_at: c.ends_at.map(|d| d.to_rfc3339()),
7213            created_at: c.created_at.to_rfc3339(),
7214            updated_at: c.updated_at.to_rfc3339(),
7215        }
7216    }
7217}
7218
7219/// Result of applying promotions
7220#[pyclass]
7221#[derive(Clone)]
7222pub struct ApplyPromotionsResult {
7223    #[pyo3(get)]
7224    original_subtotal: f64,
7225    #[pyo3(get)]
7226    total_discount: f64,
7227    #[pyo3(get)]
7228    discounted_subtotal: f64,
7229    #[pyo3(get)]
7230    original_shipping: f64,
7231    #[pyo3(get)]
7232    shipping_discount: f64,
7233    #[pyo3(get)]
7234    final_shipping: f64,
7235    #[pyo3(get)]
7236    grand_total: f64,
7237    #[pyo3(get)]
7238    applied_promotions: Vec<AppliedPromotion>,
7239}
7240
7241impl From<stateset_core::ApplyPromotionsResult> for ApplyPromotionsResult {
7242    fn from(r: stateset_core::ApplyPromotionsResult) -> Self {
7243        Self {
7244            original_subtotal: to_f64_or_nan(r.original_subtotal),
7245            total_discount: to_f64_or_nan(r.total_discount),
7246            discounted_subtotal: to_f64_or_nan(r.discounted_subtotal),
7247            original_shipping: to_f64_or_nan(r.original_shipping),
7248            shipping_discount: to_f64_or_nan(r.shipping_discount),
7249            final_shipping: to_f64_or_nan(r.final_shipping),
7250            grand_total: to_f64_or_nan(r.grand_total),
7251            applied_promotions: r.applied_promotions.into_iter().map(|a| a.into()).collect(),
7252        }
7253    }
7254}
7255
7256/// An applied promotion
7257#[pyclass]
7258#[derive(Clone)]
7259pub struct AppliedPromotion {
7260    #[pyo3(get)]
7261    promotion_id: String,
7262    #[pyo3(get)]
7263    promotion_name: String,
7264    #[pyo3(get)]
7265    coupon_code: Option<String>,
7266    #[pyo3(get)]
7267    discount_amount: f64,
7268    #[pyo3(get)]
7269    discount_type: String,
7270}
7271
7272impl From<stateset_core::AppliedPromotion> for AppliedPromotion {
7273    fn from(a: stateset_core::AppliedPromotion) -> Self {
7274        Self {
7275            promotion_id: a.promotion_id.to_string(),
7276            promotion_name: a.promotion_name,
7277            coupon_code: a.coupon_code,
7278            discount_amount: to_f64_or_nan(a.discount_amount),
7279            discount_type: format!("{:?}", a.discount_type).to_lowercase(),
7280        }
7281    }
7282}
7283
7284/// Promotion usage record
7285#[pyclass]
7286#[derive(Clone)]
7287pub struct PromotionUsage {
7288    #[pyo3(get)]
7289    id: String,
7290    #[pyo3(get)]
7291    promotion_id: String,
7292    #[pyo3(get)]
7293    coupon_id: Option<String>,
7294    #[pyo3(get)]
7295    customer_id: Option<String>,
7296    #[pyo3(get)]
7297    order_id: Option<String>,
7298    #[pyo3(get)]
7299    cart_id: Option<String>,
7300    #[pyo3(get)]
7301    discount_amount: f64,
7302    #[pyo3(get)]
7303    currency: String,
7304    #[pyo3(get)]
7305    used_at: String,
7306}
7307
7308impl From<stateset_core::PromotionUsage> for PromotionUsage {
7309    fn from(u: stateset_core::PromotionUsage) -> Self {
7310        Self {
7311            id: u.id.to_string(),
7312            promotion_id: u.promotion_id.to_string(),
7313            coupon_id: u.coupon_id.map(|id| id.to_string()),
7314            customer_id: u.customer_id.map(|id| id.to_string()),
7315            order_id: u.order_id.map(|id| id.to_string()),
7316            cart_id: u.cart_id.map(|id| id.to_string()),
7317            discount_amount: to_f64_or_nan(u.discount_amount),
7318            currency: u.currency,
7319            used_at: u.used_at.to_rfc3339(),
7320        }
7321    }
7322}
7323
7324// ============================================================================
7325// Promotions API
7326// ============================================================================
7327
7328fn parse_promotion_type(s: &str) -> stateset_core::PromotionType {
7329    match s.to_lowercase().as_str() {
7330        "percentage_off" => stateset_core::PromotionType::PercentageOff,
7331        "fixed_amount_off" => stateset_core::PromotionType::FixedAmountOff,
7332        "buy_x_get_y" | "bogo" => stateset_core::PromotionType::BuyXGetY,
7333        "free_shipping" => stateset_core::PromotionType::FreeShipping,
7334        "tiered_discount" => stateset_core::PromotionType::TieredDiscount,
7335        "bundle_discount" => stateset_core::PromotionType::BundleDiscount,
7336        _ => stateset_core::PromotionType::PercentageOff,
7337    }
7338}
7339
7340fn parse_promotion_trigger(s: &str) -> stateset_core::PromotionTrigger {
7341    match s.to_lowercase().as_str() {
7342        "automatic" => stateset_core::PromotionTrigger::Automatic,
7343        "coupon_code" => stateset_core::PromotionTrigger::CouponCode,
7344        "both" => stateset_core::PromotionTrigger::Both,
7345        _ => stateset_core::PromotionTrigger::Automatic,
7346    }
7347}
7348
7349fn parse_promotion_target(s: &str) -> stateset_core::PromotionTarget {
7350    match s.to_lowercase().as_str() {
7351        "order" => stateset_core::PromotionTarget::Order,
7352        "product" => stateset_core::PromotionTarget::Product,
7353        "category" => stateset_core::PromotionTarget::Category,
7354        "shipping" => stateset_core::PromotionTarget::Shipping,
7355        "line_item" => stateset_core::PromotionTarget::LineItem,
7356        _ => stateset_core::PromotionTarget::Order,
7357    }
7358}
7359
7360fn parse_stacking_behavior(s: &str) -> stateset_core::StackingBehavior {
7361    match s.to_lowercase().as_str() {
7362        "stackable" => stateset_core::StackingBehavior::Stackable,
7363        "exclusive" => stateset_core::StackingBehavior::Exclusive,
7364        "selective_stack" => stateset_core::StackingBehavior::SelectiveStack,
7365        _ => stateset_core::StackingBehavior::Stackable,
7366    }
7367}
7368
7369fn parse_promotion_status(s: &str) -> stateset_core::PromotionStatus {
7370    match s.to_lowercase().as_str() {
7371        "draft" => stateset_core::PromotionStatus::Draft,
7372        "scheduled" => stateset_core::PromotionStatus::Scheduled,
7373        "active" => stateset_core::PromotionStatus::Active,
7374        "paused" => stateset_core::PromotionStatus::Paused,
7375        "expired" => stateset_core::PromotionStatus::Expired,
7376        "exhausted" => stateset_core::PromotionStatus::Exhausted,
7377        "archived" => stateset_core::PromotionStatus::Archived,
7378        _ => stateset_core::PromotionStatus::Draft,
7379    }
7380}
7381
7382fn parse_coupon_status(s: &str) -> stateset_core::CouponStatus {
7383    match s.to_lowercase().as_str() {
7384        "active" => stateset_core::CouponStatus::Active,
7385        "disabled" => stateset_core::CouponStatus::Disabled,
7386        "exhausted" => stateset_core::CouponStatus::Exhausted,
7387        "expired" => stateset_core::CouponStatus::Expired,
7388        _ => stateset_core::CouponStatus::Active,
7389    }
7390}
7391
7392/// Promotions API for managing discounts and coupon codes
7393#[pyclass]
7394pub struct PromotionsApi {
7395    commerce: Arc<Mutex<RustCommerce>>,
7396}
7397
7398#[pymethods]
7399impl PromotionsApi {
7400    /// Create a new promotion.
7401    #[pyo3(signature = (name, promotion_type=None, trigger=None, target=None, stacking=None,
7402                        percentage_off=None, fixed_amount_off=None, max_discount_amount=None,
7403                        buy_quantity=None, get_quantity=None, starts_at=None, ends_at=None,
7404                        total_usage_limit=None, per_customer_limit=None, currency=None, priority=None))]
7405    fn create(
7406        &self,
7407        name: String,
7408        promotion_type: Option<String>,
7409        trigger: Option<String>,
7410        target: Option<String>,
7411        stacking: Option<String>,
7412        percentage_off: Option<f64>,
7413        fixed_amount_off: Option<f64>,
7414        max_discount_amount: Option<f64>,
7415        buy_quantity: Option<i32>,
7416        get_quantity: Option<i32>,
7417        starts_at: Option<String>,
7418        ends_at: Option<String>,
7419        total_usage_limit: Option<i32>,
7420        per_customer_limit: Option<i32>,
7421        currency: Option<String>,
7422        priority: Option<i32>,
7423    ) -> PyResult<Promotion> {
7424        let commerce = self
7425            .commerce
7426            .lock()
7427            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
7428
7429        let create = stateset_core::CreatePromotion {
7430            code: None,
7431            name,
7432            description: None,
7433            internal_notes: None,
7434            promotion_type: promotion_type.map(|s| parse_promotion_type(&s)).unwrap_or_default(),
7435            trigger: trigger.map(|s| parse_promotion_trigger(&s)).unwrap_or_default(),
7436            target: target.map(|s| parse_promotion_target(&s)).unwrap_or_default(),
7437            stacking: stacking.map(|s| parse_stacking_behavior(&s)).unwrap_or_default(),
7438            percentage_off: percentage_off.map(|v| Decimal::from_f64_retain(v).unwrap_or_default()),
7439            fixed_amount_off: fixed_amount_off
7440                .map(|v| Decimal::from_f64_retain(v).unwrap_or_default()),
7441            max_discount_amount: max_discount_amount
7442                .map(|v| Decimal::from_f64_retain(v).unwrap_or_default()),
7443            buy_quantity,
7444            get_quantity,
7445            get_discount_percent: None,
7446            tiers: None,
7447            bundle_product_ids: None,
7448            bundle_discount: None,
7449            starts_at: starts_at.and_then(|s| {
7450                chrono::DateTime::parse_from_rfc3339(&s).ok().map(|d| d.with_timezone(&chrono::Utc))
7451            }),
7452            ends_at: ends_at.and_then(|s| {
7453                chrono::DateTime::parse_from_rfc3339(&s).ok().map(|d| d.with_timezone(&chrono::Utc))
7454            }),
7455            total_usage_limit,
7456            per_customer_limit,
7457            conditions: None,
7458            applicable_product_ids: None,
7459            applicable_category_ids: None,
7460            applicable_skus: None,
7461            excluded_product_ids: None,
7462            excluded_category_ids: None,
7463            eligible_customer_ids: None,
7464            eligible_customer_groups: None,
7465            currency,
7466            priority,
7467            metadata: None,
7468        };
7469
7470        let promo = commerce
7471            .promotions()
7472            .create(create)
7473            .map_err(|e| PyRuntimeError::new_err(format!("Failed to create promotion: {}", e)))?;
7474
7475        Ok(promo.into())
7476    }
7477
7478    /// Get a promotion by ID.
7479    fn get(&self, id: String) -> PyResult<Option<Promotion>> {
7480        let commerce = self
7481            .commerce
7482            .lock()
7483            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
7484
7485        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
7486
7487        let promo = commerce
7488            .promotions()
7489            .get(uuid)
7490            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get promotion: {}", e)))?;
7491
7492        Ok(promo.map(|p| p.into()))
7493    }
7494
7495    /// Get a promotion by its internal code.
7496    fn get_by_code(&self, code: String) -> PyResult<Option<Promotion>> {
7497        let commerce = self
7498            .commerce
7499            .lock()
7500            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
7501
7502        let promo = commerce
7503            .promotions()
7504            .get_by_code(&code)
7505            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get promotion: {}", e)))?;
7506
7507        Ok(promo.map(|p| p.into()))
7508    }
7509
7510    /// List promotions with optional filtering.
7511    #[pyo3(signature = (status=None, promotion_type=None, is_active=None, limit=None, offset=None))]
7512    fn list(
7513        &self,
7514        status: Option<String>,
7515        promotion_type: Option<String>,
7516        is_active: Option<bool>,
7517        limit: Option<i32>,
7518        offset: Option<i32>,
7519    ) -> PyResult<Vec<Promotion>> {
7520        let commerce = self
7521            .commerce
7522            .lock()
7523            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
7524
7525        let filter = stateset_core::PromotionFilter {
7526            status: status.map(|s| parse_promotion_status(&s)),
7527            promotion_type: promotion_type.map(|s| parse_promotion_type(&s)),
7528            trigger: None,
7529            is_active,
7530            search: None,
7531            limit: limit.map(|v| v as u32),
7532            offset: offset.map(|v| v as u32),
7533        };
7534
7535        let promos = commerce
7536            .promotions()
7537            .list(filter)
7538            .map_err(|e| PyRuntimeError::new_err(format!("Failed to list promotions: {}", e)))?;
7539
7540        Ok(promos.into_iter().map(|p| p.into()).collect())
7541    }
7542
7543    /// Activate a promotion.
7544    fn activate(&self, id: String) -> PyResult<Promotion> {
7545        let commerce = self
7546            .commerce
7547            .lock()
7548            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
7549
7550        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
7551
7552        let promo = commerce
7553            .promotions()
7554            .activate(uuid)
7555            .map_err(|e| PyRuntimeError::new_err(format!("Failed to activate promotion: {}", e)))?;
7556
7557        Ok(promo.into())
7558    }
7559
7560    /// Deactivate (pause) a promotion.
7561    fn deactivate(&self, id: String) -> PyResult<Promotion> {
7562        let commerce = self
7563            .commerce
7564            .lock()
7565            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
7566
7567        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
7568
7569        let promo = commerce.promotions().deactivate(uuid).map_err(|e| {
7570            PyRuntimeError::new_err(format!("Failed to deactivate promotion: {}", e))
7571        })?;
7572
7573        Ok(promo.into())
7574    }
7575
7576    /// Delete a promotion.
7577    fn delete(&self, id: String) -> PyResult<()> {
7578        let commerce = self
7579            .commerce
7580            .lock()
7581            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
7582
7583        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
7584
7585        commerce
7586            .promotions()
7587            .delete(uuid)
7588            .map_err(|e| PyRuntimeError::new_err(format!("Failed to delete promotion: {}", e)))?;
7589
7590        Ok(())
7591    }
7592
7593    /// Get all active promotions.
7594    fn get_active(&self) -> PyResult<Vec<Promotion>> {
7595        let commerce = self
7596            .commerce
7597            .lock()
7598            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
7599
7600        let promos = commerce.promotions().get_active().map_err(|e| {
7601            PyRuntimeError::new_err(format!("Failed to get active promotions: {}", e))
7602        })?;
7603
7604        Ok(promos.into_iter().map(|p| p.into()).collect())
7605    }
7606
7607    // ========================================================================
7608    // Coupon Codes
7609    // ========================================================================
7610
7611    /// Create a coupon code for a promotion.
7612    #[pyo3(signature = (promotion_id, code, usage_limit=None, per_customer_limit=None, starts_at=None, ends_at=None))]
7613    fn create_coupon(
7614        &self,
7615        promotion_id: String,
7616        code: String,
7617        usage_limit: Option<i32>,
7618        per_customer_limit: Option<i32>,
7619        starts_at: Option<String>,
7620        ends_at: Option<String>,
7621    ) -> PyResult<Coupon> {
7622        let commerce = self
7623            .commerce
7624            .lock()
7625            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
7626
7627        let promo_uuid =
7628            promotion_id.parse().map_err(|_| PyValueError::new_err("Invalid promotion UUID"))?;
7629
7630        let create = stateset_core::CreateCouponCode {
7631            promotion_id: promo_uuid,
7632            code,
7633            usage_limit,
7634            per_customer_limit,
7635            starts_at: starts_at.and_then(|s| {
7636                chrono::DateTime::parse_from_rfc3339(&s).ok().map(|d| d.with_timezone(&chrono::Utc))
7637            }),
7638            ends_at: ends_at.and_then(|s| {
7639                chrono::DateTime::parse_from_rfc3339(&s).ok().map(|d| d.with_timezone(&chrono::Utc))
7640            }),
7641            metadata: None,
7642        };
7643
7644        let coupon = commerce
7645            .promotions()
7646            .create_coupon(create)
7647            .map_err(|e| PyRuntimeError::new_err(format!("Failed to create coupon: {}", e)))?;
7648
7649        Ok(coupon.into())
7650    }
7651
7652    /// Get a coupon by ID.
7653    fn get_coupon(&self, id: String) -> PyResult<Option<Coupon>> {
7654        let commerce = self
7655            .commerce
7656            .lock()
7657            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
7658
7659        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
7660
7661        let coupon = commerce
7662            .promotions()
7663            .get_coupon(uuid)
7664            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get coupon: {}", e)))?;
7665
7666        Ok(coupon.map(|c| c.into()))
7667    }
7668
7669    /// Get a coupon by its code.
7670    fn get_coupon_by_code(&self, code: String) -> PyResult<Option<Coupon>> {
7671        let commerce = self
7672            .commerce
7673            .lock()
7674            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
7675
7676        let coupon = commerce
7677            .promotions()
7678            .get_coupon_by_code(&code)
7679            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get coupon: {}", e)))?;
7680
7681        Ok(coupon.map(|c| c.into()))
7682    }
7683
7684    /// List coupons with optional filtering.
7685    #[pyo3(signature = (promotion_id=None, status=None, limit=None, offset=None))]
7686    fn list_coupons(
7687        &self,
7688        promotion_id: Option<String>,
7689        status: Option<String>,
7690        limit: Option<i32>,
7691        offset: Option<i32>,
7692    ) -> PyResult<Vec<Coupon>> {
7693        let commerce = self
7694            .commerce
7695            .lock()
7696            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
7697
7698        let filter = stateset_core::CouponFilter {
7699            promotion_id: promotion_id.and_then(|s| s.parse().ok()),
7700            status: status.map(|s| parse_coupon_status(&s)),
7701            search: None,
7702            limit: limit.map(|v| v as u32),
7703            offset: offset.map(|v| v as u32),
7704        };
7705
7706        let coupons = commerce
7707            .promotions()
7708            .list_coupons(filter)
7709            .map_err(|e| PyRuntimeError::new_err(format!("Failed to list coupons: {}", e)))?;
7710
7711        Ok(coupons.into_iter().map(|c| c.into()).collect())
7712    }
7713
7714    /// Validate a coupon code.
7715    fn validate_coupon(&self, code: String) -> PyResult<Option<Coupon>> {
7716        let commerce = self
7717            .commerce
7718            .lock()
7719            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
7720
7721        let coupon = commerce
7722            .promotions()
7723            .validate_coupon(&code)
7724            .map_err(|e| PyRuntimeError::new_err(format!("Failed to validate coupon: {}", e)))?;
7725
7726        Ok(coupon.map(|c| c.into()))
7727    }
7728
7729    // ========================================================================
7730    // Apply Promotions
7731    // ========================================================================
7732
7733    /// Apply promotions to cart/order items.
7734    #[pyo3(signature = (subtotal, coupon_codes=None, shipping_amount=None, currency=None))]
7735    fn apply(
7736        &self,
7737        subtotal: f64,
7738        coupon_codes: Option<Vec<String>>,
7739        shipping_amount: Option<f64>,
7740        currency: Option<String>,
7741    ) -> PyResult<ApplyPromotionsResult> {
7742        let commerce = self
7743            .commerce
7744            .lock()
7745            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
7746
7747        let request = stateset_core::ApplyPromotionsRequest {
7748            cart_id: None,
7749            customer_id: None,
7750            coupon_codes: coupon_codes.unwrap_or_default(),
7751            line_items: vec![],
7752            subtotal: Decimal::from_f64_retain(subtotal).unwrap_or_default(),
7753            shipping_amount: Decimal::from_f64_retain(shipping_amount.unwrap_or(0.0))
7754                .unwrap_or_default(),
7755            shipping_country: None,
7756            shipping_state: None,
7757            currency: currency.unwrap_or_else(|| "USD".to_string()),
7758            is_first_order: false,
7759        };
7760
7761        let result = commerce
7762            .promotions()
7763            .apply(request)
7764            .map_err(|e| PyRuntimeError::new_err(format!("Failed to apply promotions: {}", e)))?;
7765
7766        Ok(result.into())
7767    }
7768
7769    /// Record promotion usage (after order completion).
7770    #[pyo3(signature = (promotion_id, discount_amount, currency, coupon_id=None, customer_id=None, order_id=None, cart_id=None))]
7771    fn record_usage(
7772        &self,
7773        promotion_id: String,
7774        discount_amount: f64,
7775        currency: String,
7776        coupon_id: Option<String>,
7777        customer_id: Option<String>,
7778        order_id: Option<String>,
7779        cart_id: Option<String>,
7780    ) -> PyResult<PromotionUsage> {
7781        let commerce = self
7782            .commerce
7783            .lock()
7784            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
7785
7786        let promo_uuid =
7787            promotion_id.parse().map_err(|_| PyValueError::new_err("Invalid promotion UUID"))?;
7788
7789        let usage = commerce
7790            .promotions()
7791            .record_usage(
7792                promo_uuid,
7793                coupon_id.and_then(|s| s.parse().ok()),
7794                customer_id.and_then(|s| s.parse().ok()),
7795                order_id.and_then(|s| s.parse().ok()),
7796                cart_id.and_then(|s| s.parse().ok()),
7797                Decimal::from_f64_retain(discount_amount).unwrap_or_default(),
7798                &currency,
7799            )
7800            .map_err(|e| PyRuntimeError::new_err(format!("Failed to record usage: {}", e)))?;
7801
7802        Ok(usage.into())
7803    }
7804}
7805
7806// ============================================================================
7807// Tax API
7808// ============================================================================
7809
7810/// Tax jurisdiction data.
7811#[pyclass]
7812#[derive(Clone)]
7813pub struct TaxJurisdiction {
7814    #[pyo3(get)]
7815    id: String,
7816    #[pyo3(get)]
7817    parent_id: Option<String>,
7818    #[pyo3(get)]
7819    name: String,
7820    #[pyo3(get)]
7821    code: String,
7822    #[pyo3(get)]
7823    level: String,
7824    #[pyo3(get)]
7825    country_code: String,
7826    #[pyo3(get)]
7827    state_code: Option<String>,
7828    #[pyo3(get)]
7829    county: Option<String>,
7830    #[pyo3(get)]
7831    city: Option<String>,
7832    #[pyo3(get)]
7833    postal_codes: Vec<String>,
7834    #[pyo3(get)]
7835    active: bool,
7836    #[pyo3(get)]
7837    created_at: String,
7838    #[pyo3(get)]
7839    updated_at: String,
7840}
7841
7842impl From<stateset_core::TaxJurisdiction> for TaxJurisdiction {
7843    fn from(j: stateset_core::TaxJurisdiction) -> Self {
7844        Self {
7845            id: j.id.to_string(),
7846            parent_id: j.parent_id.map(|u| u.to_string()),
7847            name: j.name,
7848            code: j.code,
7849            level: format!("{:?}", j.level).to_lowercase(),
7850            country_code: j.country_code,
7851            state_code: j.state_code,
7852            county: j.county,
7853            city: j.city,
7854            postal_codes: j.postal_codes,
7855            active: j.active,
7856            created_at: j.created_at.to_rfc3339(),
7857            updated_at: j.updated_at.to_rfc3339(),
7858        }
7859    }
7860}
7861
7862/// Tax rate data.
7863#[pyclass]
7864#[derive(Clone)]
7865pub struct TaxRate {
7866    #[pyo3(get)]
7867    id: String,
7868    #[pyo3(get)]
7869    jurisdiction_id: String,
7870    #[pyo3(get)]
7871    tax_type: String,
7872    #[pyo3(get)]
7873    product_category: String,
7874    #[pyo3(get)]
7875    rate: f64,
7876    #[pyo3(get)]
7877    name: String,
7878    #[pyo3(get)]
7879    description: Option<String>,
7880    #[pyo3(get)]
7881    is_compound: bool,
7882    #[pyo3(get)]
7883    priority: i32,
7884    #[pyo3(get)]
7885    effective_from: String,
7886    #[pyo3(get)]
7887    effective_to: Option<String>,
7888    #[pyo3(get)]
7889    active: bool,
7890    #[pyo3(get)]
7891    created_at: String,
7892    #[pyo3(get)]
7893    updated_at: String,
7894}
7895
7896impl From<stateset_core::TaxRate> for TaxRate {
7897    fn from(r: stateset_core::TaxRate) -> Self {
7898        Self {
7899            id: r.id.to_string(),
7900            jurisdiction_id: r.jurisdiction_id.to_string(),
7901            tax_type: r.tax_type.as_str().to_string(),
7902            product_category: r.product_category.as_str().to_string(),
7903            rate: to_f64_or_nan(r.rate),
7904            name: r.name,
7905            description: r.description,
7906            is_compound: r.is_compound,
7907            priority: r.priority,
7908            effective_from: r.effective_from.to_string(),
7909            effective_to: r.effective_to.map(|d| d.to_string()),
7910            active: r.active,
7911            created_at: r.created_at.to_rfc3339(),
7912            updated_at: r.updated_at.to_rfc3339(),
7913        }
7914    }
7915}
7916
7917/// Tax exemption data.
7918#[pyclass]
7919#[derive(Clone)]
7920pub struct TaxExemption {
7921    #[pyo3(get)]
7922    id: String,
7923    #[pyo3(get)]
7924    customer_id: String,
7925    #[pyo3(get)]
7926    exemption_type: String,
7927    #[pyo3(get)]
7928    certificate_number: Option<String>,
7929    #[pyo3(get)]
7930    issuing_authority: Option<String>,
7931    #[pyo3(get)]
7932    jurisdiction_ids: Vec<String>,
7933    #[pyo3(get)]
7934    exempt_categories: Vec<String>,
7935    #[pyo3(get)]
7936    effective_from: String,
7937    #[pyo3(get)]
7938    expires_at: Option<String>,
7939    #[pyo3(get)]
7940    verified: bool,
7941    #[pyo3(get)]
7942    notes: Option<String>,
7943    #[pyo3(get)]
7944    active: bool,
7945    #[pyo3(get)]
7946    created_at: String,
7947    #[pyo3(get)]
7948    updated_at: String,
7949}
7950
7951impl From<stateset_core::TaxExemption> for TaxExemption {
7952    fn from(e: stateset_core::TaxExemption) -> Self {
7953        Self {
7954            id: e.id.to_string(),
7955            customer_id: e.customer_id.to_string(),
7956            exemption_type: format!("{:?}", e.exemption_type).to_lowercase(),
7957            certificate_number: e.certificate_number,
7958            issuing_authority: e.issuing_authority,
7959            jurisdiction_ids: e.jurisdiction_ids.iter().map(|u| u.to_string()).collect(),
7960            exempt_categories: e.exempt_categories.iter().map(|c| c.as_str().to_string()).collect(),
7961            effective_from: e.effective_from.to_string(),
7962            expires_at: e.expires_at.map(|d| d.to_string()),
7963            verified: e.verified,
7964            notes: e.notes,
7965            active: e.active,
7966            created_at: e.created_at.to_rfc3339(),
7967            updated_at: e.updated_at.to_rfc3339(),
7968        }
7969    }
7970}
7971
7972/// Tax settings data.
7973#[pyclass]
7974#[derive(Clone)]
7975pub struct TaxSettings {
7976    #[pyo3(get)]
7977    id: String,
7978    #[pyo3(get)]
7979    enabled: bool,
7980    #[pyo3(get)]
7981    calculation_method: String,
7982    #[pyo3(get)]
7983    compound_method: String,
7984    #[pyo3(get)]
7985    tax_shipping: bool,
7986    #[pyo3(get)]
7987    tax_handling: bool,
7988    #[pyo3(get)]
7989    tax_gift_wrap: bool,
7990    #[pyo3(get)]
7991    default_product_category: String,
7992    #[pyo3(get)]
7993    rounding_mode: String,
7994    #[pyo3(get)]
7995    decimal_places: i32,
7996    #[pyo3(get)]
7997    validate_addresses: bool,
7998    #[pyo3(get)]
7999    tax_provider: Option<String>,
8000    #[pyo3(get)]
8001    created_at: String,
8002    #[pyo3(get)]
8003    updated_at: String,
8004}
8005
8006impl From<stateset_core::TaxSettings> for TaxSettings {
8007    fn from(s: stateset_core::TaxSettings) -> Self {
8008        Self {
8009            id: s.id.to_string(),
8010            enabled: s.enabled,
8011            calculation_method: format!("{:?}", s.calculation_method).to_lowercase(),
8012            compound_method: format!("{:?}", s.compound_method).to_lowercase(),
8013            tax_shipping: s.tax_shipping,
8014            tax_handling: s.tax_handling,
8015            tax_gift_wrap: s.tax_gift_wrap,
8016            default_product_category: s.default_product_category.as_str().to_string(),
8017            rounding_mode: s.rounding_mode,
8018            decimal_places: s.decimal_places,
8019            validate_addresses: s.validate_addresses,
8020            tax_provider: s.tax_provider,
8021            created_at: s.created_at.to_rfc3339(),
8022            updated_at: s.updated_at.to_rfc3339(),
8023        }
8024    }
8025}
8026
8027/// Tax calculation result.
8028#[pyclass]
8029#[derive(Clone)]
8030pub struct TaxCalculationResult {
8031    #[pyo3(get)]
8032    id: String,
8033    #[pyo3(get)]
8034    total_tax: f64,
8035    #[pyo3(get)]
8036    subtotal: f64,
8037    #[pyo3(get)]
8038    total: f64,
8039    #[pyo3(get)]
8040    shipping_tax: f64,
8041    #[pyo3(get)]
8042    exemptions_applied: bool,
8043    #[pyo3(get)]
8044    calculated_at: String,
8045    #[pyo3(get)]
8046    is_estimate: bool,
8047}
8048
8049impl From<stateset_core::TaxCalculationResult> for TaxCalculationResult {
8050    fn from(r: stateset_core::TaxCalculationResult) -> Self {
8051        Self {
8052            id: r.id.to_string(),
8053            total_tax: to_f64_or_nan(r.total_tax),
8054            subtotal: to_f64_or_nan(r.subtotal),
8055            total: to_f64_or_nan(r.total),
8056            shipping_tax: to_f64_or_nan(r.shipping_tax),
8057            exemptions_applied: r.exemptions_applied,
8058            calculated_at: r.calculated_at.to_rfc3339(),
8059            is_estimate: r.is_estimate,
8060        }
8061    }
8062}
8063
8064/// US state tax info.
8065#[pyclass]
8066#[derive(Clone)]
8067pub struct UsStateTaxInfo {
8068    #[pyo3(get)]
8069    state_code: String,
8070    #[pyo3(get)]
8071    state_name: String,
8072    #[pyo3(get)]
8073    state_rate: f64,
8074    #[pyo3(get)]
8075    has_local_taxes: bool,
8076    #[pyo3(get)]
8077    origin_based: bool,
8078    #[pyo3(get)]
8079    tax_shipping: bool,
8080    #[pyo3(get)]
8081    tax_clothing: bool,
8082    #[pyo3(get)]
8083    tax_food: bool,
8084    #[pyo3(get)]
8085    tax_digital: bool,
8086}
8087
8088impl From<stateset_core::UsStateTaxInfo> for UsStateTaxInfo {
8089    fn from(i: stateset_core::UsStateTaxInfo) -> Self {
8090        Self {
8091            state_code: i.state_code,
8092            state_name: i.state_name,
8093            state_rate: to_f64_or_nan(i.state_rate),
8094            has_local_taxes: i.has_local_taxes,
8095            origin_based: i.origin_based,
8096            tax_shipping: i.tax_shipping,
8097            tax_clothing: i.tax_clothing,
8098            tax_food: i.tax_food,
8099            tax_digital: i.tax_digital,
8100        }
8101    }
8102}
8103
8104/// EU VAT info.
8105#[pyclass]
8106#[derive(Clone)]
8107pub struct EuVatInfo {
8108    #[pyo3(get)]
8109    country_code: String,
8110    #[pyo3(get)]
8111    country_name: String,
8112    #[pyo3(get)]
8113    standard_rate: f64,
8114    #[pyo3(get)]
8115    reduced_rate: Option<f64>,
8116    #[pyo3(get)]
8117    super_reduced_rate: Option<f64>,
8118    #[pyo3(get)]
8119    parking_rate: Option<f64>,
8120}
8121
8122impl From<stateset_core::EuVatInfo> for EuVatInfo {
8123    fn from(i: stateset_core::EuVatInfo) -> Self {
8124        Self {
8125            country_code: i.country_code,
8126            country_name: i.country_name,
8127            standard_rate: to_f64_or_nan(i.standard_rate),
8128            reduced_rate: i.reduced_rate.map(|d| to_f64_or_nan(d)),
8129            super_reduced_rate: i.super_reduced_rate.map(|d| to_f64_or_nan(d)),
8130            parking_rate: i.parking_rate.map(|d| to_f64_or_nan(d)),
8131        }
8132    }
8133}
8134
8135/// Canadian tax info.
8136#[pyclass]
8137#[derive(Clone)]
8138pub struct CanadianTaxInfo {
8139    #[pyo3(get)]
8140    province_code: String,
8141    #[pyo3(get)]
8142    province_name: String,
8143    #[pyo3(get)]
8144    gst_rate: f64,
8145    #[pyo3(get)]
8146    pst_rate: Option<f64>,
8147    #[pyo3(get)]
8148    hst_rate: Option<f64>,
8149    #[pyo3(get)]
8150    qst_rate: Option<f64>,
8151    #[pyo3(get)]
8152    total_rate: f64,
8153}
8154
8155impl From<stateset_core::CanadianTaxInfo> for CanadianTaxInfo {
8156    fn from(i: stateset_core::CanadianTaxInfo) -> Self {
8157        Self {
8158            province_code: i.province_code,
8159            province_name: i.province_name,
8160            gst_rate: to_f64_or_nan(i.gst_rate),
8161            pst_rate: i.pst_rate.map(|d| to_f64_or_nan(d)),
8162            hst_rate: i.hst_rate.map(|d| to_f64_or_nan(d)),
8163            qst_rate: i.qst_rate.map(|d| to_f64_or_nan(d)),
8164            total_rate: to_f64_or_nan(i.total_rate),
8165        }
8166    }
8167}
8168
8169// --- Helper Functions ---
8170
8171fn parse_tax_type(s: &str) -> stateset_core::TaxType {
8172    match s.to_lowercase().as_str() {
8173        "sales_tax" => stateset_core::TaxType::SalesTax,
8174        "vat" => stateset_core::TaxType::Vat,
8175        "gst" => stateset_core::TaxType::Gst,
8176        "hst" => stateset_core::TaxType::Hst,
8177        "pst" => stateset_core::TaxType::Pst,
8178        "qst" => stateset_core::TaxType::Qst,
8179        "consumption_tax" => stateset_core::TaxType::ConsumptionTax,
8180        "custom" => stateset_core::TaxType::Custom,
8181        _ => stateset_core::TaxType::SalesTax,
8182    }
8183}
8184
8185fn parse_product_tax_category(s: &str) -> stateset_core::ProductTaxCategory {
8186    match s.to_lowercase().as_str() {
8187        "standard" => stateset_core::ProductTaxCategory::Standard,
8188        "reduced" => stateset_core::ProductTaxCategory::Reduced,
8189        "super_reduced" => stateset_core::ProductTaxCategory::SuperReduced,
8190        "zero_rated" => stateset_core::ProductTaxCategory::ZeroRated,
8191        "exempt" => stateset_core::ProductTaxCategory::Exempt,
8192        "digital" => stateset_core::ProductTaxCategory::Digital,
8193        "clothing" => stateset_core::ProductTaxCategory::Clothing,
8194        "food" => stateset_core::ProductTaxCategory::Food,
8195        "prepared_food" => stateset_core::ProductTaxCategory::PreparedFood,
8196        "medical" => stateset_core::ProductTaxCategory::Medical,
8197        "educational" => stateset_core::ProductTaxCategory::Educational,
8198        "luxury" => stateset_core::ProductTaxCategory::Luxury,
8199        _ => stateset_core::ProductTaxCategory::Standard,
8200    }
8201}
8202
8203fn parse_jurisdiction_level(s: &str) -> stateset_core::JurisdictionLevel {
8204    match s.to_lowercase().as_str() {
8205        "country" => stateset_core::JurisdictionLevel::Country,
8206        "state" => stateset_core::JurisdictionLevel::State,
8207        "county" => stateset_core::JurisdictionLevel::County,
8208        "city" => stateset_core::JurisdictionLevel::City,
8209        "district" => stateset_core::JurisdictionLevel::District,
8210        "special" => stateset_core::JurisdictionLevel::Special,
8211        _ => stateset_core::JurisdictionLevel::Country,
8212    }
8213}
8214
8215fn parse_exemption_type(s: &str) -> stateset_core::ExemptionType {
8216    match s.to_lowercase().as_str() {
8217        "resale" => stateset_core::ExemptionType::Resale,
8218        "non_profit" | "nonprofit" => stateset_core::ExemptionType::NonProfit,
8219        "government" => stateset_core::ExemptionType::Government,
8220        "educational" => stateset_core::ExemptionType::Educational,
8221        "religious" => stateset_core::ExemptionType::Religious,
8222        "medical" => stateset_core::ExemptionType::Medical,
8223        "manufacturing" => stateset_core::ExemptionType::Manufacturing,
8224        "agricultural" => stateset_core::ExemptionType::Agricultural,
8225        "export" => stateset_core::ExemptionType::Export,
8226        "diplomatic" => stateset_core::ExemptionType::Diplomatic,
8227        _ => stateset_core::ExemptionType::Other,
8228    }
8229}
8230
8231/// Tax operations API.
8232#[pyclass]
8233pub struct TaxApi {
8234    commerce: Arc<Mutex<RustCommerce>>,
8235}
8236
8237#[pymethods]
8238impl TaxApi {
8239    // ========================================================================
8240    // Jurisdiction Operations
8241    // ========================================================================
8242
8243    /// Create a tax jurisdiction.
8244    #[pyo3(signature = (name, code, country_code, parent_id=None, level=None, state_code=None, county=None, city=None, postal_codes=None))]
8245    fn create_jurisdiction(
8246        &self,
8247        name: String,
8248        code: String,
8249        country_code: String,
8250        parent_id: Option<String>,
8251        level: Option<String>,
8252        state_code: Option<String>,
8253        county: Option<String>,
8254        city: Option<String>,
8255        postal_codes: Option<Vec<String>>,
8256    ) -> PyResult<TaxJurisdiction> {
8257        let commerce = self
8258            .commerce
8259            .lock()
8260            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
8261
8262        let create = stateset_core::CreateTaxJurisdiction {
8263            parent_id: parent_id.and_then(|s| s.parse().ok()),
8264            name,
8265            code,
8266            level: level.map(|s| parse_jurisdiction_level(&s)).unwrap_or_default(),
8267            country_code,
8268            state_code,
8269            county,
8270            city,
8271            postal_codes: postal_codes.unwrap_or_default(),
8272        };
8273
8274        let jurisdiction = commerce.tax().create_jurisdiction(create).map_err(|e| {
8275            PyRuntimeError::new_err(format!("Failed to create jurisdiction: {}", e))
8276        })?;
8277
8278        Ok(jurisdiction.into())
8279    }
8280
8281    /// Get a jurisdiction by ID.
8282    fn get_jurisdiction(&self, id: String) -> PyResult<Option<TaxJurisdiction>> {
8283        let commerce = self
8284            .commerce
8285            .lock()
8286            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
8287        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
8288
8289        let jurisdiction = commerce
8290            .tax()
8291            .get_jurisdiction(uuid)
8292            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get jurisdiction: {}", e)))?;
8293
8294        Ok(jurisdiction.map(|j| j.into()))
8295    }
8296
8297    /// Get a jurisdiction by code.
8298    fn get_jurisdiction_by_code(&self, code: String) -> PyResult<Option<TaxJurisdiction>> {
8299        let commerce = self
8300            .commerce
8301            .lock()
8302            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
8303
8304        let jurisdiction = commerce
8305            .tax()
8306            .get_jurisdiction_by_code(&code)
8307            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get jurisdiction: {}", e)))?;
8308
8309        Ok(jurisdiction.map(|j| j.into()))
8310    }
8311
8312    /// List jurisdictions.
8313    #[pyo3(signature = (country_code=None, state_code=None, level=None, active_only=None))]
8314    fn list_jurisdictions(
8315        &self,
8316        country_code: Option<String>,
8317        state_code: Option<String>,
8318        level: Option<String>,
8319        active_only: Option<bool>,
8320    ) -> PyResult<Vec<TaxJurisdiction>> {
8321        let commerce = self
8322            .commerce
8323            .lock()
8324            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
8325
8326        let filter = stateset_core::TaxJurisdictionFilter {
8327            country_code,
8328            state_code,
8329            level: level.map(|s| parse_jurisdiction_level(&s)),
8330            active_only: active_only.unwrap_or(false),
8331        };
8332
8333        let jurisdictions = commerce
8334            .tax()
8335            .list_jurisdictions(filter)
8336            .map_err(|e| PyRuntimeError::new_err(format!("Failed to list jurisdictions: {}", e)))?;
8337
8338        Ok(jurisdictions.into_iter().map(|j| j.into()).collect())
8339    }
8340
8341    // ========================================================================
8342    // Tax Rate Operations
8343    // ========================================================================
8344
8345    /// Create a tax rate.
8346    #[pyo3(signature = (jurisdiction_id, rate, name, effective_from, tax_type=None, product_category=None, description=None, is_compound=None, priority=None, effective_to=None))]
8347    fn create_rate(
8348        &self,
8349        jurisdiction_id: String,
8350        rate: f64,
8351        name: String,
8352        effective_from: String,
8353        tax_type: Option<String>,
8354        product_category: Option<String>,
8355        description: Option<String>,
8356        is_compound: Option<bool>,
8357        priority: Option<i32>,
8358        effective_to: Option<String>,
8359    ) -> PyResult<TaxRate> {
8360        let commerce = self
8361            .commerce
8362            .lock()
8363            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
8364
8365        let jid = jurisdiction_id
8366            .parse()
8367            .map_err(|_| PyValueError::new_err("Invalid jurisdiction UUID"))?;
8368
8369        let eff_from = chrono::NaiveDate::parse_from_str(&effective_from, "%Y-%m-%d")
8370            .map_err(|e| PyRuntimeError::new_err(format!("Invalid date format: {}", e)))?;
8371
8372        let create = stateset_core::CreateTaxRate {
8373            jurisdiction_id: jid,
8374            tax_type: tax_type.map(|s| parse_tax_type(&s)).unwrap_or_default(),
8375            product_category: product_category
8376                .map(|s| parse_product_tax_category(&s))
8377                .unwrap_or_default(),
8378            rate: Decimal::from_f64_retain(rate).unwrap_or_default(),
8379            name,
8380            description,
8381            is_compound: is_compound.unwrap_or(false),
8382            priority: priority.unwrap_or(0),
8383            threshold_min: None,
8384            threshold_max: None,
8385            fixed_amount: None,
8386            effective_from: eff_from,
8387            effective_to: effective_to
8388                .and_then(|s| chrono::NaiveDate::parse_from_str(&s, "%Y-%m-%d").ok()),
8389        };
8390
8391        let rate_result = commerce
8392            .tax()
8393            .create_rate(create)
8394            .map_err(|e| PyRuntimeError::new_err(format!("Failed to create rate: {}", e)))?;
8395
8396        Ok(rate_result.into())
8397    }
8398
8399    /// Get a rate by ID.
8400    fn get_rate(&self, id: String) -> PyResult<Option<TaxRate>> {
8401        let commerce = self
8402            .commerce
8403            .lock()
8404            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
8405        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
8406
8407        let rate = commerce
8408            .tax()
8409            .get_rate(uuid)
8410            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get rate: {}", e)))?;
8411
8412        Ok(rate.map(|r| r.into()))
8413    }
8414
8415    /// List tax rates.
8416    #[pyo3(signature = (jurisdiction_id=None, tax_type=None, product_category=None, active_only=None))]
8417    fn list_rates(
8418        &self,
8419        jurisdiction_id: Option<String>,
8420        tax_type: Option<String>,
8421        product_category: Option<String>,
8422        active_only: Option<bool>,
8423    ) -> PyResult<Vec<TaxRate>> {
8424        let commerce = self
8425            .commerce
8426            .lock()
8427            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
8428
8429        let filter = stateset_core::TaxRateFilter {
8430            jurisdiction_id: jurisdiction_id.and_then(|s| s.parse().ok()),
8431            tax_type: tax_type.map(|s| parse_tax_type(&s)),
8432            product_category: product_category.map(|s| parse_product_tax_category(&s)),
8433            active_only: active_only.unwrap_or(false),
8434            effective_date: None,
8435        };
8436
8437        let rates = commerce
8438            .tax()
8439            .list_rates(filter)
8440            .map_err(|e| PyRuntimeError::new_err(format!("Failed to list rates: {}", e)))?;
8441
8442        Ok(rates.into_iter().map(|r| r.into()).collect())
8443    }
8444
8445    // ========================================================================
8446    // Exemption Operations
8447    // ========================================================================
8448
8449    /// Create a tax exemption.
8450    #[pyo3(signature = (customer_id, exemption_type, effective_from, certificate_number=None, issuing_authority=None, jurisdiction_ids=None, exempt_categories=None, expires_at=None, notes=None))]
8451    fn create_exemption(
8452        &self,
8453        customer_id: String,
8454        exemption_type: String,
8455        effective_from: String,
8456        certificate_number: Option<String>,
8457        issuing_authority: Option<String>,
8458        jurisdiction_ids: Option<Vec<String>>,
8459        exempt_categories: Option<Vec<String>>,
8460        expires_at: Option<String>,
8461        notes: Option<String>,
8462    ) -> PyResult<TaxExemption> {
8463        let commerce = self
8464            .commerce
8465            .lock()
8466            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
8467
8468        let cid =
8469            customer_id.parse().map_err(|_| PyValueError::new_err("Invalid customer UUID"))?;
8470
8471        let eff_from = chrono::NaiveDate::parse_from_str(&effective_from, "%Y-%m-%d")
8472            .map_err(|e| PyRuntimeError::new_err(format!("Invalid date format: {}", e)))?;
8473
8474        let create = stateset_core::CreateTaxExemption {
8475            customer_id: cid,
8476            exemption_type: parse_exemption_type(&exemption_type),
8477            certificate_number,
8478            issuing_authority,
8479            jurisdiction_ids: jurisdiction_ids
8480                .unwrap_or_default()
8481                .into_iter()
8482                .filter_map(|s| s.parse().ok())
8483                .collect(),
8484            exempt_categories: exempt_categories
8485                .unwrap_or_default()
8486                .into_iter()
8487                .map(|s| parse_product_tax_category(&s))
8488                .collect(),
8489            effective_from: eff_from,
8490            expires_at: expires_at
8491                .and_then(|s| chrono::NaiveDate::parse_from_str(&s, "%Y-%m-%d").ok()),
8492            notes,
8493        };
8494
8495        let exemption = commerce
8496            .tax()
8497            .create_exemption(create)
8498            .map_err(|e| PyRuntimeError::new_err(format!("Failed to create exemption: {}", e)))?;
8499
8500        Ok(exemption.into())
8501    }
8502
8503    /// Get an exemption by ID.
8504    fn get_exemption(&self, id: String) -> PyResult<Option<TaxExemption>> {
8505        let commerce = self
8506            .commerce
8507            .lock()
8508            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
8509        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
8510
8511        let exemption = commerce
8512            .tax()
8513            .get_exemption(uuid)
8514            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get exemption: {}", e)))?;
8515
8516        Ok(exemption.map(|e| e.into()))
8517    }
8518
8519    /// Get exemptions for a customer.
8520    fn get_customer_exemptions(&self, customer_id: String) -> PyResult<Vec<TaxExemption>> {
8521        let commerce = self
8522            .commerce
8523            .lock()
8524            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
8525        let uuid = customer_id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
8526
8527        let exemptions = commerce
8528            .tax()
8529            .get_customer_exemptions(uuid)
8530            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get exemptions: {}", e)))?;
8531
8532        Ok(exemptions.into_iter().map(|e| e.into()).collect())
8533    }
8534
8535    /// Check if a customer is tax exempt.
8536    fn customer_is_exempt(&self, customer_id: String) -> PyResult<bool> {
8537        let commerce = self
8538            .commerce
8539            .lock()
8540            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
8541        let uuid = customer_id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
8542
8543        let is_exempt = commerce
8544            .tax()
8545            .customer_is_exempt(uuid)
8546            .map_err(|e| PyRuntimeError::new_err(format!("Failed to check exemption: {}", e)))?;
8547
8548        Ok(is_exempt)
8549    }
8550
8551    // ========================================================================
8552    // Settings Operations
8553    // ========================================================================
8554
8555    /// Get tax settings.
8556    fn get_settings(&self) -> PyResult<TaxSettings> {
8557        let commerce = self
8558            .commerce
8559            .lock()
8560            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
8561
8562        let settings = commerce
8563            .tax()
8564            .get_settings()
8565            .map_err(|e| PyRuntimeError::new_err(format!("Failed to get settings: {}", e)))?;
8566
8567        Ok(settings.into())
8568    }
8569
8570    /// Enable or disable tax calculation.
8571    fn set_enabled(&self, enabled: bool) -> PyResult<TaxSettings> {
8572        let commerce = self
8573            .commerce
8574            .lock()
8575            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
8576
8577        let settings = commerce
8578            .tax()
8579            .set_enabled(enabled)
8580            .map_err(|e| PyRuntimeError::new_err(format!("Failed to update settings: {}", e)))?;
8581
8582        Ok(settings.into())
8583    }
8584
8585    /// Check if tax calculation is enabled.
8586    fn is_enabled(&self) -> PyResult<bool> {
8587        let commerce = self
8588            .commerce
8589            .lock()
8590            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
8591
8592        let enabled = commerce
8593            .tax()
8594            .is_enabled()
8595            .map_err(|e| PyRuntimeError::new_err(format!("Failed to check settings: {}", e)))?;
8596
8597        Ok(enabled)
8598    }
8599
8600    // ========================================================================
8601    // Helper Methods
8602    // ========================================================================
8603
8604    /// Get US state tax information.
8605    #[staticmethod]
8606    fn get_us_state_info(state_code: String) -> Option<UsStateTaxInfo> {
8607        stateset_core::get_us_state_tax_info(&state_code).map(|i| i.into())
8608    }
8609
8610    /// Get EU VAT information.
8611    #[staticmethod]
8612    fn get_eu_vat_info(country_code: String) -> Option<EuVatInfo> {
8613        stateset_core::get_eu_vat_info(&country_code).map(|i| i.into())
8614    }
8615
8616    /// Get Canadian tax information.
8617    #[staticmethod]
8618    fn get_canadian_tax_info(province_code: String) -> Option<CanadianTaxInfo> {
8619        stateset_core::get_canadian_tax_info(&province_code).map(|i| i.into())
8620    }
8621
8622    /// Check if a country is in the EU.
8623    #[staticmethod]
8624    fn is_eu_country(country_code: String) -> bool {
8625        stateset_core::is_eu_member(&country_code)
8626    }
8627}
8628
8629// ============================================================================
8630// Quality Control Types
8631// ============================================================================
8632
8633#[pyclass]
8634#[derive(Clone)]
8635pub struct Inspection {
8636    #[pyo3(get)]
8637    id: String,
8638    #[pyo3(get)]
8639    inspection_number: String,
8640    #[pyo3(get)]
8641    inspection_type: String,
8642    #[pyo3(get)]
8643    status: String,
8644    #[pyo3(get)]
8645    reference_type: String,
8646    #[pyo3(get)]
8647    reference_id: String,
8648    #[pyo3(get)]
8649    inspector_id: Option<String>,
8650    #[pyo3(get)]
8651    notes: Option<String>,
8652    #[pyo3(get)]
8653    created_at: String,
8654}
8655
8656#[pymethods]
8657impl Inspection {
8658    fn __repr__(&self) -> String {
8659        format!("Inspection(number='{}', status='{}')", self.inspection_number, self.status)
8660    }
8661}
8662
8663impl From<stateset_core::Inspection> for Inspection {
8664    fn from(i: stateset_core::Inspection) -> Self {
8665        Self {
8666            id: i.id.to_string(),
8667            inspection_number: i.inspection_number,
8668            inspection_type: format!("{:?}", i.inspection_type),
8669            status: format!("{:?}", i.status),
8670            reference_type: i.reference_type,
8671            reference_id: i.reference_id.to_string(),
8672            inspector_id: i.inspector_id,
8673            notes: i.notes,
8674            created_at: i.created_at.to_rfc3339(),
8675        }
8676    }
8677}
8678
8679#[pyclass]
8680#[derive(Clone)]
8681pub struct NonConformance {
8682    #[pyo3(get)]
8683    id: String,
8684    #[pyo3(get)]
8685    ncr_number: String,
8686    #[pyo3(get)]
8687    sku: String,
8688    #[pyo3(get)]
8689    description: String,
8690    #[pyo3(get)]
8691    status: String,
8692    #[pyo3(get)]
8693    source: String,
8694    #[pyo3(get)]
8695    severity: String,
8696    #[pyo3(get)]
8697    quantity_affected: f64,
8698}
8699
8700impl From<stateset_core::NonConformance> for NonConformance {
8701    fn from(n: stateset_core::NonConformance) -> Self {
8702        Self {
8703            id: n.id.to_string(),
8704            ncr_number: n.ncr_number,
8705            sku: n.sku,
8706            description: n.description,
8707            status: format!("{:?}", n.status),
8708            source: format!("{:?}", n.source),
8709            severity: format!("{:?}", n.severity),
8710            quantity_affected: to_f64_or_nan(n.quantity_affected),
8711        }
8712    }
8713}
8714
8715#[pyclass]
8716#[derive(Clone)]
8717pub struct QualityHold {
8718    #[pyo3(get)]
8719    id: String,
8720    #[pyo3(get)]
8721    sku: String,
8722    #[pyo3(get)]
8723    reason: String,
8724    #[pyo3(get)]
8725    quantity_held: f64,
8726    #[pyo3(get)]
8727    hold_type: String,
8728    #[pyo3(get)]
8729    placed_by: String,
8730}
8731
8732impl From<stateset_core::QualityHold> for QualityHold {
8733    fn from(h: stateset_core::QualityHold) -> Self {
8734        Self {
8735            id: h.id.to_string(),
8736            sku: h.sku,
8737            reason: h.reason,
8738            quantity_held: to_f64_or_nan(h.quantity_held),
8739            hold_type: format!("{:?}", h.hold_type),
8740            placed_by: h.placed_by,
8741        }
8742    }
8743}
8744
8745// ============================================================================
8746// Quality Control API
8747// ============================================================================
8748
8749#[pyclass]
8750pub struct QualityApi {
8751    commerce: Arc<Mutex<RustCommerce>>,
8752}
8753
8754#[pymethods]
8755impl QualityApi {
8756    #[pyo3(signature = (reference_type, reference_id, inspection_type, inspector_id=None))]
8757    fn create_inspection(
8758        &self,
8759        reference_type: String,
8760        reference_id: String,
8761        inspection_type: String,
8762        inspector_id: Option<String>,
8763    ) -> PyResult<Inspection> {
8764        let commerce = self
8765            .commerce
8766            .lock()
8767            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
8768        let itype = match inspection_type.to_lowercase().as_str() {
8769            "incoming" => stateset_core::InspectionType::Incoming,
8770            "receiving" => stateset_core::InspectionType::Receiving,
8771            "in_process" => stateset_core::InspectionType::InProcess,
8772            "final" => stateset_core::InspectionType::Final,
8773            "random" => stateset_core::InspectionType::Random,
8774            _ => stateset_core::InspectionType::Incoming,
8775        };
8776        let ref_uuid =
8777            reference_id.parse().map_err(|_| PyValueError::new_err("Invalid reference UUID"))?;
8778        let inspection = commerce
8779            .quality()
8780            .create_inspection(stateset_core::CreateInspection {
8781                inspection_type: itype,
8782                reference_type,
8783                reference_id: ref_uuid,
8784                inspector_id,
8785                ..Default::default()
8786            })
8787            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
8788        Ok(inspection.into())
8789    }
8790
8791    fn get_inspection(&self, id: String) -> PyResult<Option<Inspection>> {
8792        let commerce = self
8793            .commerce
8794            .lock()
8795            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
8796        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
8797        let inspection = commerce
8798            .quality()
8799            .get_inspection(uuid)
8800            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
8801        Ok(inspection.map(|i| i.into()))
8802    }
8803
8804    fn list_inspections(&self) -> PyResult<Vec<Inspection>> {
8805        let commerce = self
8806            .commerce
8807            .lock()
8808            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
8809        let inspections = commerce
8810            .quality()
8811            .list_inspections(Default::default())
8812            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
8813        Ok(inspections.into_iter().map(|i| i.into()).collect())
8814    }
8815
8816    fn complete_inspection(&self, id: String) -> PyResult<Inspection> {
8817        let commerce = self
8818            .commerce
8819            .lock()
8820            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
8821        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
8822        let inspection = commerce
8823            .quality()
8824            .complete_inspection(uuid)
8825            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
8826        Ok(inspection.into())
8827    }
8828
8829    fn create_ncr(
8830        &self,
8831        sku: String,
8832        description: String,
8833        quantity_affected: f64,
8834        source: String,
8835        severity: String,
8836    ) -> PyResult<NonConformance> {
8837        let commerce = self
8838            .commerce
8839            .lock()
8840            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
8841        let src = match source.to_lowercase().as_str() {
8842            "inspection" => stateset_core::NonConformanceSource::Inspection,
8843            "production" | "production_defect" => {
8844                stateset_core::NonConformanceSource::ProductionDefect
8845            }
8846            "customer" | "customer_complaint" => {
8847                stateset_core::NonConformanceSource::CustomerComplaint
8848            }
8849            "supplier" | "supplier_issue" => stateset_core::NonConformanceSource::SupplierIssue,
8850            "internal_audit" => stateset_core::NonConformanceSource::InternalAudit,
8851            "shipping_damage" => stateset_core::NonConformanceSource::ShippingDamage,
8852            _ => stateset_core::NonConformanceSource::Inspection,
8853        };
8854        let sev = match severity.to_lowercase().as_str() {
8855            "critical" => stateset_core::Severity::Critical,
8856            "major" => stateset_core::Severity::Major,
8857            "minor" => stateset_core::Severity::Minor,
8858            "observation" => stateset_core::Severity::Observation,
8859            _ => stateset_core::Severity::Minor,
8860        };
8861        let ncr = commerce
8862            .quality()
8863            .create_ncr(stateset_core::CreateNonConformance {
8864                sku,
8865                description,
8866                quantity_affected: Decimal::from_f64_retain(quantity_affected).unwrap_or_default(),
8867                source: src,
8868                severity: sev,
8869                ..Default::default()
8870            })
8871            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
8872        Ok(ncr.into())
8873    }
8874
8875    fn list_ncrs(&self) -> PyResult<Vec<NonConformance>> {
8876        let commerce = self
8877            .commerce
8878            .lock()
8879            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
8880        let ncrs = commerce
8881            .quality()
8882            .list_ncrs(Default::default())
8883            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
8884        Ok(ncrs.into_iter().map(|n| n.into()).collect())
8885    }
8886
8887    fn create_hold(&self, sku: String, reason: String, quantity: f64) -> PyResult<QualityHold> {
8888        let commerce = self
8889            .commerce
8890            .lock()
8891            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
8892        let hold = commerce
8893            .quality()
8894            .create_hold(stateset_core::CreateQualityHold {
8895                sku,
8896                reason,
8897                quantity: Decimal::from_f64_retain(quantity).unwrap_or_default(),
8898                ..Default::default()
8899            })
8900            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
8901        Ok(hold.into())
8902    }
8903
8904    #[pyo3(signature = (id, released_by, release_notes=None))]
8905    fn release_hold(
8906        &self,
8907        id: String,
8908        released_by: String,
8909        release_notes: Option<String>,
8910    ) -> PyResult<QualityHold> {
8911        let commerce = self
8912            .commerce
8913            .lock()
8914            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
8915        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
8916        let hold = commerce
8917            .quality()
8918            .release_hold(uuid, stateset_core::ReleaseQualityHold { released_by, release_notes })
8919            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
8920        Ok(hold.into())
8921    }
8922}
8923
8924// ============================================================================
8925// Lot/Batch Tracking Types
8926// ============================================================================
8927
8928#[pyclass]
8929#[derive(Clone)]
8930pub struct Lot {
8931    #[pyo3(get)]
8932    id: String,
8933    #[pyo3(get)]
8934    lot_number: String,
8935    #[pyo3(get)]
8936    sku: String,
8937    #[pyo3(get)]
8938    quantity_remaining: f64,
8939    #[pyo3(get)]
8940    status: String,
8941    #[pyo3(get)]
8942    expiration_date: Option<String>,
8943    #[pyo3(get)]
8944    created_at: String,
8945}
8946
8947impl From<stateset_core::Lot> for Lot {
8948    fn from(l: stateset_core::Lot) -> Self {
8949        Self {
8950            id: l.id.to_string(),
8951            lot_number: l.lot_number,
8952            sku: l.sku,
8953            quantity_remaining: to_f64_or_nan(l.quantity_remaining),
8954            status: format!("{:?}", l.status),
8955            expiration_date: l.expiration_date.map(|d| d.to_rfc3339()),
8956            created_at: l.created_at.to_rfc3339(),
8957        }
8958    }
8959}
8960
8961// ============================================================================
8962// Lots API
8963// ============================================================================
8964
8965#[pyclass]
8966pub struct LotsApi {
8967    commerce: Arc<Mutex<RustCommerce>>,
8968}
8969
8970#[pymethods]
8971impl LotsApi {
8972    #[pyo3(signature = (sku, lot_number, quantity, expiration_date=None))]
8973    fn create_lot(
8974        &self,
8975        sku: String,
8976        lot_number: String,
8977        quantity: f64,
8978        expiration_date: Option<String>,
8979    ) -> PyResult<Lot> {
8980        let commerce = self
8981            .commerce
8982            .lock()
8983            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
8984        let exp = expiration_date
8985            .and_then(|s| chrono::DateTime::parse_from_rfc3339(&s).ok())
8986            .map(|dt| dt.with_timezone(&chrono::Utc));
8987        let lot = commerce
8988            .lots()
8989            .create(stateset_core::CreateLot {
8990                sku,
8991                lot_number: Some(lot_number),
8992                quantity: Decimal::from_f64_retain(quantity).unwrap_or_default(),
8993                expiration_date: exp,
8994                ..Default::default()
8995            })
8996            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
8997        Ok(lot.into())
8998    }
8999
9000    fn get_lot(&self, id: String) -> PyResult<Option<Lot>> {
9001        let commerce = self
9002            .commerce
9003            .lock()
9004            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9005        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
9006        let lot = commerce.lots().get(uuid).map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9007        Ok(lot.map(|l| l.into()))
9008    }
9009
9010    fn list_lots(&self) -> PyResult<Vec<Lot>> {
9011        let commerce = self
9012            .commerce
9013            .lock()
9014            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9015        let lots = commerce
9016            .lots()
9017            .list(Default::default())
9018            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9019        Ok(lots.into_iter().map(|l| l.into()).collect())
9020    }
9021
9022    fn get_lots_by_sku(&self, sku: String) -> PyResult<Vec<Lot>> {
9023        let commerce = self
9024            .commerce
9025            .lock()
9026            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9027        let lots = commerce
9028            .lots()
9029            .get_available_lots_for_sku(&sku)
9030            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9031        Ok(lots.into_iter().map(|l| l.into()).collect())
9032    }
9033
9034    fn quarantine_lot(&self, id: String, reason: String) -> PyResult<Lot> {
9035        let commerce = self
9036            .commerce
9037            .lock()
9038            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9039        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
9040        let lot = commerce
9041            .lots()
9042            .quarantine(uuid, &reason)
9043            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9044        Ok(lot.into())
9045    }
9046
9047    fn get_expiring_lots(&self, days_ahead: i32) -> PyResult<Vec<Lot>> {
9048        let commerce = self
9049            .commerce
9050            .lock()
9051            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9052        let lots = commerce
9053            .lots()
9054            .get_expiring_lots(days_ahead)
9055            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9056        Ok(lots.into_iter().map(|l| l.into()).collect())
9057    }
9058}
9059
9060// ============================================================================
9061// Serial Number Types
9062// ============================================================================
9063
9064#[pyclass]
9065#[derive(Clone)]
9066pub struct SerialNumber {
9067    #[pyo3(get)]
9068    id: String,
9069    #[pyo3(get)]
9070    serial: String,
9071    #[pyo3(get)]
9072    sku: String,
9073    #[pyo3(get)]
9074    status: String,
9075    #[pyo3(get)]
9076    created_at: String,
9077}
9078
9079impl From<stateset_core::SerialNumber> for SerialNumber {
9080    fn from(s: stateset_core::SerialNumber) -> Self {
9081        Self {
9082            id: s.id.to_string(),
9083            serial: s.serial,
9084            sku: s.sku,
9085            status: format!("{:?}", s.status),
9086            created_at: s.created_at.to_rfc3339(),
9087        }
9088    }
9089}
9090
9091// ============================================================================
9092// Serials API
9093// ============================================================================
9094
9095#[pyclass]
9096pub struct SerialsApi {
9097    commerce: Arc<Mutex<RustCommerce>>,
9098}
9099
9100#[pymethods]
9101impl SerialsApi {
9102    #[pyo3(signature = (sku, serial=None))]
9103    fn create(&self, sku: String, serial: Option<String>) -> PyResult<SerialNumber> {
9104        let commerce = self
9105            .commerce
9106            .lock()
9107            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9108        let s = commerce
9109            .serials()
9110            .create(stateset_core::CreateSerialNumber { sku, serial, ..Default::default() })
9111            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9112        Ok(s.into())
9113    }
9114
9115    fn get_by_serial(&self, serial: String) -> PyResult<Option<SerialNumber>> {
9116        let commerce = self
9117            .commerce
9118            .lock()
9119            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9120        let s = commerce
9121            .serials()
9122            .get_by_serial(&serial)
9123            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9124        Ok(s.map(|s| s.into()))
9125    }
9126
9127    fn list(&self) -> PyResult<Vec<SerialNumber>> {
9128        let commerce = self
9129            .commerce
9130            .lock()
9131            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9132        let serials = commerce
9133            .serials()
9134            .list(Default::default())
9135            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9136        Ok(serials.into_iter().map(|s| s.into()).collect())
9137    }
9138
9139    fn change_status(&self, id: String, status: String) -> PyResult<SerialNumber> {
9140        let commerce = self
9141            .commerce
9142            .lock()
9143            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9144        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
9145        let serial_status = match status.to_lowercase().as_str() {
9146            "available" => stateset_core::SerialStatus::Available,
9147            "sold" => stateset_core::SerialStatus::Sold,
9148            "returned" => stateset_core::SerialStatus::Returned,
9149            "scrapped" => stateset_core::SerialStatus::Scrapped,
9150            "reserved" => stateset_core::SerialStatus::Reserved,
9151            "shipped" => stateset_core::SerialStatus::Shipped,
9152            "quarantine" | "quarantined" => stateset_core::SerialStatus::Quarantined,
9153            _ => stateset_core::SerialStatus::Available,
9154        };
9155        let serial = commerce
9156            .serials()
9157            .change_status(stateset_core::ChangeSerialStatus {
9158                serial_id: uuid,
9159                new_status: serial_status,
9160                ..Default::default()
9161            })
9162            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9163        Ok(serial.into())
9164    }
9165}
9166
9167// ============================================================================
9168// Warehouse Types
9169// ============================================================================
9170
9171#[pyclass]
9172#[derive(Clone)]
9173pub struct Warehouse {
9174    #[pyo3(get)]
9175    id: String,
9176    #[pyo3(get)]
9177    code: String,
9178    #[pyo3(get)]
9179    name: String,
9180    #[pyo3(get)]
9181    address: Option<String>,
9182    #[pyo3(get)]
9183    is_active: bool,
9184}
9185
9186impl From<stateset_core::Warehouse> for Warehouse {
9187    fn from(w: stateset_core::Warehouse) -> Self {
9188        // Convert WarehouseAddress to a simple string representation
9189        let addr_str = if w.address.street1.is_empty() && w.address.city.is_empty() {
9190            None
9191        } else {
9192            Some(format!("{}, {}, {}", w.address.street1, w.address.city, w.address.country))
9193        };
9194        Self {
9195            id: w.id.to_string(),
9196            code: w.code,
9197            name: w.name,
9198            address: addr_str,
9199            is_active: w.is_active,
9200        }
9201    }
9202}
9203
9204#[pyclass]
9205#[derive(Clone)]
9206pub struct WarehouseLocation {
9207    #[pyo3(get)]
9208    id: String,
9209    #[pyo3(get)]
9210    warehouse_id: String,
9211    #[pyo3(get)]
9212    code: String,
9213    #[pyo3(get)]
9214    location_type: String,
9215}
9216
9217impl From<stateset_core::Location> for WarehouseLocation {
9218    fn from(l: stateset_core::Location) -> Self {
9219        Self {
9220            id: l.id.to_string(),
9221            warehouse_id: l.warehouse_id.to_string(),
9222            code: l.code,
9223            location_type: format!("{:?}", l.location_type),
9224        }
9225    }
9226}
9227
9228// ============================================================================
9229// Warehouse API
9230// ============================================================================
9231
9232#[pyclass]
9233pub struct WarehouseApi {
9234    commerce: Arc<Mutex<RustCommerce>>,
9235}
9236
9237#[pymethods]
9238impl WarehouseApi {
9239    #[pyo3(signature = (code, name, address=None))]
9240    fn create_warehouse(
9241        &self,
9242        code: String,
9243        name: String,
9244        address: Option<String>,
9245    ) -> PyResult<Warehouse> {
9246        let commerce = self
9247            .commerce
9248            .lock()
9249            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9250        let _ = address; // Address is simplified - WarehouseAddress uses Default
9251        let warehouse = commerce
9252            .warehouse()
9253            .create_warehouse(stateset_core::CreateWarehouse { code, name, ..Default::default() })
9254            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9255        Ok(warehouse.into())
9256    }
9257
9258    fn get_warehouse(&self, id: String) -> PyResult<Option<Warehouse>> {
9259        let commerce = self
9260            .commerce
9261            .lock()
9262            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9263        let wh_id: i32 = id.parse().map_err(|_| PyValueError::new_err("Invalid warehouse ID"))?;
9264        let warehouse = commerce
9265            .warehouse()
9266            .get_warehouse(wh_id)
9267            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9268        Ok(warehouse.map(|w| w.into()))
9269    }
9270
9271    fn list_warehouses(&self) -> PyResult<Vec<Warehouse>> {
9272        let commerce = self
9273            .commerce
9274            .lock()
9275            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9276        let warehouses = commerce
9277            .warehouse()
9278            .list_warehouses(Default::default())
9279            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9280        Ok(warehouses.into_iter().map(|w| w.into()).collect())
9281    }
9282
9283    #[pyo3(signature = (warehouse_id, code, location_type=None))]
9284    fn create_location(
9285        &self,
9286        warehouse_id: String,
9287        code: String,
9288        location_type: Option<String>,
9289    ) -> PyResult<WarehouseLocation> {
9290        let commerce = self
9291            .commerce
9292            .lock()
9293            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9294        let wh_id: i32 =
9295            warehouse_id.parse().map_err(|_| PyValueError::new_err("Invalid warehouse ID"))?;
9296        let loc_type = location_type
9297            .map(|t| match t.to_lowercase().as_str() {
9298                "pick" | "picking" => stateset_core::LocationType::Pick,
9299                "bulk" => stateset_core::LocationType::Bulk,
9300                "receiving" => stateset_core::LocationType::Receiving,
9301                "shipping" => stateset_core::LocationType::Shipping,
9302                "staging" => stateset_core::LocationType::Staging,
9303                "quarantine" => stateset_core::LocationType::Quarantine,
9304                _ => stateset_core::LocationType::Bulk,
9305            })
9306            .unwrap_or(stateset_core::LocationType::Bulk);
9307        let location = commerce
9308            .warehouse()
9309            .create_location(stateset_core::CreateLocation {
9310                warehouse_id: wh_id,
9311                code: Some(code),
9312                location_type: loc_type,
9313                ..Default::default()
9314            })
9315            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9316        Ok(location.into())
9317    }
9318
9319    fn get_locations_for_warehouse(
9320        &self,
9321        warehouse_id: String,
9322    ) -> PyResult<Vec<WarehouseLocation>> {
9323        let commerce = self
9324            .commerce
9325            .lock()
9326            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9327        let wh_id: i32 =
9328            warehouse_id.parse().map_err(|_| PyValueError::new_err("Invalid warehouse ID"))?;
9329        let locations = commerce
9330            .warehouse()
9331            .get_locations_for_warehouse(wh_id)
9332            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9333        Ok(locations.into_iter().map(|l| l.into()).collect())
9334    }
9335}
9336
9337// ============================================================================
9338// Receiving Types
9339// ============================================================================
9340
9341#[pyclass]
9342#[derive(Clone)]
9343pub struct Receipt {
9344    #[pyo3(get)]
9345    id: String,
9346    #[pyo3(get)]
9347    receipt_number: String,
9348    #[pyo3(get)]
9349    receipt_type: String,
9350    #[pyo3(get)]
9351    status: String,
9352    #[pyo3(get)]
9353    reference_id: Option<String>,
9354    #[pyo3(get)]
9355    supplier_id: Option<String>,
9356    #[pyo3(get)]
9357    warehouse_id: String,
9358    #[pyo3(get)]
9359    created_at: String,
9360}
9361
9362impl From<stateset_core::Receipt> for Receipt {
9363    fn from(r: stateset_core::Receipt) -> Self {
9364        Self {
9365            id: r.id.to_string(),
9366            receipt_number: r.receipt_number,
9367            receipt_type: format!("{:?}", r.receipt_type),
9368            status: format!("{:?}", r.status),
9369            reference_id: r.reference_id.map(|id| id.to_string()),
9370            supplier_id: r.supplier_id.map(|id| id.to_string()),
9371            warehouse_id: r.warehouse_id.to_string(),
9372            created_at: r.created_at.to_rfc3339(),
9373        }
9374    }
9375}
9376
9377#[pyclass]
9378#[derive(Clone)]
9379pub struct ReceiptLine {
9380    #[pyo3(get)]
9381    id: String,
9382    #[pyo3(get)]
9383    receipt_id: String,
9384    #[pyo3(get)]
9385    sku: String,
9386    #[pyo3(get)]
9387    expected_quantity: f64,
9388    #[pyo3(get)]
9389    received_quantity: f64,
9390    #[pyo3(get)]
9391    unit_cost: Option<f64>,
9392    #[pyo3(get)]
9393    status: String,
9394}
9395
9396impl From<stateset_core::ReceiptItem> for ReceiptLine {
9397    fn from(l: stateset_core::ReceiptItem) -> Self {
9398        Self {
9399            id: l.id.to_string(),
9400            receipt_id: l.receipt_id.to_string(),
9401            sku: l.sku,
9402            expected_quantity: to_f64_or_nan(l.expected_quantity),
9403            received_quantity: to_f64_or_nan(l.received_quantity),
9404            unit_cost: l.unit_cost.map(|c| to_f64_or_nan(c)),
9405            status: format!("{:?}", l.status),
9406        }
9407    }
9408}
9409
9410// ============================================================================
9411// Receiving API
9412// ============================================================================
9413
9414#[pyclass]
9415pub struct ReceivingApi {
9416    commerce: Arc<Mutex<RustCommerce>>,
9417}
9418
9419#[pymethods]
9420impl ReceivingApi {
9421    #[pyo3(signature = (warehouse_id, supplier_id=None))]
9422    fn create_receipt(
9423        &self,
9424        warehouse_id: String,
9425        supplier_id: Option<String>,
9426    ) -> PyResult<Receipt> {
9427        let commerce = self
9428            .commerce
9429            .lock()
9430            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9431        let wh_id: i32 =
9432            warehouse_id.parse().map_err(|_| PyValueError::new_err("Invalid warehouse ID"))?;
9433        let sup_uuid = supplier_id.and_then(|id| id.parse().ok());
9434        let receipt = commerce
9435            .receiving()
9436            .create_receipt(stateset_core::CreateReceipt {
9437                warehouse_id: wh_id,
9438                supplier_id: sup_uuid,
9439                ..Default::default()
9440            })
9441            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9442        Ok(receipt.into())
9443    }
9444
9445    fn get_receipt(&self, id: String) -> PyResult<Option<Receipt>> {
9446        let commerce = self
9447            .commerce
9448            .lock()
9449            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9450        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
9451        let receipt = commerce
9452            .receiving()
9453            .get_receipt(uuid)
9454            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9455        Ok(receipt.map(|r| r.into()))
9456    }
9457
9458    fn list_receipts(&self) -> PyResult<Vec<Receipt>> {
9459        let commerce = self
9460            .commerce
9461            .lock()
9462            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9463        let receipts = commerce
9464            .receiving()
9465            .list_receipts(Default::default())
9466            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9467        Ok(receipts.into_iter().map(|r| r.into()).collect())
9468    }
9469
9470    fn get_receipt_items(&self, receipt_id: String) -> PyResult<Vec<ReceiptLine>> {
9471        let commerce = self
9472            .commerce
9473            .lock()
9474            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9475        let uuid = receipt_id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
9476        let items = commerce
9477            .receiving()
9478            .get_receipt_items(uuid)
9479            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9480        Ok(items.into_iter().map(|i| i.into()).collect())
9481    }
9482
9483    fn complete_receiving(&self, id: String) -> PyResult<Receipt> {
9484        let commerce = self
9485            .commerce
9486            .lock()
9487            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9488        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
9489        let receipt = commerce
9490            .receiving()
9491            .complete_receiving(uuid)
9492            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9493        Ok(receipt.into())
9494    }
9495}
9496
9497// ============================================================================
9498// Fulfillment Types
9499// ============================================================================
9500
9501#[pyclass]
9502#[derive(Clone)]
9503pub struct Wave {
9504    #[pyo3(get)]
9505    id: String,
9506    #[pyo3(get)]
9507    wave_number: String,
9508    #[pyo3(get)]
9509    warehouse_id: String,
9510    #[pyo3(get)]
9511    status: String,
9512    #[pyo3(get)]
9513    order_count: i32,
9514    #[pyo3(get)]
9515    pick_count: i32,
9516}
9517
9518impl From<stateset_core::Wave> for Wave {
9519    fn from(w: stateset_core::Wave) -> Self {
9520        Self {
9521            id: w.id.to_string(),
9522            wave_number: w.wave_number,
9523            warehouse_id: w.warehouse_id.to_string(),
9524            status: format!("{:?}", w.status),
9525            order_count: w.order_count,
9526            pick_count: w.pick_count,
9527        }
9528    }
9529}
9530
9531#[pyclass]
9532#[derive(Clone)]
9533pub struct PickTask {
9534    #[pyo3(get)]
9535    id: String,
9536    #[pyo3(get)]
9537    order_id: String,
9538    #[pyo3(get)]
9539    sku: String,
9540    #[pyo3(get)]
9541    quantity_requested: f64,
9542    #[pyo3(get)]
9543    quantity_picked: f64,
9544    #[pyo3(get)]
9545    status: String,
9546}
9547
9548impl From<stateset_core::PickTask> for PickTask {
9549    fn from(t: stateset_core::PickTask) -> Self {
9550        Self {
9551            id: t.id.to_string(),
9552            order_id: t.order_id.to_string(),
9553            sku: t.sku,
9554            quantity_requested: to_f64_or_nan(t.quantity_requested),
9555            quantity_picked: to_f64_or_nan(t.quantity_picked),
9556            status: format!("{:?}", t.status),
9557        }
9558    }
9559}
9560
9561// ============================================================================
9562// Fulfillment API
9563// ============================================================================
9564
9565#[pyclass]
9566pub struct FulfillmentApi {
9567    commerce: Arc<Mutex<RustCommerce>>,
9568}
9569
9570#[pymethods]
9571impl FulfillmentApi {
9572    fn create_wave(&self, warehouse_id: String, order_ids: Vec<String>) -> PyResult<Wave> {
9573        let commerce = self
9574            .commerce
9575            .lock()
9576            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9577        let wh_id: i32 =
9578            warehouse_id.parse().map_err(|_| PyValueError::new_err("Invalid warehouse ID"))?;
9579        let order_uuids: Vec<uuid::Uuid> =
9580            order_ids.iter().filter_map(|id| id.parse().ok()).collect();
9581        let wave = commerce
9582            .fulfillment()
9583            .create_wave(stateset_core::CreateWave {
9584                warehouse_id: wh_id,
9585                order_ids: order_uuids,
9586                ..Default::default()
9587            })
9588            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9589        Ok(wave.into())
9590    }
9591
9592    fn get_wave(&self, id: String) -> PyResult<Option<Wave>> {
9593        let commerce = self
9594            .commerce
9595            .lock()
9596            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9597        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
9598        let wave = commerce
9599            .fulfillment()
9600            .get_wave(uuid)
9601            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9602        Ok(wave.map(|w| w.into()))
9603    }
9604
9605    fn list_waves(&self) -> PyResult<Vec<Wave>> {
9606        let commerce = self
9607            .commerce
9608            .lock()
9609            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9610        let waves = commerce
9611            .fulfillment()
9612            .list_waves(Default::default())
9613            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9614        Ok(waves.into_iter().map(|w| w.into()).collect())
9615    }
9616
9617    fn release_wave(&self, id: String) -> PyResult<Wave> {
9618        let commerce = self
9619            .commerce
9620            .lock()
9621            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9622        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
9623        let wave = commerce
9624            .fulfillment()
9625            .release_wave(uuid)
9626            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9627        Ok(wave.into())
9628    }
9629
9630    fn list_picks(&self) -> PyResult<Vec<PickTask>> {
9631        let commerce = self
9632            .commerce
9633            .lock()
9634            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9635        let picks = commerce
9636            .fulfillment()
9637            .list_picks(Default::default())
9638            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9639        Ok(picks.into_iter().map(|p| p.into()).collect())
9640    }
9641
9642    fn complete_pick(&self, id: String, quantity_picked: f64) -> PyResult<PickTask> {
9643        let commerce = self
9644            .commerce
9645            .lock()
9646            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9647        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
9648        let task = commerce
9649            .fulfillment()
9650            .complete_pick(stateset_core::CompletePick {
9651                pick_id: uuid,
9652                quantity_picked: Decimal::from_f64_retain(quantity_picked).unwrap_or_default(),
9653                quantity_short: None,
9654                short_reason: None,
9655                lot_id: None,
9656                serial_number: None,
9657                completed_by: None,
9658            })
9659            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9660        Ok(task.into())
9661    }
9662}
9663
9664// ============================================================================
9665// Accounts Payable Types
9666// ============================================================================
9667
9668#[pyclass]
9669#[derive(Clone)]
9670pub struct Bill {
9671    #[pyo3(get)]
9672    id: String,
9673    #[pyo3(get)]
9674    bill_number: String,
9675    #[pyo3(get)]
9676    supplier_id: String,
9677    #[pyo3(get)]
9678    total_amount: f64,
9679    #[pyo3(get)]
9680    amount_paid: f64,
9681    #[pyo3(get)]
9682    amount_due: f64,
9683    #[pyo3(get)]
9684    status: String,
9685    #[pyo3(get)]
9686    due_date: String,
9687}
9688
9689impl From<stateset_core::Bill> for Bill {
9690    fn from(b: stateset_core::Bill) -> Self {
9691        Self {
9692            id: b.id.to_string(),
9693            bill_number: b.bill_number,
9694            supplier_id: b.supplier_id.to_string(),
9695            total_amount: to_f64_or_nan(b.total_amount),
9696            amount_paid: to_f64_or_nan(b.amount_paid),
9697            amount_due: to_f64_or_nan(b.amount_due),
9698            status: format!("{:?}", b.status),
9699            due_date: b.due_date.to_rfc3339(),
9700        }
9701    }
9702}
9703
9704#[pyclass]
9705#[derive(Clone)]
9706pub struct ApAgingSummary {
9707    #[pyo3(get)]
9708    current: f64,
9709    #[pyo3(get)]
9710    days_1_30: f64,
9711    #[pyo3(get)]
9712    days_31_60: f64,
9713    #[pyo3(get)]
9714    days_61_90: f64,
9715    #[pyo3(get)]
9716    days_over_90: f64,
9717    #[pyo3(get)]
9718    total: f64,
9719}
9720
9721impl From<stateset_core::ApAgingSummary> for ApAgingSummary {
9722    fn from(s: stateset_core::ApAgingSummary) -> Self {
9723        Self {
9724            current: to_f64_or_nan(s.current),
9725            days_1_30: to_f64_or_nan(s.days_1_30),
9726            days_31_60: to_f64_or_nan(s.days_31_60),
9727            days_61_90: to_f64_or_nan(s.days_61_90),
9728            days_over_90: to_f64_or_nan(s.days_over_90),
9729            total: to_f64_or_nan(s.total),
9730        }
9731    }
9732}
9733
9734// ============================================================================
9735// Accounts Payable API
9736// ============================================================================
9737
9738#[pyclass]
9739pub struct AccountsPayableApi {
9740    commerce: Arc<Mutex<RustCommerce>>,
9741}
9742
9743#[pymethods]
9744impl AccountsPayableApi {
9745    fn create_bill(&self, supplier_id: String, due_date: String) -> PyResult<Bill> {
9746        let commerce = self
9747            .commerce
9748            .lock()
9749            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9750        let uuid = supplier_id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
9751        let due = chrono::DateTime::parse_from_rfc3339(&due_date)
9752            .map_err(|_| PyValueError::new_err("Invalid due_date format"))?
9753            .with_timezone(&chrono::Utc);
9754        let bill = commerce
9755            .accounts_payable()
9756            .create_bill(stateset_core::CreateBill {
9757                supplier_id: uuid,
9758                due_date: due,
9759                ..Default::default()
9760            })
9761            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9762        Ok(bill.into())
9763    }
9764
9765    fn get_bill(&self, id: String) -> PyResult<Option<Bill>> {
9766        let commerce = self
9767            .commerce
9768            .lock()
9769            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9770        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
9771        let bill = commerce
9772            .accounts_payable()
9773            .get_bill(uuid)
9774            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9775        Ok(bill.map(|b| b.into()))
9776    }
9777
9778    fn list_bills(&self) -> PyResult<Vec<Bill>> {
9779        let commerce = self
9780            .commerce
9781            .lock()
9782            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9783        let bills = commerce
9784            .accounts_payable()
9785            .list_bills(Default::default())
9786            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9787        Ok(bills.into_iter().map(|b| b.into()).collect())
9788    }
9789
9790    fn approve_bill(&self, id: String) -> PyResult<Bill> {
9791        let commerce = self
9792            .commerce
9793            .lock()
9794            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9795        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
9796        let bill = commerce
9797            .accounts_payable()
9798            .approve_bill(uuid)
9799            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9800        Ok(bill.into())
9801    }
9802
9803    #[pyo3(signature = (id, amount, payment_method=None))]
9804    fn pay_bill(&self, id: String, amount: f64, payment_method: Option<String>) -> PyResult<Bill> {
9805        let commerce = self
9806            .commerce
9807            .lock()
9808            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9809        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
9810        let pm = payment_method
9811            .map(|s| match s.to_lowercase().as_str() {
9812                "check" => stateset_core::PaymentMethodAP::Check,
9813                "ach" => stateset_core::PaymentMethodAP::Ach,
9814                "wire" => stateset_core::PaymentMethodAP::Wire,
9815                "credit_card" => stateset_core::PaymentMethodAP::CreditCard,
9816                "cash" => stateset_core::PaymentMethodAP::Cash,
9817                _ => stateset_core::PaymentMethodAP::Other,
9818            })
9819            .unwrap_or(stateset_core::PaymentMethodAP::Check);
9820        let bill = commerce
9821            .accounts_payable()
9822            .pay_bill(
9823                uuid,
9824                stateset_core::PayBill {
9825                    amount: Decimal::from_f64_retain(amount).unwrap_or_default(),
9826                    payment_method: pm,
9827                    ..Default::default()
9828                },
9829            )
9830            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9831        Ok(bill.into())
9832    }
9833
9834    fn get_aging_summary(&self) -> PyResult<ApAgingSummary> {
9835        let commerce = self
9836            .commerce
9837            .lock()
9838            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9839        let summary = commerce
9840            .accounts_payable()
9841            .get_aging_summary()
9842            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9843        Ok(summary.into())
9844    }
9845
9846    fn get_overdue_bills(&self) -> PyResult<Vec<Bill>> {
9847        let commerce = self
9848            .commerce
9849            .lock()
9850            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9851        let bills = commerce
9852            .accounts_payable()
9853            .get_overdue_bills()
9854            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9855        Ok(bills.into_iter().map(|b| b.into()).collect())
9856    }
9857}
9858
9859// ============================================================================
9860// Accounts Receivable Types
9861// ============================================================================
9862
9863#[pyclass]
9864#[derive(Clone)]
9865pub struct ArAgingSummary {
9866    #[pyo3(get)]
9867    current: f64,
9868    #[pyo3(get)]
9869    days_1_30: f64,
9870    #[pyo3(get)]
9871    days_31_60: f64,
9872    #[pyo3(get)]
9873    days_61_90: f64,
9874    #[pyo3(get)]
9875    days_over_90: f64,
9876    #[pyo3(get)]
9877    total: f64,
9878}
9879
9880impl From<stateset_core::ArAgingSummary> for ArAgingSummary {
9881    fn from(s: stateset_core::ArAgingSummary) -> Self {
9882        Self {
9883            current: to_f64_or_nan(s.current),
9884            days_1_30: to_f64_or_nan(s.days_1_30),
9885            days_31_60: to_f64_or_nan(s.days_31_60),
9886            days_61_90: to_f64_or_nan(s.days_61_90),
9887            days_over_90: to_f64_or_nan(s.days_over_90),
9888            total: to_f64_or_nan(s.total),
9889        }
9890    }
9891}
9892
9893#[pyclass]
9894#[derive(Clone)]
9895pub struct CreditMemo {
9896    #[pyo3(get)]
9897    id: String,
9898    #[pyo3(get)]
9899    credit_memo_number: String,
9900    #[pyo3(get)]
9901    customer_id: String,
9902    #[pyo3(get)]
9903    amount: f64,
9904    #[pyo3(get)]
9905    reason: String,
9906    #[pyo3(get)]
9907    status: String,
9908}
9909
9910impl From<stateset_core::CreditMemo> for CreditMemo {
9911    fn from(m: stateset_core::CreditMemo) -> Self {
9912        Self {
9913            id: m.id.to_string(),
9914            credit_memo_number: m.credit_memo_number,
9915            customer_id: m.customer_id.to_string(),
9916            amount: to_f64_or_nan(m.amount),
9917            reason: format!("{:?}", m.reason),
9918            status: format!("{:?}", m.status),
9919        }
9920    }
9921}
9922
9923// ============================================================================
9924// Accounts Receivable API
9925// ============================================================================
9926
9927#[pyclass]
9928pub struct AccountsReceivableApi {
9929    commerce: Arc<Mutex<RustCommerce>>,
9930}
9931
9932#[pymethods]
9933impl AccountsReceivableApi {
9934    fn get_aging_summary(&self) -> PyResult<ArAgingSummary> {
9935        let commerce = self
9936            .commerce
9937            .lock()
9938            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9939        let summary = commerce
9940            .accounts_receivable()
9941            .get_aging_summary()
9942            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9943        Ok(summary.into())
9944    }
9945
9946    fn create_credit_memo(
9947        &self,
9948        customer_id: String,
9949        amount: f64,
9950        reason: String,
9951    ) -> PyResult<CreditMemo> {
9952        let commerce = self
9953            .commerce
9954            .lock()
9955            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9956        let uuid = customer_id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
9957        let r = match reason.to_lowercase().as_str() {
9958            "returned_goods" | "return" => stateset_core::CreditMemoReason::ReturnedGoods,
9959            "pricing_error" | "price" => stateset_core::CreditMemoReason::PricingError,
9960            "overpayment" => stateset_core::CreditMemoReason::Overpayment,
9961            "damaged" => stateset_core::CreditMemoReason::Damaged,
9962            "service_credit" | "service" => stateset_core::CreditMemoReason::ServiceCredit,
9963            "goodwill" | "adjustment" => stateset_core::CreditMemoReason::GoodwillAdjustment,
9964            _ => stateset_core::CreditMemoReason::Other,
9965        };
9966        let memo = commerce
9967            .accounts_receivable()
9968            .create_credit_memo(stateset_core::CreateCreditMemo {
9969                customer_id: uuid,
9970                amount: Decimal::from_f64_retain(amount).unwrap_or_default(),
9971                reason: r,
9972                original_invoice_id: None,
9973                notes: None,
9974            })
9975            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9976        Ok(memo.into())
9977    }
9978
9979    #[pyo3(signature = (days=None))]
9980    fn get_dso(&self, days: Option<i32>) -> PyResult<f64> {
9981        let commerce = self
9982            .commerce
9983            .lock()
9984            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
9985        let dso = commerce
9986            .accounts_receivable()
9987            .get_dso(days.unwrap_or(30))
9988            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
9989        Ok(to_f64_or_nan(dso))
9990    }
9991}
9992
9993// ============================================================================
9994// Cost Accounting Types
9995// ============================================================================
9996
9997#[pyclass]
9998#[derive(Clone)]
9999pub struct ItemCost {
10000    #[pyo3(get)]
10001    sku: String,
10002    #[pyo3(get)]
10003    standard_cost: f64,
10004    #[pyo3(get)]
10005    average_cost: f64,
10006    #[pyo3(get)]
10007    last_cost: f64,
10008    #[pyo3(get)]
10009    material_cost: f64,
10010    #[pyo3(get)]
10011    labor_cost: f64,
10012    #[pyo3(get)]
10013    overhead_cost: f64,
10014}
10015
10016impl From<stateset_core::ItemCost> for ItemCost {
10017    fn from(c: stateset_core::ItemCost) -> Self {
10018        Self {
10019            sku: c.sku,
10020            standard_cost: to_f64_or_nan(c.standard_cost),
10021            average_cost: to_f64_or_nan(c.average_cost),
10022            last_cost: to_f64_or_nan(c.last_cost),
10023            material_cost: to_f64_or_nan(c.material_cost),
10024            labor_cost: to_f64_or_nan(c.labor_cost),
10025            overhead_cost: to_f64_or_nan(c.overhead_cost),
10026        }
10027    }
10028}
10029
10030#[pyclass]
10031#[derive(Clone)]
10032pub struct InventoryValuation {
10033    #[pyo3(get)]
10034    total_value: f64,
10035    #[pyo3(get)]
10036    total_quantity: f64,
10037    #[pyo3(get)]
10038    average_unit_cost: f64,
10039}
10040
10041impl From<stateset_core::InventoryValuation> for InventoryValuation {
10042    fn from(v: stateset_core::InventoryValuation) -> Self {
10043        Self {
10044            total_value: to_f64_or_nan(v.total_value),
10045            total_quantity: to_f64_or_nan(v.total_quantity),
10046            average_unit_cost: to_f64_or_nan(v.average_unit_cost),
10047        }
10048    }
10049}
10050
10051// ============================================================================
10052// Cost Accounting API
10053// ============================================================================
10054
10055#[pyclass]
10056pub struct CostAccountingApi {
10057    commerce: Arc<Mutex<RustCommerce>>,
10058}
10059
10060#[pymethods]
10061impl CostAccountingApi {
10062    fn get_item_cost(&self, sku: String) -> PyResult<Option<ItemCost>> {
10063        let commerce = self
10064            .commerce
10065            .lock()
10066            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
10067        let cost = commerce
10068            .cost_accounting()
10069            .get_item_cost(&sku)
10070            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
10071        Ok(cost.map(|c| c.into()))
10072    }
10073
10074    #[pyo3(signature = (sku, standard_cost=None, material_cost=None, labor_cost=None, overhead_cost=None))]
10075    fn set_item_cost(
10076        &self,
10077        sku: String,
10078        standard_cost: Option<f64>,
10079        material_cost: Option<f64>,
10080        labor_cost: Option<f64>,
10081        overhead_cost: Option<f64>,
10082    ) -> PyResult<ItemCost> {
10083        let commerce = self
10084            .commerce
10085            .lock()
10086            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
10087        let cost = commerce
10088            .cost_accounting()
10089            .set_item_cost(stateset_core::SetItemCost {
10090                sku,
10091                standard_cost: standard_cost.and_then(Decimal::from_f64_retain),
10092                material_cost: material_cost.and_then(Decimal::from_f64_retain),
10093                labor_cost: labor_cost.and_then(Decimal::from_f64_retain),
10094                overhead_cost: overhead_cost.and_then(Decimal::from_f64_retain),
10095                ..Default::default()
10096            })
10097            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
10098        Ok(cost.into())
10099    }
10100
10101    fn update_average_cost(
10102        &self,
10103        sku: String,
10104        quantity: f64,
10105        unit_cost: f64,
10106    ) -> PyResult<ItemCost> {
10107        let commerce = self
10108            .commerce
10109            .lock()
10110            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
10111        let cost = commerce
10112            .cost_accounting()
10113            .update_average_cost(
10114                &sku,
10115                Decimal::from_f64_retain(quantity).unwrap_or_default(),
10116                Decimal::from_f64_retain(unit_cost).unwrap_or_default(),
10117            )
10118            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
10119        Ok(cost.into())
10120    }
10121
10122    #[pyo3(signature = (cost_method=None))]
10123    fn get_inventory_valuation(&self, cost_method: Option<String>) -> PyResult<InventoryValuation> {
10124        let commerce = self
10125            .commerce
10126            .lock()
10127            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
10128        let method = cost_method
10129            .and_then(|m| match m.to_lowercase().as_str() {
10130                "standard" => Some(stateset_core::CostMethod::Standard),
10131                "average" => Some(stateset_core::CostMethod::Average),
10132                "fifo" => Some(stateset_core::CostMethod::Fifo),
10133                "lifo" => Some(stateset_core::CostMethod::Lifo),
10134                _ => None,
10135            })
10136            .unwrap_or(stateset_core::CostMethod::Average);
10137        let valuation = commerce
10138            .cost_accounting()
10139            .get_inventory_valuation(method)
10140            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
10141        Ok(valuation.into())
10142    }
10143
10144    fn get_total_inventory_value(&self) -> PyResult<f64> {
10145        let commerce = self
10146            .commerce
10147            .lock()
10148            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
10149        let value = commerce
10150            .cost_accounting()
10151            .get_total_inventory_value()
10152            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
10153        Ok(to_f64_or_nan(value))
10154    }
10155}
10156
10157// ============================================================================
10158// Credit Management Types
10159// ============================================================================
10160
10161#[pyclass]
10162#[derive(Clone)]
10163pub struct CreditAccount {
10164    #[pyo3(get)]
10165    id: String,
10166    #[pyo3(get)]
10167    customer_id: String,
10168    #[pyo3(get)]
10169    credit_limit: f64,
10170    #[pyo3(get)]
10171    current_balance: f64,
10172    #[pyo3(get)]
10173    available_credit: f64,
10174    #[pyo3(get)]
10175    status: String,
10176    #[pyo3(get)]
10177    payment_terms: Option<String>,
10178}
10179
10180impl From<stateset_core::CreditAccount> for CreditAccount {
10181    fn from(a: stateset_core::CreditAccount) -> Self {
10182        Self {
10183            id: a.id.to_string(),
10184            customer_id: a.customer_id.to_string(),
10185            credit_limit: to_f64_or_nan(a.credit_limit),
10186            current_balance: to_f64_or_nan(a.current_balance),
10187            available_credit: to_f64_or_nan(a.available_credit),
10188            status: format!("{:?}", a.status),
10189            payment_terms: a.payment_terms,
10190        }
10191    }
10192}
10193
10194#[pyclass]
10195#[derive(Clone)]
10196pub struct CreditCheckResult {
10197    #[pyo3(get)]
10198    approved: bool,
10199    #[pyo3(get)]
10200    reason: Option<String>,
10201    #[pyo3(get)]
10202    available_credit: f64,
10203    #[pyo3(get)]
10204    requires_approval: bool,
10205}
10206
10207impl From<stateset_core::CreditCheckResult> for CreditCheckResult {
10208    fn from(r: stateset_core::CreditCheckResult) -> Self {
10209        Self {
10210            approved: r.approved,
10211            reason: r.reason.map(|r| format!("{:?}", r)),
10212            available_credit: to_f64_or_nan(r.available_credit),
10213            requires_approval: r.requires_approval,
10214        }
10215    }
10216}
10217
10218// ============================================================================
10219// Credit Management API
10220// ============================================================================
10221
10222#[pyclass]
10223pub struct CreditApi {
10224    commerce: Arc<Mutex<RustCommerce>>,
10225}
10226
10227#[pymethods]
10228impl CreditApi {
10229    #[pyo3(signature = (customer_id, credit_limit, payment_terms=None))]
10230    fn create_credit_account(
10231        &self,
10232        customer_id: String,
10233        credit_limit: f64,
10234        payment_terms: Option<String>,
10235    ) -> PyResult<CreditAccount> {
10236        let commerce = self
10237            .commerce
10238            .lock()
10239            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
10240        let uuid = customer_id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
10241        let account = commerce
10242            .credit()
10243            .create_credit_account(stateset_core::CreateCreditAccount {
10244                customer_id: uuid,
10245                credit_limit: Decimal::from_f64_retain(credit_limit).unwrap_or_default(),
10246                payment_terms,
10247                ..Default::default()
10248            })
10249            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
10250        Ok(account.into())
10251    }
10252
10253    fn get_credit_account_by_customer(
10254        &self,
10255        customer_id: String,
10256    ) -> PyResult<Option<CreditAccount>> {
10257        let commerce = self
10258            .commerce
10259            .lock()
10260            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
10261        let uuid = customer_id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
10262        let account = commerce
10263            .credit()
10264            .get_credit_account_by_customer(uuid)
10265            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
10266        Ok(account.map(|a| a.into()))
10267    }
10268
10269    fn check_credit(&self, customer_id: String, order_amount: f64) -> PyResult<CreditCheckResult> {
10270        let commerce = self
10271            .commerce
10272            .lock()
10273            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
10274        let uuid = customer_id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
10275        let result = commerce
10276            .credit()
10277            .check_credit(uuid, Decimal::from_f64_retain(order_amount).unwrap_or_default())
10278            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
10279        Ok(result.into())
10280    }
10281
10282    fn adjust_credit_limit(
10283        &self,
10284        customer_id: String,
10285        new_limit: f64,
10286        reason: String,
10287    ) -> PyResult<CreditAccount> {
10288        let commerce = self
10289            .commerce
10290            .lock()
10291            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
10292        let uuid = customer_id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
10293        let account = commerce
10294            .credit()
10295            .adjust_credit_limit(
10296                uuid,
10297                Decimal::from_f64_retain(new_limit).unwrap_or_default(),
10298                &reason,
10299            )
10300            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
10301        Ok(account.into())
10302    }
10303
10304    fn get_over_limit_customers(&self) -> PyResult<Vec<CreditAccount>> {
10305        let commerce = self
10306            .commerce
10307            .lock()
10308            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
10309        let accounts = commerce
10310            .credit()
10311            .get_over_limit_customers()
10312            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
10313        Ok(accounts.into_iter().map(|a| a.into()).collect())
10314    }
10315}
10316
10317// ============================================================================
10318// Backorder Management Types
10319// ============================================================================
10320
10321#[pyclass]
10322#[derive(Clone)]
10323pub struct Backorder {
10324    #[pyo3(get)]
10325    id: String,
10326    #[pyo3(get)]
10327    backorder_number: String,
10328    #[pyo3(get)]
10329    order_id: String,
10330    #[pyo3(get)]
10331    customer_id: String,
10332    #[pyo3(get)]
10333    sku: String,
10334    #[pyo3(get)]
10335    quantity_ordered: f64,
10336    #[pyo3(get)]
10337    quantity_fulfilled: f64,
10338    #[pyo3(get)]
10339    quantity_remaining: f64,
10340    #[pyo3(get)]
10341    status: String,
10342    #[pyo3(get)]
10343    priority: String,
10344}
10345
10346impl From<stateset_core::Backorder> for Backorder {
10347    fn from(b: stateset_core::Backorder) -> Self {
10348        Self {
10349            id: b.id.to_string(),
10350            backorder_number: b.backorder_number,
10351            order_id: b.order_id.to_string(),
10352            customer_id: b.customer_id.to_string(),
10353            sku: b.sku,
10354            quantity_ordered: to_f64_or_nan(b.quantity_ordered),
10355            quantity_fulfilled: to_f64_or_nan(b.quantity_fulfilled),
10356            quantity_remaining: to_f64_or_nan(b.quantity_remaining),
10357            status: format!("{:?}", b.status),
10358            priority: format!("{:?}", b.priority),
10359        }
10360    }
10361}
10362
10363#[pyclass]
10364#[derive(Clone)]
10365pub struct BackorderSummary {
10366    #[pyo3(get)]
10367    total_backorders: i32,
10368    #[pyo3(get)]
10369    total_quantity: f64,
10370    #[pyo3(get)]
10371    critical_count: i32,
10372    #[pyo3(get)]
10373    overdue_count: i32,
10374}
10375
10376impl From<stateset_core::BackorderSummary> for BackorderSummary {
10377    fn from(s: stateset_core::BackorderSummary) -> Self {
10378        Self {
10379            total_backorders: s.total_backorders,
10380            total_quantity: to_f64_or_nan(s.total_quantity),
10381            critical_count: s.critical_count,
10382            overdue_count: s.overdue_count,
10383        }
10384    }
10385}
10386
10387// ============================================================================
10388// Backorder Management API
10389// ============================================================================
10390
10391#[pyclass]
10392pub struct BackorderApi {
10393    commerce: Arc<Mutex<RustCommerce>>,
10394}
10395
10396#[pymethods]
10397impl BackorderApi {
10398    #[pyo3(signature = (order_id, customer_id, sku, quantity, priority=None))]
10399    fn create_backorder(
10400        &self,
10401        order_id: String,
10402        customer_id: String,
10403        sku: String,
10404        quantity: f64,
10405        priority: Option<String>,
10406    ) -> PyResult<Backorder> {
10407        let commerce = self
10408            .commerce
10409            .lock()
10410            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
10411        let ord_uuid = order_id.parse().map_err(|_| PyValueError::new_err("Invalid order UUID"))?;
10412        let cust_uuid =
10413            customer_id.parse().map_err(|_| PyValueError::new_err("Invalid customer UUID"))?;
10414        let prio = priority.and_then(|p| match p.to_lowercase().as_str() {
10415            "low" => Some(stateset_core::BackorderPriority::Low),
10416            "normal" => Some(stateset_core::BackorderPriority::Normal),
10417            "high" => Some(stateset_core::BackorderPriority::High),
10418            "critical" => Some(stateset_core::BackorderPriority::Critical),
10419            _ => None,
10420        });
10421        let backorder = commerce
10422            .backorder()
10423            .create_backorder(stateset_core::CreateBackorder {
10424                order_id: ord_uuid,
10425                customer_id: cust_uuid,
10426                sku,
10427                quantity: Decimal::from_f64_retain(quantity).unwrap_or_default(),
10428                priority: prio,
10429                order_line_id: None,
10430                expected_date: None,
10431                promised_date: None,
10432                source_location_id: None,
10433                notes: None,
10434            })
10435            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
10436        Ok(backorder.into())
10437    }
10438
10439    fn get_backorder(&self, id: String) -> PyResult<Option<Backorder>> {
10440        let commerce = self
10441            .commerce
10442            .lock()
10443            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
10444        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
10445        let backorder = commerce
10446            .backorder()
10447            .get_backorder(uuid)
10448            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
10449        Ok(backorder.map(|b| b.into()))
10450    }
10451
10452    fn list_backorders(&self) -> PyResult<Vec<Backorder>> {
10453        let commerce = self
10454            .commerce
10455            .lock()
10456            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
10457        let backorders = commerce
10458            .backorder()
10459            .list_backorders(Default::default())
10460            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
10461        Ok(backorders.into_iter().map(|b| b.into()).collect())
10462    }
10463
10464    fn fulfill_backorder(&self, id: String, quantity: f64) -> PyResult<Backorder> {
10465        let commerce = self
10466            .commerce
10467            .lock()
10468            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
10469        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
10470        let backorder = commerce
10471            .backorder()
10472            .fulfill_backorder(stateset_core::FulfillBackorder {
10473                backorder_id: uuid,
10474                quantity: Decimal::from_f64_retain(quantity).unwrap_or_default(),
10475                source_type: stateset_core::FulfillmentSourceType::Inventory,
10476                source_id: None,
10477                notes: None,
10478                fulfilled_by: None,
10479            })
10480            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
10481        Ok(backorder.into())
10482    }
10483
10484    fn cancel_backorder(&self, id: String) -> PyResult<Backorder> {
10485        let commerce = self
10486            .commerce
10487            .lock()
10488            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
10489        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
10490        let backorder = commerce
10491            .backorder()
10492            .cancel_backorder(uuid)
10493            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
10494        Ok(backorder.into())
10495    }
10496
10497    fn get_summary(&self) -> PyResult<BackorderSummary> {
10498        let commerce = self
10499            .commerce
10500            .lock()
10501            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
10502        let summary = commerce
10503            .backorder()
10504            .get_summary()
10505            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
10506        Ok(summary.into())
10507    }
10508
10509    fn get_overdue_backorders(&self) -> PyResult<Vec<Backorder>> {
10510        let commerce = self
10511            .commerce
10512            .lock()
10513            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
10514        let backorders = commerce
10515            .backorder()
10516            .get_overdue_backorders()
10517            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
10518        Ok(backorders.into_iter().map(|b| b.into()).collect())
10519    }
10520}
10521
10522// ============================================================================
10523// General Ledger Types
10524// ============================================================================
10525
10526#[pyclass]
10527#[derive(Clone)]
10528pub struct GlAccount {
10529    #[pyo3(get)]
10530    id: String,
10531    #[pyo3(get)]
10532    account_number: String,
10533    #[pyo3(get)]
10534    name: String,
10535    #[pyo3(get)]
10536    account_type: String,
10537    #[pyo3(get)]
10538    current_balance: f64,
10539    #[pyo3(get)]
10540    status: String,
10541}
10542
10543impl From<stateset_core::GlAccount> for GlAccount {
10544    fn from(a: stateset_core::GlAccount) -> Self {
10545        Self {
10546            id: a.id.to_string(),
10547            account_number: a.account_number,
10548            name: a.name,
10549            account_type: format!("{:?}", a.account_type),
10550            current_balance: to_f64_or_nan(a.current_balance),
10551            status: format!("{:?}", a.status),
10552        }
10553    }
10554}
10555
10556#[pyclass]
10557#[derive(Clone)]
10558pub struct JournalEntry {
10559    #[pyo3(get)]
10560    id: String,
10561    #[pyo3(get)]
10562    entry_number: String,
10563    #[pyo3(get)]
10564    description: String,
10565    #[pyo3(get)]
10566    status: String,
10567    #[pyo3(get)]
10568    entry_date: String,
10569}
10570
10571impl From<stateset_core::JournalEntry> for JournalEntry {
10572    fn from(e: stateset_core::JournalEntry) -> Self {
10573        Self {
10574            id: e.id.to_string(),
10575            entry_number: e.entry_number,
10576            description: e.description,
10577            status: format!("{:?}", e.status),
10578            entry_date: e.entry_date.to_string(),
10579        }
10580    }
10581}
10582
10583#[pyclass]
10584#[derive(Clone)]
10585pub struct TrialBalance {
10586    #[pyo3(get)]
10587    total_debits: f64,
10588    #[pyo3(get)]
10589    total_credits: f64,
10590    #[pyo3(get)]
10591    is_balanced: bool,
10592}
10593
10594impl From<stateset_core::TrialBalance> for TrialBalance {
10595    fn from(t: stateset_core::TrialBalance) -> Self {
10596        Self {
10597            total_debits: to_f64_or_nan(t.total_debits),
10598            total_credits: to_f64_or_nan(t.total_credits),
10599            is_balanced: t.is_balanced,
10600        }
10601    }
10602}
10603
10604// ============================================================================
10605// General Ledger API
10606// ============================================================================
10607
10608#[pyclass]
10609pub struct GeneralLedgerApi {
10610    commerce: Arc<Mutex<RustCommerce>>,
10611}
10612
10613#[pymethods]
10614impl GeneralLedgerApi {
10615    #[pyo3(signature = (account_number, name, account_type, description=None))]
10616    fn create_account(
10617        &self,
10618        account_number: String,
10619        name: String,
10620        account_type: String,
10621        description: Option<String>,
10622    ) -> PyResult<GlAccount> {
10623        let commerce = self
10624            .commerce
10625            .lock()
10626            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
10627        let acct_type = match account_type.to_lowercase().as_str() {
10628            "asset" => stateset_core::AccountType::Asset,
10629            "liability" => stateset_core::AccountType::Liability,
10630            "equity" => stateset_core::AccountType::Equity,
10631            "revenue" => stateset_core::AccountType::Revenue,
10632            "expense" => stateset_core::AccountType::Expense,
10633            _ => stateset_core::AccountType::Asset,
10634        };
10635        let account = commerce
10636            .general_ledger()
10637            .create_account(stateset_core::CreateGlAccount {
10638                account_number,
10639                name,
10640                account_type: acct_type,
10641                description,
10642                account_sub_type: None,
10643                parent_account_id: None,
10644                is_header: None,
10645                is_posting: None,
10646                currency: None,
10647            })
10648            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
10649        Ok(account.into())
10650    }
10651
10652    fn get_account(&self, id: String) -> PyResult<Option<GlAccount>> {
10653        let commerce = self
10654            .commerce
10655            .lock()
10656            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
10657        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
10658        let account = commerce
10659            .general_ledger()
10660            .get_account(uuid)
10661            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
10662        Ok(account.map(|a| a.into()))
10663    }
10664
10665    fn get_account_by_number(&self, account_number: String) -> PyResult<Option<GlAccount>> {
10666        let commerce = self
10667            .commerce
10668            .lock()
10669            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
10670        let account = commerce
10671            .general_ledger()
10672            .get_account_by_number(&account_number)
10673            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
10674        Ok(account.map(|a| a.into()))
10675    }
10676
10677    fn list_accounts(&self) -> PyResult<Vec<GlAccount>> {
10678        let commerce = self
10679            .commerce
10680            .lock()
10681            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
10682        let accounts = commerce
10683            .general_ledger()
10684            .list_accounts(Default::default())
10685            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
10686        Ok(accounts.into_iter().map(|a| a.into()).collect())
10687    }
10688
10689    fn get_journal_entry(&self, id: String) -> PyResult<Option<JournalEntry>> {
10690        let commerce = self
10691            .commerce
10692            .lock()
10693            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
10694        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
10695        let entry = commerce
10696            .general_ledger()
10697            .get_journal_entry(uuid)
10698            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
10699        Ok(entry.map(|e| e.into()))
10700    }
10701
10702    fn post_journal_entry(&self, id: String, posted_by: String) -> PyResult<JournalEntry> {
10703        let commerce = self
10704            .commerce
10705            .lock()
10706            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
10707        let uuid = id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
10708        let entry = commerce
10709            .general_ledger()
10710            .post_journal_entry(uuid, &posted_by)
10711            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
10712        Ok(entry.into())
10713    }
10714
10715    #[pyo3(signature = (as_of_date=None))]
10716    fn get_trial_balance(&self, as_of_date: Option<String>) -> PyResult<TrialBalance> {
10717        let commerce = self
10718            .commerce
10719            .lock()
10720            .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
10721        let date = as_of_date
10722            .and_then(|s| chrono::NaiveDate::parse_from_str(&s, "%Y-%m-%d").ok())
10723            .unwrap_or_else(|| chrono::Utc::now().date_naive());
10724        let balance = commerce
10725            .general_ledger()
10726            .get_trial_balance(date)
10727            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
10728        Ok(balance.into())
10729    }
10730}
10731
10732// ============================================================================
10733// Vector Search Types
10734// ============================================================================
10735
10736/// Vector search API for semantic similarity search.
10737///
10738/// Uses OpenAI text-embedding-3-small for generating embeddings.
10739#[pyclass]
10740pub struct VectorSearch {
10741    commerce: Arc<Mutex<RustCommerce>>,
10742    api_key: String,
10743}
10744
10745/// Product search result with similarity score.
10746#[pyclass]
10747#[derive(Clone)]
10748pub struct ProductSearchResult {
10749    #[pyo3(get)]
10750    pub id: String,
10751    #[pyo3(get)]
10752    pub name: String,
10753    #[pyo3(get)]
10754    pub description: String,
10755    #[pyo3(get)]
10756    pub distance: f64,
10757    #[pyo3(get)]
10758    pub score: f64,
10759}
10760
10761/// Customer search result with similarity score.
10762#[pyclass]
10763#[derive(Clone)]
10764pub struct CustomerSearchResult {
10765    #[pyo3(get)]
10766    pub id: String,
10767    #[pyo3(get)]
10768    pub name: String,
10769    #[pyo3(get)]
10770    pub email: String,
10771    #[pyo3(get)]
10772    pub distance: f64,
10773    #[pyo3(get)]
10774    pub score: f64,
10775}
10776
10777/// Embedding statistics.
10778#[pyclass]
10779#[derive(Clone)]
10780pub struct EmbeddingStats {
10781    #[pyo3(get)]
10782    pub product_count: u64,
10783    #[pyo3(get)]
10784    pub customer_count: u64,
10785    #[pyo3(get)]
10786    pub order_count: u64,
10787    #[pyo3(get)]
10788    pub inventory_count: u64,
10789    #[pyo3(get)]
10790    pub total_count: u64,
10791    #[pyo3(get)]
10792    pub model: String,
10793    #[pyo3(get)]
10794    pub dimensions: u32,
10795}
10796
10797#[pymethods]
10798impl VectorSearch {
10799    /// Search products using natural language query.
10800    ///
10801    /// Args:
10802    ///     query: Natural language search query (e.g., "wireless bluetooth headphones")
10803    ///     limit: Maximum number of results to return (default: 10)
10804    ///
10805    /// Returns:
10806    ///     List of ProductSearchResult sorted by relevance
10807    #[pyo3(signature = (query, limit=None))]
10808    fn search_products(
10809        &self,
10810        query: String,
10811        limit: Option<usize>,
10812    ) -> PyResult<Vec<ProductSearchResult>> {
10813        let vector = {
10814            let commerce = self
10815                .commerce
10816                .lock()
10817                .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
10818
10819            commerce
10820                .vector(self.api_key.clone())
10821                .map_err(|e| PyRuntimeError::new_err(format!("Vector init failed: {}", e)))?
10822        };
10823        let results = vector
10824            .search_products(&query, limit.unwrap_or(10))
10825            .map_err(|e| PyRuntimeError::new_err(format!("Search failed: {}", e)))?;
10826
10827        Ok(results
10828            .into_iter()
10829            .map(|r| ProductSearchResult {
10830                id: r.entity.id.to_string(),
10831                name: r.entity.name.clone(),
10832                description: r.entity.description.clone(),
10833                distance: r.distance as f64,
10834                score: r.score as f64,
10835            })
10836            .collect())
10837    }
10838
10839    /// Search customers using natural language query.
10840    ///
10841    /// Args:
10842    ///     query: Natural language search query (e.g., "enterprise customers in tech")
10843    ///     limit: Maximum number of results to return (default: 10)
10844    ///
10845    /// Returns:
10846    ///     List of CustomerSearchResult sorted by relevance
10847    #[pyo3(signature = (query, limit=None))]
10848    fn search_customers(
10849        &self,
10850        query: String,
10851        limit: Option<usize>,
10852    ) -> PyResult<Vec<CustomerSearchResult>> {
10853        let vector = {
10854            let commerce = self
10855                .commerce
10856                .lock()
10857                .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
10858
10859            commerce
10860                .vector(self.api_key.clone())
10861                .map_err(|e| PyRuntimeError::new_err(format!("Vector init failed: {}", e)))?
10862        };
10863        let results = vector
10864            .search_customers(&query, limit.unwrap_or(10))
10865            .map_err(|e| PyRuntimeError::new_err(format!("Search failed: {}", e)))?;
10866
10867        Ok(results
10868            .into_iter()
10869            .map(|r| CustomerSearchResult {
10870                id: r.entity.id.to_string(),
10871                name: format!("{} {}", r.entity.first_name, r.entity.last_name),
10872                email: r.entity.email.clone(),
10873                distance: r.distance as f64,
10874                score: r.score as f64,
10875            })
10876            .collect())
10877    }
10878
10879    /// Index a product for vector search.
10880    ///
10881    /// Args:
10882    ///     product_id: UUID of the product to index
10883    fn index_product(&self, product_id: String) -> PyResult<()> {
10884        let uuid: uuid::Uuid =
10885            product_id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
10886
10887        let (product, vector) = {
10888            let commerce = self
10889                .commerce
10890                .lock()
10891                .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
10892
10893            let product = commerce
10894                .products()
10895                .get(uuid)
10896                .map_err(|e| PyRuntimeError::new_err(format!("Failed to get product: {}", e)))?
10897                .ok_or_else(|| PyValueError::new_err("Product not found"))?;
10898
10899            let vector = commerce
10900                .vector(self.api_key.clone())
10901                .map_err(|e| PyRuntimeError::new_err(format!("Vector init failed: {}", e)))?;
10902
10903            (product, vector)
10904        };
10905        vector
10906            .index_product(&product)
10907            .map_err(|e| PyRuntimeError::new_err(format!("Indexing failed: {}", e)))?;
10908
10909        Ok(())
10910    }
10911
10912    /// Index a customer for vector search.
10913    ///
10914    /// Args:
10915    ///     customer_id: UUID of the customer to index
10916    fn index_customer(&self, customer_id: String) -> PyResult<()> {
10917        let uuid: uuid::Uuid =
10918            customer_id.parse().map_err(|_| PyValueError::new_err("Invalid UUID"))?;
10919
10920        let (customer, vector) = {
10921            let commerce = self
10922                .commerce
10923                .lock()
10924                .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
10925
10926            let customer = commerce
10927                .customers()
10928                .get(uuid)
10929                .map_err(|e| PyRuntimeError::new_err(format!("Failed to get customer: {}", e)))?
10930                .ok_or_else(|| PyValueError::new_err("Customer not found"))?;
10931
10932            let vector = commerce
10933                .vector(self.api_key.clone())
10934                .map_err(|e| PyRuntimeError::new_err(format!("Vector init failed: {}", e)))?;
10935
10936            (customer, vector)
10937        };
10938        vector
10939            .index_customer(&customer)
10940            .map_err(|e| PyRuntimeError::new_err(format!("Indexing failed: {}", e)))?;
10941
10942        Ok(())
10943    }
10944
10945    /// Index all products for vector search.
10946    ///
10947    /// Returns:
10948    ///     Number of products indexed
10949    fn index_all_products(&self) -> PyResult<u64> {
10950        let (products, vector) = {
10951            let commerce = self
10952                .commerce
10953                .lock()
10954                .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
10955
10956            let products = commerce
10957                .products()
10958                .list(Default::default())
10959                .map_err(|e| PyRuntimeError::new_err(format!("Failed to list products: {}", e)))?;
10960
10961            let vector = commerce
10962                .vector(self.api_key.clone())
10963                .map_err(|e| PyRuntimeError::new_err(format!("Vector init failed: {}", e)))?;
10964
10965            (products, vector)
10966        };
10967        let count = vector
10968            .index_products(&products)
10969            .map_err(|e| PyRuntimeError::new_err(format!("Indexing failed: {}", e)))?;
10970
10971        Ok(count as u64)
10972    }
10973
10974    /// Index all customers for vector search.
10975    ///
10976    /// Returns:
10977    ///     Number of customers indexed
10978    fn index_all_customers(&self) -> PyResult<u64> {
10979        let (customers, vector) = {
10980            let commerce = self
10981                .commerce
10982                .lock()
10983                .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
10984
10985            let customers = commerce
10986                .customers()
10987                .list(Default::default())
10988                .map_err(|e| PyRuntimeError::new_err(format!("Failed to list customers: {}", e)))?;
10989
10990            let vector = commerce
10991                .vector(self.api_key.clone())
10992                .map_err(|e| PyRuntimeError::new_err(format!("Vector init failed: {}", e)))?;
10993
10994            (customers, vector)
10995        };
10996        let count = vector
10997            .index_customers(&customers)
10998            .map_err(|e| PyRuntimeError::new_err(format!("Indexing failed: {}", e)))?;
10999
11000        Ok(count as u64)
11001    }
11002
11003    /// Get embedding statistics.
11004    ///
11005    /// Returns:
11006    ///     EmbeddingStats with counts by entity type
11007    fn stats(&self) -> PyResult<EmbeddingStats> {
11008        let vector = {
11009            let commerce = self
11010                .commerce
11011                .lock()
11012                .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
11013
11014            commerce
11015                .vector(self.api_key.clone())
11016                .map_err(|e| PyRuntimeError::new_err(format!("Vector init failed: {}", e)))?
11017        };
11018        let stats =
11019            vector.stats().map_err(|e| PyRuntimeError::new_err(format!("Stats failed: {}", e)))?;
11020
11021        let product_count = *stats.counts.get(&stateset_core::EntityType::Product).unwrap_or(&0);
11022        let customer_count = *stats.counts.get(&stateset_core::EntityType::Customer).unwrap_or(&0);
11023        let order_count = *stats.counts.get(&stateset_core::EntityType::Order).unwrap_or(&0);
11024        let inventory_count =
11025            *stats.counts.get(&stateset_core::EntityType::InventoryItem).unwrap_or(&0);
11026
11027        Ok(EmbeddingStats {
11028            product_count,
11029            customer_count,
11030            order_count,
11031            inventory_count,
11032            total_count: product_count + customer_count + order_count + inventory_count,
11033            model: stats.model,
11034            dimensions: stats.dimensions as u32,
11035        })
11036    }
11037
11038    /// Clear all embeddings for a specific entity type.
11039    ///
11040    /// Args:
11041    ///     entity_type: One of "products", "customers", "orders", "inventory"
11042    ///
11043    /// Returns:
11044    ///     Number of embeddings cleared
11045    fn clear(&self, entity_type: String) -> PyResult<u64> {
11046        let et: stateset_core::EntityType =
11047            entity_type.parse().map_err(|e: String| PyValueError::new_err(e))?;
11048
11049        let vector = {
11050            let commerce = self
11051                .commerce
11052                .lock()
11053                .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
11054
11055            commerce
11056                .vector(self.api_key.clone())
11057                .map_err(|e| PyRuntimeError::new_err(format!("Vector init failed: {}", e)))?
11058        };
11059        let count = vector
11060            .clear(et)
11061            .map_err(|e| PyRuntimeError::new_err(format!("Clear failed: {}", e)))?;
11062
11063        Ok(count)
11064    }
11065
11066    /// Clear all embeddings.
11067    ///
11068    /// Returns:
11069    ///     Total number of embeddings cleared
11070    fn clear_all(&self) -> PyResult<u64> {
11071        let vector = {
11072            let commerce = self
11073                .commerce
11074                .lock()
11075                .map_err(|e| PyRuntimeError::new_err(format!("Lock error: {}", e)))?;
11076
11077            commerce
11078                .vector(self.api_key.clone())
11079                .map_err(|e| PyRuntimeError::new_err(format!("Vector init failed: {}", e)))?
11080        };
11081        let count = vector
11082            .clear_all()
11083            .map_err(|e| PyRuntimeError::new_err(format!("Clear failed: {}", e)))?;
11084
11085        Ok(count)
11086    }
11087}
11088
11089// ============================================================================
11090// Module Definition
11091// ============================================================================
11092
11093/// StateSet Embedded Commerce - Local-first commerce library
11094#[pymodule]
11095fn stateset_embedded(m: &Bound<'_, PyModule>) -> PyResult<()> {
11096    // Core
11097    m.add_class::<Commerce>()?;
11098
11099    // Customers
11100    m.add_class::<Customers>()?;
11101    m.add_class::<Customer>()?;
11102
11103    // Orders
11104    m.add_class::<Orders>()?;
11105    m.add_class::<Order>()?;
11106    m.add_class::<OrderItem>()?;
11107    m.add_class::<CreateOrderItemInput>()?;
11108
11109    // Products
11110    m.add_class::<Products>()?;
11111    m.add_class::<Product>()?;
11112    m.add_class::<ProductVariant>()?;
11113    m.add_class::<CreateProductVariantInput>()?;
11114
11115    // Custom Objects (custom states / metaobjects)
11116    m.add_class::<CustomObjectsApi>()?;
11117    m.add_class::<CustomObjectType>()?;
11118    m.add_class::<CustomFieldDefinition>()?;
11119    m.add_class::<CustomFieldDefinitionInput>()?;
11120    m.add_class::<CustomObject>()?;
11121
11122    // Inventory
11123    m.add_class::<Inventory>()?;
11124    m.add_class::<InventoryItem>()?;
11125    m.add_class::<StockLevel>()?;
11126    m.add_class::<Reservation>()?;
11127
11128    // Returns
11129    m.add_class::<Returns>()?;
11130    m.add_class::<Return>()?;
11131    m.add_class::<CreateReturnItemInput>()?;
11132
11133    // Payments
11134    m.add_class::<Payments>()?;
11135    m.add_class::<Payment>()?;
11136    m.add_class::<Refund>()?;
11137
11138    // Shipments
11139    m.add_class::<Shipments>()?;
11140    m.add_class::<Shipment>()?;
11141
11142    // Warranties
11143    m.add_class::<Warranties>()?;
11144    m.add_class::<Warranty>()?;
11145    m.add_class::<WarrantyClaim>()?;
11146
11147    // Purchase Orders
11148    m.add_class::<PurchaseOrders>()?;
11149    m.add_class::<Supplier>()?;
11150    m.add_class::<PurchaseOrder>()?;
11151
11152    // Invoices
11153    m.add_class::<Invoices>()?;
11154    m.add_class::<Invoice>()?;
11155
11156    // Bill of Materials
11157    m.add_class::<BomApi>()?;
11158    m.add_class::<Bom>()?;
11159    m.add_class::<BomComponent>()?;
11160
11161    // Work Orders
11162    m.add_class::<WorkOrders>()?;
11163    m.add_class::<WorkOrder>()?;
11164
11165    // Carts
11166    m.add_class::<Carts>()?;
11167    m.add_class::<Cart>()?;
11168    m.add_class::<CartItem>()?;
11169    m.add_class::<CartAddress>()?;
11170    m.add_class::<AddCartItemInput>()?;
11171    m.add_class::<ShippingRate>()?;
11172    m.add_class::<CheckoutResult>()?;
11173
11174    // Analytics
11175    m.add_class::<Analytics>()?;
11176    m.add_class::<SalesSummary>()?;
11177    m.add_class::<RevenueByPeriod>()?;
11178    m.add_class::<TopProduct>()?;
11179    m.add_class::<ProductPerformance>()?;
11180    m.add_class::<CustomerMetrics>()?;
11181    m.add_class::<TopCustomer>()?;
11182    m.add_class::<InventoryHealth>()?;
11183    m.add_class::<LowStockItem>()?;
11184    m.add_class::<InventoryMovement>()?;
11185    m.add_class::<OrderStatusBreakdown>()?;
11186    m.add_class::<FulfillmentMetrics>()?;
11187    m.add_class::<ReturnMetrics>()?;
11188    m.add_class::<DemandForecast>()?;
11189    m.add_class::<RevenueForecast>()?;
11190
11191    // Currency
11192    m.add_class::<CurrencyOperations>()?;
11193    m.add_class::<ExchangeRate>()?;
11194    m.add_class::<ConversionResult>()?;
11195    m.add_class::<StoreCurrencySettings>()?;
11196    m.add_class::<SetExchangeRateInput>()?;
11197
11198    // Subscriptions
11199    m.add_class::<Subscriptions>()?;
11200    m.add_class::<SubscriptionPlan>()?;
11201    m.add_class::<Subscription>()?;
11202    m.add_class::<BillingCycle>()?;
11203    m.add_class::<SubscriptionEvent>()?;
11204
11205    // Promotions
11206    m.add_class::<PromotionsApi>()?;
11207    m.add_class::<Promotion>()?;
11208    m.add_class::<Coupon>()?;
11209    m.add_class::<ApplyPromotionsResult>()?;
11210    m.add_class::<AppliedPromotion>()?;
11211    m.add_class::<PromotionUsage>()?;
11212
11213    // Tax
11214    m.add_class::<TaxApi>()?;
11215    m.add_class::<TaxJurisdiction>()?;
11216    m.add_class::<TaxRate>()?;
11217    m.add_class::<TaxExemption>()?;
11218    m.add_class::<TaxSettings>()?;
11219    m.add_class::<TaxCalculationResult>()?;
11220    m.add_class::<UsStateTaxInfo>()?;
11221    m.add_class::<EuVatInfo>()?;
11222    m.add_class::<CanadianTaxInfo>()?;
11223
11224    // Quality Control
11225    m.add_class::<QualityApi>()?;
11226    m.add_class::<Inspection>()?;
11227    m.add_class::<NonConformance>()?;
11228    m.add_class::<QualityHold>()?;
11229
11230    // Lots/Batch Tracking
11231    m.add_class::<LotsApi>()?;
11232    m.add_class::<Lot>()?;
11233
11234    // Serial Numbers
11235    m.add_class::<SerialsApi>()?;
11236    m.add_class::<SerialNumber>()?;
11237
11238    // Warehouse
11239    m.add_class::<WarehouseApi>()?;
11240    m.add_class::<Warehouse>()?;
11241    m.add_class::<WarehouseLocation>()?;
11242
11243    // Receiving
11244    m.add_class::<ReceivingApi>()?;
11245    m.add_class::<Receipt>()?;
11246    m.add_class::<ReceiptLine>()?;
11247
11248    // Fulfillment
11249    m.add_class::<FulfillmentApi>()?;
11250    m.add_class::<Wave>()?;
11251    m.add_class::<PickTask>()?;
11252
11253    // Accounts Payable
11254    m.add_class::<AccountsPayableApi>()?;
11255    m.add_class::<Bill>()?;
11256    m.add_class::<ApAgingSummary>()?;
11257
11258    // Accounts Receivable
11259    m.add_class::<AccountsReceivableApi>()?;
11260    m.add_class::<ArAgingSummary>()?;
11261    m.add_class::<CreditMemo>()?;
11262
11263    // Cost Accounting
11264    m.add_class::<CostAccountingApi>()?;
11265    m.add_class::<ItemCost>()?;
11266    m.add_class::<InventoryValuation>()?;
11267
11268    // Credit Management
11269    m.add_class::<CreditApi>()?;
11270    m.add_class::<CreditAccount>()?;
11271    m.add_class::<CreditCheckResult>()?;
11272
11273    // Backorder Management
11274    m.add_class::<BackorderApi>()?;
11275    m.add_class::<Backorder>()?;
11276    m.add_class::<BackorderSummary>()?;
11277
11278    // General Ledger
11279    m.add_class::<GeneralLedgerApi>()?;
11280    m.add_class::<GlAccount>()?;
11281    m.add_class::<JournalEntry>()?;
11282    m.add_class::<TrialBalance>()?;
11283
11284    // Vector Search
11285    m.add_class::<VectorSearch>()?;
11286    m.add_class::<ProductSearchResult>()?;
11287    m.add_class::<CustomerSearchResult>()?;
11288    m.add_class::<EmbeddingStats>()?;
11289
11290    Ok(())
11291}