Skip to main content

stateset_embedded/
lib.rs

1//! PHP bindings for StateSet Embedded Commerce
2//!
3//! Provides a local-first commerce library with SQLite storage for PHP.
4//!
5//! ```php
6//! <?php
7//! use StateSet\Commerce;
8//!
9//! $commerce = new Commerce("./store.db");
10//! $customer = $commerce->customers()->create("alice@example.com", "Alice", "Smith");
11//! ```
12
13use ext_php_rs::prelude::*;
14use rust_decimal::Decimal;
15use rust_decimal::prelude::ToPrimitive;
16use stateset_embedded::Commerce as RustCommerce;
17use std::sync::{Arc, Mutex};
18
19// ============================================================================
20// Helper Macros
21// ============================================================================
22
23macro_rules! lock_commerce {
24    ($commerce:expr) => {
25        $commerce.lock().map_err(|e| PhpException::default(format!("Lock error: {}", e)))?
26    };
27}
28
29macro_rules! parse_uuid {
30    ($id:expr, $name:expr) => {
31        $id.parse().map_err(|_| PhpException::default(format!("Invalid {} UUID", $name)))?
32    };
33}
34
35fn to_f64_or_nan<T>(value: T) -> f64
36where
37    T: TryInto<f64>,
38    <T as TryInto<f64>>::Error: std::fmt::Display,
39{
40    match value.try_into() {
41        Ok(converted) => converted,
42        Err(err) => {
43            eprintln!("stateset-embedded: failed to convert to f64: {}", err);
44            f64::NAN
45        }
46    }
47}
48
49// ============================================================================
50// Commerce
51// ============================================================================
52
53/// Main Commerce instance for local commerce operations.
54#[php_class(name = "StateSet\\Commerce")]
55#[derive(Clone)]
56pub struct Commerce {
57    inner: Arc<Mutex<RustCommerce>>,
58}
59
60#[php_impl]
61impl Commerce {
62    pub fn __construct(db_path: String) -> PhpResult<Self> {
63        let commerce = RustCommerce::new(&db_path)
64            .map_err(|e| PhpException::default(format!("Failed to initialize commerce: {}", e)))?;
65
66        Ok(Self { inner: Arc::new(Mutex::new(commerce)) })
67    }
68
69    pub fn customers(&self) -> Customers {
70        Customers { commerce: self.inner.clone() }
71    }
72
73    pub fn orders(&self) -> Orders {
74        Orders { commerce: self.inner.clone() }
75    }
76
77    pub fn products(&self) -> Products {
78        Products { commerce: self.inner.clone() }
79    }
80
81    pub fn inventory(&self) -> Inventory {
82        Inventory { commerce: self.inner.clone() }
83    }
84
85    pub fn returns(&self) -> Returns {
86        Returns { commerce: self.inner.clone() }
87    }
88
89    pub fn payments(&self) -> Payments {
90        Payments { commerce: self.inner.clone() }
91    }
92
93    pub fn shipments(&self) -> Shipments {
94        Shipments { commerce: self.inner.clone() }
95    }
96
97    pub fn warranties(&self) -> Warranties {
98        Warranties { commerce: self.inner.clone() }
99    }
100
101    pub fn purchase_orders(&self) -> PurchaseOrders {
102        PurchaseOrders { commerce: self.inner.clone() }
103    }
104
105    pub fn invoices(&self) -> Invoices {
106        Invoices { commerce: self.inner.clone() }
107    }
108
109    pub fn bom(&self) -> BomApi {
110        BomApi { commerce: self.inner.clone() }
111    }
112
113    pub fn work_orders(&self) -> WorkOrders {
114        WorkOrders { commerce: self.inner.clone() }
115    }
116
117    pub fn carts(&self) -> Carts {
118        Carts { commerce: self.inner.clone() }
119    }
120
121    pub fn analytics(&self) -> Analytics {
122        Analytics { commerce: self.inner.clone() }
123    }
124
125    pub fn currency(&self) -> CurrencyOps {
126        CurrencyOps { commerce: self.inner.clone() }
127    }
128
129    pub fn subscriptions(&self) -> Subscriptions {
130        Subscriptions { commerce: self.inner.clone() }
131    }
132
133    pub fn promotions(&self) -> Promotions {
134        Promotions { commerce: self.inner.clone() }
135    }
136
137    pub fn tax(&self) -> Tax {
138        Tax { commerce: self.inner.clone() }
139    }
140
141    pub fn quality(&self) -> Quality {
142        Quality { commerce: self.inner.clone() }
143    }
144
145    pub fn lots(&self) -> Lots {
146        Lots { commerce: self.inner.clone() }
147    }
148
149    pub fn serials(&self) -> Serials {
150        Serials { commerce: self.inner.clone() }
151    }
152
153    pub fn warehouse(&self) -> WarehouseApi {
154        WarehouseApi { commerce: self.inner.clone() }
155    }
156
157    pub fn receiving(&self) -> Receiving {
158        Receiving { commerce: self.inner.clone() }
159    }
160
161    pub fn fulfillment(&self) -> Fulfillment {
162        Fulfillment { commerce: self.inner.clone() }
163    }
164
165    pub fn accounts_payable(&self) -> AccountsPayable {
166        AccountsPayable { commerce: self.inner.clone() }
167    }
168
169    pub fn accounts_receivable(&self) -> AccountsReceivable {
170        AccountsReceivable { commerce: self.inner.clone() }
171    }
172
173    pub fn cost_accounting(&self) -> CostAccounting {
174        CostAccounting { commerce: self.inner.clone() }
175    }
176
177    pub fn credit(&self) -> CreditApi {
178        CreditApi { commerce: self.inner.clone() }
179    }
180
181    pub fn backorders(&self) -> Backorders {
182        Backorders { commerce: self.inner.clone() }
183    }
184
185    pub fn general_ledger(&self) -> GeneralLedger {
186        GeneralLedger { commerce: self.inner.clone() }
187    }
188}
189
190// ============================================================================
191// Customer Types
192// ============================================================================
193
194#[php_class(name = "StateSet\\Customer")]
195#[derive(Clone)]
196pub struct Customer {
197    id: String,
198    email: String,
199    first_name: String,
200    last_name: String,
201    phone: Option<String>,
202    status: String,
203    accepts_marketing: bool,
204    created_at: String,
205    updated_at: String,
206}
207
208#[php_impl]
209impl Customer {
210    #[getter]
211    pub fn get_id(&self) -> String {
212        self.id.clone()
213    }
214
215    #[getter]
216    pub fn get_email(&self) -> String {
217        self.email.clone()
218    }
219
220    #[getter]
221    pub fn get_first_name(&self) -> String {
222        self.first_name.clone()
223    }
224
225    #[getter]
226    pub fn get_last_name(&self) -> String {
227        self.last_name.clone()
228    }
229
230    #[getter]
231    pub fn get_phone(&self) -> Option<String> {
232        self.phone.clone()
233    }
234
235    #[getter]
236    pub fn get_status(&self) -> String {
237        self.status.clone()
238    }
239
240    #[getter]
241    pub fn get_accepts_marketing(&self) -> bool {
242        self.accepts_marketing
243    }
244
245    #[getter]
246    pub fn get_created_at(&self) -> String {
247        self.created_at.clone()
248    }
249
250    #[getter]
251    pub fn get_updated_at(&self) -> String {
252        self.updated_at.clone()
253    }
254
255    pub fn get_full_name(&self) -> String {
256        format!("{} {}", self.first_name, self.last_name)
257    }
258
259    pub fn __to_string(&self) -> String {
260        format!(
261            "Customer(id={}, email={}, name={} {})",
262            self.id, self.email, self.first_name, self.last_name
263        )
264    }
265}
266
267impl From<stateset_core::Customer> for Customer {
268    fn from(c: stateset_core::Customer) -> Self {
269        Self {
270            id: c.id.to_string(),
271            email: c.email,
272            first_name: c.first_name,
273            last_name: c.last_name,
274            phone: c.phone,
275            status: format!("{}", c.status),
276            accepts_marketing: c.accepts_marketing,
277            created_at: c.created_at.to_rfc3339(),
278            updated_at: c.updated_at.to_rfc3339(),
279        }
280    }
281}
282
283// ============================================================================
284// Customers API
285// ============================================================================
286
287#[php_class(name = "StateSet\\Customers")]
288#[derive(Clone)]
289pub struct Customers {
290    commerce: Arc<Mutex<RustCommerce>>,
291}
292
293#[php_impl]
294impl Customers {
295    pub fn create(
296        &self,
297        email: String,
298        first_name: String,
299        last_name: String,
300        phone: Option<String>,
301        accepts_marketing: Option<bool>,
302    ) -> PhpResult<Customer> {
303        let commerce = lock_commerce!(self.commerce);
304
305        let customer = commerce
306            .customers()
307            .create(stateset_core::CreateCustomer {
308                email,
309                first_name,
310                last_name,
311                phone,
312                accepts_marketing,
313                ..Default::default()
314            })
315            .map_err(|e| PhpException::default(format!("Failed to create customer: {}", e)))?;
316
317        Ok(customer.into())
318    }
319
320    pub fn get(&self, id: String) -> PhpResult<Option<Customer>> {
321        let commerce = lock_commerce!(self.commerce);
322        let uuid = parse_uuid!(id, "customer");
323
324        let customer = commerce
325            .customers()
326            .get(uuid)
327            .map_err(|e| PhpException::default(format!("Failed to get customer: {}", e)))?;
328
329        Ok(customer.map(|c| c.into()))
330    }
331
332    pub fn get_by_email(&self, email: String) -> PhpResult<Option<Customer>> {
333        let commerce = lock_commerce!(self.commerce);
334
335        let customer = commerce
336            .customers()
337            .get_by_email(&email)
338            .map_err(|e| PhpException::default(format!("Failed to get customer: {}", e)))?;
339
340        Ok(customer.map(|c| c.into()))
341    }
342
343    pub fn list(&self) -> PhpResult<Vec<Customer>> {
344        let commerce = lock_commerce!(self.commerce);
345
346        let customers = commerce
347            .customers()
348            .list(Default::default())
349            .map_err(|e| PhpException::default(format!("Failed to list customers: {}", e)))?;
350
351        Ok(customers.into_iter().map(|c| c.into()).collect())
352    }
353
354    pub fn count(&self) -> PhpResult<i64> {
355        let commerce = lock_commerce!(self.commerce);
356
357        let count = commerce
358            .customers()
359            .count(Default::default())
360            .map_err(|e| PhpException::default(format!("Failed to count customers: {}", e)))?;
361
362        Ok(count)
363    }
364}
365
366// ============================================================================
367// Order Types
368// ============================================================================
369
370#[php_class(name = "StateSet\\OrderItem")]
371#[derive(Clone)]
372pub struct OrderItem {
373    id: String,
374    sku: String,
375    name: String,
376    quantity: i32,
377    unit_price: f64,
378    total: f64,
379}
380
381#[php_impl]
382impl OrderItem {
383    #[getter]
384    pub fn get_id(&self) -> String {
385        self.id.clone()
386    }
387
388    #[getter]
389    pub fn get_sku(&self) -> String {
390        self.sku.clone()
391    }
392
393    #[getter]
394    pub fn get_name(&self) -> String {
395        self.name.clone()
396    }
397
398    #[getter]
399    pub fn get_quantity(&self) -> i32 {
400        self.quantity
401    }
402
403    #[getter]
404    pub fn get_unit_price(&self) -> f64 {
405        self.unit_price
406    }
407
408    #[getter]
409    pub fn get_total(&self) -> f64 {
410        self.total
411    }
412
413    pub fn __to_string(&self) -> String {
414        format!("OrderItem(sku={}, qty={}, price={})", self.sku, self.quantity, self.unit_price)
415    }
416}
417
418#[php_class(name = "StateSet\\Order")]
419#[derive(Clone)]
420pub struct Order {
421    id: String,
422    order_number: String,
423    customer_id: String,
424    status: String,
425    total_amount: f64,
426    currency: String,
427    payment_status: String,
428    fulfillment_status: String,
429    tracking_number: Option<String>,
430    items: Vec<OrderItem>,
431    version: i32,
432    created_at: String,
433    updated_at: String,
434}
435
436#[php_impl]
437impl Order {
438    #[getter]
439    pub fn get_id(&self) -> String {
440        self.id.clone()
441    }
442
443    #[getter]
444    pub fn get_order_number(&self) -> String {
445        self.order_number.clone()
446    }
447
448    #[getter]
449    pub fn get_customer_id(&self) -> String {
450        self.customer_id.clone()
451    }
452
453    #[getter]
454    pub fn get_status(&self) -> String {
455        self.status.clone()
456    }
457
458    #[getter]
459    pub fn get_total_amount(&self) -> f64 {
460        self.total_amount
461    }
462
463    #[getter]
464    pub fn get_currency(&self) -> String {
465        self.currency.clone()
466    }
467
468    #[getter]
469    pub fn get_payment_status(&self) -> String {
470        self.payment_status.clone()
471    }
472
473    #[getter]
474    pub fn get_fulfillment_status(&self) -> String {
475        self.fulfillment_status.clone()
476    }
477
478    #[getter]
479    pub fn get_tracking_number(&self) -> Option<String> {
480        self.tracking_number.clone()
481    }
482
483    #[getter]
484    pub fn get_items(&self) -> Vec<OrderItem> {
485        self.items.clone()
486    }
487
488    #[getter]
489    pub fn get_version(&self) -> i32 {
490        self.version
491    }
492
493    #[getter]
494    pub fn get_created_at(&self) -> String {
495        self.created_at.clone()
496    }
497
498    #[getter]
499    pub fn get_updated_at(&self) -> String {
500        self.updated_at.clone()
501    }
502
503    pub fn get_item_count(&self) -> usize {
504        self.items.len()
505    }
506
507    pub fn __to_string(&self) -> String {
508        format!(
509            "Order(number={}, status={}, total={} {})",
510            self.order_number, self.status, self.total_amount, self.currency
511        )
512    }
513}
514
515impl From<stateset_core::Order> for Order {
516    fn from(o: stateset_core::Order) -> Self {
517        Self {
518            id: o.id.to_string(),
519            order_number: o.order_number,
520            customer_id: o.customer_id.to_string(),
521            status: format!("{}", o.status),
522            total_amount: to_f64_or_nan(o.total_amount),
523            currency: o.currency,
524            payment_status: format!("{}", o.payment_status),
525            fulfillment_status: format!("{}", o.fulfillment_status),
526            tracking_number: o.tracking_number,
527            items: o
528                .items
529                .into_iter()
530                .map(|i| OrderItem {
531                    id: i.id.to_string(),
532                    sku: i.sku,
533                    name: i.name,
534                    quantity: i.quantity,
535                    unit_price: to_f64_or_nan(i.unit_price),
536                    total: to_f64_or_nan(i.total),
537                })
538                .collect(),
539            version: o.version,
540            created_at: o.created_at.to_rfc3339(),
541            updated_at: o.updated_at.to_rfc3339(),
542        }
543    }
544}
545
546// ============================================================================
547// Orders API
548// ============================================================================
549
550#[php_class(name = "StateSet\\Orders")]
551#[derive(Clone)]
552pub struct Orders {
553    commerce: Arc<Mutex<RustCommerce>>,
554}
555
556#[php_impl]
557impl Orders {
558    pub fn create(
559        &self,
560        customer_id: String,
561        items: Vec<ext_php_rs::types::ZendHashTable>,
562        currency: Option<String>,
563        notes: Option<String>,
564    ) -> PhpResult<Order> {
565        let commerce = lock_commerce!(self.commerce);
566        let cust_uuid = parse_uuid!(customer_id, "customer");
567
568        let order_items: Vec<stateset_core::CreateOrderItem> = items
569            .into_iter()
570            .map(|h| {
571                let sku: String = h.get("sku").and_then(|v| v.string().ok()).unwrap_or_default();
572                let name: String = h.get("name").and_then(|v| v.string().ok()).unwrap_or_default();
573                let quantity: i32 =
574                    h.get("quantity").and_then(|v| v.long().ok()).map(|l| l as i32).unwrap_or(1);
575                let unit_price: f64 =
576                    h.get("unit_price").and_then(|v| v.double().ok()).unwrap_or(0.0);
577
578                stateset_core::CreateOrderItem {
579                    product_id: Default::default(),
580                    variant_id: None,
581                    sku,
582                    name,
583                    quantity,
584                    unit_price: Decimal::from_f64_retain(unit_price).unwrap_or_default(),
585                    ..Default::default()
586                }
587            })
588            .collect();
589
590        let order = commerce
591            .orders()
592            .create(stateset_core::CreateOrder {
593                customer_id: cust_uuid,
594                items: order_items,
595                currency,
596                notes,
597                ..Default::default()
598            })
599            .map_err(|e| PhpException::default(format!("Failed to create order: {}", e)))?;
600
601        Ok(order.into())
602    }
603
604    pub fn get(&self, id: String) -> PhpResult<Option<Order>> {
605        let commerce = lock_commerce!(self.commerce);
606        let uuid = parse_uuid!(id, "order");
607
608        let order = commerce
609            .orders()
610            .get(uuid)
611            .map_err(|e| PhpException::default(format!("Failed to get order: {}", e)))?;
612
613        Ok(order.map(|o| o.into()))
614    }
615
616    pub fn list(&self) -> PhpResult<Vec<Order>> {
617        let commerce = lock_commerce!(self.commerce);
618
619        let orders = commerce
620            .orders()
621            .list(Default::default())
622            .map_err(|e| PhpException::default(format!("Failed to list orders: {}", e)))?;
623
624        Ok(orders.into_iter().map(|o| o.into()).collect())
625    }
626
627    pub fn count(&self) -> PhpResult<i64> {
628        let commerce = lock_commerce!(self.commerce);
629
630        let count = commerce
631            .orders()
632            .count(Default::default())
633            .map_err(|e| PhpException::default(format!("Failed to count orders: {}", e)))?;
634
635        Ok(count)
636    }
637
638    pub fn ship(
639        &self,
640        id: String,
641        tracking_number: Option<String>,
642        carrier: Option<String>,
643    ) -> PhpResult<Order> {
644        let commerce = lock_commerce!(self.commerce);
645        let uuid = parse_uuid!(id, "order");
646
647        let order = commerce
648            .orders()
649            .ship(uuid, tracking_number, carrier)
650            .map_err(|e| PhpException::default(format!("Failed to ship order: {}", e)))?;
651
652        Ok(order.into())
653    }
654
655    pub fn cancel(&self, id: String, reason: Option<String>) -> PhpResult<Order> {
656        let commerce = lock_commerce!(self.commerce);
657        let uuid = parse_uuid!(id, "order");
658
659        let order = commerce
660            .orders()
661            .cancel(uuid, reason)
662            .map_err(|e| PhpException::default(format!("Failed to cancel order: {}", e)))?;
663
664        Ok(order.into())
665    }
666
667    pub fn confirm(&self, id: String) -> PhpResult<Order> {
668        let commerce = lock_commerce!(self.commerce);
669        let uuid = parse_uuid!(id, "order");
670
671        let order = commerce
672            .orders()
673            .confirm(uuid)
674            .map_err(|e| PhpException::default(format!("Failed to confirm order: {}", e)))?;
675
676        Ok(order.into())
677    }
678
679    pub fn deliver(&self, id: String) -> PhpResult<Order> {
680        let commerce = lock_commerce!(self.commerce);
681        let uuid = parse_uuid!(id, "order");
682
683        let order = commerce
684            .orders()
685            .deliver(uuid)
686            .map_err(|e| PhpException::default(format!("Failed to deliver order: {}", e)))?;
687
688        Ok(order.into())
689    }
690}
691
692// ============================================================================
693// Product Types
694// ============================================================================
695
696#[php_class(name = "StateSet\\ProductVariant")]
697#[derive(Clone)]
698pub struct ProductVariant {
699    id: String,
700    sku: String,
701    name: String,
702    price: f64,
703    compare_at_price: Option<f64>,
704    inventory_quantity: i32,
705    weight: Option<f64>,
706    barcode: Option<String>,
707}
708
709#[php_impl]
710impl ProductVariant {
711    #[getter]
712    pub fn get_id(&self) -> String {
713        self.id.clone()
714    }
715
716    #[getter]
717    pub fn get_sku(&self) -> String {
718        self.sku.clone()
719    }
720
721    #[getter]
722    pub fn get_name(&self) -> String {
723        self.name.clone()
724    }
725
726    #[getter]
727    pub fn get_price(&self) -> f64 {
728        self.price
729    }
730
731    #[getter]
732    pub fn get_compare_at_price(&self) -> Option<f64> {
733        self.compare_at_price
734    }
735
736    #[getter]
737    pub fn get_inventory_quantity(&self) -> i32 {
738        self.inventory_quantity
739    }
740
741    #[getter]
742    pub fn get_weight(&self) -> Option<f64> {
743        self.weight
744    }
745
746    #[getter]
747    pub fn get_barcode(&self) -> Option<String> {
748        self.barcode.clone()
749    }
750}
751
752#[php_class(name = "StateSet\\Product")]
753#[derive(Clone)]
754pub struct Product {
755    id: String,
756    name: String,
757    description: Option<String>,
758    vendor: Option<String>,
759    product_type: Option<String>,
760    status: String,
761    tags: Vec<String>,
762    variants: Vec<ProductVariant>,
763    created_at: String,
764    updated_at: String,
765}
766
767#[php_impl]
768impl Product {
769    #[getter]
770    pub fn get_id(&self) -> String {
771        self.id.clone()
772    }
773
774    #[getter]
775    pub fn get_name(&self) -> String {
776        self.name.clone()
777    }
778
779    #[getter]
780    pub fn get_description(&self) -> Option<String> {
781        self.description.clone()
782    }
783
784    #[getter]
785    pub fn get_vendor(&self) -> Option<String> {
786        self.vendor.clone()
787    }
788
789    #[getter]
790    pub fn get_product_type(&self) -> Option<String> {
791        self.product_type.clone()
792    }
793
794    #[getter]
795    pub fn get_status(&self) -> String {
796        self.status.clone()
797    }
798
799    #[getter]
800    pub fn get_tags(&self) -> Vec<String> {
801        self.tags.clone()
802    }
803
804    #[getter]
805    pub fn get_variants(&self) -> Vec<ProductVariant> {
806        self.variants.clone()
807    }
808
809    #[getter]
810    pub fn get_created_at(&self) -> String {
811        self.created_at.clone()
812    }
813
814    #[getter]
815    pub fn get_updated_at(&self) -> String {
816        self.updated_at.clone()
817    }
818
819    pub fn __to_string(&self) -> String {
820        format!("Product(id={}, name={}, status={})", self.id, self.name, self.status)
821    }
822}
823
824impl From<stateset_core::Product> for Product {
825    fn from(p: stateset_core::Product) -> Self {
826        Self {
827            id: p.id.to_string(),
828            name: p.name,
829            description: p.description,
830            vendor: p.vendor,
831            product_type: p.product_type,
832            status: format!("{}", p.status),
833            tags: p.tags,
834            variants: p
835                .variants
836                .into_iter()
837                .map(|v| ProductVariant {
838                    id: v.id.to_string(),
839                    sku: v.sku,
840                    name: v.name,
841                    price: to_f64_or_nan(v.price),
842                    compare_at_price: v.compare_at_price.and_then(|p| p.to_f64()),
843                    inventory_quantity: v.inventory_quantity,
844                    weight: v.weight.and_then(|w| w.to_f64()),
845                    barcode: v.barcode,
846                })
847                .collect(),
848            created_at: p.created_at.to_rfc3339(),
849            updated_at: p.updated_at.to_rfc3339(),
850        }
851    }
852}
853
854// ============================================================================
855// Products API
856// ============================================================================
857
858#[php_class(name = "StateSet\\Products")]
859#[derive(Clone)]
860pub struct Products {
861    commerce: Arc<Mutex<RustCommerce>>,
862}
863
864#[php_impl]
865impl Products {
866    pub fn create(
867        &self,
868        name: String,
869        description: Option<String>,
870        vendor: Option<String>,
871        product_type: Option<String>,
872    ) -> PhpResult<Product> {
873        let commerce = lock_commerce!(self.commerce);
874
875        let product = commerce
876            .products()
877            .create(stateset_core::CreateProduct {
878                name,
879                description,
880                vendor,
881                product_type,
882                ..Default::default()
883            })
884            .map_err(|e| PhpException::default(format!("Failed to create product: {}", e)))?;
885
886        Ok(product.into())
887    }
888
889    pub fn get(&self, id: String) -> PhpResult<Option<Product>> {
890        let commerce = lock_commerce!(self.commerce);
891        let uuid = parse_uuid!(id, "product");
892
893        let product = commerce
894            .products()
895            .get(uuid)
896            .map_err(|e| PhpException::default(format!("Failed to get product: {}", e)))?;
897
898        Ok(product.map(|p| p.into()))
899    }
900
901    pub fn list(&self) -> PhpResult<Vec<Product>> {
902        let commerce = lock_commerce!(self.commerce);
903
904        let products = commerce
905            .products()
906            .list(Default::default())
907            .map_err(|e| PhpException::default(format!("Failed to list products: {}", e)))?;
908
909        Ok(products.into_iter().map(|p| p.into()).collect())
910    }
911
912    pub fn count(&self) -> PhpResult<i64> {
913        let commerce = lock_commerce!(self.commerce);
914
915        let count = commerce
916            .products()
917            .count(Default::default())
918            .map_err(|e| PhpException::default(format!("Failed to count products: {}", e)))?;
919
920        Ok(count)
921    }
922
923    pub fn get_by_sku(&self, sku: String) -> PhpResult<Option<ProductVariant>> {
924        let commerce = lock_commerce!(self.commerce);
925
926        let variant = commerce
927            .products()
928            .get_variant_by_sku(&sku)
929            .map_err(|e| PhpException::default(format!("Failed to get variant: {}", e)))?;
930
931        Ok(variant.map(|v| ProductVariant {
932            id: v.id.to_string(),
933            sku: v.sku,
934            name: v.name,
935            price: to_f64_or_nan(v.price),
936            compare_at_price: v.compare_at_price.and_then(|p| p.to_f64()),
937            inventory_quantity: v.inventory_quantity,
938            weight: v.weight.and_then(|w| w.to_f64()),
939            barcode: v.barcode,
940        }))
941    }
942}
943
944// ============================================================================
945// Inventory Types & API
946// ============================================================================
947
948#[php_class(name = "StateSet\\InventoryItem")]
949#[derive(Clone)]
950pub struct InventoryItem {
951    id: String,
952    sku: String,
953    quantity_on_hand: i32,
954    quantity_reserved: i32,
955    quantity_available: i32,
956    reorder_point: Option<i32>,
957    reorder_quantity: Option<i32>,
958    location_id: Option<String>,
959}
960
961#[php_impl]
962impl InventoryItem {
963    #[getter]
964    pub fn get_id(&self) -> String {
965        self.id.clone()
966    }
967
968    #[getter]
969    pub fn get_sku(&self) -> String {
970        self.sku.clone()
971    }
972
973    #[getter]
974    pub fn get_quantity_on_hand(&self) -> i32 {
975        self.quantity_on_hand
976    }
977
978    #[getter]
979    pub fn get_quantity_reserved(&self) -> i32 {
980        self.quantity_reserved
981    }
982
983    #[getter]
984    pub fn get_quantity_available(&self) -> i32 {
985        self.quantity_available
986    }
987
988    #[getter]
989    pub fn get_reorder_point(&self) -> Option<i32> {
990        self.reorder_point
991    }
992
993    #[getter]
994    pub fn get_reorder_quantity(&self) -> Option<i32> {
995        self.reorder_quantity
996    }
997
998    #[getter]
999    pub fn get_location_id(&self) -> Option<String> {
1000        self.location_id.clone()
1001    }
1002
1003    pub fn __to_string(&self) -> String {
1004        format!("InventoryItem(sku={}, available={})", self.sku, self.quantity_available)
1005    }
1006}
1007
1008impl From<stateset_core::InventoryItem> for InventoryItem {
1009    fn from(i: stateset_core::InventoryItem) -> Self {
1010        Self {
1011            id: i.id.to_string(),
1012            sku: i.sku,
1013            quantity_on_hand: i.quantity_on_hand,
1014            quantity_reserved: i.quantity_reserved,
1015            quantity_available: i.quantity_available,
1016            reorder_point: i.reorder_point,
1017            reorder_quantity: i.reorder_quantity,
1018            location_id: i.location_id.map(|id| id.to_string()),
1019        }
1020    }
1021}
1022
1023#[php_class(name = "StateSet\\Inventory")]
1024#[derive(Clone)]
1025pub struct Inventory {
1026    commerce: Arc<Mutex<RustCommerce>>,
1027}
1028
1029#[php_impl]
1030impl Inventory {
1031    pub fn create(
1032        &self,
1033        sku: String,
1034        quantity: i32,
1035        reorder_point: Option<i32>,
1036        reorder_quantity: Option<i32>,
1037    ) -> PhpResult<InventoryItem> {
1038        let commerce = lock_commerce!(self.commerce);
1039
1040        let item = commerce
1041            .inventory()
1042            .create(stateset_core::CreateInventoryItem {
1043                sku,
1044                quantity_on_hand: quantity,
1045                reorder_point,
1046                reorder_quantity,
1047                ..Default::default()
1048            })
1049            .map_err(|e| PhpException::default(format!("Failed to create inventory: {}", e)))?;
1050
1051        Ok(item.into())
1052    }
1053
1054    pub fn get(&self, id: String) -> PhpResult<Option<InventoryItem>> {
1055        let commerce = lock_commerce!(self.commerce);
1056        let uuid = parse_uuid!(id, "inventory");
1057
1058        let item = commerce
1059            .inventory()
1060            .get(uuid)
1061            .map_err(|e| PhpException::default(format!("Failed to get inventory: {}", e)))?;
1062
1063        Ok(item.map(|i| i.into()))
1064    }
1065
1066    pub fn get_by_sku(&self, sku: String) -> PhpResult<Option<InventoryItem>> {
1067        let commerce = lock_commerce!(self.commerce);
1068
1069        let item = commerce
1070            .inventory()
1071            .get_by_sku(&sku)
1072            .map_err(|e| PhpException::default(format!("Failed to get inventory: {}", e)))?;
1073
1074        Ok(item.map(|i| i.into()))
1075    }
1076
1077    pub fn list(&self) -> PhpResult<Vec<InventoryItem>> {
1078        let commerce = lock_commerce!(self.commerce);
1079
1080        let items = commerce
1081            .inventory()
1082            .list(Default::default())
1083            .map_err(|e| PhpException::default(format!("Failed to list inventory: {}", e)))?;
1084
1085        Ok(items.into_iter().map(|i| i.into()).collect())
1086    }
1087
1088    pub fn adjust(
1089        &self,
1090        id: String,
1091        adjustment: i32,
1092        reason: Option<String>,
1093    ) -> PhpResult<InventoryItem> {
1094        let commerce = lock_commerce!(self.commerce);
1095        let uuid = parse_uuid!(id, "inventory");
1096
1097        let item = commerce
1098            .inventory()
1099            .adjust(uuid, adjustment, reason)
1100            .map_err(|e| PhpException::default(format!("Failed to adjust inventory: {}", e)))?;
1101
1102        Ok(item.into())
1103    }
1104
1105    pub fn reserve(
1106        &self,
1107        id: String,
1108        quantity: i32,
1109        order_id: Option<String>,
1110    ) -> PhpResult<InventoryItem> {
1111        let commerce = lock_commerce!(self.commerce);
1112        let uuid = parse_uuid!(id, "inventory");
1113        let order_uuid = order_id.and_then(|s| s.parse().ok());
1114
1115        let item = commerce
1116            .inventory()
1117            .reserve(uuid, quantity, order_uuid)
1118            .map_err(|e| PhpException::default(format!("Failed to reserve inventory: {}", e)))?;
1119
1120        Ok(item.into())
1121    }
1122
1123    pub fn release(&self, id: String, quantity: i32) -> PhpResult<InventoryItem> {
1124        let commerce = lock_commerce!(self.commerce);
1125        let uuid = parse_uuid!(id, "inventory");
1126
1127        let item = commerce
1128            .inventory()
1129            .release(uuid, quantity)
1130            .map_err(|e| PhpException::default(format!("Failed to release inventory: {}", e)))?;
1131
1132        Ok(item.into())
1133    }
1134}
1135
1136// ============================================================================
1137// Returns Types & API
1138// ============================================================================
1139
1140#[php_class(name = "StateSet\\ReturnRequest")]
1141#[derive(Clone)]
1142pub struct Return {
1143    id: String,
1144    order_id: String,
1145    customer_id: String,
1146    status: String,
1147    reason: String,
1148    refund_amount: f64,
1149    created_at: String,
1150    updated_at: String,
1151}
1152
1153#[php_impl]
1154impl Return {
1155    #[getter]
1156    pub fn get_id(&self) -> String {
1157        self.id.clone()
1158    }
1159
1160    #[getter]
1161    pub fn get_order_id(&self) -> String {
1162        self.order_id.clone()
1163    }
1164
1165    #[getter]
1166    pub fn get_customer_id(&self) -> String {
1167        self.customer_id.clone()
1168    }
1169
1170    #[getter]
1171    pub fn get_status(&self) -> String {
1172        self.status.clone()
1173    }
1174
1175    #[getter]
1176    pub fn get_reason(&self) -> String {
1177        self.reason.clone()
1178    }
1179
1180    #[getter]
1181    pub fn get_refund_amount(&self) -> f64 {
1182        self.refund_amount
1183    }
1184
1185    #[getter]
1186    pub fn get_created_at(&self) -> String {
1187        self.created_at.clone()
1188    }
1189
1190    #[getter]
1191    pub fn get_updated_at(&self) -> String {
1192        self.updated_at.clone()
1193    }
1194
1195    pub fn __to_string(&self) -> String {
1196        format!("Return(id={}, status={}, refund={})", self.id, self.status, self.refund_amount)
1197    }
1198}
1199
1200impl From<stateset_core::Return> for Return {
1201    fn from(r: stateset_core::Return) -> Self {
1202        Self {
1203            id: r.id.to_string(),
1204            order_id: r.order_id.to_string(),
1205            customer_id: r.customer_id.to_string(),
1206            status: format!("{}", r.status),
1207            reason: r.reason,
1208            refund_amount: to_f64_or_nan(r.refund_amount),
1209            created_at: r.created_at.to_rfc3339(),
1210            updated_at: r.updated_at.to_rfc3339(),
1211        }
1212    }
1213}
1214
1215#[php_class(name = "StateSet\\Returns")]
1216#[derive(Clone)]
1217pub struct Returns {
1218    commerce: Arc<Mutex<RustCommerce>>,
1219}
1220
1221#[php_impl]
1222impl Returns {
1223    pub fn create(&self, order_id: String, reason: String) -> PhpResult<Return> {
1224        let commerce = lock_commerce!(self.commerce);
1225        let uuid = parse_uuid!(order_id, "order");
1226
1227        let ret = commerce
1228            .returns()
1229            .create(stateset_core::CreateReturn { order_id: uuid, reason, ..Default::default() })
1230            .map_err(|e| PhpException::default(format!("Failed to create return: {}", e)))?;
1231
1232        Ok(ret.into())
1233    }
1234
1235    pub fn get(&self, id: String) -> PhpResult<Option<Return>> {
1236        let commerce = lock_commerce!(self.commerce);
1237        let uuid = parse_uuid!(id, "return");
1238
1239        let ret = commerce
1240            .returns()
1241            .get(uuid)
1242            .map_err(|e| PhpException::default(format!("Failed to get return: {}", e)))?;
1243
1244        Ok(ret.map(|r| r.into()))
1245    }
1246
1247    pub fn list(&self) -> PhpResult<Vec<Return>> {
1248        let commerce = lock_commerce!(self.commerce);
1249
1250        let returns = commerce
1251            .returns()
1252            .list(Default::default())
1253            .map_err(|e| PhpException::default(format!("Failed to list returns: {}", e)))?;
1254
1255        Ok(returns.into_iter().map(|r| r.into()).collect())
1256    }
1257
1258    pub fn approve(&self, id: String, refund_amount: Option<f64>) -> PhpResult<Return> {
1259        let commerce = lock_commerce!(self.commerce);
1260        let uuid = parse_uuid!(id, "return");
1261        let amount = refund_amount.map(|a| Decimal::from_f64_retain(a).unwrap_or_default());
1262
1263        let ret = commerce
1264            .returns()
1265            .approve(uuid, amount)
1266            .map_err(|e| PhpException::default(format!("Failed to approve return: {}", e)))?;
1267
1268        Ok(ret.into())
1269    }
1270
1271    pub fn reject(&self, id: String, reason: Option<String>) -> PhpResult<Return> {
1272        let commerce = lock_commerce!(self.commerce);
1273        let uuid = parse_uuid!(id, "return");
1274
1275        let ret = commerce
1276            .returns()
1277            .reject(uuid, reason)
1278            .map_err(|e| PhpException::default(format!("Failed to reject return: {}", e)))?;
1279
1280        Ok(ret.into())
1281    }
1282}
1283
1284// ============================================================================
1285// Payments API
1286// ============================================================================
1287
1288#[php_class(name = "StateSet\\Payments")]
1289#[derive(Clone)]
1290pub struct Payments {
1291    commerce: Arc<Mutex<RustCommerce>>,
1292}
1293
1294#[php_impl]
1295impl Payments {
1296    pub fn record(&self, order_id: String, amount: f64, method: Option<String>) -> PhpResult<bool> {
1297        let commerce = lock_commerce!(self.commerce);
1298        let uuid = parse_uuid!(order_id, "order");
1299        let decimal_amount = Decimal::from_f64_retain(amount).unwrap_or_default();
1300
1301        commerce
1302            .payments()
1303            .record(uuid, decimal_amount, method)
1304            .map_err(|e| PhpException::default(format!("Failed to record payment: {}", e)))?;
1305
1306        Ok(true)
1307    }
1308}
1309
1310// ============================================================================
1311// Carts Types & API
1312// ============================================================================
1313
1314#[php_class(name = "StateSet\\CartItem")]
1315#[derive(Clone)]
1316pub struct CartItem {
1317    id: String,
1318    sku: String,
1319    name: String,
1320    quantity: i32,
1321    unit_price: f64,
1322    total: f64,
1323}
1324
1325#[php_impl]
1326impl CartItem {
1327    #[getter]
1328    pub fn get_id(&self) -> String {
1329        self.id.clone()
1330    }
1331
1332    #[getter]
1333    pub fn get_sku(&self) -> String {
1334        self.sku.clone()
1335    }
1336
1337    #[getter]
1338    pub fn get_name(&self) -> String {
1339        self.name.clone()
1340    }
1341
1342    #[getter]
1343    pub fn get_quantity(&self) -> i32 {
1344        self.quantity
1345    }
1346
1347    #[getter]
1348    pub fn get_unit_price(&self) -> f64 {
1349        self.unit_price
1350    }
1351
1352    #[getter]
1353    pub fn get_total(&self) -> f64 {
1354        self.total
1355    }
1356}
1357
1358#[php_class(name = "StateSet\\Cart")]
1359#[derive(Clone)]
1360pub struct Cart {
1361    id: String,
1362    customer_id: Option<String>,
1363    status: String,
1364    items: Vec<CartItem>,
1365    subtotal: f64,
1366    total: f64,
1367    currency: String,
1368    created_at: String,
1369    updated_at: String,
1370}
1371
1372#[php_impl]
1373impl Cart {
1374    #[getter]
1375    pub fn get_id(&self) -> String {
1376        self.id.clone()
1377    }
1378
1379    #[getter]
1380    pub fn get_customer_id(&self) -> Option<String> {
1381        self.customer_id.clone()
1382    }
1383
1384    #[getter]
1385    pub fn get_status(&self) -> String {
1386        self.status.clone()
1387    }
1388
1389    #[getter]
1390    pub fn get_items(&self) -> Vec<CartItem> {
1391        self.items.clone()
1392    }
1393
1394    #[getter]
1395    pub fn get_subtotal(&self) -> f64 {
1396        self.subtotal
1397    }
1398
1399    #[getter]
1400    pub fn get_total(&self) -> f64 {
1401        self.total
1402    }
1403
1404    #[getter]
1405    pub fn get_currency(&self) -> String {
1406        self.currency.clone()
1407    }
1408
1409    #[getter]
1410    pub fn get_created_at(&self) -> String {
1411        self.created_at.clone()
1412    }
1413
1414    #[getter]
1415    pub fn get_updated_at(&self) -> String {
1416        self.updated_at.clone()
1417    }
1418
1419    pub fn __to_string(&self) -> String {
1420        format!(
1421            "Cart(id={}, items={}, total={} {})",
1422            self.id,
1423            self.items.len(),
1424            self.total,
1425            self.currency
1426        )
1427    }
1428}
1429
1430impl From<stateset_core::Cart> for Cart {
1431    fn from(c: stateset_core::Cart) -> Self {
1432        Self {
1433            id: c.id.to_string(),
1434            customer_id: c.customer_id.map(|id| id.to_string()),
1435            status: format!("{}", c.status),
1436            items: c
1437                .items
1438                .into_iter()
1439                .map(|i| CartItem {
1440                    id: i.id.to_string(),
1441                    sku: i.sku,
1442                    name: i.name,
1443                    quantity: i.quantity,
1444                    unit_price: to_f64_or_nan(i.unit_price),
1445                    total: to_f64_or_nan(i.total),
1446                })
1447                .collect(),
1448            subtotal: to_f64_or_nan(c.subtotal),
1449            total: to_f64_or_nan(c.total),
1450            currency: c.currency,
1451            created_at: c.created_at.to_rfc3339(),
1452            updated_at: c.updated_at.to_rfc3339(),
1453        }
1454    }
1455}
1456
1457#[php_class(name = "StateSet\\Carts")]
1458#[derive(Clone)]
1459pub struct Carts {
1460    commerce: Arc<Mutex<RustCommerce>>,
1461}
1462
1463#[php_impl]
1464impl Carts {
1465    pub fn create(&self, customer_id: Option<String>, currency: Option<String>) -> PhpResult<Cart> {
1466        let commerce = lock_commerce!(self.commerce);
1467        let cust_uuid = customer_id.and_then(|s| s.parse().ok());
1468
1469        let cart = commerce
1470            .carts()
1471            .create(stateset_core::CreateCart {
1472                customer_id: cust_uuid,
1473                currency,
1474                ..Default::default()
1475            })
1476            .map_err(|e| PhpException::default(format!("Failed to create cart: {}", e)))?;
1477
1478        Ok(cart.into())
1479    }
1480
1481    pub fn get(&self, id: String) -> PhpResult<Option<Cart>> {
1482        let commerce = lock_commerce!(self.commerce);
1483        let uuid = parse_uuid!(id, "cart");
1484
1485        let cart = commerce
1486            .carts()
1487            .get(uuid)
1488            .map_err(|e| PhpException::default(format!("Failed to get cart: {}", e)))?;
1489
1490        Ok(cart.map(|c| c.into()))
1491    }
1492
1493    pub fn list(&self) -> PhpResult<Vec<Cart>> {
1494        let commerce = lock_commerce!(self.commerce);
1495
1496        let carts = commerce
1497            .carts()
1498            .list(Default::default())
1499            .map_err(|e| PhpException::default(format!("Failed to list carts: {}", e)))?;
1500
1501        Ok(carts.into_iter().map(|c| c.into()).collect())
1502    }
1503
1504    pub fn add_item(
1505        &self,
1506        cart_id: String,
1507        sku: String,
1508        name: String,
1509        quantity: i32,
1510        unit_price: f64,
1511    ) -> PhpResult<Cart> {
1512        let commerce = lock_commerce!(self.commerce);
1513        let uuid = parse_uuid!(cart_id, "cart");
1514        let price = Decimal::from_f64_retain(unit_price).unwrap_or_default();
1515
1516        let cart = commerce
1517            .carts()
1518            .add_item(
1519                uuid,
1520                stateset_core::AddCartItem {
1521                    sku,
1522                    name,
1523                    quantity,
1524                    unit_price: price,
1525                    ..Default::default()
1526                },
1527            )
1528            .map_err(|e| PhpException::default(format!("Failed to add item: {}", e)))?;
1529
1530        Ok(cart.into())
1531    }
1532
1533    pub fn checkout(&self, cart_id: String) -> PhpResult<Order> {
1534        let commerce = lock_commerce!(self.commerce);
1535        let uuid = parse_uuid!(cart_id, "cart");
1536
1537        let order = commerce
1538            .carts()
1539            .checkout(uuid)
1540            .map_err(|e| PhpException::default(format!("Failed to checkout: {}", e)))?;
1541
1542        Ok(order.into())
1543    }
1544}
1545
1546// ============================================================================
1547// Analytics API
1548// ============================================================================
1549
1550#[php_class(name = "StateSet\\SalesSummary")]
1551#[derive(Clone)]
1552pub struct SalesSummary {
1553    total_revenue: f64,
1554    total_orders: i64,
1555    average_order_value: f64,
1556    total_items_sold: i64,
1557}
1558
1559#[php_impl]
1560impl SalesSummary {
1561    #[getter]
1562    pub fn get_total_revenue(&self) -> f64 {
1563        self.total_revenue
1564    }
1565
1566    #[getter]
1567    pub fn get_total_orders(&self) -> i64 {
1568        self.total_orders
1569    }
1570
1571    #[getter]
1572    pub fn get_average_order_value(&self) -> f64 {
1573        self.average_order_value
1574    }
1575
1576    #[getter]
1577    pub fn get_total_items_sold(&self) -> i64 {
1578        self.total_items_sold
1579    }
1580}
1581
1582#[php_class(name = "StateSet\\Analytics")]
1583#[derive(Clone)]
1584pub struct Analytics {
1585    commerce: Arc<Mutex<RustCommerce>>,
1586}
1587
1588#[php_impl]
1589impl Analytics {
1590    pub fn sales_summary(&self, days: Option<i64>) -> PhpResult<SalesSummary> {
1591        let commerce = lock_commerce!(self.commerce);
1592
1593        let summary = commerce
1594            .analytics()
1595            .sales_summary(days.unwrap_or(30))
1596            .map_err(|e| PhpException::default(format!("Failed to get sales summary: {}", e)))?;
1597
1598        Ok(SalesSummary {
1599            total_revenue: to_f64_or_nan(summary.total_revenue),
1600            total_orders: summary.total_orders,
1601            average_order_value: to_f64_or_nan(summary.average_order_value),
1602            total_items_sold: summary.total_items_sold,
1603        })
1604    }
1605}
1606
1607// ============================================================================
1608// Shipments Types & API
1609// ============================================================================
1610
1611#[php_class(name = "StateSet\\Shipment")]
1612#[derive(Clone)]
1613pub struct Shipment {
1614    id: String,
1615    order_id: String,
1616    tracking_number: Option<String>,
1617    carrier: Option<String>,
1618    status: String,
1619    shipped_at: Option<String>,
1620    delivered_at: Option<String>,
1621    created_at: String,
1622    updated_at: String,
1623}
1624
1625#[php_impl]
1626impl Shipment {
1627    #[getter]
1628    pub fn get_id(&self) -> String {
1629        self.id.clone()
1630    }
1631
1632    #[getter]
1633    pub fn get_order_id(&self) -> String {
1634        self.order_id.clone()
1635    }
1636
1637    #[getter]
1638    pub fn get_tracking_number(&self) -> Option<String> {
1639        self.tracking_number.clone()
1640    }
1641
1642    #[getter]
1643    pub fn get_carrier(&self) -> Option<String> {
1644        self.carrier.clone()
1645    }
1646
1647    #[getter]
1648    pub fn get_status(&self) -> String {
1649        self.status.clone()
1650    }
1651
1652    #[getter]
1653    pub fn get_shipped_at(&self) -> Option<String> {
1654        self.shipped_at.clone()
1655    }
1656
1657    #[getter]
1658    pub fn get_delivered_at(&self) -> Option<String> {
1659        self.delivered_at.clone()
1660    }
1661
1662    #[getter]
1663    pub fn get_created_at(&self) -> String {
1664        self.created_at.clone()
1665    }
1666
1667    #[getter]
1668    pub fn get_updated_at(&self) -> String {
1669        self.updated_at.clone()
1670    }
1671
1672    pub fn __to_string(&self) -> String {
1673        format!(
1674            "Shipment(id={}, status={}, tracking={:?})",
1675            self.id, self.status, self.tracking_number
1676        )
1677    }
1678}
1679
1680impl From<stateset_core::Shipment> for Shipment {
1681    fn from(s: stateset_core::Shipment) -> Self {
1682        Self {
1683            id: s.id.to_string(),
1684            order_id: s.order_id.to_string(),
1685            tracking_number: s.tracking_number,
1686            carrier: s.carrier,
1687            status: format!("{}", s.status),
1688            shipped_at: s.shipped_at.map(|t| t.to_rfc3339()),
1689            delivered_at: s.delivered_at.map(|t| t.to_rfc3339()),
1690            created_at: s.created_at.to_rfc3339(),
1691            updated_at: s.updated_at.to_rfc3339(),
1692        }
1693    }
1694}
1695
1696#[php_class(name = "StateSet\\Shipments")]
1697#[derive(Clone)]
1698pub struct Shipments {
1699    commerce: Arc<Mutex<RustCommerce>>,
1700}
1701
1702#[php_impl]
1703impl Shipments {
1704    pub fn create(
1705        &self,
1706        order_id: String,
1707        tracking_number: Option<String>,
1708        carrier: Option<String>,
1709    ) -> PhpResult<Shipment> {
1710        let commerce = lock_commerce!(self.commerce);
1711        let uuid = parse_uuid!(order_id, "order");
1712
1713        let shipment = commerce
1714            .shipments()
1715            .create(stateset_core::CreateShipment {
1716                order_id: uuid,
1717                tracking_number,
1718                carrier,
1719                ..Default::default()
1720            })
1721            .map_err(|e| PhpException::default(format!("Failed to create shipment: {}", e)))?;
1722
1723        Ok(shipment.into())
1724    }
1725
1726    pub fn get(&self, id: String) -> PhpResult<Option<Shipment>> {
1727        let commerce = lock_commerce!(self.commerce);
1728        let uuid = parse_uuid!(id, "shipment");
1729
1730        let shipment = commerce
1731            .shipments()
1732            .get(uuid)
1733            .map_err(|e| PhpException::default(format!("Failed to get shipment: {}", e)))?;
1734
1735        Ok(shipment.map(|s| s.into()))
1736    }
1737
1738    pub fn get_by_tracking(&self, tracking_number: String) -> PhpResult<Option<Shipment>> {
1739        let commerce = lock_commerce!(self.commerce);
1740
1741        let shipment = commerce
1742            .shipments()
1743            .get_by_tracking(&tracking_number)
1744            .map_err(|e| PhpException::default(format!("Failed to get shipment: {}", e)))?;
1745
1746        Ok(shipment.map(|s| s.into()))
1747    }
1748
1749    pub fn list(&self) -> PhpResult<Vec<Shipment>> {
1750        let commerce = lock_commerce!(self.commerce);
1751
1752        let shipments = commerce
1753            .shipments()
1754            .list(Default::default())
1755            .map_err(|e| PhpException::default(format!("Failed to list shipments: {}", e)))?;
1756
1757        Ok(shipments.into_iter().map(|s| s.into()).collect())
1758    }
1759
1760    pub fn for_order(&self, order_id: String) -> PhpResult<Vec<Shipment>> {
1761        let commerce = lock_commerce!(self.commerce);
1762        let uuid = parse_uuid!(order_id, "order");
1763
1764        let shipments = commerce
1765            .shipments()
1766            .for_order(uuid)
1767            .map_err(|e| PhpException::default(format!("Failed to get shipments: {}", e)))?;
1768
1769        Ok(shipments.into_iter().map(|s| s.into()).collect())
1770    }
1771
1772    pub fn ship(&self, id: String) -> PhpResult<Shipment> {
1773        let commerce = lock_commerce!(self.commerce);
1774        let uuid = parse_uuid!(id, "shipment");
1775
1776        let shipment = commerce
1777            .shipments()
1778            .ship(uuid)
1779            .map_err(|e| PhpException::default(format!("Failed to ship: {}", e)))?;
1780
1781        Ok(shipment.into())
1782    }
1783
1784    pub fn mark_delivered(&self, id: String) -> PhpResult<Shipment> {
1785        let commerce = lock_commerce!(self.commerce);
1786        let uuid = parse_uuid!(id, "shipment");
1787
1788        let shipment = commerce
1789            .shipments()
1790            .mark_delivered(uuid)
1791            .map_err(|e| PhpException::default(format!("Failed to mark delivered: {}", e)))?;
1792
1793        Ok(shipment.into())
1794    }
1795
1796    pub fn cancel(&self, id: String) -> PhpResult<Shipment> {
1797        let commerce = lock_commerce!(self.commerce);
1798        let uuid = parse_uuid!(id, "shipment");
1799
1800        let shipment = commerce
1801            .shipments()
1802            .cancel(uuid)
1803            .map_err(|e| PhpException::default(format!("Failed to cancel shipment: {}", e)))?;
1804
1805        Ok(shipment.into())
1806    }
1807
1808    pub fn count(&self) -> PhpResult<i64> {
1809        let commerce = lock_commerce!(self.commerce);
1810
1811        let count = commerce
1812            .shipments()
1813            .count(Default::default())
1814            .map_err(|e| PhpException::default(format!("Failed to count shipments: {}", e)))?;
1815
1816        Ok(count)
1817    }
1818}
1819
1820// ============================================================================
1821// Warranties Types & API
1822// ============================================================================
1823
1824#[php_class(name = "StateSet\\Warranty")]
1825#[derive(Clone)]
1826pub struct Warranty {
1827    id: String,
1828    product_id: String,
1829    order_id: Option<String>,
1830    customer_id: String,
1831    warranty_type: String,
1832    status: String,
1833    start_date: String,
1834    end_date: String,
1835    created_at: String,
1836}
1837
1838#[php_impl]
1839impl Warranty {
1840    #[getter]
1841    pub fn get_id(&self) -> String {
1842        self.id.clone()
1843    }
1844
1845    #[getter]
1846    pub fn get_product_id(&self) -> String {
1847        self.product_id.clone()
1848    }
1849
1850    #[getter]
1851    pub fn get_order_id(&self) -> Option<String> {
1852        self.order_id.clone()
1853    }
1854
1855    #[getter]
1856    pub fn get_customer_id(&self) -> String {
1857        self.customer_id.clone()
1858    }
1859
1860    #[getter]
1861    pub fn get_warranty_type(&self) -> String {
1862        self.warranty_type.clone()
1863    }
1864
1865    #[getter]
1866    pub fn get_status(&self) -> String {
1867        self.status.clone()
1868    }
1869
1870    #[getter]
1871    pub fn get_start_date(&self) -> String {
1872        self.start_date.clone()
1873    }
1874
1875    #[getter]
1876    pub fn get_end_date(&self) -> String {
1877        self.end_date.clone()
1878    }
1879
1880    #[getter]
1881    pub fn get_created_at(&self) -> String {
1882        self.created_at.clone()
1883    }
1884
1885    pub fn __to_string(&self) -> String {
1886        format!("Warranty(id={}, type={}, status={})", self.id, self.warranty_type, self.status)
1887    }
1888}
1889
1890impl From<stateset_core::Warranty> for Warranty {
1891    fn from(w: stateset_core::Warranty) -> Self {
1892        Self {
1893            id: w.id.to_string(),
1894            product_id: w.product_id.to_string(),
1895            order_id: w.order_id.map(|id| id.to_string()),
1896            customer_id: w.customer_id.to_string(),
1897            warranty_type: format!("{}", w.warranty_type),
1898            status: format!("{}", w.status),
1899            start_date: w.start_date.to_rfc3339(),
1900            end_date: w.end_date.to_rfc3339(),
1901            created_at: w.created_at.to_rfc3339(),
1902        }
1903    }
1904}
1905
1906#[php_class(name = "StateSet\\WarrantyClaim")]
1907#[derive(Clone)]
1908pub struct WarrantyClaim {
1909    id: String,
1910    warranty_id: String,
1911    description: String,
1912    status: String,
1913    resolution: Option<String>,
1914    created_at: String,
1915}
1916
1917#[php_impl]
1918impl WarrantyClaim {
1919    #[getter]
1920    pub fn get_id(&self) -> String {
1921        self.id.clone()
1922    }
1923
1924    #[getter]
1925    pub fn get_warranty_id(&self) -> String {
1926        self.warranty_id.clone()
1927    }
1928
1929    #[getter]
1930    pub fn get_description(&self) -> String {
1931        self.description.clone()
1932    }
1933
1934    #[getter]
1935    pub fn get_status(&self) -> String {
1936        self.status.clone()
1937    }
1938
1939    #[getter]
1940    pub fn get_resolution(&self) -> Option<String> {
1941        self.resolution.clone()
1942    }
1943
1944    #[getter]
1945    pub fn get_created_at(&self) -> String {
1946        self.created_at.clone()
1947    }
1948}
1949
1950impl From<stateset_core::WarrantyClaim> for WarrantyClaim {
1951    fn from(c: stateset_core::WarrantyClaim) -> Self {
1952        Self {
1953            id: c.id.to_string(),
1954            warranty_id: c.warranty_id.to_string(),
1955            description: c.description,
1956            status: format!("{}", c.status),
1957            resolution: c.resolution,
1958            created_at: c.created_at.to_rfc3339(),
1959        }
1960    }
1961}
1962
1963#[php_class(name = "StateSet\\Warranties")]
1964#[derive(Clone)]
1965pub struct Warranties {
1966    commerce: Arc<Mutex<RustCommerce>>,
1967}
1968
1969#[php_impl]
1970impl Warranties {
1971    pub fn create(
1972        &self,
1973        product_id: String,
1974        customer_id: String,
1975        warranty_type: String,
1976        duration_months: i32,
1977    ) -> PhpResult<Warranty> {
1978        let commerce = lock_commerce!(self.commerce);
1979        let prod_uuid = parse_uuid!(product_id, "product");
1980        let cust_uuid = parse_uuid!(customer_id, "customer");
1981
1982        let warranty = commerce
1983            .warranties()
1984            .create(stateset_core::CreateWarranty {
1985                product_id: prod_uuid,
1986                customer_id: cust_uuid,
1987                warranty_type,
1988                duration_months,
1989                ..Default::default()
1990            })
1991            .map_err(|e| PhpException::default(format!("Failed to create warranty: {}", e)))?;
1992
1993        Ok(warranty.into())
1994    }
1995
1996    pub fn get(&self, id: String) -> PhpResult<Option<Warranty>> {
1997        let commerce = lock_commerce!(self.commerce);
1998        let uuid = parse_uuid!(id, "warranty");
1999
2000        let warranty = commerce
2001            .warranties()
2002            .get(uuid)
2003            .map_err(|e| PhpException::default(format!("Failed to get warranty: {}", e)))?;
2004
2005        Ok(warranty.map(|w| w.into()))
2006    }
2007
2008    pub fn list(&self) -> PhpResult<Vec<Warranty>> {
2009        let commerce = lock_commerce!(self.commerce);
2010
2011        let warranties = commerce
2012            .warranties()
2013            .list(Default::default())
2014            .map_err(|e| PhpException::default(format!("Failed to list warranties: {}", e)))?;
2015
2016        Ok(warranties.into_iter().map(|w| w.into()).collect())
2017    }
2018
2019    pub fn for_customer(&self, customer_id: String) -> PhpResult<Vec<Warranty>> {
2020        let commerce = lock_commerce!(self.commerce);
2021        let uuid = parse_uuid!(customer_id, "customer");
2022
2023        let warranties = commerce
2024            .warranties()
2025            .for_customer(uuid)
2026            .map_err(|e| PhpException::default(format!("Failed to get warranties: {}", e)))?;
2027
2028        Ok(warranties.into_iter().map(|w| w.into()).collect())
2029    }
2030
2031    pub fn is_valid(&self, id: String) -> PhpResult<bool> {
2032        let commerce = lock_commerce!(self.commerce);
2033        let uuid = parse_uuid!(id, "warranty");
2034
2035        let valid = commerce
2036            .warranties()
2037            .is_valid(uuid)
2038            .map_err(|e| PhpException::default(format!("Failed to check warranty: {}", e)))?;
2039
2040        Ok(valid)
2041    }
2042
2043    pub fn create_claim(
2044        &self,
2045        warranty_id: String,
2046        description: String,
2047    ) -> PhpResult<WarrantyClaim> {
2048        let commerce = lock_commerce!(self.commerce);
2049        let uuid = parse_uuid!(warranty_id, "warranty");
2050
2051        let claim = commerce
2052            .warranties()
2053            .create_claim(stateset_core::CreateWarrantyClaim {
2054                warranty_id: uuid,
2055                description,
2056                ..Default::default()
2057            })
2058            .map_err(|e| PhpException::default(format!("Failed to create claim: {}", e)))?;
2059
2060        Ok(claim.into())
2061    }
2062
2063    pub fn approve_claim(
2064        &self,
2065        claim_id: String,
2066        resolution: Option<String>,
2067    ) -> PhpResult<WarrantyClaim> {
2068        let commerce = lock_commerce!(self.commerce);
2069        let uuid = parse_uuid!(claim_id, "claim");
2070
2071        let claim = commerce
2072            .warranties()
2073            .approve_claim(uuid, resolution)
2074            .map_err(|e| PhpException::default(format!("Failed to approve claim: {}", e)))?;
2075
2076        Ok(claim.into())
2077    }
2078
2079    pub fn deny_claim(&self, claim_id: String, reason: Option<String>) -> PhpResult<WarrantyClaim> {
2080        let commerce = lock_commerce!(self.commerce);
2081        let uuid = parse_uuid!(claim_id, "claim");
2082
2083        let claim = commerce
2084            .warranties()
2085            .deny_claim(uuid, reason)
2086            .map_err(|e| PhpException::default(format!("Failed to deny claim: {}", e)))?;
2087
2088        Ok(claim.into())
2089    }
2090
2091    pub fn count(&self) -> PhpResult<i64> {
2092        let commerce = lock_commerce!(self.commerce);
2093
2094        let count = commerce
2095            .warranties()
2096            .count(Default::default())
2097            .map_err(|e| PhpException::default(format!("Failed to count warranties: {}", e)))?;
2098
2099        Ok(count)
2100    }
2101}
2102
2103// ============================================================================
2104// PurchaseOrders Types & API
2105// ============================================================================
2106
2107#[php_class(name = "StateSet\\Supplier")]
2108#[derive(Clone)]
2109pub struct Supplier {
2110    id: String,
2111    name: String,
2112    email: Option<String>,
2113    phone: Option<String>,
2114    status: String,
2115    created_at: String,
2116}
2117
2118#[php_impl]
2119impl Supplier {
2120    #[getter]
2121    pub fn get_id(&self) -> String {
2122        self.id.clone()
2123    }
2124
2125    #[getter]
2126    pub fn get_name(&self) -> String {
2127        self.name.clone()
2128    }
2129
2130    #[getter]
2131    pub fn get_email(&self) -> Option<String> {
2132        self.email.clone()
2133    }
2134
2135    #[getter]
2136    pub fn get_phone(&self) -> Option<String> {
2137        self.phone.clone()
2138    }
2139
2140    #[getter]
2141    pub fn get_status(&self) -> String {
2142        self.status.clone()
2143    }
2144
2145    #[getter]
2146    pub fn get_created_at(&self) -> String {
2147        self.created_at.clone()
2148    }
2149}
2150
2151impl From<stateset_core::Supplier> for Supplier {
2152    fn from(s: stateset_core::Supplier) -> Self {
2153        Self {
2154            id: s.id.to_string(),
2155            name: s.name,
2156            email: s.email,
2157            phone: s.phone,
2158            status: format!("{}", s.status),
2159            created_at: s.created_at.to_rfc3339(),
2160        }
2161    }
2162}
2163
2164#[php_class(name = "StateSet\\PurchaseOrder")]
2165#[derive(Clone)]
2166pub struct PurchaseOrder {
2167    id: String,
2168    po_number: String,
2169    supplier_id: String,
2170    status: String,
2171    total_amount: f64,
2172    currency: String,
2173    expected_date: Option<String>,
2174    created_at: String,
2175    updated_at: String,
2176}
2177
2178#[php_impl]
2179impl PurchaseOrder {
2180    #[getter]
2181    pub fn get_id(&self) -> String {
2182        self.id.clone()
2183    }
2184
2185    #[getter]
2186    pub fn get_po_number(&self) -> String {
2187        self.po_number.clone()
2188    }
2189
2190    #[getter]
2191    pub fn get_supplier_id(&self) -> String {
2192        self.supplier_id.clone()
2193    }
2194
2195    #[getter]
2196    pub fn get_status(&self) -> String {
2197        self.status.clone()
2198    }
2199
2200    #[getter]
2201    pub fn get_total_amount(&self) -> f64 {
2202        self.total_amount
2203    }
2204
2205    #[getter]
2206    pub fn get_currency(&self) -> String {
2207        self.currency.clone()
2208    }
2209
2210    #[getter]
2211    pub fn get_expected_date(&self) -> Option<String> {
2212        self.expected_date.clone()
2213    }
2214
2215    #[getter]
2216    pub fn get_created_at(&self) -> String {
2217        self.created_at.clone()
2218    }
2219
2220    #[getter]
2221    pub fn get_updated_at(&self) -> String {
2222        self.updated_at.clone()
2223    }
2224
2225    pub fn __to_string(&self) -> String {
2226        format!(
2227            "PurchaseOrder(number={}, status={}, total={})",
2228            self.po_number, self.status, self.total_amount
2229        )
2230    }
2231}
2232
2233impl From<stateset_core::PurchaseOrder> for PurchaseOrder {
2234    fn from(p: stateset_core::PurchaseOrder) -> Self {
2235        Self {
2236            id: p.id.to_string(),
2237            po_number: p.po_number,
2238            supplier_id: p.supplier_id.to_string(),
2239            status: format!("{}", p.status),
2240            total_amount: to_f64_or_nan(p.total_amount),
2241            currency: p.currency,
2242            expected_date: p.expected_date.map(|d| d.to_rfc3339()),
2243            created_at: p.created_at.to_rfc3339(),
2244            updated_at: p.updated_at.to_rfc3339(),
2245        }
2246    }
2247}
2248
2249#[php_class(name = "StateSet\\PurchaseOrders")]
2250#[derive(Clone)]
2251pub struct PurchaseOrders {
2252    commerce: Arc<Mutex<RustCommerce>>,
2253}
2254
2255#[php_impl]
2256impl PurchaseOrders {
2257    pub fn create_supplier(
2258        &self,
2259        name: String,
2260        email: Option<String>,
2261        phone: Option<String>,
2262    ) -> PhpResult<Supplier> {
2263        let commerce = lock_commerce!(self.commerce);
2264
2265        let supplier = commerce
2266            .purchase_orders()
2267            .create_supplier(stateset_core::CreateSupplier {
2268                name,
2269                email,
2270                phone,
2271                ..Default::default()
2272            })
2273            .map_err(|e| PhpException::default(format!("Failed to create supplier: {}", e)))?;
2274
2275        Ok(supplier.into())
2276    }
2277
2278    pub fn get_supplier(&self, id: String) -> PhpResult<Option<Supplier>> {
2279        let commerce = lock_commerce!(self.commerce);
2280        let uuid = parse_uuid!(id, "supplier");
2281
2282        let supplier = commerce
2283            .purchase_orders()
2284            .get_supplier(uuid)
2285            .map_err(|e| PhpException::default(format!("Failed to get supplier: {}", e)))?;
2286
2287        Ok(supplier.map(|s| s.into()))
2288    }
2289
2290    pub fn list_suppliers(&self) -> PhpResult<Vec<Supplier>> {
2291        let commerce = lock_commerce!(self.commerce);
2292
2293        let suppliers = commerce
2294            .purchase_orders()
2295            .list_suppliers(Default::default())
2296            .map_err(|e| PhpException::default(format!("Failed to list suppliers: {}", e)))?;
2297
2298        Ok(suppliers.into_iter().map(|s| s.into()).collect())
2299    }
2300
2301    pub fn create(
2302        &self,
2303        supplier_id: String,
2304        currency: Option<String>,
2305    ) -> PhpResult<PurchaseOrder> {
2306        let commerce = lock_commerce!(self.commerce);
2307        let uuid = parse_uuid!(supplier_id, "supplier");
2308
2309        let po = commerce
2310            .purchase_orders()
2311            .create(stateset_core::CreatePurchaseOrder {
2312                supplier_id: uuid,
2313                currency,
2314                ..Default::default()
2315            })
2316            .map_err(|e| PhpException::default(format!("Failed to create PO: {}", e)))?;
2317
2318        Ok(po.into())
2319    }
2320
2321    pub fn get(&self, id: String) -> PhpResult<Option<PurchaseOrder>> {
2322        let commerce = lock_commerce!(self.commerce);
2323        let uuid = parse_uuid!(id, "purchase_order");
2324
2325        let po = commerce
2326            .purchase_orders()
2327            .get(uuid)
2328            .map_err(|e| PhpException::default(format!("Failed to get PO: {}", e)))?;
2329
2330        Ok(po.map(|p| p.into()))
2331    }
2332
2333    pub fn list(&self) -> PhpResult<Vec<PurchaseOrder>> {
2334        let commerce = lock_commerce!(self.commerce);
2335
2336        let pos = commerce
2337            .purchase_orders()
2338            .list(Default::default())
2339            .map_err(|e| PhpException::default(format!("Failed to list POs: {}", e)))?;
2340
2341        Ok(pos.into_iter().map(|p| p.into()).collect())
2342    }
2343
2344    pub fn submit(&self, id: String) -> PhpResult<PurchaseOrder> {
2345        let commerce = lock_commerce!(self.commerce);
2346        let uuid = parse_uuid!(id, "purchase_order");
2347
2348        let po = commerce
2349            .purchase_orders()
2350            .submit(uuid)
2351            .map_err(|e| PhpException::default(format!("Failed to submit PO: {}", e)))?;
2352
2353        Ok(po.into())
2354    }
2355
2356    pub fn approve(&self, id: String) -> PhpResult<PurchaseOrder> {
2357        let commerce = lock_commerce!(self.commerce);
2358        let uuid = parse_uuid!(id, "purchase_order");
2359
2360        let po = commerce
2361            .purchase_orders()
2362            .approve(uuid)
2363            .map_err(|e| PhpException::default(format!("Failed to approve PO: {}", e)))?;
2364
2365        Ok(po.into())
2366    }
2367
2368    pub fn cancel(&self, id: String) -> PhpResult<PurchaseOrder> {
2369        let commerce = lock_commerce!(self.commerce);
2370        let uuid = parse_uuid!(id, "purchase_order");
2371
2372        let po = commerce
2373            .purchase_orders()
2374            .cancel(uuid)
2375            .map_err(|e| PhpException::default(format!("Failed to cancel PO: {}", e)))?;
2376
2377        Ok(po.into())
2378    }
2379
2380    pub fn complete(&self, id: String) -> PhpResult<PurchaseOrder> {
2381        let commerce = lock_commerce!(self.commerce);
2382        let uuid = parse_uuid!(id, "purchase_order");
2383
2384        let po = commerce
2385            .purchase_orders()
2386            .complete(uuid)
2387            .map_err(|e| PhpException::default(format!("Failed to complete PO: {}", e)))?;
2388
2389        Ok(po.into())
2390    }
2391
2392    pub fn count(&self) -> PhpResult<i64> {
2393        let commerce = lock_commerce!(self.commerce);
2394
2395        let count = commerce
2396            .purchase_orders()
2397            .count(Default::default())
2398            .map_err(|e| PhpException::default(format!("Failed to count POs: {}", e)))?;
2399
2400        Ok(count)
2401    }
2402}
2403
2404// ============================================================================
2405// Invoices Types & API
2406// ============================================================================
2407
2408#[php_class(name = "StateSet\\Invoice")]
2409#[derive(Clone)]
2410pub struct Invoice {
2411    id: String,
2412    invoice_number: String,
2413    customer_id: String,
2414    order_id: Option<String>,
2415    status: String,
2416    subtotal: f64,
2417    tax: f64,
2418    total: f64,
2419    currency: String,
2420    due_date: Option<String>,
2421    paid_at: Option<String>,
2422    created_at: String,
2423    updated_at: String,
2424}
2425
2426#[php_impl]
2427impl Invoice {
2428    #[getter]
2429    pub fn get_id(&self) -> String {
2430        self.id.clone()
2431    }
2432
2433    #[getter]
2434    pub fn get_invoice_number(&self) -> String {
2435        self.invoice_number.clone()
2436    }
2437
2438    #[getter]
2439    pub fn get_customer_id(&self) -> String {
2440        self.customer_id.clone()
2441    }
2442
2443    #[getter]
2444    pub fn get_order_id(&self) -> Option<String> {
2445        self.order_id.clone()
2446    }
2447
2448    #[getter]
2449    pub fn get_status(&self) -> String {
2450        self.status.clone()
2451    }
2452
2453    #[getter]
2454    pub fn get_subtotal(&self) -> f64 {
2455        self.subtotal
2456    }
2457
2458    #[getter]
2459    pub fn get_tax(&self) -> f64 {
2460        self.tax
2461    }
2462
2463    #[getter]
2464    pub fn get_total(&self) -> f64 {
2465        self.total
2466    }
2467
2468    #[getter]
2469    pub fn get_currency(&self) -> String {
2470        self.currency.clone()
2471    }
2472
2473    #[getter]
2474    pub fn get_due_date(&self) -> Option<String> {
2475        self.due_date.clone()
2476    }
2477
2478    #[getter]
2479    pub fn get_paid_at(&self) -> Option<String> {
2480        self.paid_at.clone()
2481    }
2482
2483    #[getter]
2484    pub fn get_created_at(&self) -> String {
2485        self.created_at.clone()
2486    }
2487
2488    #[getter]
2489    pub fn get_updated_at(&self) -> String {
2490        self.updated_at.clone()
2491    }
2492
2493    pub fn __to_string(&self) -> String {
2494        format!(
2495            "Invoice(number={}, status={}, total={})",
2496            self.invoice_number, self.status, self.total
2497        )
2498    }
2499}
2500
2501impl From<stateset_core::Invoice> for Invoice {
2502    fn from(i: stateset_core::Invoice) -> Self {
2503        Self {
2504            id: i.id.to_string(),
2505            invoice_number: i.invoice_number,
2506            customer_id: i.customer_id.to_string(),
2507            order_id: i.order_id.map(|id| id.to_string()),
2508            status: format!("{}", i.status),
2509            subtotal: to_f64_or_nan(i.subtotal),
2510            tax: to_f64_or_nan(i.tax),
2511            total: to_f64_or_nan(i.total),
2512            currency: i.currency,
2513            due_date: i.due_date.map(|d| d.to_rfc3339()),
2514            paid_at: i.paid_at.map(|d| d.to_rfc3339()),
2515            created_at: i.created_at.to_rfc3339(),
2516            updated_at: i.updated_at.to_rfc3339(),
2517        }
2518    }
2519}
2520
2521#[php_class(name = "StateSet\\Invoices")]
2522#[derive(Clone)]
2523pub struct Invoices {
2524    commerce: Arc<Mutex<RustCommerce>>,
2525}
2526
2527#[php_impl]
2528impl Invoices {
2529    pub fn create(
2530        &self,
2531        customer_id: String,
2532        order_id: Option<String>,
2533        due_days: Option<i32>,
2534    ) -> PhpResult<Invoice> {
2535        let commerce = lock_commerce!(self.commerce);
2536        let cust_uuid = parse_uuid!(customer_id, "customer");
2537        let order_uuid = order_id.and_then(|s| s.parse().ok());
2538
2539        let invoice = commerce
2540            .invoices()
2541            .create(stateset_core::CreateInvoice {
2542                customer_id: cust_uuid,
2543                order_id: order_uuid,
2544                due_days,
2545                ..Default::default()
2546            })
2547            .map_err(|e| PhpException::default(format!("Failed to create invoice: {}", e)))?;
2548
2549        Ok(invoice.into())
2550    }
2551
2552    pub fn get(&self, id: String) -> PhpResult<Option<Invoice>> {
2553        let commerce = lock_commerce!(self.commerce);
2554        let uuid = parse_uuid!(id, "invoice");
2555
2556        let invoice = commerce
2557            .invoices()
2558            .get(uuid)
2559            .map_err(|e| PhpException::default(format!("Failed to get invoice: {}", e)))?;
2560
2561        Ok(invoice.map(|i| i.into()))
2562    }
2563
2564    pub fn list(&self) -> PhpResult<Vec<Invoice>> {
2565        let commerce = lock_commerce!(self.commerce);
2566
2567        let invoices = commerce
2568            .invoices()
2569            .list(Default::default())
2570            .map_err(|e| PhpException::default(format!("Failed to list invoices: {}", e)))?;
2571
2572        Ok(invoices.into_iter().map(|i| i.into()).collect())
2573    }
2574
2575    pub fn for_customer(&self, customer_id: String) -> PhpResult<Vec<Invoice>> {
2576        let commerce = lock_commerce!(self.commerce);
2577        let uuid = parse_uuid!(customer_id, "customer");
2578
2579        let invoices = commerce
2580            .invoices()
2581            .for_customer(uuid)
2582            .map_err(|e| PhpException::default(format!("Failed to get invoices: {}", e)))?;
2583
2584        Ok(invoices.into_iter().map(|i| i.into()).collect())
2585    }
2586
2587    pub fn send(&self, id: String) -> PhpResult<Invoice> {
2588        let commerce = lock_commerce!(self.commerce);
2589        let uuid = parse_uuid!(id, "invoice");
2590
2591        let invoice = commerce
2592            .invoices()
2593            .send(uuid)
2594            .map_err(|e| PhpException::default(format!("Failed to send invoice: {}", e)))?;
2595
2596        Ok(invoice.into())
2597    }
2598
2599    pub fn record_payment(&self, id: String, amount: f64) -> PhpResult<Invoice> {
2600        let commerce = lock_commerce!(self.commerce);
2601        let uuid = parse_uuid!(id, "invoice");
2602        let decimal_amount = Decimal::from_f64_retain(amount).unwrap_or_default();
2603
2604        let invoice = commerce
2605            .invoices()
2606            .record_payment(uuid, decimal_amount)
2607            .map_err(|e| PhpException::default(format!("Failed to record payment: {}", e)))?;
2608
2609        Ok(invoice.into())
2610    }
2611
2612    pub fn void(&self, id: String) -> PhpResult<Invoice> {
2613        let commerce = lock_commerce!(self.commerce);
2614        let uuid = parse_uuid!(id, "invoice");
2615
2616        let invoice = commerce
2617            .invoices()
2618            .void(uuid)
2619            .map_err(|e| PhpException::default(format!("Failed to void invoice: {}", e)))?;
2620
2621        Ok(invoice.into())
2622    }
2623
2624    pub fn get_overdue(&self) -> PhpResult<Vec<Invoice>> {
2625        let commerce = lock_commerce!(self.commerce);
2626
2627        let invoices = commerce
2628            .invoices()
2629            .get_overdue()
2630            .map_err(|e| PhpException::default(format!("Failed to get overdue invoices: {}", e)))?;
2631
2632        Ok(invoices.into_iter().map(|i| i.into()).collect())
2633    }
2634
2635    pub fn customer_balance(&self, customer_id: String) -> PhpResult<f64> {
2636        let commerce = lock_commerce!(self.commerce);
2637        let uuid = parse_uuid!(customer_id, "customer");
2638
2639        let balance = commerce
2640            .invoices()
2641            .customer_balance(uuid)
2642            .map_err(|e| PhpException::default(format!("Failed to get balance: {}", e)))?;
2643
2644        Ok(to_f64_or_nan(balance))
2645    }
2646
2647    pub fn count(&self) -> PhpResult<i64> {
2648        let commerce = lock_commerce!(self.commerce);
2649
2650        let count = commerce
2651            .invoices()
2652            .count(Default::default())
2653            .map_err(|e| PhpException::default(format!("Failed to count invoices: {}", e)))?;
2654
2655        Ok(count)
2656    }
2657}
2658
2659// ============================================================================
2660// BOM Types & API
2661// ============================================================================
2662
2663#[php_class(name = "StateSet\\BomComponent")]
2664#[derive(Clone)]
2665pub struct BomComponent {
2666    id: String,
2667    bom_id: String,
2668    component_sku: String,
2669    quantity: i32,
2670    unit_cost: f64,
2671}
2672
2673#[php_impl]
2674impl BomComponent {
2675    #[getter]
2676    pub fn get_id(&self) -> String {
2677        self.id.clone()
2678    }
2679
2680    #[getter]
2681    pub fn get_bom_id(&self) -> String {
2682        self.bom_id.clone()
2683    }
2684
2685    #[getter]
2686    pub fn get_component_sku(&self) -> String {
2687        self.component_sku.clone()
2688    }
2689
2690    #[getter]
2691    pub fn get_quantity(&self) -> i32 {
2692        self.quantity
2693    }
2694
2695    #[getter]
2696    pub fn get_unit_cost(&self) -> f64 {
2697        self.unit_cost
2698    }
2699}
2700
2701impl From<stateset_core::BomComponent> for BomComponent {
2702    fn from(c: stateset_core::BomComponent) -> Self {
2703        Self {
2704            id: c.id.to_string(),
2705            bom_id: c.bom_id.to_string(),
2706            component_sku: c.component_sku,
2707            quantity: c.quantity,
2708            unit_cost: to_f64_or_nan(c.unit_cost),
2709        }
2710    }
2711}
2712
2713#[php_class(name = "StateSet\\BillOfMaterials")]
2714#[derive(Clone)]
2715pub struct BillOfMaterials {
2716    id: String,
2717    product_id: String,
2718    name: String,
2719    version: String,
2720    status: String,
2721    total_cost: f64,
2722    created_at: String,
2723    updated_at: String,
2724}
2725
2726#[php_impl]
2727impl BillOfMaterials {
2728    #[getter]
2729    pub fn get_id(&self) -> String {
2730        self.id.clone()
2731    }
2732
2733    #[getter]
2734    pub fn get_product_id(&self) -> String {
2735        self.product_id.clone()
2736    }
2737
2738    #[getter]
2739    pub fn get_name(&self) -> String {
2740        self.name.clone()
2741    }
2742
2743    #[getter]
2744    pub fn get_version(&self) -> String {
2745        self.version.clone()
2746    }
2747
2748    #[getter]
2749    pub fn get_status(&self) -> String {
2750        self.status.clone()
2751    }
2752
2753    #[getter]
2754    pub fn get_total_cost(&self) -> f64 {
2755        self.total_cost
2756    }
2757
2758    #[getter]
2759    pub fn get_created_at(&self) -> String {
2760        self.created_at.clone()
2761    }
2762
2763    #[getter]
2764    pub fn get_updated_at(&self) -> String {
2765        self.updated_at.clone()
2766    }
2767
2768    pub fn __to_string(&self) -> String {
2769        format!("BillOfMaterials(id={}, name={}, version={})", self.id, self.name, self.version)
2770    }
2771}
2772
2773impl From<stateset_core::BillOfMaterials> for BillOfMaterials {
2774    fn from(b: stateset_core::BillOfMaterials) -> Self {
2775        Self {
2776            id: b.id.to_string(),
2777            product_id: b.product_id.to_string(),
2778            name: b.name,
2779            version: b.version,
2780            status: format!("{}", b.status),
2781            total_cost: to_f64_or_nan(b.total_cost),
2782            created_at: b.created_at.to_rfc3339(),
2783            updated_at: b.updated_at.to_rfc3339(),
2784        }
2785    }
2786}
2787
2788#[php_class(name = "StateSet\\BomApi")]
2789#[derive(Clone)]
2790pub struct BomApi {
2791    commerce: Arc<Mutex<RustCommerce>>,
2792}
2793
2794#[php_impl]
2795impl BomApi {
2796    pub fn create(
2797        &self,
2798        product_id: String,
2799        name: String,
2800        version: Option<String>,
2801    ) -> PhpResult<BillOfMaterials> {
2802        let commerce = lock_commerce!(self.commerce);
2803        let uuid = parse_uuid!(product_id, "product");
2804
2805        let bom = commerce
2806            .bom()
2807            .create(stateset_core::CreateBom {
2808                product_id: uuid,
2809                name,
2810                version,
2811                ..Default::default()
2812            })
2813            .map_err(|e| PhpException::default(format!("Failed to create BOM: {}", e)))?;
2814
2815        Ok(bom.into())
2816    }
2817
2818    pub fn get(&self, id: String) -> PhpResult<Option<BillOfMaterials>> {
2819        let commerce = lock_commerce!(self.commerce);
2820        let uuid = parse_uuid!(id, "bom");
2821
2822        let bom = commerce
2823            .bom()
2824            .get(uuid)
2825            .map_err(|e| PhpException::default(format!("Failed to get BOM: {}", e)))?;
2826
2827        Ok(bom.map(|b| b.into()))
2828    }
2829
2830    pub fn list(&self) -> PhpResult<Vec<BillOfMaterials>> {
2831        let commerce = lock_commerce!(self.commerce);
2832
2833        let boms = commerce
2834            .bom()
2835            .list(Default::default())
2836            .map_err(|e| PhpException::default(format!("Failed to list BOMs: {}", e)))?;
2837
2838        Ok(boms.into_iter().map(|b| b.into()).collect())
2839    }
2840
2841    pub fn add_component(
2842        &self,
2843        bom_id: String,
2844        component_sku: String,
2845        quantity: i32,
2846        unit_cost: f64,
2847    ) -> PhpResult<BomComponent> {
2848        let commerce = lock_commerce!(self.commerce);
2849        let uuid = parse_uuid!(bom_id, "bom");
2850        let cost = Decimal::from_f64_retain(unit_cost).unwrap_or_default();
2851
2852        let component = commerce
2853            .bom()
2854            .add_component(stateset_core::AddBomComponent {
2855                bom_id: uuid,
2856                component_sku,
2857                quantity,
2858                unit_cost: cost,
2859            })
2860            .map_err(|e| PhpException::default(format!("Failed to add component: {}", e)))?;
2861
2862        Ok(component.into())
2863    }
2864
2865    pub fn get_components(&self, bom_id: String) -> PhpResult<Vec<BomComponent>> {
2866        let commerce = lock_commerce!(self.commerce);
2867        let uuid = parse_uuid!(bom_id, "bom");
2868
2869        let components = commerce
2870            .bom()
2871            .get_components(uuid)
2872            .map_err(|e| PhpException::default(format!("Failed to get components: {}", e)))?;
2873
2874        Ok(components.into_iter().map(|c| c.into()).collect())
2875    }
2876
2877    pub fn remove_component(&self, component_id: String) -> PhpResult<bool> {
2878        let commerce = lock_commerce!(self.commerce);
2879        let uuid = parse_uuid!(component_id, "component");
2880
2881        commerce
2882            .bom()
2883            .remove_component(uuid)
2884            .map_err(|e| PhpException::default(format!("Failed to remove component: {}", e)))?;
2885
2886        Ok(true)
2887    }
2888
2889    pub fn activate(&self, id: String) -> PhpResult<BillOfMaterials> {
2890        let commerce = lock_commerce!(self.commerce);
2891        let uuid = parse_uuid!(id, "bom");
2892
2893        let bom = commerce
2894            .bom()
2895            .activate(uuid)
2896            .map_err(|e| PhpException::default(format!("Failed to activate BOM: {}", e)))?;
2897
2898        Ok(bom.into())
2899    }
2900
2901    pub fn delete(&self, id: String) -> PhpResult<bool> {
2902        let commerce = lock_commerce!(self.commerce);
2903        let uuid = parse_uuid!(id, "bom");
2904
2905        commerce
2906            .bom()
2907            .delete(uuid)
2908            .map_err(|e| PhpException::default(format!("Failed to delete BOM: {}", e)))?;
2909
2910        Ok(true)
2911    }
2912
2913    pub fn count(&self) -> PhpResult<i64> {
2914        let commerce = lock_commerce!(self.commerce);
2915
2916        let count = commerce
2917            .bom()
2918            .count(Default::default())
2919            .map_err(|e| PhpException::default(format!("Failed to count BOMs: {}", e)))?;
2920
2921        Ok(count)
2922    }
2923}
2924
2925// ============================================================================
2926// WorkOrders Types & API
2927// ============================================================================
2928
2929#[php_class(name = "StateSet\\WorkOrder")]
2930#[derive(Clone)]
2931pub struct WorkOrder {
2932    id: String,
2933    work_order_number: String,
2934    bom_id: String,
2935    quantity: i32,
2936    status: String,
2937    priority: String,
2938    started_at: Option<String>,
2939    completed_at: Option<String>,
2940    created_at: String,
2941    updated_at: String,
2942}
2943
2944#[php_impl]
2945impl WorkOrder {
2946    #[getter]
2947    pub fn get_id(&self) -> String {
2948        self.id.clone()
2949    }
2950
2951    #[getter]
2952    pub fn get_work_order_number(&self) -> String {
2953        self.work_order_number.clone()
2954    }
2955
2956    #[getter]
2957    pub fn get_bom_id(&self) -> String {
2958        self.bom_id.clone()
2959    }
2960
2961    #[getter]
2962    pub fn get_quantity(&self) -> i32 {
2963        self.quantity
2964    }
2965
2966    #[getter]
2967    pub fn get_status(&self) -> String {
2968        self.status.clone()
2969    }
2970
2971    #[getter]
2972    pub fn get_priority(&self) -> String {
2973        self.priority.clone()
2974    }
2975
2976    #[getter]
2977    pub fn get_started_at(&self) -> Option<String> {
2978        self.started_at.clone()
2979    }
2980
2981    #[getter]
2982    pub fn get_completed_at(&self) -> Option<String> {
2983        self.completed_at.clone()
2984    }
2985
2986    #[getter]
2987    pub fn get_created_at(&self) -> String {
2988        self.created_at.clone()
2989    }
2990
2991    #[getter]
2992    pub fn get_updated_at(&self) -> String {
2993        self.updated_at.clone()
2994    }
2995
2996    pub fn __to_string(&self) -> String {
2997        format!(
2998            "WorkOrder(number={}, status={}, qty={})",
2999            self.work_order_number, self.status, self.quantity
3000        )
3001    }
3002}
3003
3004impl From<stateset_core::WorkOrder> for WorkOrder {
3005    fn from(w: stateset_core::WorkOrder) -> Self {
3006        Self {
3007            id: w.id.to_string(),
3008            work_order_number: w.work_order_number,
3009            bom_id: w.bom_id.to_string(),
3010            quantity: w.quantity,
3011            status: format!("{}", w.status),
3012            priority: format!("{}", w.priority),
3013            started_at: w.started_at.map(|t| t.to_rfc3339()),
3014            completed_at: w.completed_at.map(|t| t.to_rfc3339()),
3015            created_at: w.created_at.to_rfc3339(),
3016            updated_at: w.updated_at.to_rfc3339(),
3017        }
3018    }
3019}
3020
3021#[php_class(name = "StateSet\\WorkOrders")]
3022#[derive(Clone)]
3023pub struct WorkOrders {
3024    commerce: Arc<Mutex<RustCommerce>>,
3025}
3026
3027#[php_impl]
3028impl WorkOrders {
3029    pub fn create(
3030        &self,
3031        bom_id: String,
3032        quantity: i32,
3033        priority: Option<String>,
3034    ) -> PhpResult<WorkOrder> {
3035        let commerce = lock_commerce!(self.commerce);
3036        let uuid = parse_uuid!(bom_id, "bom");
3037
3038        let work_order = commerce
3039            .work_orders()
3040            .create(stateset_core::CreateWorkOrder {
3041                bom_id: uuid,
3042                quantity,
3043                priority,
3044                ..Default::default()
3045            })
3046            .map_err(|e| PhpException::default(format!("Failed to create work order: {}", e)))?;
3047
3048        Ok(work_order.into())
3049    }
3050
3051    pub fn get(&self, id: String) -> PhpResult<Option<WorkOrder>> {
3052        let commerce = lock_commerce!(self.commerce);
3053        let uuid = parse_uuid!(id, "work_order");
3054
3055        let work_order = commerce
3056            .work_orders()
3057            .get(uuid)
3058            .map_err(|e| PhpException::default(format!("Failed to get work order: {}", e)))?;
3059
3060        Ok(work_order.map(|w| w.into()))
3061    }
3062
3063    pub fn list(&self) -> PhpResult<Vec<WorkOrder>> {
3064        let commerce = lock_commerce!(self.commerce);
3065
3066        let work_orders = commerce
3067            .work_orders()
3068            .list(Default::default())
3069            .map_err(|e| PhpException::default(format!("Failed to list work orders: {}", e)))?;
3070
3071        Ok(work_orders.into_iter().map(|w| w.into()).collect())
3072    }
3073
3074    pub fn start(&self, id: String) -> PhpResult<WorkOrder> {
3075        let commerce = lock_commerce!(self.commerce);
3076        let uuid = parse_uuid!(id, "work_order");
3077
3078        let work_order = commerce
3079            .work_orders()
3080            .start(uuid)
3081            .map_err(|e| PhpException::default(format!("Failed to start work order: {}", e)))?;
3082
3083        Ok(work_order.into())
3084    }
3085
3086    pub fn complete(&self, id: String) -> PhpResult<WorkOrder> {
3087        let commerce = lock_commerce!(self.commerce);
3088        let uuid = parse_uuid!(id, "work_order");
3089
3090        let work_order = commerce
3091            .work_orders()
3092            .complete(uuid)
3093            .map_err(|e| PhpException::default(format!("Failed to complete work order: {}", e)))?;
3094
3095        Ok(work_order.into())
3096    }
3097
3098    pub fn hold(&self, id: String) -> PhpResult<WorkOrder> {
3099        let commerce = lock_commerce!(self.commerce);
3100        let uuid = parse_uuid!(id, "work_order");
3101
3102        let work_order = commerce
3103            .work_orders()
3104            .hold(uuid)
3105            .map_err(|e| PhpException::default(format!("Failed to hold work order: {}", e)))?;
3106
3107        Ok(work_order.into())
3108    }
3109
3110    pub fn resume(&self, id: String) -> PhpResult<WorkOrder> {
3111        let commerce = lock_commerce!(self.commerce);
3112        let uuid = parse_uuid!(id, "work_order");
3113
3114        let work_order = commerce
3115            .work_orders()
3116            .resume(uuid)
3117            .map_err(|e| PhpException::default(format!("Failed to resume work order: {}", e)))?;
3118
3119        Ok(work_order.into())
3120    }
3121
3122    pub fn cancel(&self, id: String) -> PhpResult<WorkOrder> {
3123        let commerce = lock_commerce!(self.commerce);
3124        let uuid = parse_uuid!(id, "work_order");
3125
3126        let work_order = commerce
3127            .work_orders()
3128            .cancel(uuid)
3129            .map_err(|e| PhpException::default(format!("Failed to cancel work order: {}", e)))?;
3130
3131        Ok(work_order.into())
3132    }
3133
3134    pub fn count(&self) -> PhpResult<i64> {
3135        let commerce = lock_commerce!(self.commerce);
3136
3137        let count = commerce
3138            .work_orders()
3139            .count(Default::default())
3140            .map_err(|e| PhpException::default(format!("Failed to count work orders: {}", e)))?;
3141
3142        Ok(count)
3143    }
3144}
3145
3146// ============================================================================
3147// CurrencyOps Types & API
3148// ============================================================================
3149
3150#[php_class(name = "StateSet\\ExchangeRate")]
3151#[derive(Clone)]
3152pub struct ExchangeRate {
3153    from_currency: String,
3154    to_currency: String,
3155    rate: f64,
3156    updated_at: String,
3157}
3158
3159#[php_impl]
3160impl ExchangeRate {
3161    #[getter]
3162    pub fn get_from_currency(&self) -> String {
3163        self.from_currency.clone()
3164    }
3165
3166    #[getter]
3167    pub fn get_to_currency(&self) -> String {
3168        self.to_currency.clone()
3169    }
3170
3171    #[getter]
3172    pub fn get_rate(&self) -> f64 {
3173        self.rate
3174    }
3175
3176    #[getter]
3177    pub fn get_updated_at(&self) -> String {
3178        self.updated_at.clone()
3179    }
3180}
3181
3182impl From<stateset_core::ExchangeRate> for ExchangeRate {
3183    fn from(r: stateset_core::ExchangeRate) -> Self {
3184        Self {
3185            from_currency: r.from_currency,
3186            to_currency: r.to_currency,
3187            rate: to_f64_or_nan(r.rate),
3188            updated_at: r.updated_at.to_rfc3339(),
3189        }
3190    }
3191}
3192
3193#[php_class(name = "StateSet\\CurrencyOps")]
3194#[derive(Clone)]
3195pub struct CurrencyOps {
3196    commerce: Arc<Mutex<RustCommerce>>,
3197}
3198
3199#[php_impl]
3200impl CurrencyOps {
3201    pub fn get_rate(&self, from: String, to: String) -> PhpResult<Option<ExchangeRate>> {
3202        let commerce = lock_commerce!(self.commerce);
3203
3204        let rate = commerce
3205            .currency()
3206            .get_rate(&from, &to)
3207            .map_err(|e| PhpException::default(format!("Failed to get rate: {}", e)))?;
3208
3209        Ok(rate.map(|r| r.into()))
3210    }
3211
3212    pub fn list_rates(&self) -> PhpResult<Vec<ExchangeRate>> {
3213        let commerce = lock_commerce!(self.commerce);
3214
3215        let rates = commerce
3216            .currency()
3217            .list_rates()
3218            .map_err(|e| PhpException::default(format!("Failed to list rates: {}", e)))?;
3219
3220        Ok(rates.into_iter().map(|r| r.into()).collect())
3221    }
3222
3223    pub fn set_rate(&self, from: String, to: String, rate: f64) -> PhpResult<ExchangeRate> {
3224        let commerce = lock_commerce!(self.commerce);
3225        let decimal_rate = Decimal::from_f64_retain(rate).unwrap_or_default();
3226
3227        let exchange_rate = commerce
3228            .currency()
3229            .set_rate(&from, &to, decimal_rate)
3230            .map_err(|e| PhpException::default(format!("Failed to set rate: {}", e)))?;
3231
3232        Ok(exchange_rate.into())
3233    }
3234
3235    pub fn convert(&self, amount: f64, from: String, to: String) -> PhpResult<f64> {
3236        let commerce = lock_commerce!(self.commerce);
3237        let decimal_amount = Decimal::from_f64_retain(amount).unwrap_or_default();
3238
3239        let converted = commerce
3240            .currency()
3241            .convert(decimal_amount, &from, &to)
3242            .map_err(|e| PhpException::default(format!("Failed to convert: {}", e)))?;
3243
3244        Ok(to_f64_or_nan(converted))
3245    }
3246
3247    pub fn base_currency(&self) -> PhpResult<String> {
3248        let commerce = lock_commerce!(self.commerce);
3249
3250        let base = commerce
3251            .currency()
3252            .base_currency()
3253            .map_err(|e| PhpException::default(format!("Failed to get base currency: {}", e)))?;
3254
3255        Ok(base)
3256    }
3257
3258    pub fn enabled_currencies(&self) -> PhpResult<Vec<String>> {
3259        let commerce = lock_commerce!(self.commerce);
3260
3261        let currencies = commerce
3262            .currency()
3263            .enabled_currencies()
3264            .map_err(|e| PhpException::default(format!("Failed to get currencies: {}", e)))?;
3265
3266        Ok(currencies)
3267    }
3268
3269    pub fn format(&self, amount: f64, currency: String) -> PhpResult<String> {
3270        let commerce = lock_commerce!(self.commerce);
3271        let decimal_amount = Decimal::from_f64_retain(amount).unwrap_or_default();
3272
3273        let formatted = commerce
3274            .currency()
3275            .format(decimal_amount, &currency)
3276            .map_err(|e| PhpException::default(format!("Failed to format: {}", e)))?;
3277
3278        Ok(formatted)
3279    }
3280}
3281
3282// ============================================================================
3283// Subscriptions Types & API
3284// ============================================================================
3285
3286#[php_class(name = "StateSet\\SubscriptionPlan")]
3287#[derive(Clone)]
3288pub struct SubscriptionPlan {
3289    id: String,
3290    name: String,
3291    description: Option<String>,
3292    price: f64,
3293    currency: String,
3294    interval: String,
3295    interval_count: i32,
3296    trial_days: Option<i32>,
3297    status: String,
3298    created_at: String,
3299}
3300
3301#[php_impl]
3302impl SubscriptionPlan {
3303    #[getter]
3304    pub fn get_id(&self) -> String {
3305        self.id.clone()
3306    }
3307
3308    #[getter]
3309    pub fn get_name(&self) -> String {
3310        self.name.clone()
3311    }
3312
3313    #[getter]
3314    pub fn get_description(&self) -> Option<String> {
3315        self.description.clone()
3316    }
3317
3318    #[getter]
3319    pub fn get_price(&self) -> f64 {
3320        self.price
3321    }
3322
3323    #[getter]
3324    pub fn get_currency(&self) -> String {
3325        self.currency.clone()
3326    }
3327
3328    #[getter]
3329    pub fn get_interval(&self) -> String {
3330        self.interval.clone()
3331    }
3332
3333    #[getter]
3334    pub fn get_interval_count(&self) -> i32 {
3335        self.interval_count
3336    }
3337
3338    #[getter]
3339    pub fn get_trial_days(&self) -> Option<i32> {
3340        self.trial_days
3341    }
3342
3343    #[getter]
3344    pub fn get_status(&self) -> String {
3345        self.status.clone()
3346    }
3347
3348    #[getter]
3349    pub fn get_created_at(&self) -> String {
3350        self.created_at.clone()
3351    }
3352}
3353
3354impl From<stateset_core::SubscriptionPlan> for SubscriptionPlan {
3355    fn from(p: stateset_core::SubscriptionPlan) -> Self {
3356        Self {
3357            id: p.id.to_string(),
3358            name: p.name,
3359            description: p.description,
3360            price: to_f64_or_nan(p.price),
3361            currency: p.currency,
3362            interval: format!("{}", p.interval),
3363            interval_count: p.interval_count,
3364            trial_days: p.trial_days,
3365            status: format!("{}", p.status),
3366            created_at: p.created_at.to_rfc3339(),
3367        }
3368    }
3369}
3370
3371#[php_class(name = "StateSet\\Subscription")]
3372#[derive(Clone)]
3373pub struct Subscription {
3374    id: String,
3375    plan_id: String,
3376    customer_id: String,
3377    status: String,
3378    current_period_start: String,
3379    current_period_end: String,
3380    canceled_at: Option<String>,
3381    created_at: String,
3382    updated_at: String,
3383}
3384
3385#[php_impl]
3386impl Subscription {
3387    #[getter]
3388    pub fn get_id(&self) -> String {
3389        self.id.clone()
3390    }
3391
3392    #[getter]
3393    pub fn get_plan_id(&self) -> String {
3394        self.plan_id.clone()
3395    }
3396
3397    #[getter]
3398    pub fn get_customer_id(&self) -> String {
3399        self.customer_id.clone()
3400    }
3401
3402    #[getter]
3403    pub fn get_status(&self) -> String {
3404        self.status.clone()
3405    }
3406
3407    #[getter]
3408    pub fn get_current_period_start(&self) -> String {
3409        self.current_period_start.clone()
3410    }
3411
3412    #[getter]
3413    pub fn get_current_period_end(&self) -> String {
3414        self.current_period_end.clone()
3415    }
3416
3417    #[getter]
3418    pub fn get_canceled_at(&self) -> Option<String> {
3419        self.canceled_at.clone()
3420    }
3421
3422    #[getter]
3423    pub fn get_created_at(&self) -> String {
3424        self.created_at.clone()
3425    }
3426
3427    #[getter]
3428    pub fn get_updated_at(&self) -> String {
3429        self.updated_at.clone()
3430    }
3431
3432    pub fn __to_string(&self) -> String {
3433        format!("Subscription(id={}, status={})", self.id, self.status)
3434    }
3435}
3436
3437impl From<stateset_core::Subscription> for Subscription {
3438    fn from(s: stateset_core::Subscription) -> Self {
3439        Self {
3440            id: s.id.to_string(),
3441            plan_id: s.plan_id.to_string(),
3442            customer_id: s.customer_id.to_string(),
3443            status: format!("{}", s.status),
3444            current_period_start: s.current_period_start.to_rfc3339(),
3445            current_period_end: s.current_period_end.to_rfc3339(),
3446            canceled_at: s.canceled_at.map(|t| t.to_rfc3339()),
3447            created_at: s.created_at.to_rfc3339(),
3448            updated_at: s.updated_at.to_rfc3339(),
3449        }
3450    }
3451}
3452
3453#[php_class(name = "StateSet\\Subscriptions")]
3454#[derive(Clone)]
3455pub struct Subscriptions {
3456    commerce: Arc<Mutex<RustCommerce>>,
3457}
3458
3459#[php_impl]
3460impl Subscriptions {
3461    pub fn create_plan(
3462        &self,
3463        name: String,
3464        price: f64,
3465        interval: String,
3466        interval_count: Option<i32>,
3467        trial_days: Option<i32>,
3468    ) -> PhpResult<SubscriptionPlan> {
3469        let commerce = lock_commerce!(self.commerce);
3470        let decimal_price = Decimal::from_f64_retain(price).unwrap_or_default();
3471
3472        let plan = commerce
3473            .subscriptions()
3474            .create_plan(stateset_core::CreateSubscriptionPlan {
3475                name,
3476                price: decimal_price,
3477                interval,
3478                interval_count,
3479                trial_days,
3480                ..Default::default()
3481            })
3482            .map_err(|e| PhpException::default(format!("Failed to create plan: {}", e)))?;
3483
3484        Ok(plan.into())
3485    }
3486
3487    pub fn get_plan(&self, id: String) -> PhpResult<Option<SubscriptionPlan>> {
3488        let commerce = lock_commerce!(self.commerce);
3489        let uuid = parse_uuid!(id, "plan");
3490
3491        let plan = commerce
3492            .subscriptions()
3493            .get_plan(uuid)
3494            .map_err(|e| PhpException::default(format!("Failed to get plan: {}", e)))?;
3495
3496        Ok(plan.map(|p| p.into()))
3497    }
3498
3499    pub fn list_plans(&self) -> PhpResult<Vec<SubscriptionPlan>> {
3500        let commerce = lock_commerce!(self.commerce);
3501
3502        let plans = commerce
3503            .subscriptions()
3504            .list_plans(Default::default())
3505            .map_err(|e| PhpException::default(format!("Failed to list plans: {}", e)))?;
3506
3507        Ok(plans.into_iter().map(|p| p.into()).collect())
3508    }
3509
3510    pub fn subscribe(&self, plan_id: String, customer_id: String) -> PhpResult<Subscription> {
3511        let commerce = lock_commerce!(self.commerce);
3512        let plan_uuid = parse_uuid!(plan_id, "plan");
3513        let cust_uuid = parse_uuid!(customer_id, "customer");
3514
3515        let subscription = commerce
3516            .subscriptions()
3517            .subscribe(stateset_core::CreateSubscription {
3518                plan_id: plan_uuid,
3519                customer_id: cust_uuid,
3520                ..Default::default()
3521            })
3522            .map_err(|e| PhpException::default(format!("Failed to subscribe: {}", e)))?;
3523
3524        Ok(subscription.into())
3525    }
3526
3527    pub fn get(&self, id: String) -> PhpResult<Option<Subscription>> {
3528        let commerce = lock_commerce!(self.commerce);
3529        let uuid = parse_uuid!(id, "subscription");
3530
3531        let subscription = commerce
3532            .subscriptions()
3533            .get(uuid)
3534            .map_err(|e| PhpException::default(format!("Failed to get subscription: {}", e)))?;
3535
3536        Ok(subscription.map(|s| s.into()))
3537    }
3538
3539    pub fn list(&self) -> PhpResult<Vec<Subscription>> {
3540        let commerce = lock_commerce!(self.commerce);
3541
3542        let subscriptions = commerce
3543            .subscriptions()
3544            .list(Default::default())
3545            .map_err(|e| PhpException::default(format!("Failed to list subscriptions: {}", e)))?;
3546
3547        Ok(subscriptions.into_iter().map(|s| s.into()).collect())
3548    }
3549
3550    pub fn pause(&self, id: String) -> PhpResult<Subscription> {
3551        let commerce = lock_commerce!(self.commerce);
3552        let uuid = parse_uuid!(id, "subscription");
3553
3554        let subscription = commerce
3555            .subscriptions()
3556            .pause(uuid)
3557            .map_err(|e| PhpException::default(format!("Failed to pause subscription: {}", e)))?;
3558
3559        Ok(subscription.into())
3560    }
3561
3562    pub fn resume(&self, id: String) -> PhpResult<Subscription> {
3563        let commerce = lock_commerce!(self.commerce);
3564        let uuid = parse_uuid!(id, "subscription");
3565
3566        let subscription = commerce
3567            .subscriptions()
3568            .resume(uuid)
3569            .map_err(|e| PhpException::default(format!("Failed to resume subscription: {}", e)))?;
3570
3571        Ok(subscription.into())
3572    }
3573
3574    pub fn cancel(&self, id: String, at_period_end: Option<bool>) -> PhpResult<Subscription> {
3575        let commerce = lock_commerce!(self.commerce);
3576        let uuid = parse_uuid!(id, "subscription");
3577
3578        let subscription = commerce
3579            .subscriptions()
3580            .cancel(uuid, at_period_end.unwrap_or(true))
3581            .map_err(|e| PhpException::default(format!("Failed to cancel subscription: {}", e)))?;
3582
3583        Ok(subscription.into())
3584    }
3585
3586    pub fn for_customer(&self, customer_id: String) -> PhpResult<Vec<Subscription>> {
3587        let commerce = lock_commerce!(self.commerce);
3588        let uuid = parse_uuid!(customer_id, "customer");
3589
3590        let subscriptions = commerce
3591            .subscriptions()
3592            .for_customer(uuid)
3593            .map_err(|e| PhpException::default(format!("Failed to get subscriptions: {}", e)))?;
3594
3595        Ok(subscriptions.into_iter().map(|s| s.into()).collect())
3596    }
3597
3598    pub fn is_active(&self, id: String) -> PhpResult<bool> {
3599        let commerce = lock_commerce!(self.commerce);
3600        let uuid = parse_uuid!(id, "subscription");
3601
3602        let active = commerce
3603            .subscriptions()
3604            .is_active(uuid)
3605            .map_err(|e| PhpException::default(format!("Failed to check subscription: {}", e)))?;
3606
3607        Ok(active)
3608    }
3609}
3610
3611// ============================================================================
3612// Promotions Types & API
3613// ============================================================================
3614
3615#[php_class(name = "StateSet\\Promotion")]
3616#[derive(Clone)]
3617pub struct Promotion {
3618    id: String,
3619    code: String,
3620    name: String,
3621    description: Option<String>,
3622    discount_type: String,
3623    discount_value: f64,
3624    min_purchase: Option<f64>,
3625    max_uses: Option<i32>,
3626    uses_count: i32,
3627    starts_at: Option<String>,
3628    ends_at: Option<String>,
3629    status: String,
3630    created_at: String,
3631}
3632
3633#[php_impl]
3634impl Promotion {
3635    #[getter]
3636    pub fn get_id(&self) -> String {
3637        self.id.clone()
3638    }
3639
3640    #[getter]
3641    pub fn get_code(&self) -> String {
3642        self.code.clone()
3643    }
3644
3645    #[getter]
3646    pub fn get_name(&self) -> String {
3647        self.name.clone()
3648    }
3649
3650    #[getter]
3651    pub fn get_description(&self) -> Option<String> {
3652        self.description.clone()
3653    }
3654
3655    #[getter]
3656    pub fn get_discount_type(&self) -> String {
3657        self.discount_type.clone()
3658    }
3659
3660    #[getter]
3661    pub fn get_discount_value(&self) -> f64 {
3662        self.discount_value
3663    }
3664
3665    #[getter]
3666    pub fn get_min_purchase(&self) -> Option<f64> {
3667        self.min_purchase
3668    }
3669
3670    #[getter]
3671    pub fn get_max_uses(&self) -> Option<i32> {
3672        self.max_uses
3673    }
3674
3675    #[getter]
3676    pub fn get_uses_count(&self) -> i32 {
3677        self.uses_count
3678    }
3679
3680    #[getter]
3681    pub fn get_starts_at(&self) -> Option<String> {
3682        self.starts_at.clone()
3683    }
3684
3685    #[getter]
3686    pub fn get_ends_at(&self) -> Option<String> {
3687        self.ends_at.clone()
3688    }
3689
3690    #[getter]
3691    pub fn get_status(&self) -> String {
3692        self.status.clone()
3693    }
3694
3695    #[getter]
3696    pub fn get_created_at(&self) -> String {
3697        self.created_at.clone()
3698    }
3699
3700    pub fn __to_string(&self) -> String {
3701        format!(
3702            "Promotion(code={}, type={}, value={})",
3703            self.code, self.discount_type, self.discount_value
3704        )
3705    }
3706}
3707
3708impl From<stateset_core::Promotion> for Promotion {
3709    fn from(p: stateset_core::Promotion) -> Self {
3710        Self {
3711            id: p.id.to_string(),
3712            code: p.code,
3713            name: p.name,
3714            description: p.description,
3715            discount_type: format!("{}", p.discount_type),
3716            discount_value: to_f64_or_nan(p.discount_value),
3717            min_purchase: p.min_purchase.and_then(|m| m.to_f64()),
3718            max_uses: p.max_uses,
3719            uses_count: p.uses_count,
3720            starts_at: p.starts_at.map(|t| t.to_rfc3339()),
3721            ends_at: p.ends_at.map(|t| t.to_rfc3339()),
3722            status: format!("{}", p.status),
3723            created_at: p.created_at.to_rfc3339(),
3724        }
3725    }
3726}
3727
3728#[php_class(name = "StateSet\\Promotions")]
3729#[derive(Clone)]
3730pub struct Promotions {
3731    commerce: Arc<Mutex<RustCommerce>>,
3732}
3733
3734#[php_impl]
3735impl Promotions {
3736    pub fn create(
3737        &self,
3738        code: String,
3739        name: String,
3740        discount_type: String,
3741        discount_value: f64,
3742        min_purchase: Option<f64>,
3743        max_uses: Option<i32>,
3744    ) -> PhpResult<Promotion> {
3745        let commerce = lock_commerce!(self.commerce);
3746        let decimal_value = Decimal::from_f64_retain(discount_value).unwrap_or_default();
3747        let decimal_min = min_purchase.map(|m| Decimal::from_f64_retain(m).unwrap_or_default());
3748
3749        let promotion = commerce
3750            .promotions()
3751            .create(stateset_core::CreatePromotion {
3752                code,
3753                name,
3754                discount_type,
3755                discount_value: decimal_value,
3756                min_purchase: decimal_min,
3757                max_uses,
3758                ..Default::default()
3759            })
3760            .map_err(|e| PhpException::default(format!("Failed to create promotion: {}", e)))?;
3761
3762        Ok(promotion.into())
3763    }
3764
3765    pub fn get(&self, id: String) -> PhpResult<Option<Promotion>> {
3766        let commerce = lock_commerce!(self.commerce);
3767        let uuid = parse_uuid!(id, "promotion");
3768
3769        let promotion = commerce
3770            .promotions()
3771            .get(uuid)
3772            .map_err(|e| PhpException::default(format!("Failed to get promotion: {}", e)))?;
3773
3774        Ok(promotion.map(|p| p.into()))
3775    }
3776
3777    pub fn get_by_code(&self, code: String) -> PhpResult<Option<Promotion>> {
3778        let commerce = lock_commerce!(self.commerce);
3779
3780        let promotion = commerce
3781            .promotions()
3782            .get_by_code(&code)
3783            .map_err(|e| PhpException::default(format!("Failed to get promotion: {}", e)))?;
3784
3785        Ok(promotion.map(|p| p.into()))
3786    }
3787
3788    pub fn list(&self) -> PhpResult<Vec<Promotion>> {
3789        let commerce = lock_commerce!(self.commerce);
3790
3791        let promotions = commerce
3792            .promotions()
3793            .list(Default::default())
3794            .map_err(|e| PhpException::default(format!("Failed to list promotions: {}", e)))?;
3795
3796        Ok(promotions.into_iter().map(|p| p.into()).collect())
3797    }
3798
3799    pub fn activate(&self, id: String) -> PhpResult<Promotion> {
3800        let commerce = lock_commerce!(self.commerce);
3801        let uuid = parse_uuid!(id, "promotion");
3802
3803        let promotion = commerce
3804            .promotions()
3805            .activate(uuid)
3806            .map_err(|e| PhpException::default(format!("Failed to activate promotion: {}", e)))?;
3807
3808        Ok(promotion.into())
3809    }
3810
3811    pub fn deactivate(&self, id: String) -> PhpResult<Promotion> {
3812        let commerce = lock_commerce!(self.commerce);
3813        let uuid = parse_uuid!(id, "promotion");
3814
3815        let promotion = commerce
3816            .promotions()
3817            .deactivate(uuid)
3818            .map_err(|e| PhpException::default(format!("Failed to deactivate promotion: {}", e)))?;
3819
3820        Ok(promotion.into())
3821    }
3822
3823    pub fn get_active(&self) -> PhpResult<Vec<Promotion>> {
3824        let commerce = lock_commerce!(self.commerce);
3825
3826        let promotions = commerce.promotions().get_active().map_err(|e| {
3827            PhpException::default(format!("Failed to get active promotions: {}", e))
3828        })?;
3829
3830        Ok(promotions.into_iter().map(|p| p.into()).collect())
3831    }
3832
3833    pub fn is_valid(&self, code: String, order_total: Option<f64>) -> PhpResult<bool> {
3834        let commerce = lock_commerce!(self.commerce);
3835        let decimal_total = order_total.map(|t| Decimal::from_f64_retain(t).unwrap_or_default());
3836
3837        let valid = commerce
3838            .promotions()
3839            .is_valid(&code, decimal_total)
3840            .map_err(|e| PhpException::default(format!("Failed to validate promotion: {}", e)))?;
3841
3842        Ok(valid)
3843    }
3844
3845    pub fn delete(&self, id: String) -> PhpResult<bool> {
3846        let commerce = lock_commerce!(self.commerce);
3847        let uuid = parse_uuid!(id, "promotion");
3848
3849        commerce
3850            .promotions()
3851            .delete(uuid)
3852            .map_err(|e| PhpException::default(format!("Failed to delete promotion: {}", e)))?;
3853
3854        Ok(true)
3855    }
3856}
3857
3858// ============================================================================
3859// Tax Types & API
3860// ============================================================================
3861
3862#[php_class(name = "StateSet\\TaxJurisdiction")]
3863#[derive(Clone)]
3864pub struct TaxJurisdiction {
3865    id: String,
3866    name: String,
3867    country: String,
3868    state: Option<String>,
3869    city: Option<String>,
3870    postal_code: Option<String>,
3871    status: String,
3872}
3873
3874#[php_impl]
3875impl TaxJurisdiction {
3876    #[getter]
3877    pub fn get_id(&self) -> String {
3878        self.id.clone()
3879    }
3880
3881    #[getter]
3882    pub fn get_name(&self) -> String {
3883        self.name.clone()
3884    }
3885
3886    #[getter]
3887    pub fn get_country(&self) -> String {
3888        self.country.clone()
3889    }
3890
3891    #[getter]
3892    pub fn get_state(&self) -> Option<String> {
3893        self.state.clone()
3894    }
3895
3896    #[getter]
3897    pub fn get_city(&self) -> Option<String> {
3898        self.city.clone()
3899    }
3900
3901    #[getter]
3902    pub fn get_postal_code(&self) -> Option<String> {
3903        self.postal_code.clone()
3904    }
3905
3906    #[getter]
3907    pub fn get_status(&self) -> String {
3908        self.status.clone()
3909    }
3910}
3911
3912impl From<stateset_core::TaxJurisdiction> for TaxJurisdiction {
3913    fn from(j: stateset_core::TaxJurisdiction) -> Self {
3914        Self {
3915            id: j.id.to_string(),
3916            name: j.name,
3917            country: j.country,
3918            state: j.state,
3919            city: j.city,
3920            postal_code: j.postal_code,
3921            status: format!("{}", j.status),
3922        }
3923    }
3924}
3925
3926#[php_class(name = "StateSet\\TaxRate")]
3927#[derive(Clone)]
3928pub struct TaxRate {
3929    id: String,
3930    jurisdiction_id: String,
3931    name: String,
3932    rate: f64,
3933    tax_type: String,
3934    status: String,
3935}
3936
3937#[php_impl]
3938impl TaxRate {
3939    #[getter]
3940    pub fn get_id(&self) -> String {
3941        self.id.clone()
3942    }
3943
3944    #[getter]
3945    pub fn get_jurisdiction_id(&self) -> String {
3946        self.jurisdiction_id.clone()
3947    }
3948
3949    #[getter]
3950    pub fn get_name(&self) -> String {
3951        self.name.clone()
3952    }
3953
3954    #[getter]
3955    pub fn get_rate(&self) -> f64 {
3956        self.rate
3957    }
3958
3959    #[getter]
3960    pub fn get_tax_type(&self) -> String {
3961        self.tax_type.clone()
3962    }
3963
3964    #[getter]
3965    pub fn get_status(&self) -> String {
3966        self.status.clone()
3967    }
3968}
3969
3970impl From<stateset_core::TaxRate> for TaxRate {
3971    fn from(r: stateset_core::TaxRate) -> Self {
3972        Self {
3973            id: r.id.to_string(),
3974            jurisdiction_id: r.jurisdiction_id.to_string(),
3975            name: r.name,
3976            rate: to_f64_or_nan(r.rate),
3977            tax_type: format!("{}", r.tax_type),
3978            status: format!("{}", r.status),
3979        }
3980    }
3981}
3982
3983#[php_class(name = "StateSet\\Tax")]
3984#[derive(Clone)]
3985pub struct Tax {
3986    commerce: Arc<Mutex<RustCommerce>>,
3987}
3988
3989#[php_impl]
3990impl Tax {
3991    pub fn calculate(&self, amount: f64, jurisdiction_id: String) -> PhpResult<f64> {
3992        let commerce = lock_commerce!(self.commerce);
3993        let decimal_amount = Decimal::from_f64_retain(amount).unwrap_or_default();
3994        let uuid = parse_uuid!(jurisdiction_id, "jurisdiction");
3995
3996        let tax = commerce
3997            .tax()
3998            .calculate(decimal_amount, uuid)
3999            .map_err(|e| PhpException::default(format!("Failed to calculate tax: {}", e)))?;
4000
4001        Ok(to_f64_or_nan(tax))
4002    }
4003
4004    pub fn get_effective_rate(&self, jurisdiction_id: String) -> PhpResult<f64> {
4005        let commerce = lock_commerce!(self.commerce);
4006        let uuid = parse_uuid!(jurisdiction_id, "jurisdiction");
4007
4008        let rate = commerce
4009            .tax()
4010            .get_effective_rate(uuid)
4011            .map_err(|e| PhpException::default(format!("Failed to get rate: {}", e)))?;
4012
4013        Ok(to_f64_or_nan(rate))
4014    }
4015
4016    pub fn list_jurisdictions(&self) -> PhpResult<Vec<TaxJurisdiction>> {
4017        let commerce = lock_commerce!(self.commerce);
4018
4019        let jurisdictions = commerce
4020            .tax()
4021            .list_jurisdictions(Default::default())
4022            .map_err(|e| PhpException::default(format!("Failed to list jurisdictions: {}", e)))?;
4023
4024        Ok(jurisdictions.into_iter().map(|j| j.into()).collect())
4025    }
4026
4027    pub fn create_jurisdiction(
4028        &self,
4029        name: String,
4030        country: String,
4031        state: Option<String>,
4032        city: Option<String>,
4033    ) -> PhpResult<TaxJurisdiction> {
4034        let commerce = lock_commerce!(self.commerce);
4035
4036        let jurisdiction = commerce
4037            .tax()
4038            .create_jurisdiction(stateset_core::CreateTaxJurisdiction {
4039                name,
4040                country,
4041                state,
4042                city,
4043                ..Default::default()
4044            })
4045            .map_err(|e| PhpException::default(format!("Failed to create jurisdiction: {}", e)))?;
4046
4047        Ok(jurisdiction.into())
4048    }
4049
4050    pub fn list_rates(&self, jurisdiction_id: String) -> PhpResult<Vec<TaxRate>> {
4051        let commerce = lock_commerce!(self.commerce);
4052        let uuid = parse_uuid!(jurisdiction_id, "jurisdiction");
4053
4054        let rates = commerce
4055            .tax()
4056            .list_rates(uuid)
4057            .map_err(|e| PhpException::default(format!("Failed to list rates: {}", e)))?;
4058
4059        Ok(rates.into_iter().map(|r| r.into()).collect())
4060    }
4061
4062    pub fn create_rate(
4063        &self,
4064        jurisdiction_id: String,
4065        name: String,
4066        rate: f64,
4067        tax_type: String,
4068    ) -> PhpResult<TaxRate> {
4069        let commerce = lock_commerce!(self.commerce);
4070        let uuid = parse_uuid!(jurisdiction_id, "jurisdiction");
4071        let decimal_rate = Decimal::from_f64_retain(rate).unwrap_or_default();
4072
4073        let tax_rate = commerce
4074            .tax()
4075            .create_rate(stateset_core::CreateTaxRate {
4076                jurisdiction_id: uuid,
4077                name,
4078                rate: decimal_rate,
4079                tax_type,
4080                ..Default::default()
4081            })
4082            .map_err(|e| PhpException::default(format!("Failed to create rate: {}", e)))?;
4083
4084        Ok(tax_rate.into())
4085    }
4086
4087    pub fn is_enabled(&self) -> PhpResult<bool> {
4088        let commerce = lock_commerce!(self.commerce);
4089
4090        let enabled = commerce
4091            .tax()
4092            .is_enabled()
4093            .map_err(|e| PhpException::default(format!("Failed to check tax status: {}", e)))?;
4094
4095        Ok(enabled)
4096    }
4097
4098    pub fn set_enabled(&self, enabled: bool) -> PhpResult<bool> {
4099        let commerce = lock_commerce!(self.commerce);
4100
4101        commerce
4102            .tax()
4103            .set_enabled(enabled)
4104            .map_err(|e| PhpException::default(format!("Failed to set tax status: {}", e)))?;
4105
4106        Ok(true)
4107    }
4108}
4109
4110// ============================================================================
4111// Quality API
4112// ============================================================================
4113
4114#[php_class(name = "StateSet\\Quality")]
4115#[derive(Clone)]
4116pub struct Quality {
4117    commerce: Arc<Mutex<RustCommerce>>,
4118}
4119
4120#[php_impl]
4121impl Quality {
4122    pub fn create_inspection(
4123        &self,
4124        inspection_type: String,
4125        reference_type: String,
4126        reference_id: String,
4127    ) -> PhpResult<String> {
4128        let commerce = lock_commerce!(self.commerce);
4129        let ref_uuid = parse_uuid!(reference_id, "reference");
4130
4131        let inspection = commerce
4132            .quality()
4133            .create_inspection(stateset_core::CreateInspection {
4134                inspection_type,
4135                reference_type,
4136                reference_id: ref_uuid,
4137                ..Default::default()
4138            })
4139            .map_err(|e| PhpException::default(format!("Failed to create inspection: {}", e)))?;
4140
4141        Ok(inspection.id.to_string())
4142    }
4143
4144    pub fn list_inspections(&self) -> PhpResult<Vec<String>> {
4145        let commerce = lock_commerce!(self.commerce);
4146
4147        let inspections = commerce
4148            .quality()
4149            .list_inspections(Default::default())
4150            .map_err(|e| PhpException::default(format!("Failed to list inspections: {}", e)))?;
4151
4152        Ok(inspections.into_iter().map(|i| i.id.to_string()).collect())
4153    }
4154
4155    pub fn create_hold(
4156        &self,
4157        sku: String,
4158        quantity: i32,
4159        reason: String,
4160        hold_type: String,
4161    ) -> PhpResult<String> {
4162        let commerce = lock_commerce!(self.commerce);
4163
4164        let hold = commerce
4165            .quality()
4166            .create_hold(stateset_core::CreateQualityHold {
4167                sku,
4168                quantity_held: quantity,
4169                reason,
4170                hold_type,
4171                ..Default::default()
4172            })
4173            .map_err(|e| PhpException::default(format!("Failed to create hold: {}", e)))?;
4174
4175        Ok(hold.id.to_string())
4176    }
4177
4178    pub fn release_hold(&self, id: String, released_by: String) -> PhpResult<bool> {
4179        let commerce = lock_commerce!(self.commerce);
4180        let uuid = parse_uuid!(id, "hold");
4181
4182        commerce
4183            .quality()
4184            .release_hold(uuid, &released_by)
4185            .map_err(|e| PhpException::default(format!("Failed to release hold: {}", e)))?;
4186
4187        Ok(true)
4188    }
4189}
4190
4191// ============================================================================
4192// Lots API
4193// ============================================================================
4194
4195#[php_class(name = "StateSet\\Lots")]
4196#[derive(Clone)]
4197pub struct Lots {
4198    commerce: Arc<Mutex<RustCommerce>>,
4199}
4200
4201#[php_impl]
4202impl Lots {
4203    pub fn create(&self, sku: String, quantity_produced: i32) -> PhpResult<String> {
4204        let commerce = lock_commerce!(self.commerce);
4205
4206        let lot = commerce
4207            .lots()
4208            .create(stateset_core::CreateLot { sku, quantity_produced, ..Default::default() })
4209            .map_err(|e| PhpException::default(format!("Failed to create lot: {}", e)))?;
4210
4211        Ok(lot.id.to_string())
4212    }
4213
4214    pub fn get(&self, id: String) -> PhpResult<Option<String>> {
4215        let commerce = lock_commerce!(self.commerce);
4216        let uuid = parse_uuid!(id, "lot");
4217
4218        let lot = commerce
4219            .lots()
4220            .get(uuid)
4221            .map_err(|e| PhpException::default(format!("Failed to get lot: {}", e)))?;
4222
4223        Ok(lot.map(|l| l.lot_number))
4224    }
4225
4226    pub fn list(&self) -> PhpResult<Vec<String>> {
4227        let commerce = lock_commerce!(self.commerce);
4228
4229        let lots = commerce
4230            .lots()
4231            .list(Default::default())
4232            .map_err(|e| PhpException::default(format!("Failed to list lots: {}", e)))?;
4233
4234        Ok(lots.into_iter().map(|l| l.id.to_string()).collect())
4235    }
4236
4237    pub fn quarantine(&self, id: String, reason: String) -> PhpResult<bool> {
4238        let commerce = lock_commerce!(self.commerce);
4239        let uuid = parse_uuid!(id, "lot");
4240
4241        commerce
4242            .lots()
4243            .quarantine(uuid, &reason)
4244            .map_err(|e| PhpException::default(format!("Failed to quarantine: {}", e)))?;
4245
4246        Ok(true)
4247    }
4248
4249    pub fn release_quarantine(&self, id: String) -> PhpResult<bool> {
4250        let commerce = lock_commerce!(self.commerce);
4251        let uuid = parse_uuid!(id, "lot");
4252
4253        commerce
4254            .lots()
4255            .release_quarantine(uuid)
4256            .map_err(|e| PhpException::default(format!("Failed to release: {}", e)))?;
4257
4258        Ok(true)
4259    }
4260}
4261
4262// ============================================================================
4263// Serials API
4264// ============================================================================
4265
4266#[php_class(name = "StateSet\\Serials")]
4267#[derive(Clone)]
4268pub struct Serials {
4269    commerce: Arc<Mutex<RustCommerce>>,
4270}
4271
4272#[php_impl]
4273impl Serials {
4274    pub fn create(&self, sku: String, lot_number: Option<String>) -> PhpResult<String> {
4275        let commerce = lock_commerce!(self.commerce);
4276
4277        let serial = commerce
4278            .serials()
4279            .create(stateset_core::CreateSerial { sku, lot_number, ..Default::default() })
4280            .map_err(|e| PhpException::default(format!("Failed to create serial: {}", e)))?;
4281
4282        Ok(serial.serial_number)
4283    }
4284
4285    pub fn get(&self, id: String) -> PhpResult<Option<String>> {
4286        let commerce = lock_commerce!(self.commerce);
4287        let uuid = parse_uuid!(id, "serial");
4288
4289        let serial = commerce
4290            .serials()
4291            .get(uuid)
4292            .map_err(|e| PhpException::default(format!("Failed to get serial: {}", e)))?;
4293
4294        Ok(serial.map(|s| s.serial_number))
4295    }
4296
4297    pub fn list(&self) -> PhpResult<Vec<String>> {
4298        let commerce = lock_commerce!(self.commerce);
4299
4300        let serials = commerce
4301            .serials()
4302            .list(Default::default())
4303            .map_err(|e| PhpException::default(format!("Failed to list serials: {}", e)))?;
4304
4305        Ok(serials.into_iter().map(|s| s.serial_number).collect())
4306    }
4307
4308    pub fn mark_sold(
4309        &self,
4310        id: String,
4311        customer_id: String,
4312        order_id: Option<String>,
4313    ) -> PhpResult<bool> {
4314        let commerce = lock_commerce!(self.commerce);
4315        let uuid = parse_uuid!(id, "serial");
4316        let cust_uuid = parse_uuid!(customer_id, "customer");
4317        let ord_uuid = order_id
4318            .map(|o| o.parse())
4319            .transpose()
4320            .map_err(|_| PhpException::default("Invalid order UUID".to_string()))?;
4321
4322        commerce
4323            .serials()
4324            .mark_sold(uuid, cust_uuid, ord_uuid)
4325            .map_err(|e| PhpException::default(format!("Failed to mark sold: {}", e)))?;
4326
4327        Ok(true)
4328    }
4329}
4330
4331// ============================================================================
4332// Warehouse API
4333// ============================================================================
4334
4335#[php_class(name = "StateSet\\Warehouse")]
4336#[derive(Clone)]
4337pub struct WarehouseApi {
4338    commerce: Arc<Mutex<RustCommerce>>,
4339}
4340
4341#[php_impl]
4342impl WarehouseApi {
4343    pub fn create_warehouse(
4344        &self,
4345        code: String,
4346        name: String,
4347        warehouse_type: String,
4348    ) -> PhpResult<i32> {
4349        let commerce = lock_commerce!(self.commerce);
4350
4351        let warehouse = commerce
4352            .warehouse()
4353            .create_warehouse(stateset_core::CreateWarehouse {
4354                code,
4355                name,
4356                warehouse_type,
4357                ..Default::default()
4358            })
4359            .map_err(|e| PhpException::default(format!("Failed to create warehouse: {}", e)))?;
4360
4361        Ok(warehouse.id)
4362    }
4363
4364    pub fn get_warehouse(&self, id: i32) -> PhpResult<Option<String>> {
4365        let commerce = lock_commerce!(self.commerce);
4366
4367        let warehouse = commerce
4368            .warehouse()
4369            .get_warehouse(id)
4370            .map_err(|e| PhpException::default(format!("Failed to get warehouse: {}", e)))?;
4371
4372        Ok(warehouse.map(|w| w.name))
4373    }
4374
4375    pub fn list_warehouses(&self) -> PhpResult<Vec<i32>> {
4376        let commerce = lock_commerce!(self.commerce);
4377
4378        let warehouses = commerce
4379            .warehouse()
4380            .list_warehouses()
4381            .map_err(|e| PhpException::default(format!("Failed to list warehouses: {}", e)))?;
4382
4383        Ok(warehouses.into_iter().map(|w| w.id).collect())
4384    }
4385
4386    pub fn create_location(
4387        &self,
4388        warehouse_id: i32,
4389        location_type: String,
4390        zone: Option<String>,
4391        aisle: Option<String>,
4392    ) -> PhpResult<i32> {
4393        let commerce = lock_commerce!(self.commerce);
4394
4395        let location = commerce
4396            .warehouse()
4397            .create_location(stateset_core::CreateLocation {
4398                warehouse_id,
4399                location_type,
4400                zone,
4401                aisle,
4402                ..Default::default()
4403            })
4404            .map_err(|e| PhpException::default(format!("Failed to create location: {}", e)))?;
4405
4406        Ok(location.id)
4407    }
4408}
4409
4410// ============================================================================
4411// Receiving API
4412// ============================================================================
4413
4414#[php_class(name = "StateSet\\Receiving")]
4415#[derive(Clone)]
4416pub struct Receiving {
4417    commerce: Arc<Mutex<RustCommerce>>,
4418}
4419
4420#[php_impl]
4421impl Receiving {
4422    pub fn create_receipt(
4423        &self,
4424        receipt_type: String,
4425        warehouse_id: i32,
4426        po_id: Option<String>,
4427    ) -> PhpResult<String> {
4428        let commerce = lock_commerce!(self.commerce);
4429        let po_uuid = po_id
4430            .map(|p| p.parse())
4431            .transpose()
4432            .map_err(|_| PhpException::default("Invalid PO UUID".to_string()))?;
4433
4434        let receipt = commerce
4435            .receiving()
4436            .create_receipt(stateset_core::CreateReceipt {
4437                receipt_type,
4438                warehouse_id,
4439                purchase_order_id: po_uuid,
4440                ..Default::default()
4441            })
4442            .map_err(|e| PhpException::default(format!("Failed to create receipt: {}", e)))?;
4443
4444        Ok(receipt.id.to_string())
4445    }
4446
4447    pub fn get_receipt(&self, id: String) -> PhpResult<Option<String>> {
4448        let commerce = lock_commerce!(self.commerce);
4449        let uuid = parse_uuid!(id, "receipt");
4450
4451        let receipt = commerce
4452            .receiving()
4453            .get_receipt(uuid)
4454            .map_err(|e| PhpException::default(format!("Failed to get receipt: {}", e)))?;
4455
4456        Ok(receipt.map(|r| r.receipt_number))
4457    }
4458
4459    pub fn list_receipts(&self) -> PhpResult<Vec<String>> {
4460        let commerce = lock_commerce!(self.commerce);
4461
4462        let receipts = commerce
4463            .receiving()
4464            .list_receipts(Default::default())
4465            .map_err(|e| PhpException::default(format!("Failed to list receipts: {}", e)))?;
4466
4467        Ok(receipts.into_iter().map(|r| r.id.to_string()).collect())
4468    }
4469
4470    pub fn complete_receipt(&self, id: String) -> PhpResult<bool> {
4471        let commerce = lock_commerce!(self.commerce);
4472        let uuid = parse_uuid!(id, "receipt");
4473
4474        commerce
4475            .receiving()
4476            .complete_receipt(uuid)
4477            .map_err(|e| PhpException::default(format!("Failed to complete receipt: {}", e)))?;
4478
4479        Ok(true)
4480    }
4481}
4482
4483// ============================================================================
4484// Fulfillment API
4485// ============================================================================
4486
4487#[php_class(name = "StateSet\\Fulfillment")]
4488#[derive(Clone)]
4489pub struct Fulfillment {
4490    commerce: Arc<Mutex<RustCommerce>>,
4491}
4492
4493#[php_impl]
4494impl Fulfillment {
4495    pub fn create_wave(
4496        &self,
4497        warehouse_id: i32,
4498        order_ids: Vec<String>,
4499        priority: i32,
4500    ) -> PhpResult<String> {
4501        let commerce = lock_commerce!(self.commerce);
4502        let uuids: Result<Vec<_>, _> = order_ids.iter().map(|id| id.parse()).collect();
4503        let uuids = uuids.map_err(|_| PhpException::default("Invalid order UUID".to_string()))?;
4504
4505        let wave = commerce
4506            .fulfillment()
4507            .create_wave(stateset_core::CreateWave {
4508                warehouse_id,
4509                order_ids: uuids,
4510                priority,
4511                ..Default::default()
4512            })
4513            .map_err(|e| PhpException::default(format!("Failed to create wave: {}", e)))?;
4514
4515        Ok(wave.id.to_string())
4516    }
4517
4518    pub fn get_wave(&self, id: String) -> PhpResult<Option<String>> {
4519        let commerce = lock_commerce!(self.commerce);
4520        let uuid = parse_uuid!(id, "wave");
4521
4522        let wave = commerce
4523            .fulfillment()
4524            .get_wave(uuid)
4525            .map_err(|e| PhpException::default(format!("Failed to get wave: {}", e)))?;
4526
4527        Ok(wave.map(|w| w.wave_number))
4528    }
4529
4530    pub fn release_wave(&self, id: String) -> PhpResult<bool> {
4531        let commerce = lock_commerce!(self.commerce);
4532        let uuid = parse_uuid!(id, "wave");
4533
4534        commerce
4535            .fulfillment()
4536            .release_wave(uuid)
4537            .map_err(|e| PhpException::default(format!("Failed to release wave: {}", e)))?;
4538
4539        Ok(true)
4540    }
4541
4542    pub fn complete_wave(&self, id: String) -> PhpResult<bool> {
4543        let commerce = lock_commerce!(self.commerce);
4544        let uuid = parse_uuid!(id, "wave");
4545
4546        commerce
4547            .fulfillment()
4548            .complete_wave(uuid)
4549            .map_err(|e| PhpException::default(format!("Failed to complete wave: {}", e)))?;
4550
4551        Ok(true)
4552    }
4553}
4554
4555// ============================================================================
4556// Accounts Payable API
4557// ============================================================================
4558
4559#[php_class(name = "StateSet\\AccountsPayable")]
4560#[derive(Clone)]
4561pub struct AccountsPayable {
4562    commerce: Arc<Mutex<RustCommerce>>,
4563}
4564
4565#[php_impl]
4566impl AccountsPayable {
4567    pub fn create_bill(
4568        &self,
4569        supplier_id: String,
4570        due_date: String,
4571        payment_terms: Option<String>,
4572    ) -> PhpResult<String> {
4573        let commerce = lock_commerce!(self.commerce);
4574        let supp_uuid = parse_uuid!(supplier_id, "supplier");
4575
4576        let bill = commerce
4577            .accounts_payable()
4578            .create_bill(stateset_core::CreateBill {
4579                supplier_id: supp_uuid,
4580                due_date,
4581                payment_terms,
4582                ..Default::default()
4583            })
4584            .map_err(|e| PhpException::default(format!("Failed to create bill: {}", e)))?;
4585
4586        Ok(bill.id.to_string())
4587    }
4588
4589    pub fn get_bill(&self, id: String) -> PhpResult<Option<String>> {
4590        let commerce = lock_commerce!(self.commerce);
4591        let uuid = parse_uuid!(id, "bill");
4592
4593        let bill = commerce
4594            .accounts_payable()
4595            .get_bill(uuid)
4596            .map_err(|e| PhpException::default(format!("Failed to get bill: {}", e)))?;
4597
4598        Ok(bill.map(|b| b.bill_number))
4599    }
4600
4601    pub fn list_bills(&self) -> PhpResult<Vec<String>> {
4602        let commerce = lock_commerce!(self.commerce);
4603
4604        let bills = commerce
4605            .accounts_payable()
4606            .list_bills(Default::default())
4607            .map_err(|e| PhpException::default(format!("Failed to list bills: {}", e)))?;
4608
4609        Ok(bills.into_iter().map(|b| b.id.to_string()).collect())
4610    }
4611
4612    pub fn get_total_outstanding(&self) -> PhpResult<f64> {
4613        let commerce = lock_commerce!(self.commerce);
4614
4615        let total = commerce
4616            .accounts_payable()
4617            .get_total_outstanding()
4618            .map_err(|e| PhpException::default(format!("Failed to get total: {}", e)))?;
4619
4620        Ok(to_f64_or_nan(total))
4621    }
4622}
4623
4624// ============================================================================
4625// Accounts Receivable API
4626// ============================================================================
4627
4628#[php_class(name = "StateSet\\AccountsReceivable")]
4629#[derive(Clone)]
4630pub struct AccountsReceivable {
4631    commerce: Arc<Mutex<RustCommerce>>,
4632}
4633
4634#[php_impl]
4635impl AccountsReceivable {
4636    pub fn get_total_outstanding(&self) -> PhpResult<f64> {
4637        let commerce = lock_commerce!(self.commerce);
4638
4639        let total = commerce
4640            .accounts_receivable()
4641            .get_total_outstanding()
4642            .map_err(|e| PhpException::default(format!("Failed to get total: {}", e)))?;
4643
4644        Ok(to_f64_or_nan(total))
4645    }
4646
4647    pub fn get_dso(&self, days: i32) -> PhpResult<f64> {
4648        let commerce = lock_commerce!(self.commerce);
4649
4650        let dso = commerce
4651            .accounts_receivable()
4652            .get_dso(days)
4653            .map_err(|e| PhpException::default(format!("Failed to get DSO: {}", e)))?;
4654
4655        Ok(dso)
4656    }
4657
4658    pub fn create_credit_memo(
4659        &self,
4660        customer_id: String,
4661        amount: f64,
4662        reason: String,
4663    ) -> PhpResult<String> {
4664        let commerce = lock_commerce!(self.commerce);
4665        let cust_uuid = parse_uuid!(customer_id, "customer");
4666        let decimal_amount = Decimal::from_f64_retain(amount).unwrap_or_default();
4667
4668        let memo = commerce
4669            .accounts_receivable()
4670            .create_credit_memo(stateset_core::CreateCreditMemo {
4671                customer_id: cust_uuid,
4672                amount: decimal_amount,
4673                reason,
4674                ..Default::default()
4675            })
4676            .map_err(|e| PhpException::default(format!("Failed to create credit memo: {}", e)))?;
4677
4678        Ok(memo.id.to_string())
4679    }
4680}
4681
4682// ============================================================================
4683// Cost Accounting API
4684// ============================================================================
4685
4686#[php_class(name = "StateSet\\CostAccounting")]
4687#[derive(Clone)]
4688pub struct CostAccounting {
4689    commerce: Arc<Mutex<RustCommerce>>,
4690}
4691
4692#[php_impl]
4693impl CostAccounting {
4694    pub fn get_item_cost(&self, sku: String) -> PhpResult<Option<f64>> {
4695        let commerce = lock_commerce!(self.commerce);
4696
4697        let cost = commerce
4698            .cost_accounting()
4699            .get_item_cost(&sku)
4700            .map_err(|e| PhpException::default(format!("Failed to get cost: {}", e)))?;
4701
4702        Ok(cost.map(|c| to_f64_or_nan(c.current_cost)))
4703    }
4704
4705    pub fn set_item_cost(
4706        &self,
4707        sku: String,
4708        standard_cost: f64,
4709        current_cost: Option<f64>,
4710    ) -> PhpResult<bool> {
4711        let commerce = lock_commerce!(self.commerce);
4712        let std = Decimal::from_f64_retain(standard_cost).unwrap_or_default();
4713        let curr = current_cost.map(|c| Decimal::from_f64_retain(c).unwrap_or_default());
4714
4715        commerce
4716            .cost_accounting()
4717            .set_item_cost(&sku, std, curr)
4718            .map_err(|e| PhpException::default(format!("Failed to set cost: {}", e)))?;
4719
4720        Ok(true)
4721    }
4722
4723    pub fn get_total_inventory_value(&self) -> PhpResult<f64> {
4724        let commerce = lock_commerce!(self.commerce);
4725
4726        let total = commerce
4727            .cost_accounting()
4728            .get_total_inventory_value()
4729            .map_err(|e| PhpException::default(format!("Failed to get total: {}", e)))?;
4730
4731        Ok(to_f64_or_nan(total))
4732    }
4733}
4734
4735// ============================================================================
4736// Credit API
4737// ============================================================================
4738
4739#[php_class(name = "StateSet\\Credit")]
4740#[derive(Clone)]
4741pub struct CreditApi {
4742    commerce: Arc<Mutex<RustCommerce>>,
4743}
4744
4745#[php_impl]
4746impl CreditApi {
4747    pub fn create_account(&self, customer_id: String, credit_limit: f64) -> PhpResult<String> {
4748        let commerce = lock_commerce!(self.commerce);
4749        let cust_uuid = parse_uuid!(customer_id, "customer");
4750        let limit = Decimal::from_f64_retain(credit_limit).unwrap_or_default();
4751
4752        let account = commerce
4753            .credit()
4754            .create_account(stateset_core::CreateCreditAccount {
4755                customer_id: cust_uuid,
4756                credit_limit: limit,
4757                ..Default::default()
4758            })
4759            .map_err(|e| PhpException::default(format!("Failed to create account: {}", e)))?;
4760
4761        Ok(account.id.to_string())
4762    }
4763
4764    pub fn check_credit(&self, customer_id: String, order_amount: f64) -> PhpResult<bool> {
4765        let commerce = lock_commerce!(self.commerce);
4766        let cust_uuid = parse_uuid!(customer_id, "customer");
4767        let amount = Decimal::from_f64_retain(order_amount).unwrap_or_default();
4768
4769        let result = commerce
4770            .credit()
4771            .check_credit(cust_uuid, amount)
4772            .map_err(|e| PhpException::default(format!("Failed to check credit: {}", e)))?;
4773
4774        Ok(result.approved)
4775    }
4776
4777    pub fn adjust_limit(
4778        &self,
4779        customer_id: String,
4780        new_limit: f64,
4781        reason: String,
4782    ) -> PhpResult<bool> {
4783        let commerce = lock_commerce!(self.commerce);
4784        let cust_uuid = parse_uuid!(customer_id, "customer");
4785        let limit = Decimal::from_f64_retain(new_limit).unwrap_or_default();
4786
4787        commerce
4788            .credit()
4789            .adjust_limit(cust_uuid, limit, &reason)
4790            .map_err(|e| PhpException::default(format!("Failed to adjust limit: {}", e)))?;
4791
4792        Ok(true)
4793    }
4794}
4795
4796// ============================================================================
4797// Backorders API
4798// ============================================================================
4799
4800#[php_class(name = "StateSet\\Backorders")]
4801#[derive(Clone)]
4802pub struct Backorders {
4803    commerce: Arc<Mutex<RustCommerce>>,
4804}
4805
4806#[php_impl]
4807impl Backorders {
4808    pub fn create(
4809        &self,
4810        order_id: String,
4811        sku: String,
4812        quantity: i32,
4813        expected_date: Option<String>,
4814    ) -> PhpResult<String> {
4815        let commerce = lock_commerce!(self.commerce);
4816        let ord_uuid = parse_uuid!(order_id, "order");
4817
4818        let backorder = commerce
4819            .backorders()
4820            .create(stateset_core::CreateBackorder {
4821                order_id: ord_uuid,
4822                sku,
4823                quantity,
4824                expected_date,
4825                ..Default::default()
4826            })
4827            .map_err(|e| PhpException::default(format!("Failed to create backorder: {}", e)))?;
4828
4829        Ok(backorder.id.to_string())
4830    }
4831
4832    pub fn get(&self, id: String) -> PhpResult<Option<String>> {
4833        let commerce = lock_commerce!(self.commerce);
4834        let uuid = parse_uuid!(id, "backorder");
4835
4836        let backorder = commerce
4837            .backorders()
4838            .get(uuid)
4839            .map_err(|e| PhpException::default(format!("Failed to get backorder: {}", e)))?;
4840
4841        Ok(backorder.map(|b| b.backorder_number))
4842    }
4843
4844    pub fn list(&self) -> PhpResult<Vec<String>> {
4845        let commerce = lock_commerce!(self.commerce);
4846
4847        let backorders = commerce
4848            .backorders()
4849            .list(Default::default())
4850            .map_err(|e| PhpException::default(format!("Failed to list backorders: {}", e)))?;
4851
4852        Ok(backorders.into_iter().map(|b| b.id.to_string()).collect())
4853    }
4854
4855    pub fn cancel(&self, id: String) -> PhpResult<bool> {
4856        let commerce = lock_commerce!(self.commerce);
4857        let uuid = parse_uuid!(id, "backorder");
4858
4859        commerce
4860            .backorders()
4861            .cancel(uuid)
4862            .map_err(|e| PhpException::default(format!("Failed to cancel backorder: {}", e)))?;
4863
4864        Ok(true)
4865    }
4866
4867    pub fn count_pending(&self) -> PhpResult<i32> {
4868        let commerce = lock_commerce!(self.commerce);
4869
4870        let count = commerce
4871            .backorders()
4872            .count_pending()
4873            .map_err(|e| PhpException::default(format!("Failed to count: {}", e)))?;
4874
4875        Ok(count as i32)
4876    }
4877}
4878
4879// ============================================================================
4880// General Ledger API
4881// ============================================================================
4882
4883#[php_class(name = "StateSet\\GeneralLedger")]
4884#[derive(Clone)]
4885pub struct GeneralLedger {
4886    commerce: Arc<Mutex<RustCommerce>>,
4887}
4888
4889#[php_impl]
4890impl GeneralLedger {
4891    pub fn create_account(
4892        &self,
4893        account_number: String,
4894        name: String,
4895        account_type: String,
4896    ) -> PhpResult<String> {
4897        let commerce = lock_commerce!(self.commerce);
4898
4899        let account = commerce
4900            .general_ledger()
4901            .create_account(stateset_core::CreateGlAccount {
4902                account_number,
4903                name,
4904                account_type,
4905                ..Default::default()
4906            })
4907            .map_err(|e| PhpException::default(format!("Failed to create account: {}", e)))?;
4908
4909        Ok(account.id.to_string())
4910    }
4911
4912    pub fn get_account(&self, id: String) -> PhpResult<Option<String>> {
4913        let commerce = lock_commerce!(self.commerce);
4914        let uuid = parse_uuid!(id, "account");
4915
4916        let account = commerce
4917            .general_ledger()
4918            .get_account(uuid)
4919            .map_err(|e| PhpException::default(format!("Failed to get account: {}", e)))?;
4920
4921        Ok(account.map(|a| a.name))
4922    }
4923
4924    pub fn list_accounts(&self) -> PhpResult<Vec<String>> {
4925        let commerce = lock_commerce!(self.commerce);
4926
4927        let accounts = commerce
4928            .general_ledger()
4929            .list_accounts()
4930            .map_err(|e| PhpException::default(format!("Failed to list accounts: {}", e)))?;
4931
4932        Ok(accounts.into_iter().map(|a| a.id.to_string()).collect())
4933    }
4934
4935    pub fn get_account_balance(
4936        &self,
4937        account_id: String,
4938        as_of_date: Option<String>,
4939    ) -> PhpResult<f64> {
4940        let commerce = lock_commerce!(self.commerce);
4941        let uuid = parse_uuid!(account_id, "account");
4942
4943        let balance = commerce
4944            .general_ledger()
4945            .get_account_balance(uuid, as_of_date.as_deref())
4946            .map_err(|e| PhpException::default(format!("Failed to get balance: {}", e)))?;
4947
4948        Ok(to_f64_or_nan(balance))
4949    }
4950
4951    pub fn initialize_chart_of_accounts(&self) -> PhpResult<Vec<String>> {
4952        let commerce = lock_commerce!(self.commerce);
4953
4954        let accounts = commerce
4955            .general_ledger()
4956            .initialize_chart_of_accounts()
4957            .map_err(|e| PhpException::default(format!("Failed to initialize COA: {}", e)))?;
4958
4959        Ok(accounts.into_iter().map(|a| a.id.to_string()).collect())
4960    }
4961}
4962
4963// ============================================================================
4964// Module Registration
4965// ============================================================================
4966
4967#[php_module]
4968pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
4969    module
4970}