1use pyo3::exceptions::{PyRuntimeError, PyValueError};
17use pyo3::prelude::*;
18use rust_decimal::Decimal;
19use serde_json;
20use ::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#[pyclass]
48pub struct Commerce {
49 inner: Arc<Mutex<RustCommerce>>,
50}
51
52#[pymethods]
53impl Commerce {
54 #[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 #[getter]
69 fn customers(&self) -> Customers {
70 Customers { commerce: self.inner.clone() }
71 }
72
73 #[getter]
75 fn orders(&self) -> Orders {
76 Orders { commerce: self.inner.clone() }
77 }
78
79 #[getter]
81 fn products(&self) -> Products {
82 Products { commerce: self.inner.clone() }
83 }
84
85 #[getter]
87 fn custom_objects(&self) -> CustomObjectsApi {
88 CustomObjectsApi { commerce: self.inner.clone() }
89 }
90
91 #[getter]
93 fn custom_states(&self) -> CustomObjectsApi {
94 self.custom_objects()
95 }
96
97 #[getter]
99 fn inventory(&self) -> Inventory {
100 Inventory { commerce: self.inner.clone() }
101 }
102
103 #[getter]
105 fn returns(&self) -> Returns {
106 Returns { commerce: self.inner.clone() }
107 }
108
109 #[getter]
111 fn payments(&self) -> Payments {
112 Payments { commerce: self.inner.clone() }
113 }
114
115 #[getter]
117 fn shipments(&self) -> Shipments {
118 Shipments { commerce: self.inner.clone() }
119 }
120
121 #[getter]
123 fn warranties(&self) -> Warranties {
124 Warranties { commerce: self.inner.clone() }
125 }
126
127 #[getter]
129 fn purchase_orders(&self) -> PurchaseOrders {
130 PurchaseOrders { commerce: self.inner.clone() }
131 }
132
133 #[getter]
135 fn invoices(&self) -> Invoices {
136 Invoices { commerce: self.inner.clone() }
137 }
138
139 #[getter]
141 fn bom(&self) -> BomApi {
142 BomApi { commerce: self.inner.clone() }
143 }
144
145 #[getter]
147 fn work_orders(&self) -> WorkOrders {
148 WorkOrders { commerce: self.inner.clone() }
149 }
150
151 #[getter]
153 fn carts(&self) -> Carts {
154 Carts { commerce: self.inner.clone() }
155 }
156
157 #[getter]
159 fn analytics(&self) -> Analytics {
160 Analytics { commerce: self.inner.clone() }
161 }
162
163 #[getter]
165 fn currency(&self) -> CurrencyOperations {
166 CurrencyOperations { commerce: self.inner.clone() }
167 }
168
169 #[getter]
171 fn subscriptions(&self) -> Subscriptions {
172 Subscriptions { commerce: self.inner.clone() }
173 }
174
175 #[getter]
177 fn promotions(&self) -> PromotionsApi {
178 PromotionsApi { commerce: self.inner.clone() }
179 }
180
181 #[getter]
183 fn tax(&self) -> TaxApi {
184 TaxApi { commerce: self.inner.clone() }
185 }
186
187 #[getter]
189 fn quality(&self) -> QualityApi {
190 QualityApi { commerce: self.inner.clone() }
191 }
192
193 #[getter]
195 fn lots(&self) -> LotsApi {
196 LotsApi { commerce: self.inner.clone() }
197 }
198
199 #[getter]
201 fn serials(&self) -> SerialsApi {
202 SerialsApi { commerce: self.inner.clone() }
203 }
204
205 #[getter]
207 fn warehouse(&self) -> WarehouseApi {
208 WarehouseApi { commerce: self.inner.clone() }
209 }
210
211 #[getter]
213 fn receiving(&self) -> ReceivingApi {
214 ReceivingApi { commerce: self.inner.clone() }
215 }
216
217 #[getter]
219 fn fulfillment(&self) -> FulfillmentApi {
220 FulfillmentApi { commerce: self.inner.clone() }
221 }
222
223 #[getter]
225 fn accounts_payable(&self) -> AccountsPayableApi {
226 AccountsPayableApi { commerce: self.inner.clone() }
227 }
228
229 #[getter]
231 fn accounts_receivable(&self) -> AccountsReceivableApi {
232 AccountsReceivableApi { commerce: self.inner.clone() }
233 }
234
235 #[getter]
237 fn cost_accounting(&self) -> CostAccountingApi {
238 CostAccountingApi { commerce: self.inner.clone() }
239 }
240
241 #[getter]
243 fn credit(&self) -> CreditApi {
244 CreditApi { commerce: self.inner.clone() }
245 }
246
247 #[getter]
249 fn backorder(&self) -> BackorderApi {
250 BackorderApi { commerce: self.inner.clone() }
251 }
252
253 #[getter]
255 fn general_ledger(&self) -> GeneralLedgerApi {
256 GeneralLedgerApi { commerce: self.inner.clone() }
257 }
258
259 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#[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 #[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#[pyclass]
337pub struct Customers {
338 commerce: Arc<Mutex<RustCommerce>>,
339}
340
341#[pymethods]
342impl Customers {
343 #[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 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 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 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 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#[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#[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 #[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#[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#[pyclass]
612pub struct Orders {
613 commerce: Arc<Mutex<RustCommerce>>,
614}
615
616#[pymethods]
617impl Orders {
618 #[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 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 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 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 #[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 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 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#[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#[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#[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#[pyclass]
934pub struct Products {
935 commerce: Arc<Mutex<RustCommerce>>,
936}
937
938#[pymethods]
939impl Products {
940 #[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 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 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 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 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#[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#[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#[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#[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 #[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#[pyclass]
1219pub struct CustomObjectsApi {
1220 commerce: Arc<Mutex<RustCommerce>>,
1221}
1222
1223#[pymethods]
1224impl CustomObjectsApi {
1225 #[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 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 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 #[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 #[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 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 #[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 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 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 #[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 #[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 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#[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#[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#[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#[pyclass]
1664pub struct Inventory {
1665 commerce: Arc<Mutex<RustCommerce>>,
1666}
1667
1668#[pymethods]
1669impl Inventory {
1670 #[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 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 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 #[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 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 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#[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#[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#[pyclass]
1900pub struct Returns {
1901 commerce: Arc<Mutex<RustCommerce>>,
1902}
1903
1904#[pymethods]
1905impl Returns {
1906 #[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 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 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 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 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 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#[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#[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#[pyclass]
2190pub struct Payments {
2191 commerce: Arc<Mutex<RustCommerce>>,
2192}
2193
2194#[pymethods]
2195impl Payments {
2196 #[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 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 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 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 #[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 #[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 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#[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#[pyclass]
2444pub struct Shipments {
2445 commerce: Arc<Mutex<RustCommerce>>,
2446}
2447
2448#[pymethods]
2449impl Shipments {
2450 #[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 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 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 #[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 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 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 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#[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#[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#[pyclass]
2705pub struct Warranties {
2706 commerce: Arc<Mutex<RustCommerce>>,
2707}
2708
2709#[pymethods]
2710impl Warranties {
2711 #[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 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 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 #[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 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 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 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 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#[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#[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#[pyclass]
2984pub struct PurchaseOrders {
2985 commerce: Arc<Mutex<RustCommerce>>,
2986}
2987
2988#[pymethods]
2989impl PurchaseOrders {
2990 #[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 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 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 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 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 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 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 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 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 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 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#[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#[pyclass]
3257pub struct Invoices {
3258 commerce: Arc<Mutex<RustCommerce>>,
3259}
3260
3261#[pymethods]
3262impl Invoices {
3263 #[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 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 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 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 #[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 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 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 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#[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#[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#[pyclass]
3515pub struct BomApi {
3516 commerce: Arc<Mutex<RustCommerce>>,
3517}
3518
3519#[pymethods]
3520impl BomApi {
3521 #[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 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 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 #[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 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 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 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#[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#[pyclass]
3734pub struct WorkOrders {
3735 commerce: Arc<Mutex<RustCommerce>>,
3736}
3737
3738#[pymethods]
3739impl WorkOrders {
3740 #[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 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 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 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 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 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 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#[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#[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#[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#[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#[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 _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 #[getter]
4213 fn items(&self) -> Vec<CartItem> {
4214 self._items.clone()
4215 }
4216
4217 #[getter]
4219 fn shipping_address(&self) -> Option<CartAddress> {
4220 self._shipping_address.clone()
4221 }
4222
4223 #[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#[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#[pyclass]
4333pub struct Carts {
4334 commerce: Arc<Mutex<RustCommerce>>,
4335}
4336
4337#[pymethods]
4338impl Carts {
4339 #[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 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 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 #[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 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 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 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 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 #[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 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 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 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 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 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 #[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 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
5192fn 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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[pyclass]
5701pub struct Analytics {
5702 commerce: Arc<Mutex<RustCommerce>>,
5703}
5704
5705#[pymethods]
5706impl Analytics {
5707 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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
5971fn 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#[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#[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#[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#[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#[pyclass]
6127pub struct CurrencyOperations {
6128 commerce: Arc<Mutex<RustCommerce>>,
6129}
6130
6131#[pymethods]
6132impl CurrencyOperations {
6133 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 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 #[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 #[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("e_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 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 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 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 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 #[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 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(¤cy_code)?)
6362 .map_err(|e| PyRuntimeError::new_err(format!("Failed to set base currency: {}", e)))?;
6363
6364 Ok(settings.into())
6365 }
6366
6367 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 ¤cy_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 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(¤cy_code)?)
6397 .map_err(|e| PyRuntimeError::new_err(format!("Failed to check currency: {}", e)))
6398 }
6399
6400 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 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 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(¤cy_code)?))
6440 }
6441}
6442
6443#[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, 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#[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#[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#[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#[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 #[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 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 #[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 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 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 #[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 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 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 #[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 #[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 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 #[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 #[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 #[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 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 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#[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#[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#[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#[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#[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
7324fn 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#[pyclass]
7394pub struct PromotionsApi {
7395 commerce: Arc<Mutex<RustCommerce>>,
7396}
7397
7398#[pymethods]
7399impl PromotionsApi {
7400 #[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 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 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 #[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 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 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 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 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 #[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 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 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 #[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 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 #[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 #[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 ¤cy,
7799 )
7800 .map_err(|e| PyRuntimeError::new_err(format!("Failed to record usage: {}", e)))?;
7801
7802 Ok(usage.into())
7803 }
7804}
7805
7806#[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#[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#[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#[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#[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#[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#[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#[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
8169fn 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#[pyclass]
8233pub struct TaxApi {
8234 commerce: Arc<Mutex<RustCommerce>>,
8235}
8236
8237#[pymethods]
8238impl TaxApi {
8239 #[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 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 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 #[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 #[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 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 #[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 #[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 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 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 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 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 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 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 #[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 #[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 #[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 #[staticmethod]
8624 fn is_eu_country(country_code: String) -> bool {
8625 stateset_core::is_eu_member(&country_code)
8626 }
8627}
8628
8629#[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#[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#[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#[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#[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#[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#[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 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#[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; 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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[pyclass]
10740pub struct VectorSearch {
10741 commerce: Arc<Mutex<RustCommerce>>,
10742 api_key: String,
10743}
10744
10745#[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#[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#[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 #[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 #[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 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 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 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 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 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 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 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#[pymodule]
11095fn stateset_embedded(m: &Bound<'_, PyModule>) -> PyResult<()> {
11096 m.add_class::<Commerce>()?;
11098
11099 m.add_class::<Customers>()?;
11101 m.add_class::<Customer>()?;
11102
11103 m.add_class::<Orders>()?;
11105 m.add_class::<Order>()?;
11106 m.add_class::<OrderItem>()?;
11107 m.add_class::<CreateOrderItemInput>()?;
11108
11109 m.add_class::<Products>()?;
11111 m.add_class::<Product>()?;
11112 m.add_class::<ProductVariant>()?;
11113 m.add_class::<CreateProductVariantInput>()?;
11114
11115 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 m.add_class::<Inventory>()?;
11124 m.add_class::<InventoryItem>()?;
11125 m.add_class::<StockLevel>()?;
11126 m.add_class::<Reservation>()?;
11127
11128 m.add_class::<Returns>()?;
11130 m.add_class::<Return>()?;
11131 m.add_class::<CreateReturnItemInput>()?;
11132
11133 m.add_class::<Payments>()?;
11135 m.add_class::<Payment>()?;
11136 m.add_class::<Refund>()?;
11137
11138 m.add_class::<Shipments>()?;
11140 m.add_class::<Shipment>()?;
11141
11142 m.add_class::<Warranties>()?;
11144 m.add_class::<Warranty>()?;
11145 m.add_class::<WarrantyClaim>()?;
11146
11147 m.add_class::<PurchaseOrders>()?;
11149 m.add_class::<Supplier>()?;
11150 m.add_class::<PurchaseOrder>()?;
11151
11152 m.add_class::<Invoices>()?;
11154 m.add_class::<Invoice>()?;
11155
11156 m.add_class::<BomApi>()?;
11158 m.add_class::<Bom>()?;
11159 m.add_class::<BomComponent>()?;
11160
11161 m.add_class::<WorkOrders>()?;
11163 m.add_class::<WorkOrder>()?;
11164
11165 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 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 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 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 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 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 m.add_class::<QualityApi>()?;
11226 m.add_class::<Inspection>()?;
11227 m.add_class::<NonConformance>()?;
11228 m.add_class::<QualityHold>()?;
11229
11230 m.add_class::<LotsApi>()?;
11232 m.add_class::<Lot>()?;
11233
11234 m.add_class::<SerialsApi>()?;
11236 m.add_class::<SerialNumber>()?;
11237
11238 m.add_class::<WarehouseApi>()?;
11240 m.add_class::<Warehouse>()?;
11241 m.add_class::<WarehouseLocation>()?;
11242
11243 m.add_class::<ReceivingApi>()?;
11245 m.add_class::<Receipt>()?;
11246 m.add_class::<ReceiptLine>()?;
11247
11248 m.add_class::<FulfillmentApi>()?;
11250 m.add_class::<Wave>()?;
11251 m.add_class::<PickTask>()?;
11252
11253 m.add_class::<AccountsPayableApi>()?;
11255 m.add_class::<Bill>()?;
11256 m.add_class::<ApAgingSummary>()?;
11257
11258 m.add_class::<AccountsReceivableApi>()?;
11260 m.add_class::<ArAgingSummary>()?;
11261 m.add_class::<CreditMemo>()?;
11262
11263 m.add_class::<CostAccountingApi>()?;
11265 m.add_class::<ItemCost>()?;
11266 m.add_class::<InventoryValuation>()?;
11267
11268 m.add_class::<CreditApi>()?;
11270 m.add_class::<CreditAccount>()?;
11271 m.add_class::<CreditCheckResult>()?;
11272
11273 m.add_class::<BackorderApi>()?;
11275 m.add_class::<Backorder>()?;
11276 m.add_class::<BackorderSummary>()?;
11277
11278 m.add_class::<GeneralLedgerApi>()?;
11280 m.add_class::<GlAccount>()?;
11281 m.add_class::<JournalEntry>()?;
11282 m.add_class::<TrialBalance>()?;
11283
11284 m.add_class::<VectorSearch>()?;
11286 m.add_class::<ProductSearchResult>()?;
11287 m.add_class::<CustomerSearchResult>()?;
11288 m.add_class::<EmbeddingStats>()?;
11289
11290 Ok(())
11291}