Skip to main content

stateset_embedded_wasm/
lib.rs

1//! WASM bindings for StateSet Embedded Commerce
2//!
3//! Provides an in-memory commerce library for browser environments.
4//!
5//! ```javascript
6//! import init, { Commerce } from '@stateset/embedded-wasm';
7//!
8//! async function main() {
9//!   await init();
10//!   const commerce = new Commerce();
11//!   const customer = commerce.createCustomer({
12//!     email: "alice@example.com",
13//!     firstName: "Alice",
14//!     lastName: "Smith"
15//!   });
16//! }
17//! ```
18
19#![allow(dead_code)]
20
21use chrono::Utc;
22use serde::{Deserialize, Serialize};
23use std::cell::RefCell;
24use std::collections::HashMap;
25use std::fmt;
26use std::rc::Rc;
27use uuid::Uuid;
28use wasm_bindgen::prelude::*;
29
30// Initialize panic hook for better error messages
31#[wasm_bindgen(start)]
32pub fn init() {
33    console_error_panic_hook::set_once();
34}
35
36// ============================================================================
37// In-Memory Store
38// ============================================================================
39
40#[derive(Default)]
41struct Store {
42    customers: HashMap<Uuid, CustomerData>,
43    orders: HashMap<Uuid, OrderData>,
44    order_items: HashMap<Uuid, Vec<OrderItemData>>,
45    products: HashMap<Uuid, ProductData>,
46    variants: HashMap<Uuid, VariantData>,
47    inventory_items: HashMap<i64, InventoryItemData>,
48    inventory_by_sku: HashMap<String, i64>,
49    inventory_balances: HashMap<i64, InventoryBalanceData>,
50    reservations: HashMap<Uuid, ReservationData>,
51    returns: HashMap<Uuid, ReturnData>,
52    return_items: HashMap<Uuid, Vec<ReturnItemData>>,
53    // New modules
54    payments: HashMap<Uuid, PaymentData>,
55    refunds: HashMap<Uuid, RefundData>,
56    shipments: HashMap<Uuid, ShipmentData>,
57    warranties: HashMap<Uuid, WarrantyData>,
58    warranty_claims: HashMap<Uuid, WarrantyClaimData>,
59    suppliers: HashMap<Uuid, SupplierData>,
60    purchase_orders: HashMap<Uuid, PurchaseOrderData>,
61    invoices: HashMap<Uuid, InvoiceData>,
62    boms: HashMap<Uuid, BomData>,
63    bom_components: HashMap<Uuid, Vec<BomComponentData>>,
64    work_orders: HashMap<Uuid, WorkOrderData>,
65    // Carts
66    carts: HashMap<Uuid, CartData>,
67    cart_items: HashMap<Uuid, Vec<CartItemData>>,
68    // Subscriptions
69    subscription_plans: HashMap<Uuid, SubscriptionPlanData>,
70    subscriptions: HashMap<Uuid, SubscriptionData>,
71    billing_cycles: HashMap<Uuid, BillingCycleData>,
72    subscription_events: HashMap<Uuid, Vec<SubscriptionEventData>>,
73    // Promotions
74    promotions: HashMap<Uuid, PromotionData>,
75    coupons: HashMap<Uuid, CouponData>,
76    promotion_usages: Vec<PromotionUsageData>,
77    // Tax
78    tax_jurisdictions: HashMap<Uuid, TaxJurisdictionData>,
79    tax_rates: HashMap<Uuid, TaxRateData>,
80    tax_exemptions: HashMap<Uuid, TaxExemptionData>,
81    tax_settings: Option<TaxSettingsData>,
82    // Counters
83    next_inventory_id: i64,
84    next_order_number: u64,
85    next_payment_number: u64,
86    next_shipment_number: u64,
87    next_warranty_number: u64,
88    next_claim_number: u64,
89    next_supplier_code: u64,
90    next_po_number: u64,
91    next_invoice_number: u64,
92    next_bom_number: u64,
93    next_work_order_number: u64,
94    next_cart_number: u64,
95    next_plan_number: u64,
96    next_subscription_number: u64,
97    next_billing_cycle_number: u64,
98    next_promotion_code_number: u64,
99}
100
101type StoreRef = Rc<RefCell<Store>>;
102
103// ============================================================================
104// Money Helpers
105// ============================================================================
106
107// Monetary values are stored in minor units (cents), rounded to 2 decimals.
108#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
109struct Money(i64);
110
111impl Money {
112    const SCALE: i64 = 100;
113
114    fn zero() -> Self {
115        Money(0)
116    }
117
118    fn from_f64(value: f64) -> Self {
119        if value.is_finite() {
120            Money((value * Self::SCALE as f64).round() as i64)
121        } else {
122            Money::zero()
123        }
124    }
125
126    fn to_f64(self) -> f64 {
127        self.0 as f64 / Self::SCALE as f64
128    }
129
130    fn mul_rate(self, rate: f64) -> Self {
131        if !rate.is_finite() {
132            return Money::zero();
133        }
134        Money(((self.0 as f64) * rate).round() as i64)
135    }
136}
137
138impl std::ops::Add for Money {
139    type Output = Money;
140
141    fn add(self, rhs: Money) -> Money {
142        Money(self.0 + rhs.0)
143    }
144}
145
146impl std::ops::AddAssign for Money {
147    fn add_assign(&mut self, rhs: Money) {
148        self.0 += rhs.0;
149    }
150}
151
152impl std::ops::Sub for Money {
153    type Output = Money;
154
155    fn sub(self, rhs: Money) -> Money {
156        Money(self.0 - rhs.0)
157    }
158}
159
160impl std::ops::SubAssign for Money {
161    fn sub_assign(&mut self, rhs: Money) {
162        self.0 -= rhs.0;
163    }
164}
165
166impl std::ops::Mul<i32> for Money {
167    type Output = Money;
168
169    fn mul(self, rhs: i32) -> Money {
170        Money(self.0.saturating_mul(rhs as i64))
171    }
172}
173
174impl Serialize for Money {
175    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
176    where
177        S: serde::Serializer,
178    {
179        serializer.serialize_f64(self.to_f64())
180    }
181}
182
183impl<'de> Deserialize<'de> for Money {
184    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
185    where
186        D: serde::Deserializer<'de>,
187    {
188        struct MoneyVisitor;
189
190        impl<'de> serde::de::Visitor<'de> for MoneyVisitor {
191            type Value = Money;
192
193            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
194                formatter.write_str("a monetary amount")
195            }
196
197            fn visit_f64<E>(self, value: f64) -> Result<Money, E>
198            where
199                E: serde::de::Error,
200            {
201                Ok(Money::from_f64(value))
202            }
203
204            fn visit_i64<E>(self, value: i64) -> Result<Money, E>
205            where
206                E: serde::de::Error,
207            {
208                Ok(Money::from_f64(value as f64))
209            }
210
211            fn visit_u64<E>(self, value: u64) -> Result<Money, E>
212            where
213                E: serde::de::Error,
214            {
215                Ok(Money::from_f64(value as f64))
216            }
217
218            fn visit_str<E>(self, value: &str) -> Result<Money, E>
219            where
220                E: serde::de::Error,
221            {
222                let parsed: f64 = value.parse().map_err(E::custom)?;
223                Ok(Money::from_f64(parsed))
224            }
225
226            fn visit_string<E>(self, value: String) -> Result<Money, E>
227            where
228                E: serde::de::Error,
229            {
230                self.visit_str(&value)
231            }
232        }
233
234        deserializer.deserialize_any(MoneyVisitor)
235    }
236}
237
238// ============================================================================
239// Internal Data Types
240// ============================================================================
241
242#[derive(Clone)]
243struct CustomerData {
244    id: Uuid,
245    email: String,
246    first_name: String,
247    last_name: String,
248    phone: Option<String>,
249    status: String,
250    accepts_marketing: bool,
251    created_at: String,
252    updated_at: String,
253}
254
255#[derive(Clone)]
256struct OrderData {
257    id: Uuid,
258    order_number: String,
259    customer_id: Uuid,
260    status: String,
261    total_amount: Money,
262    currency: String,
263    payment_status: String,
264    fulfillment_status: String,
265    tracking_number: Option<String>,
266    notes: Option<String>,
267    version: i32,
268    created_at: String,
269    updated_at: String,
270}
271
272#[derive(Clone, Serialize)]
273struct OrderItemData {
274    id: Uuid,
275    order_id: Uuid,
276    sku: String,
277    name: String,
278    quantity: i32,
279    unit_price: Money,
280    total: Money,
281}
282
283#[derive(Clone)]
284struct ProductData {
285    id: Uuid,
286    name: String,
287    slug: String,
288    description: String,
289    status: String,
290    created_at: String,
291    updated_at: String,
292}
293
294#[derive(Clone)]
295struct VariantData {
296    id: Uuid,
297    product_id: Uuid,
298    sku: String,
299    name: String,
300    price: Money,
301    compare_at_price: Option<Money>,
302    is_default: bool,
303}
304
305#[derive(Clone)]
306struct InventoryItemData {
307    id: i64,
308    sku: String,
309    name: String,
310    description: Option<String>,
311    unit_of_measure: String,
312    is_active: bool,
313}
314
315#[derive(Clone)]
316struct InventoryBalanceData {
317    item_id: i64,
318    on_hand: f64,
319    allocated: f64,
320}
321
322#[derive(Clone)]
323struct ReservationData {
324    id: Uuid,
325    item_id: i64,
326    quantity: f64,
327    status: String,
328    reference_type: String,
329    reference_id: String,
330}
331
332#[derive(Clone)]
333struct ReturnData {
334    id: Uuid,
335    order_id: Uuid,
336    status: String,
337    reason: String,
338    reason_details: Option<String>,
339    version: i32,
340    created_at: String,
341}
342
343#[derive(Clone)]
344struct ReturnItemData {
345    id: Uuid,
346    return_id: Uuid,
347    order_item_id: Uuid,
348    quantity: i32,
349}
350
351#[derive(Clone)]
352struct PaymentData {
353    id: Uuid,
354    payment_number: String,
355    order_id: Option<Uuid>,
356    customer_id: Option<Uuid>,
357    amount: Money,
358    currency: String,
359    status: String,
360    payment_method: Option<String>,
361    version: i32,
362    created_at: String,
363    updated_at: String,
364}
365
366#[derive(Clone)]
367struct RefundData {
368    id: Uuid,
369    payment_id: Uuid,
370    amount: Money,
371    reason: Option<String>,
372    status: String,
373    created_at: String,
374}
375
376#[derive(Clone)]
377struct ShipmentData {
378    id: Uuid,
379    shipment_number: String,
380    order_id: Uuid,
381    carrier: Option<String>,
382    tracking_number: Option<String>,
383    status: String,
384    shipped_at: Option<String>,
385    delivered_at: Option<String>,
386    version: i32,
387    created_at: String,
388    updated_at: String,
389}
390
391#[derive(Clone)]
392struct WarrantyData {
393    id: Uuid,
394    warranty_number: String,
395    customer_id: Uuid,
396    product_id: Option<Uuid>,
397    order_id: Option<Uuid>,
398    status: String,
399    duration_months: i32,
400    start_date: String,
401    end_date: String,
402    created_at: String,
403}
404
405#[derive(Clone)]
406struct WarrantyClaimData {
407    id: Uuid,
408    claim_number: String,
409    warranty_id: Uuid,
410    issue_description: String,
411    status: String,
412    resolution: Option<String>,
413    created_at: String,
414}
415
416#[derive(Clone)]
417struct SupplierData {
418    id: Uuid,
419    supplier_code: String,
420    name: String,
421    email: Option<String>,
422    phone: Option<String>,
423    status: String,
424    created_at: String,
425}
426
427#[derive(Clone)]
428struct PurchaseOrderData {
429    id: Uuid,
430    po_number: String,
431    supplier_id: Uuid,
432    status: String,
433    total_amount: Money,
434    currency: String,
435    created_at: String,
436    updated_at: String,
437}
438
439#[derive(Clone)]
440struct InvoiceData {
441    id: Uuid,
442    invoice_number: String,
443    customer_id: Uuid,
444    order_id: Option<Uuid>,
445    status: String,
446    subtotal: Money,
447    tax_amount: Money,
448    total: Money,
449    amount_paid: Money,
450    due_date: Option<String>,
451    created_at: String,
452    updated_at: String,
453}
454
455#[derive(Clone)]
456struct BomData {
457    id: Uuid,
458    bom_number: String,
459    sku: String,
460    name: String,
461    description: Option<String>,
462    status: String,
463    version: i32,
464    created_at: String,
465    updated_at: String,
466}
467
468#[derive(Clone)]
469struct BomComponentData {
470    id: Uuid,
471    bom_id: Uuid,
472    component_sku: String,
473    component_name: String,
474    quantity: f64,
475    unit_of_measure: String,
476}
477
478#[derive(Clone)]
479struct WorkOrderData {
480    id: Uuid,
481    work_order_number: String,
482    bom_id: Uuid,
483    status: String,
484    quantity_to_build: f64,
485    quantity_built: f64,
486    priority: String,
487    scheduled_start: Option<String>,
488    scheduled_end: Option<String>,
489    version: i32,
490    created_at: String,
491    updated_at: String,
492}
493
494#[derive(Clone)]
495struct CartData {
496    id: Uuid,
497    cart_number: String,
498    customer_id: Option<Uuid>,
499    status: String,
500    currency: String,
501    subtotal: Money,
502    tax_amount: Money,
503    shipping_amount: Money,
504    discount_amount: Money,
505    grand_total: Money,
506    customer_email: Option<String>,
507    customer_name: Option<String>,
508    payment_method: Option<String>,
509    payment_status: String,
510    fulfillment_type: String,
511    shipping_method: Option<String>,
512    coupon_code: Option<String>,
513    notes: Option<String>,
514    created_at: String,
515    updated_at: String,
516    expires_at: Option<String>,
517}
518
519#[derive(Clone)]
520struct CartItemData {
521    id: Uuid,
522    cart_id: Uuid,
523    sku: String,
524    name: String,
525    description: Option<String>,
526    quantity: i32,
527    unit_price: Money,
528    total: Money,
529}
530
531#[derive(Clone, Serialize)]
532struct CartAddressData {
533    first_name: String,
534    last_name: String,
535    line1: String,
536    city: String,
537    postal_code: String,
538    country: String,
539    company: Option<String>,
540    line2: Option<String>,
541    state: Option<String>,
542    phone: Option<String>,
543    email: Option<String>,
544}
545
546// Subscription data types
547#[derive(Clone)]
548struct SubscriptionPlanData {
549    id: Uuid,
550    code: String,
551    name: String,
552    description: Option<String>,
553    billing_interval: String,
554    billing_interval_count: i32,
555    price: Money,
556    currency: String,
557    setup_fee: Money,
558    trial_days: i32,
559    status: String,
560    created_at: String,
561    updated_at: String,
562}
563
564#[derive(Clone)]
565struct SubscriptionData {
566    id: Uuid,
567    subscription_number: String,
568    customer_id: Uuid,
569    plan_id: Uuid,
570    status: String,
571    current_period_start: String,
572    current_period_end: String,
573    trial_start: Option<String>,
574    trial_end: Option<String>,
575    cancelled_at: Option<String>,
576    cancel_at_period_end: bool,
577    pause_start: Option<String>,
578    pause_end: Option<String>,
579    price: Money,
580    currency: String,
581    created_at: String,
582    updated_at: String,
583}
584
585#[derive(Clone)]
586struct BillingCycleData {
587    id: Uuid,
588    cycle_number: String,
589    subscription_id: Uuid,
590    status: String,
591    period_start: String,
592    period_end: String,
593    amount: Money,
594    currency: String,
595    payment_id: Option<Uuid>,
596    invoice_id: Option<Uuid>,
597    created_at: String,
598    updated_at: String,
599}
600
601#[derive(Clone)]
602struct SubscriptionEventData {
603    id: Uuid,
604    subscription_id: Uuid,
605    event_type: String,
606    description: String,
607    created_at: String,
608}
609
610#[derive(Clone)]
611struct PromotionData {
612    id: Uuid,
613    code: String,
614    name: String,
615    description: Option<String>,
616    promotion_type: String,
617    trigger: String,
618    target: String,
619    stacking: String,
620    status: String,
621    percentage_off: Option<f64>,
622    fixed_amount_off: Option<Money>,
623    max_discount_amount: Option<Money>,
624    buy_quantity: Option<i32>,
625    get_quantity: Option<i32>,
626    starts_at: String,
627    ends_at: Option<String>,
628    total_usage_limit: Option<i32>,
629    per_customer_limit: Option<i32>,
630    usage_count: i32,
631    currency: String,
632    priority: i32,
633    created_at: String,
634    updated_at: String,
635}
636
637#[derive(Clone)]
638struct CouponData {
639    id: Uuid,
640    promotion_id: Uuid,
641    code: String,
642    status: String,
643    usage_limit: Option<i32>,
644    per_customer_limit: Option<i32>,
645    usage_count: i32,
646    starts_at: Option<String>,
647    ends_at: Option<String>,
648    created_at: String,
649    updated_at: String,
650}
651
652#[derive(Clone)]
653struct PromotionUsageData {
654    id: Uuid,
655    promotion_id: Uuid,
656    coupon_id: Option<Uuid>,
657    customer_id: Option<Uuid>,
658    order_id: Option<Uuid>,
659    cart_id: Option<Uuid>,
660    discount_amount: Money,
661    currency: String,
662    used_at: String,
663}
664
665// ============================================================================
666// JS Return Types (plain objects via serde)
667// ============================================================================
668
669#[derive(Serialize)]
670#[serde(rename_all = "camelCase")]
671struct JsCustomer {
672    id: String,
673    email: String,
674    first_name: String,
675    last_name: String,
676    full_name: String,
677    phone: Option<String>,
678    status: String,
679    accepts_marketing: bool,
680    created_at: String,
681    updated_at: String,
682}
683
684impl From<&CustomerData> for JsCustomer {
685    fn from(data: &CustomerData) -> Self {
686        JsCustomer {
687            id: data.id.to_string(),
688            email: data.email.clone(),
689            first_name: data.first_name.clone(),
690            last_name: data.last_name.clone(),
691            full_name: format!("{} {}", data.first_name, data.last_name),
692            phone: data.phone.clone(),
693            status: data.status.clone(),
694            accepts_marketing: data.accepts_marketing,
695            created_at: data.created_at.clone(),
696            updated_at: data.updated_at.clone(),
697        }
698    }
699}
700
701#[derive(Serialize)]
702#[serde(rename_all = "camelCase")]
703struct JsOrderItem {
704    id: String,
705    sku: String,
706    name: String,
707    quantity: i32,
708    unit_price: Money,
709    total: Money,
710}
711
712impl From<&OrderItemData> for JsOrderItem {
713    fn from(data: &OrderItemData) -> Self {
714        JsOrderItem {
715            id: data.id.to_string(),
716            sku: data.sku.clone(),
717            name: data.name.clone(),
718            quantity: data.quantity,
719            unit_price: data.unit_price,
720            total: data.total,
721        }
722    }
723}
724
725#[derive(Serialize)]
726#[serde(rename_all = "camelCase")]
727struct JsOrder {
728    id: String,
729    order_number: String,
730    customer_id: String,
731    status: String,
732    total_amount: Money,
733    currency: String,
734    payment_status: String,
735    fulfillment_status: String,
736    tracking_number: Option<String>,
737    items: Vec<JsOrderItem>,
738    version: i32,
739    created_at: String,
740    updated_at: String,
741}
742
743#[derive(Serialize)]
744#[serde(rename_all = "camelCase")]
745struct JsProduct {
746    id: String,
747    name: String,
748    slug: String,
749    description: String,
750    status: String,
751    created_at: String,
752    updated_at: String,
753}
754
755impl From<&ProductData> for JsProduct {
756    fn from(data: &ProductData) -> Self {
757        JsProduct {
758            id: data.id.to_string(),
759            name: data.name.clone(),
760            slug: data.slug.clone(),
761            description: data.description.clone(),
762            status: data.status.clone(),
763            created_at: data.created_at.clone(),
764            updated_at: data.updated_at.clone(),
765        }
766    }
767}
768
769#[derive(Serialize)]
770#[serde(rename_all = "camelCase")]
771struct JsProductVariant {
772    id: String,
773    product_id: String,
774    sku: String,
775    name: String,
776    price: Money,
777    compare_at_price: Option<Money>,
778    is_default: bool,
779}
780
781impl From<&VariantData> for JsProductVariant {
782    fn from(data: &VariantData) -> Self {
783        JsProductVariant {
784            id: data.id.to_string(),
785            product_id: data.product_id.to_string(),
786            sku: data.sku.clone(),
787            name: data.name.clone(),
788            price: data.price,
789            compare_at_price: data.compare_at_price,
790            is_default: data.is_default,
791        }
792    }
793}
794
795#[derive(Serialize)]
796#[serde(rename_all = "camelCase")]
797struct JsInventoryItem {
798    id: i64,
799    sku: String,
800    name: String,
801    description: Option<String>,
802    unit_of_measure: String,
803    is_active: bool,
804}
805
806impl From<&InventoryItemData> for JsInventoryItem {
807    fn from(data: &InventoryItemData) -> Self {
808        JsInventoryItem {
809            id: data.id,
810            sku: data.sku.clone(),
811            name: data.name.clone(),
812            description: data.description.clone(),
813            unit_of_measure: data.unit_of_measure.clone(),
814            is_active: data.is_active,
815        }
816    }
817}
818
819#[derive(Serialize)]
820#[serde(rename_all = "camelCase")]
821struct JsStockLevel {
822    sku: String,
823    name: String,
824    total_on_hand: f64,
825    total_allocated: f64,
826    total_available: f64,
827}
828
829#[derive(Serialize)]
830#[serde(rename_all = "camelCase")]
831struct JsReservation {
832    id: String,
833    item_id: i64,
834    quantity: f64,
835    status: String,
836}
837
838impl From<&ReservationData> for JsReservation {
839    fn from(data: &ReservationData) -> Self {
840        JsReservation {
841            id: data.id.to_string(),
842            item_id: data.item_id,
843            quantity: data.quantity,
844            status: data.status.clone(),
845        }
846    }
847}
848
849#[derive(Serialize)]
850#[serde(rename_all = "camelCase")]
851struct JsReturn {
852    id: String,
853    order_id: String,
854    status: String,
855    reason: String,
856    reason_details: Option<String>,
857    version: i32,
858    created_at: String,
859}
860
861impl From<&ReturnData> for JsReturn {
862    fn from(data: &ReturnData) -> Self {
863        JsReturn {
864            id: data.id.to_string(),
865            order_id: data.order_id.to_string(),
866            status: data.status.clone(),
867            reason: data.reason.clone(),
868            reason_details: data.reason_details.clone(),
869            version: data.version,
870            created_at: data.created_at.clone(),
871        }
872    }
873}
874
875#[derive(Serialize)]
876#[serde(rename_all = "camelCase")]
877struct JsPayment {
878    id: String,
879    payment_number: String,
880    order_id: Option<String>,
881    customer_id: Option<String>,
882    amount: Money,
883    currency: String,
884    status: String,
885    payment_method: Option<String>,
886    version: i32,
887    created_at: String,
888    updated_at: String,
889}
890
891impl From<&PaymentData> for JsPayment {
892    fn from(data: &PaymentData) -> Self {
893        JsPayment {
894            id: data.id.to_string(),
895            payment_number: data.payment_number.clone(),
896            order_id: data.order_id.map(|id| id.to_string()),
897            customer_id: data.customer_id.map(|id| id.to_string()),
898            amount: data.amount,
899            currency: data.currency.clone(),
900            status: data.status.clone(),
901            payment_method: data.payment_method.clone(),
902            version: data.version,
903            created_at: data.created_at.clone(),
904            updated_at: data.updated_at.clone(),
905        }
906    }
907}
908
909#[derive(Serialize)]
910#[serde(rename_all = "camelCase")]
911struct JsRefund {
912    id: String,
913    payment_id: String,
914    amount: Money,
915    reason: Option<String>,
916    status: String,
917    created_at: String,
918}
919
920impl From<&RefundData> for JsRefund {
921    fn from(data: &RefundData) -> Self {
922        JsRefund {
923            id: data.id.to_string(),
924            payment_id: data.payment_id.to_string(),
925            amount: data.amount,
926            reason: data.reason.clone(),
927            status: data.status.clone(),
928            created_at: data.created_at.clone(),
929        }
930    }
931}
932
933#[derive(Serialize)]
934#[serde(rename_all = "camelCase")]
935struct JsShipment {
936    id: String,
937    shipment_number: String,
938    order_id: String,
939    carrier: Option<String>,
940    tracking_number: Option<String>,
941    status: String,
942    shipped_at: Option<String>,
943    delivered_at: Option<String>,
944    version: i32,
945    created_at: String,
946    updated_at: String,
947}
948
949impl From<&ShipmentData> for JsShipment {
950    fn from(data: &ShipmentData) -> Self {
951        JsShipment {
952            id: data.id.to_string(),
953            shipment_number: data.shipment_number.clone(),
954            order_id: data.order_id.to_string(),
955            carrier: data.carrier.clone(),
956            tracking_number: data.tracking_number.clone(),
957            status: data.status.clone(),
958            shipped_at: data.shipped_at.clone(),
959            delivered_at: data.delivered_at.clone(),
960            version: data.version,
961            created_at: data.created_at.clone(),
962            updated_at: data.updated_at.clone(),
963        }
964    }
965}
966
967#[derive(Serialize)]
968#[serde(rename_all = "camelCase")]
969struct JsWarranty {
970    id: String,
971    warranty_number: String,
972    customer_id: String,
973    product_id: Option<String>,
974    order_id: Option<String>,
975    status: String,
976    duration_months: i32,
977    start_date: String,
978    end_date: String,
979    created_at: String,
980}
981
982impl From<&WarrantyData> for JsWarranty {
983    fn from(data: &WarrantyData) -> Self {
984        JsWarranty {
985            id: data.id.to_string(),
986            warranty_number: data.warranty_number.clone(),
987            customer_id: data.customer_id.to_string(),
988            product_id: data.product_id.map(|id| id.to_string()),
989            order_id: data.order_id.map(|id| id.to_string()),
990            status: data.status.clone(),
991            duration_months: data.duration_months,
992            start_date: data.start_date.clone(),
993            end_date: data.end_date.clone(),
994            created_at: data.created_at.clone(),
995        }
996    }
997}
998
999#[derive(Serialize)]
1000#[serde(rename_all = "camelCase")]
1001struct JsWarrantyClaim {
1002    id: String,
1003    claim_number: String,
1004    warranty_id: String,
1005    issue_description: String,
1006    status: String,
1007    resolution: Option<String>,
1008    created_at: String,
1009}
1010
1011impl From<&WarrantyClaimData> for JsWarrantyClaim {
1012    fn from(data: &WarrantyClaimData) -> Self {
1013        JsWarrantyClaim {
1014            id: data.id.to_string(),
1015            claim_number: data.claim_number.clone(),
1016            warranty_id: data.warranty_id.to_string(),
1017            issue_description: data.issue_description.clone(),
1018            status: data.status.clone(),
1019            resolution: data.resolution.clone(),
1020            created_at: data.created_at.clone(),
1021        }
1022    }
1023}
1024
1025#[derive(Serialize)]
1026#[serde(rename_all = "camelCase")]
1027struct JsSupplier {
1028    id: String,
1029    supplier_code: String,
1030    name: String,
1031    email: Option<String>,
1032    phone: Option<String>,
1033    status: String,
1034    created_at: String,
1035}
1036
1037impl From<&SupplierData> for JsSupplier {
1038    fn from(data: &SupplierData) -> Self {
1039        JsSupplier {
1040            id: data.id.to_string(),
1041            supplier_code: data.supplier_code.clone(),
1042            name: data.name.clone(),
1043            email: data.email.clone(),
1044            phone: data.phone.clone(),
1045            status: data.status.clone(),
1046            created_at: data.created_at.clone(),
1047        }
1048    }
1049}
1050
1051#[derive(Serialize)]
1052#[serde(rename_all = "camelCase")]
1053struct JsPurchaseOrder {
1054    id: String,
1055    po_number: String,
1056    supplier_id: String,
1057    status: String,
1058    total_amount: Money,
1059    currency: String,
1060    created_at: String,
1061    updated_at: String,
1062}
1063
1064impl From<&PurchaseOrderData> for JsPurchaseOrder {
1065    fn from(data: &PurchaseOrderData) -> Self {
1066        JsPurchaseOrder {
1067            id: data.id.to_string(),
1068            po_number: data.po_number.clone(),
1069            supplier_id: data.supplier_id.to_string(),
1070            status: data.status.clone(),
1071            total_amount: data.total_amount,
1072            currency: data.currency.clone(),
1073            created_at: data.created_at.clone(),
1074            updated_at: data.updated_at.clone(),
1075        }
1076    }
1077}
1078
1079#[derive(Serialize)]
1080#[serde(rename_all = "camelCase")]
1081struct JsInvoice {
1082    id: String,
1083    invoice_number: String,
1084    customer_id: String,
1085    order_id: Option<String>,
1086    status: String,
1087    subtotal: Money,
1088    tax_amount: Money,
1089    total: Money,
1090    amount_paid: Money,
1091    balance_due: Money,
1092    due_date: Option<String>,
1093    created_at: String,
1094    updated_at: String,
1095}
1096
1097impl From<&InvoiceData> for JsInvoice {
1098    fn from(data: &InvoiceData) -> Self {
1099        JsInvoice {
1100            id: data.id.to_string(),
1101            invoice_number: data.invoice_number.clone(),
1102            customer_id: data.customer_id.to_string(),
1103            order_id: data.order_id.map(|id| id.to_string()),
1104            status: data.status.clone(),
1105            subtotal: data.subtotal,
1106            tax_amount: data.tax_amount,
1107            total: data.total,
1108            amount_paid: data.amount_paid,
1109            balance_due: data.total - data.amount_paid,
1110            due_date: data.due_date.clone(),
1111            created_at: data.created_at.clone(),
1112            updated_at: data.updated_at.clone(),
1113        }
1114    }
1115}
1116
1117#[derive(Serialize)]
1118#[serde(rename_all = "camelCase")]
1119struct JsBom {
1120    id: String,
1121    bom_number: String,
1122    sku: String,
1123    name: String,
1124    description: Option<String>,
1125    status: String,
1126    version: i32,
1127    created_at: String,
1128    updated_at: String,
1129}
1130
1131impl From<&BomData> for JsBom {
1132    fn from(data: &BomData) -> Self {
1133        JsBom {
1134            id: data.id.to_string(),
1135            bom_number: data.bom_number.clone(),
1136            sku: data.sku.clone(),
1137            name: data.name.clone(),
1138            description: data.description.clone(),
1139            status: data.status.clone(),
1140            version: data.version,
1141            created_at: data.created_at.clone(),
1142            updated_at: data.updated_at.clone(),
1143        }
1144    }
1145}
1146
1147#[derive(Serialize)]
1148#[serde(rename_all = "camelCase")]
1149struct JsBomComponent {
1150    id: String,
1151    bom_id: String,
1152    component_sku: String,
1153    component_name: String,
1154    quantity: f64,
1155    unit_of_measure: String,
1156}
1157
1158impl From<&BomComponentData> for JsBomComponent {
1159    fn from(data: &BomComponentData) -> Self {
1160        JsBomComponent {
1161            id: data.id.to_string(),
1162            bom_id: data.bom_id.to_string(),
1163            component_sku: data.component_sku.clone(),
1164            component_name: data.component_name.clone(),
1165            quantity: data.quantity,
1166            unit_of_measure: data.unit_of_measure.clone(),
1167        }
1168    }
1169}
1170
1171#[derive(Serialize)]
1172#[serde(rename_all = "camelCase")]
1173struct JsWorkOrder {
1174    id: String,
1175    work_order_number: String,
1176    bom_id: String,
1177    status: String,
1178    quantity_to_build: f64,
1179    quantity_built: f64,
1180    priority: String,
1181    scheduled_start: Option<String>,
1182    scheduled_end: Option<String>,
1183    version: i32,
1184    created_at: String,
1185    updated_at: String,
1186}
1187
1188impl From<&WorkOrderData> for JsWorkOrder {
1189    fn from(data: &WorkOrderData) -> Self {
1190        JsWorkOrder {
1191            id: data.id.to_string(),
1192            work_order_number: data.work_order_number.clone(),
1193            bom_id: data.bom_id.to_string(),
1194            status: data.status.clone(),
1195            quantity_to_build: data.quantity_to_build,
1196            quantity_built: data.quantity_built,
1197            priority: data.priority.clone(),
1198            scheduled_start: data.scheduled_start.clone(),
1199            scheduled_end: data.scheduled_end.clone(),
1200            version: data.version,
1201            created_at: data.created_at.clone(),
1202            updated_at: data.updated_at.clone(),
1203        }
1204    }
1205}
1206
1207#[derive(Serialize)]
1208#[serde(rename_all = "camelCase")]
1209struct JsCartItem {
1210    id: String,
1211    cart_id: String,
1212    sku: String,
1213    name: String,
1214    description: Option<String>,
1215    quantity: i32,
1216    unit_price: Money,
1217    total: Money,
1218}
1219
1220impl From<&CartItemData> for JsCartItem {
1221    fn from(data: &CartItemData) -> Self {
1222        JsCartItem {
1223            id: data.id.to_string(),
1224            cart_id: data.cart_id.to_string(),
1225            sku: data.sku.clone(),
1226            name: data.name.clone(),
1227            description: data.description.clone(),
1228            quantity: data.quantity,
1229            unit_price: data.unit_price,
1230            total: data.total,
1231        }
1232    }
1233}
1234
1235#[derive(Serialize)]
1236#[serde(rename_all = "camelCase")]
1237struct JsCart {
1238    id: String,
1239    cart_number: String,
1240    customer_id: Option<String>,
1241    status: String,
1242    currency: String,
1243    subtotal: Money,
1244    tax_amount: Money,
1245    shipping_amount: Money,
1246    discount_amount: Money,
1247    grand_total: Money,
1248    customer_email: Option<String>,
1249    customer_name: Option<String>,
1250    payment_method: Option<String>,
1251    payment_status: String,
1252    fulfillment_type: String,
1253    shipping_method: Option<String>,
1254    coupon_code: Option<String>,
1255    notes: Option<String>,
1256    item_count: usize,
1257    items: Vec<JsCartItem>,
1258    created_at: String,
1259    updated_at: String,
1260    expires_at: Option<String>,
1261}
1262
1263// Subscription JS types
1264#[derive(Serialize)]
1265#[serde(rename_all = "camelCase")]
1266struct JsSubscriptionPlan {
1267    id: String,
1268    code: String,
1269    name: String,
1270    description: Option<String>,
1271    billing_interval: String,
1272    billing_interval_count: i32,
1273    price: Money,
1274    currency: String,
1275    setup_fee: Money,
1276    trial_days: i32,
1277    status: String,
1278    created_at: String,
1279    updated_at: String,
1280}
1281
1282impl From<&SubscriptionPlanData> for JsSubscriptionPlan {
1283    fn from(data: &SubscriptionPlanData) -> Self {
1284        JsSubscriptionPlan {
1285            id: data.id.to_string(),
1286            code: data.code.clone(),
1287            name: data.name.clone(),
1288            description: data.description.clone(),
1289            billing_interval: data.billing_interval.clone(),
1290            billing_interval_count: data.billing_interval_count,
1291            price: data.price,
1292            currency: data.currency.clone(),
1293            setup_fee: data.setup_fee,
1294            trial_days: data.trial_days,
1295            status: data.status.clone(),
1296            created_at: data.created_at.clone(),
1297            updated_at: data.updated_at.clone(),
1298        }
1299    }
1300}
1301
1302#[derive(Serialize)]
1303#[serde(rename_all = "camelCase")]
1304struct JsSubscription {
1305    id: String,
1306    subscription_number: String,
1307    customer_id: String,
1308    plan_id: String,
1309    status: String,
1310    current_period_start: String,
1311    current_period_end: String,
1312    trial_start: Option<String>,
1313    trial_end: Option<String>,
1314    cancelled_at: Option<String>,
1315    cancel_at_period_end: bool,
1316    pause_start: Option<String>,
1317    pause_end: Option<String>,
1318    price: Money,
1319    currency: String,
1320    created_at: String,
1321    updated_at: String,
1322}
1323
1324impl From<&SubscriptionData> for JsSubscription {
1325    fn from(data: &SubscriptionData) -> Self {
1326        JsSubscription {
1327            id: data.id.to_string(),
1328            subscription_number: data.subscription_number.clone(),
1329            customer_id: data.customer_id.to_string(),
1330            plan_id: data.plan_id.to_string(),
1331            status: data.status.clone(),
1332            current_period_start: data.current_period_start.clone(),
1333            current_period_end: data.current_period_end.clone(),
1334            trial_start: data.trial_start.clone(),
1335            trial_end: data.trial_end.clone(),
1336            cancelled_at: data.cancelled_at.clone(),
1337            cancel_at_period_end: data.cancel_at_period_end,
1338            pause_start: data.pause_start.clone(),
1339            pause_end: data.pause_end.clone(),
1340            price: data.price,
1341            currency: data.currency.clone(),
1342            created_at: data.created_at.clone(),
1343            updated_at: data.updated_at.clone(),
1344        }
1345    }
1346}
1347
1348#[derive(Serialize)]
1349#[serde(rename_all = "camelCase")]
1350struct JsBillingCycle {
1351    id: String,
1352    cycle_number: String,
1353    subscription_id: String,
1354    status: String,
1355    period_start: String,
1356    period_end: String,
1357    amount: Money,
1358    currency: String,
1359    payment_id: Option<String>,
1360    invoice_id: Option<String>,
1361    created_at: String,
1362    updated_at: String,
1363}
1364
1365impl From<&BillingCycleData> for JsBillingCycle {
1366    fn from(data: &BillingCycleData) -> Self {
1367        JsBillingCycle {
1368            id: data.id.to_string(),
1369            cycle_number: data.cycle_number.clone(),
1370            subscription_id: data.subscription_id.to_string(),
1371            status: data.status.clone(),
1372            period_start: data.period_start.clone(),
1373            period_end: data.period_end.clone(),
1374            amount: data.amount,
1375            currency: data.currency.clone(),
1376            payment_id: data.payment_id.map(|id| id.to_string()),
1377            invoice_id: data.invoice_id.map(|id| id.to_string()),
1378            created_at: data.created_at.clone(),
1379            updated_at: data.updated_at.clone(),
1380        }
1381    }
1382}
1383
1384#[derive(Serialize)]
1385#[serde(rename_all = "camelCase")]
1386struct JsSubscriptionEvent {
1387    id: String,
1388    subscription_id: String,
1389    event_type: String,
1390    description: String,
1391    created_at: String,
1392}
1393
1394impl From<&SubscriptionEventData> for JsSubscriptionEvent {
1395    fn from(data: &SubscriptionEventData) -> Self {
1396        JsSubscriptionEvent {
1397            id: data.id.to_string(),
1398            subscription_id: data.subscription_id.to_string(),
1399            event_type: data.event_type.clone(),
1400            description: data.description.clone(),
1401            created_at: data.created_at.clone(),
1402        }
1403    }
1404}
1405
1406#[derive(Serialize)]
1407#[serde(rename_all = "camelCase")]
1408struct JsPromotion {
1409    id: String,
1410    code: String,
1411    name: String,
1412    description: Option<String>,
1413    promotion_type: String,
1414    trigger: String,
1415    target: String,
1416    stacking: String,
1417    status: String,
1418    percentage_off: Option<f64>,
1419    fixed_amount_off: Option<Money>,
1420    max_discount_amount: Option<Money>,
1421    buy_quantity: Option<i32>,
1422    get_quantity: Option<i32>,
1423    starts_at: String,
1424    ends_at: Option<String>,
1425    total_usage_limit: Option<i32>,
1426    per_customer_limit: Option<i32>,
1427    usage_count: i32,
1428    currency: String,
1429    priority: i32,
1430    created_at: String,
1431    updated_at: String,
1432}
1433
1434impl From<&PromotionData> for JsPromotion {
1435    fn from(data: &PromotionData) -> Self {
1436        JsPromotion {
1437            id: data.id.to_string(),
1438            code: data.code.clone(),
1439            name: data.name.clone(),
1440            description: data.description.clone(),
1441            promotion_type: data.promotion_type.clone(),
1442            trigger: data.trigger.clone(),
1443            target: data.target.clone(),
1444            stacking: data.stacking.clone(),
1445            status: data.status.clone(),
1446            percentage_off: data.percentage_off,
1447            fixed_amount_off: data.fixed_amount_off,
1448            max_discount_amount: data.max_discount_amount,
1449            buy_quantity: data.buy_quantity,
1450            get_quantity: data.get_quantity,
1451            starts_at: data.starts_at.clone(),
1452            ends_at: data.ends_at.clone(),
1453            total_usage_limit: data.total_usage_limit,
1454            per_customer_limit: data.per_customer_limit,
1455            usage_count: data.usage_count,
1456            currency: data.currency.clone(),
1457            priority: data.priority,
1458            created_at: data.created_at.clone(),
1459            updated_at: data.updated_at.clone(),
1460        }
1461    }
1462}
1463
1464#[derive(Serialize)]
1465#[serde(rename_all = "camelCase")]
1466struct JsCoupon {
1467    id: String,
1468    promotion_id: String,
1469    code: String,
1470    status: String,
1471    usage_limit: Option<i32>,
1472    per_customer_limit: Option<i32>,
1473    usage_count: i32,
1474    starts_at: Option<String>,
1475    ends_at: Option<String>,
1476    created_at: String,
1477    updated_at: String,
1478}
1479
1480impl From<&CouponData> for JsCoupon {
1481    fn from(data: &CouponData) -> Self {
1482        JsCoupon {
1483            id: data.id.to_string(),
1484            promotion_id: data.promotion_id.to_string(),
1485            code: data.code.clone(),
1486            status: data.status.clone(),
1487            usage_limit: data.usage_limit,
1488            per_customer_limit: data.per_customer_limit,
1489            usage_count: data.usage_count,
1490            starts_at: data.starts_at.clone(),
1491            ends_at: data.ends_at.clone(),
1492            created_at: data.created_at.clone(),
1493            updated_at: data.updated_at.clone(),
1494        }
1495    }
1496}
1497
1498#[derive(Serialize)]
1499#[serde(rename_all = "camelCase")]
1500struct JsPromotionUsage {
1501    id: String,
1502    promotion_id: String,
1503    coupon_id: Option<String>,
1504    customer_id: Option<String>,
1505    order_id: Option<String>,
1506    cart_id: Option<String>,
1507    discount_amount: Money,
1508    currency: String,
1509    used_at: String,
1510}
1511
1512impl From<&PromotionUsageData> for JsPromotionUsage {
1513    fn from(data: &PromotionUsageData) -> Self {
1514        JsPromotionUsage {
1515            id: data.id.to_string(),
1516            promotion_id: data.promotion_id.to_string(),
1517            coupon_id: data.coupon_id.map(|id| id.to_string()),
1518            customer_id: data.customer_id.map(|id| id.to_string()),
1519            order_id: data.order_id.map(|id| id.to_string()),
1520            cart_id: data.cart_id.map(|id| id.to_string()),
1521            discount_amount: data.discount_amount,
1522            currency: data.currency.clone(),
1523            used_at: data.used_at.clone(),
1524        }
1525    }
1526}
1527
1528#[derive(Serialize)]
1529#[serde(rename_all = "camelCase")]
1530struct JsApplyPromotionsResult {
1531    original_subtotal: Money,
1532    total_discount: Money,
1533    discounted_subtotal: Money,
1534    original_shipping: Money,
1535    shipping_discount: Money,
1536    final_shipping: Money,
1537    grand_total: Money,
1538    applied_promotions: Vec<JsAppliedPromotion>,
1539}
1540
1541#[derive(Serialize)]
1542#[serde(rename_all = "camelCase")]
1543struct JsAppliedPromotion {
1544    promotion_id: String,
1545    promotion_name: String,
1546    coupon_code: Option<String>,
1547    discount_amount: Money,
1548    discount_type: String,
1549}
1550
1551#[derive(Serialize)]
1552#[serde(rename_all = "camelCase")]
1553struct JsCheckoutResult {
1554    order_id: String,
1555    order_number: String,
1556    cart_id: String,
1557    total_charged: Money,
1558    currency: String,
1559}
1560
1561// ============================================================================
1562// Input Types
1563// ============================================================================
1564
1565#[derive(Deserialize)]
1566#[serde(rename_all = "camelCase")]
1567struct CreateCustomerInput {
1568    email: String,
1569    first_name: String,
1570    last_name: String,
1571    phone: Option<String>,
1572    accepts_marketing: Option<bool>,
1573}
1574
1575#[derive(Deserialize)]
1576#[serde(rename_all = "camelCase")]
1577struct CreateOrderItemInput {
1578    sku: String,
1579    name: String,
1580    quantity: i32,
1581    unit_price: Money,
1582}
1583
1584#[derive(Deserialize)]
1585#[serde(rename_all = "camelCase")]
1586struct CreateOrderInput {
1587    customer_id: String,
1588    items: Vec<CreateOrderItemInput>,
1589    currency: Option<String>,
1590    notes: Option<String>,
1591}
1592
1593#[derive(Deserialize)]
1594#[serde(rename_all = "camelCase")]
1595struct CreateVariantInput {
1596    sku: String,
1597    name: Option<String>,
1598    price: Money,
1599    compare_at_price: Option<Money>,
1600}
1601
1602#[derive(Deserialize)]
1603#[serde(rename_all = "camelCase")]
1604struct CreateProductInput {
1605    name: String,
1606    description: Option<String>,
1607    variants: Option<Vec<CreateVariantInput>>,
1608}
1609
1610#[derive(Deserialize)]
1611#[serde(rename_all = "camelCase")]
1612struct CreateInventoryItemInput {
1613    sku: String,
1614    name: String,
1615    description: Option<String>,
1616    initial_quantity: Option<f64>,
1617    reorder_point: Option<f64>,
1618}
1619
1620#[derive(Deserialize)]
1621#[serde(rename_all = "camelCase")]
1622struct CreateReturnItemInput {
1623    order_item_id: String,
1624    quantity: i32,
1625}
1626
1627#[derive(Deserialize)]
1628#[serde(rename_all = "camelCase")]
1629struct CreateReturnInput {
1630    order_id: String,
1631    reason: String,
1632    items: Vec<CreateReturnItemInput>,
1633    reason_details: Option<String>,
1634}
1635
1636#[derive(Deserialize)]
1637#[serde(rename_all = "camelCase")]
1638struct CreatePaymentInput {
1639    amount: Money,
1640    currency: Option<String>,
1641    order_id: Option<String>,
1642    customer_id: Option<String>,
1643    payment_method: Option<String>,
1644}
1645
1646#[derive(Deserialize)]
1647#[serde(rename_all = "camelCase")]
1648struct CreateRefundInput {
1649    payment_id: String,
1650    amount: Money,
1651    reason: Option<String>,
1652}
1653
1654#[derive(Deserialize)]
1655#[serde(rename_all = "camelCase")]
1656struct CreateShipmentInput {
1657    order_id: String,
1658    carrier: Option<String>,
1659    tracking_number: Option<String>,
1660}
1661
1662#[derive(Deserialize)]
1663#[serde(rename_all = "camelCase")]
1664struct CreateWarrantyInput {
1665    customer_id: String,
1666    product_id: Option<String>,
1667    order_id: Option<String>,
1668    duration_months: Option<i32>,
1669}
1670
1671#[derive(Deserialize)]
1672#[serde(rename_all = "camelCase")]
1673struct CreateWarrantyClaimInput {
1674    warranty_id: String,
1675    issue_description: String,
1676}
1677
1678#[derive(Deserialize)]
1679#[serde(rename_all = "camelCase")]
1680struct CreateSupplierInput {
1681    name: String,
1682    email: Option<String>,
1683    phone: Option<String>,
1684}
1685
1686#[derive(Deserialize)]
1687#[serde(rename_all = "camelCase")]
1688struct CreatePurchaseOrderInput {
1689    supplier_id: String,
1690    currency: Option<String>,
1691}
1692
1693#[derive(Deserialize)]
1694#[serde(rename_all = "camelCase")]
1695struct CreateInvoiceInput {
1696    customer_id: String,
1697    order_id: Option<String>,
1698    subtotal: Money,
1699    tax_amount: Option<Money>,
1700    due_date: Option<String>,
1701}
1702
1703#[derive(Deserialize)]
1704#[serde(rename_all = "camelCase")]
1705struct CreateBomInput {
1706    sku: String,
1707    name: String,
1708    description: Option<String>,
1709}
1710
1711#[derive(Deserialize)]
1712#[serde(rename_all = "camelCase")]
1713struct AddBomComponentInput {
1714    component_sku: String,
1715    component_name: String,
1716    quantity: f64,
1717    unit_of_measure: Option<String>,
1718}
1719
1720#[derive(Deserialize)]
1721#[serde(rename_all = "camelCase")]
1722struct CreateWorkOrderInput {
1723    bom_id: String,
1724    quantity_to_build: f64,
1725    priority: Option<String>,
1726    scheduled_start: Option<String>,
1727    scheduled_end: Option<String>,
1728}
1729
1730#[derive(Deserialize)]
1731#[serde(rename_all = "camelCase")]
1732struct CreateCartInput {
1733    customer_id: Option<String>,
1734    customer_email: Option<String>,
1735    customer_name: Option<String>,
1736    currency: Option<String>,
1737}
1738
1739#[derive(Deserialize)]
1740#[serde(rename_all = "camelCase")]
1741struct AddCartItemInput {
1742    sku: String,
1743    name: String,
1744    quantity: i32,
1745    unit_price: Money,
1746    description: Option<String>,
1747}
1748
1749#[derive(Deserialize)]
1750#[serde(rename_all = "camelCase")]
1751struct SetCartPaymentInput {
1752    payment_method: String,
1753    payment_token: Option<String>,
1754}
1755
1756#[derive(Deserialize)]
1757#[serde(rename_all = "camelCase")]
1758struct CreatePromotionInput {
1759    code: Option<String>,
1760    name: String,
1761    description: Option<String>,
1762    promotion_type: Option<String>,
1763    trigger: Option<String>,
1764    target: Option<String>,
1765    stacking: Option<String>,
1766    percentage_off: Option<f64>,
1767    fixed_amount_off: Option<Money>,
1768    max_discount_amount: Option<Money>,
1769    buy_quantity: Option<i32>,
1770    get_quantity: Option<i32>,
1771    starts_at: Option<String>,
1772    ends_at: Option<String>,
1773    total_usage_limit: Option<i32>,
1774    per_customer_limit: Option<i32>,
1775    currency: Option<String>,
1776    priority: Option<i32>,
1777}
1778
1779#[derive(Deserialize)]
1780#[serde(rename_all = "camelCase")]
1781struct UpdatePromotionInput {
1782    name: Option<String>,
1783    description: Option<String>,
1784    status: Option<String>,
1785    percentage_off: Option<f64>,
1786    fixed_amount_off: Option<Money>,
1787    max_discount_amount: Option<Money>,
1788    starts_at: Option<String>,
1789    ends_at: Option<String>,
1790    total_usage_limit: Option<i32>,
1791    per_customer_limit: Option<i32>,
1792    priority: Option<i32>,
1793}
1794
1795#[derive(Deserialize)]
1796#[serde(rename_all = "camelCase")]
1797struct CreateCouponInput {
1798    promotion_id: String,
1799    code: String,
1800    usage_limit: Option<i32>,
1801    per_customer_limit: Option<i32>,
1802    starts_at: Option<String>,
1803    ends_at: Option<String>,
1804}
1805
1806#[derive(Deserialize)]
1807#[serde(rename_all = "camelCase")]
1808struct ApplyPromotionsInput {
1809    cart_id: Option<String>,
1810    customer_id: Option<String>,
1811    coupon_codes: Option<Vec<String>>,
1812    subtotal: Money,
1813    shipping_amount: Option<Money>,
1814    currency: Option<String>,
1815}
1816
1817// ============================================================================
1818// Commerce - Main Entry Point
1819// ============================================================================
1820
1821/// Main Commerce instance for browser-based commerce operations.
1822/// Uses in-memory storage (data is lost on page refresh).
1823#[wasm_bindgen]
1824pub struct Commerce {
1825    store: StoreRef,
1826}
1827
1828#[wasm_bindgen]
1829impl Commerce {
1830    /// Create a new Commerce instance with in-memory storage.
1831    #[wasm_bindgen(constructor)]
1832    pub fn new() -> Commerce {
1833        Commerce { store: Rc::new(RefCell::new(Store::default())) }
1834    }
1835
1836    /// Get the customers API.
1837    #[wasm_bindgen(getter)]
1838    pub fn customers(&self) -> Customers {
1839        Customers { store: Rc::clone(&self.store) }
1840    }
1841
1842    /// Get the orders API.
1843    #[wasm_bindgen(getter)]
1844    pub fn orders(&self) -> Orders {
1845        Orders { store: Rc::clone(&self.store) }
1846    }
1847
1848    /// Get the products API.
1849    #[wasm_bindgen(getter)]
1850    pub fn products(&self) -> Products {
1851        Products { store: Rc::clone(&self.store) }
1852    }
1853
1854    /// Get the inventory API.
1855    #[wasm_bindgen(getter)]
1856    pub fn inventory(&self) -> Inventory {
1857        Inventory { store: Rc::clone(&self.store) }
1858    }
1859
1860    /// Get the returns API.
1861    #[wasm_bindgen(getter)]
1862    pub fn returns(&self) -> Returns {
1863        Returns { store: Rc::clone(&self.store) }
1864    }
1865
1866    /// Get the payments API.
1867    #[wasm_bindgen(getter)]
1868    pub fn payments(&self) -> Payments {
1869        Payments { store: Rc::clone(&self.store) }
1870    }
1871
1872    /// Get the shipments API.
1873    #[wasm_bindgen(getter)]
1874    pub fn shipments(&self) -> Shipments {
1875        Shipments { store: Rc::clone(&self.store) }
1876    }
1877
1878    /// Get the warranties API.
1879    #[wasm_bindgen(getter)]
1880    pub fn warranties(&self) -> Warranties {
1881        Warranties { store: Rc::clone(&self.store) }
1882    }
1883
1884    /// Get the purchase orders API.
1885    #[wasm_bindgen(getter, js_name = purchaseOrders)]
1886    pub fn purchase_orders(&self) -> PurchaseOrders {
1887        PurchaseOrders { store: Rc::clone(&self.store) }
1888    }
1889
1890    /// Get the invoices API.
1891    #[wasm_bindgen(getter)]
1892    pub fn invoices(&self) -> Invoices {
1893        Invoices { store: Rc::clone(&self.store) }
1894    }
1895
1896    /// Get the bill of materials API.
1897    #[wasm_bindgen(getter)]
1898    pub fn bom(&self) -> Bom {
1899        Bom { store: Rc::clone(&self.store) }
1900    }
1901
1902    /// Get the work orders API.
1903    #[wasm_bindgen(getter, js_name = workOrders)]
1904    pub fn work_orders(&self) -> WorkOrders {
1905        WorkOrders { store: Rc::clone(&self.store) }
1906    }
1907
1908    /// Get the carts API.
1909    #[wasm_bindgen(getter)]
1910    pub fn carts(&self) -> Carts {
1911        Carts { store: Rc::clone(&self.store) }
1912    }
1913
1914    /// Get the subscriptions API.
1915    #[wasm_bindgen(getter)]
1916    pub fn subscriptions(&self) -> Subscriptions {
1917        Subscriptions { store: Rc::clone(&self.store) }
1918    }
1919
1920    /// Get the promotions API.
1921    #[wasm_bindgen(getter)]
1922    pub fn promotions(&self) -> Promotions {
1923        Promotions { store: Rc::clone(&self.store) }
1924    }
1925
1926    /// Get the tax API.
1927    #[wasm_bindgen(getter)]
1928    pub fn tax(&self) -> Tax {
1929        Tax { store: Rc::clone(&self.store) }
1930    }
1931}
1932
1933impl Default for Commerce {
1934    fn default() -> Self {
1935        Self::new()
1936    }
1937}
1938
1939// ============================================================================
1940// Customers API
1941// ============================================================================
1942
1943/// Customer management operations.
1944#[wasm_bindgen]
1945pub struct Customers {
1946    store: StoreRef,
1947}
1948
1949#[wasm_bindgen]
1950impl Customers {
1951    /// Create a new customer.
1952    pub fn create(&self, input: JsValue) -> Result<JsValue, JsValue> {
1953        let input: CreateCustomerInput =
1954            serde_wasm_bindgen::from_value(input).map_err(|e| JsValue::from_str(&e.to_string()))?;
1955
1956        let now = Utc::now().to_rfc3339();
1957        let id = Uuid::new_v4();
1958
1959        let data = CustomerData {
1960            id,
1961            email: input.email,
1962            first_name: input.first_name,
1963            last_name: input.last_name,
1964            phone: input.phone,
1965            status: "active".to_string(),
1966            accepts_marketing: input.accepts_marketing.unwrap_or(false),
1967            created_at: now.clone(),
1968            updated_at: now,
1969        };
1970
1971        self.store.borrow_mut().customers.insert(id, data.clone());
1972
1973        let js_customer: JsCustomer = (&data).into();
1974        serde_wasm_bindgen::to_value(&js_customer).map_err(|e| JsValue::from_str(&e.to_string()))
1975    }
1976
1977    /// Get a customer by ID.
1978    pub fn get(&self, id: &str) -> Result<JsValue, JsValue> {
1979        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
1980        let store = self.store.borrow();
1981
1982        match store.customers.get(&uuid) {
1983            Some(data) => {
1984                let js_customer: JsCustomer = data.into();
1985                serde_wasm_bindgen::to_value(&js_customer)
1986                    .map_err(|e| JsValue::from_str(&e.to_string()))
1987            }
1988            None => Ok(JsValue::NULL),
1989        }
1990    }
1991
1992    /// Get a customer by email.
1993    #[wasm_bindgen(js_name = getByEmail)]
1994    pub fn get_by_email(&self, email: &str) -> Result<JsValue, JsValue> {
1995        let store = self.store.borrow();
1996
1997        match store.customers.values().find(|c| c.email == email) {
1998            Some(data) => {
1999                let js_customer: JsCustomer = data.into();
2000                serde_wasm_bindgen::to_value(&js_customer)
2001                    .map_err(|e| JsValue::from_str(&e.to_string()))
2002            }
2003            None => Ok(JsValue::NULL),
2004        }
2005    }
2006
2007    /// List all customers.
2008    pub fn list(&self) -> Result<JsValue, JsValue> {
2009        let store = self.store.borrow();
2010        let customers: Vec<JsCustomer> = store.customers.values().map(|data| data.into()).collect();
2011
2012        serde_wasm_bindgen::to_value(&customers).map_err(|e| JsValue::from_str(&e.to_string()))
2013    }
2014
2015    /// Count customers.
2016    pub fn count(&self) -> u32 {
2017        self.store.borrow().customers.len() as u32
2018    }
2019}
2020
2021// ============================================================================
2022// Orders API
2023// ============================================================================
2024
2025/// Order management operations.
2026#[wasm_bindgen]
2027pub struct Orders {
2028    store: StoreRef,
2029}
2030
2031#[wasm_bindgen]
2032impl Orders {
2033    /// Create a new order.
2034    pub fn create(&self, input: JsValue) -> Result<JsValue, JsValue> {
2035        let input: CreateOrderInput =
2036            serde_wasm_bindgen::from_value(input).map_err(|e| JsValue::from_str(&e.to_string()))?;
2037
2038        let customer_id = Uuid::parse_str(&input.customer_id)
2039            .map_err(|_| JsValue::from_str("Invalid customer UUID"))?;
2040
2041        let now = Utc::now().to_rfc3339();
2042        let id = Uuid::new_v4();
2043
2044        let mut store = self.store.borrow_mut();
2045        store.next_order_number += 1;
2046        let order_number = format!("ORD-{}", store.next_order_number);
2047
2048        // Calculate total and create items
2049        let mut total = Money::zero();
2050        let mut items = Vec::new();
2051
2052        for item_input in &input.items {
2053            let item_total = item_input.unit_price * item_input.quantity;
2054            total += item_total;
2055
2056            items.push(OrderItemData {
2057                id: Uuid::new_v4(),
2058                order_id: id,
2059                sku: item_input.sku.clone(),
2060                name: item_input.name.clone(),
2061                quantity: item_input.quantity,
2062                unit_price: item_input.unit_price,
2063                total: item_total,
2064            });
2065        }
2066
2067        let data = OrderData {
2068            id,
2069            order_number: order_number.clone(),
2070            customer_id,
2071            status: "pending".to_string(),
2072            total_amount: total,
2073            currency: input.currency.unwrap_or_else(|| "USD".to_string()),
2074            payment_status: "pending".to_string(),
2075            fulfillment_status: "unfulfilled".to_string(),
2076            tracking_number: None,
2077            notes: input.notes,
2078            version: 1,
2079            created_at: now.clone(),
2080            updated_at: now,
2081        };
2082
2083        store.orders.insert(id, data.clone());
2084        store.order_items.insert(id, items.clone());
2085
2086        let js_items: Vec<JsOrderItem> = items.iter().map(|i| i.into()).collect();
2087
2088        let js_order = JsOrder {
2089            id: data.id.to_string(),
2090            order_number: data.order_number,
2091            customer_id: data.customer_id.to_string(),
2092            status: data.status,
2093            total_amount: data.total_amount,
2094            currency: data.currency,
2095            payment_status: data.payment_status,
2096            fulfillment_status: data.fulfillment_status,
2097            tracking_number: data.tracking_number,
2098            items: js_items,
2099            version: data.version,
2100            created_at: data.created_at,
2101            updated_at: data.updated_at,
2102        };
2103
2104        serde_wasm_bindgen::to_value(&js_order).map_err(|e| JsValue::from_str(&e.to_string()))
2105    }
2106
2107    /// Get an order by ID.
2108    pub fn get(&self, id: &str) -> Result<JsValue, JsValue> {
2109        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
2110        let store = self.store.borrow();
2111
2112        match store.orders.get(&uuid) {
2113            Some(data) => {
2114                let items = store.order_items.get(&uuid).cloned().unwrap_or_default();
2115                let js_items: Vec<JsOrderItem> = items.iter().map(|i| i.into()).collect();
2116
2117                let js_order = JsOrder {
2118                    id: data.id.to_string(),
2119                    order_number: data.order_number.clone(),
2120                    customer_id: data.customer_id.to_string(),
2121                    status: data.status.clone(),
2122                    total_amount: data.total_amount,
2123                    currency: data.currency.clone(),
2124                    payment_status: data.payment_status.clone(),
2125                    fulfillment_status: data.fulfillment_status.clone(),
2126                    tracking_number: data.tracking_number.clone(),
2127                    items: js_items,
2128                    version: data.version,
2129                    created_at: data.created_at.clone(),
2130                    updated_at: data.updated_at.clone(),
2131                };
2132
2133                serde_wasm_bindgen::to_value(&js_order)
2134                    .map_err(|e| JsValue::from_str(&e.to_string()))
2135            }
2136            None => Ok(JsValue::NULL),
2137        }
2138    }
2139
2140    /// Get order items.
2141    #[wasm_bindgen(js_name = getItems)]
2142    pub fn get_items(&self, order_id: &str) -> Result<JsValue, JsValue> {
2143        let uuid = Uuid::parse_str(order_id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
2144        let store = self.store.borrow();
2145
2146        let items: Vec<JsOrderItem> = store
2147            .order_items
2148            .get(&uuid)
2149            .map(|items| items.iter().map(|i| i.into()).collect())
2150            .unwrap_or_default();
2151
2152        serde_wasm_bindgen::to_value(&items).map_err(|e| JsValue::from_str(&e.to_string()))
2153    }
2154
2155    /// List all orders.
2156    pub fn list(&self) -> Result<JsValue, JsValue> {
2157        let store = self.store.borrow();
2158        let orders: Vec<JsOrder> = store
2159            .orders
2160            .values()
2161            .map(|data| {
2162                let items = store.order_items.get(&data.id).cloned().unwrap_or_default();
2163                let js_items: Vec<JsOrderItem> = items.iter().map(|i| i.into()).collect();
2164
2165                JsOrder {
2166                    id: data.id.to_string(),
2167                    order_number: data.order_number.clone(),
2168                    customer_id: data.customer_id.to_string(),
2169                    status: data.status.clone(),
2170                    total_amount: data.total_amount,
2171                    currency: data.currency.clone(),
2172                    payment_status: data.payment_status.clone(),
2173                    fulfillment_status: data.fulfillment_status.clone(),
2174                    tracking_number: data.tracking_number.clone(),
2175                    items: js_items,
2176                    version: data.version,
2177                    created_at: data.created_at.clone(),
2178                    updated_at: data.updated_at.clone(),
2179                }
2180            })
2181            .collect();
2182
2183        serde_wasm_bindgen::to_value(&orders).map_err(|e| JsValue::from_str(&e.to_string()))
2184    }
2185
2186    /// Update order status.
2187    #[wasm_bindgen(js_name = updateStatus)]
2188    pub fn update_status(&self, id: &str, status: &str) -> Result<JsValue, JsValue> {
2189        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
2190        let mut store = self.store.borrow_mut();
2191
2192        // Update the order
2193        {
2194            let data =
2195                store.orders.get_mut(&uuid).ok_or_else(|| JsValue::from_str("Order not found"))?;
2196
2197            data.status = status.to_string();
2198            data.updated_at = Utc::now().to_rfc3339();
2199        }
2200
2201        // Now get immutable references for the response
2202        let data = store.orders.get(&uuid).ok_or_else(|| JsValue::from_str("Order not found"))?;
2203        let items = store.order_items.get(&uuid).cloned().unwrap_or_default();
2204        let js_items: Vec<JsOrderItem> = items.iter().map(|i| i.into()).collect();
2205
2206        let js_order = JsOrder {
2207            id: data.id.to_string(),
2208            order_number: data.order_number.clone(),
2209            customer_id: data.customer_id.to_string(),
2210            status: data.status.clone(),
2211            total_amount: data.total_amount,
2212            currency: data.currency.clone(),
2213            payment_status: data.payment_status.clone(),
2214            fulfillment_status: data.fulfillment_status.clone(),
2215            tracking_number: data.tracking_number.clone(),
2216            items: js_items,
2217            version: data.version,
2218            created_at: data.created_at.clone(),
2219            updated_at: data.updated_at.clone(),
2220        };
2221
2222        serde_wasm_bindgen::to_value(&js_order).map_err(|e| JsValue::from_str(&e.to_string()))
2223    }
2224
2225    /// Ship an order.
2226    pub fn ship(&self, id: &str, tracking_number: Option<String>) -> Result<JsValue, JsValue> {
2227        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
2228        let mut store = self.store.borrow_mut();
2229
2230        // Update the order
2231        {
2232            let data =
2233                store.orders.get_mut(&uuid).ok_or_else(|| JsValue::from_str("Order not found"))?;
2234
2235            data.status = "shipped".to_string();
2236            data.fulfillment_status = "shipped".to_string();
2237            data.tracking_number = tracking_number;
2238            data.updated_at = Utc::now().to_rfc3339();
2239        }
2240
2241        // Now get immutable references for the response
2242        let data = store.orders.get(&uuid).ok_or_else(|| JsValue::from_str("Order not found"))?;
2243        let items = store.order_items.get(&uuid).cloned().unwrap_or_default();
2244        let js_items: Vec<JsOrderItem> = items.iter().map(|i| i.into()).collect();
2245
2246        let js_order = JsOrder {
2247            id: data.id.to_string(),
2248            order_number: data.order_number.clone(),
2249            customer_id: data.customer_id.to_string(),
2250            status: data.status.clone(),
2251            total_amount: data.total_amount,
2252            currency: data.currency.clone(),
2253            payment_status: data.payment_status.clone(),
2254            fulfillment_status: data.fulfillment_status.clone(),
2255            tracking_number: data.tracking_number.clone(),
2256            items: js_items,
2257            version: data.version,
2258            created_at: data.created_at.clone(),
2259            updated_at: data.updated_at.clone(),
2260        };
2261
2262        serde_wasm_bindgen::to_value(&js_order).map_err(|e| JsValue::from_str(&e.to_string()))
2263    }
2264
2265    /// Cancel an order.
2266    pub fn cancel(&self, id: &str) -> Result<JsValue, JsValue> {
2267        self.update_status(id, "cancelled")
2268    }
2269
2270    /// Count orders.
2271    pub fn count(&self) -> u32 {
2272        self.store.borrow().orders.len() as u32
2273    }
2274}
2275
2276// ============================================================================
2277// Products API
2278// ============================================================================
2279
2280/// Product catalog operations.
2281#[wasm_bindgen]
2282pub struct Products {
2283    store: StoreRef,
2284}
2285
2286#[wasm_bindgen]
2287impl Products {
2288    /// Create a new product.
2289    pub fn create(&self, input: JsValue) -> Result<JsValue, JsValue> {
2290        let input: CreateProductInput =
2291            serde_wasm_bindgen::from_value(input).map_err(|e| JsValue::from_str(&e.to_string()))?;
2292
2293        let now = Utc::now().to_rfc3339();
2294        let id = Uuid::new_v4();
2295        let slug = input.name.to_lowercase().replace(' ', "-");
2296
2297        let data = ProductData {
2298            id,
2299            name: input.name.clone(),
2300            slug,
2301            description: input.description.unwrap_or_default(),
2302            status: "draft".to_string(),
2303            created_at: now.clone(),
2304            updated_at: now,
2305        };
2306
2307        let mut store = self.store.borrow_mut();
2308        store.products.insert(id, data.clone());
2309
2310        // Create variants if provided
2311        if let Some(variants) = input.variants {
2312            for (i, v) in variants.into_iter().enumerate() {
2313                let variant_id = Uuid::new_v4();
2314                let variant = VariantData {
2315                    id: variant_id,
2316                    product_id: id,
2317                    sku: v.sku,
2318                    name: v.name.unwrap_or_else(|| input.name.clone()),
2319                    price: v.price,
2320                    compare_at_price: v.compare_at_price,
2321                    is_default: i == 0,
2322                };
2323                store.variants.insert(variant_id, variant);
2324            }
2325        }
2326
2327        let js_product: JsProduct = (&data).into();
2328        serde_wasm_bindgen::to_value(&js_product).map_err(|e| JsValue::from_str(&e.to_string()))
2329    }
2330
2331    /// Get a product by ID.
2332    pub fn get(&self, id: &str) -> Result<JsValue, JsValue> {
2333        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
2334        let store = self.store.borrow();
2335
2336        match store.products.get(&uuid) {
2337            Some(data) => {
2338                let js_product: JsProduct = data.into();
2339                serde_wasm_bindgen::to_value(&js_product)
2340                    .map_err(|e| JsValue::from_str(&e.to_string()))
2341            }
2342            None => Ok(JsValue::NULL),
2343        }
2344    }
2345
2346    /// Get a product variant by SKU.
2347    #[wasm_bindgen(js_name = getVariantBySku)]
2348    pub fn get_variant_by_sku(&self, sku: &str) -> Result<JsValue, JsValue> {
2349        let store = self.store.borrow();
2350
2351        match store.variants.values().find(|v| v.sku == sku) {
2352            Some(data) => {
2353                let js_variant: JsProductVariant = data.into();
2354                serde_wasm_bindgen::to_value(&js_variant)
2355                    .map_err(|e| JsValue::from_str(&e.to_string()))
2356            }
2357            None => Ok(JsValue::NULL),
2358        }
2359    }
2360
2361    /// List all products.
2362    pub fn list(&self) -> Result<JsValue, JsValue> {
2363        let store = self.store.borrow();
2364        let products: Vec<JsProduct> = store.products.values().map(|data| data.into()).collect();
2365
2366        serde_wasm_bindgen::to_value(&products).map_err(|e| JsValue::from_str(&e.to_string()))
2367    }
2368
2369    /// Count products.
2370    pub fn count(&self) -> u32 {
2371        self.store.borrow().products.len() as u32
2372    }
2373}
2374
2375// ============================================================================
2376// Inventory API
2377// ============================================================================
2378
2379/// Inventory management operations.
2380#[wasm_bindgen]
2381pub struct Inventory {
2382    store: StoreRef,
2383}
2384
2385#[wasm_bindgen]
2386impl Inventory {
2387    /// Create a new inventory item.
2388    #[wasm_bindgen(js_name = createItem)]
2389    pub fn create_item(&self, input: JsValue) -> Result<JsValue, JsValue> {
2390        let input: CreateInventoryItemInput =
2391            serde_wasm_bindgen::from_value(input).map_err(|e| JsValue::from_str(&e.to_string()))?;
2392
2393        let mut store = self.store.borrow_mut();
2394        store.next_inventory_id += 1;
2395        let id = store.next_inventory_id;
2396
2397        let data = InventoryItemData {
2398            id,
2399            sku: input.sku.clone(),
2400            name: input.name,
2401            description: input.description,
2402            unit_of_measure: "each".to_string(),
2403            is_active: true,
2404        };
2405
2406        let balance = InventoryBalanceData {
2407            item_id: id,
2408            on_hand: input.initial_quantity.unwrap_or(0.0),
2409            allocated: 0.0,
2410        };
2411
2412        store.inventory_items.insert(id, data.clone());
2413        store.inventory_by_sku.insert(input.sku, id);
2414        store.inventory_balances.insert(id, balance);
2415
2416        let js_item: JsInventoryItem = (&data).into();
2417        serde_wasm_bindgen::to_value(&js_item).map_err(|e| JsValue::from_str(&e.to_string()))
2418    }
2419
2420    /// Get stock level for a SKU.
2421    #[wasm_bindgen(js_name = getStock)]
2422    pub fn get_stock(&self, sku: &str) -> Result<JsValue, JsValue> {
2423        let store = self.store.borrow();
2424
2425        let item_id = match store.inventory_by_sku.get(sku) {
2426            Some(id) => *id,
2427            None => return Ok(JsValue::NULL),
2428        };
2429
2430        let item = match store.inventory_items.get(&item_id) {
2431            Some(i) => i,
2432            None => return Ok(JsValue::NULL),
2433        };
2434
2435        let balance = match store.inventory_balances.get(&item_id) {
2436            Some(b) => b,
2437            None => return Ok(JsValue::NULL),
2438        };
2439
2440        let stock = JsStockLevel {
2441            sku: item.sku.clone(),
2442            name: item.name.clone(),
2443            total_on_hand: balance.on_hand,
2444            total_allocated: balance.allocated,
2445            total_available: balance.on_hand - balance.allocated,
2446        };
2447
2448        serde_wasm_bindgen::to_value(&stock).map_err(|e| JsValue::from_str(&e.to_string()))
2449    }
2450
2451    /// Adjust inventory quantity.
2452    pub fn adjust(&self, sku: &str, quantity: f64, _reason: &str) -> Result<(), JsValue> {
2453        let mut store = self.store.borrow_mut();
2454
2455        let item_id = store
2456            .inventory_by_sku
2457            .get(sku)
2458            .copied()
2459            .ok_or_else(|| JsValue::from_str("SKU not found"))?;
2460
2461        let balance = store
2462            .inventory_balances
2463            .get_mut(&item_id)
2464            .ok_or_else(|| JsValue::from_str("Balance not found"))?;
2465
2466        balance.on_hand += quantity;
2467        Ok(())
2468    }
2469
2470    /// Reserve inventory for an order.
2471    pub fn reserve(
2472        &self,
2473        sku: &str,
2474        quantity: f64,
2475        reference_type: &str,
2476        reference_id: &str,
2477    ) -> Result<JsValue, JsValue> {
2478        let mut store = self.store.borrow_mut();
2479
2480        let item_id = store
2481            .inventory_by_sku
2482            .get(sku)
2483            .copied()
2484            .ok_or_else(|| JsValue::from_str("SKU not found"))?;
2485
2486        let balance = store
2487            .inventory_balances
2488            .get_mut(&item_id)
2489            .ok_or_else(|| JsValue::from_str("Balance not found"))?;
2490
2491        let available = balance.on_hand - balance.allocated;
2492        if quantity > available {
2493            return Err(JsValue::from_str("Insufficient stock"));
2494        }
2495
2496        balance.allocated += quantity;
2497
2498        let id = Uuid::new_v4();
2499        let reservation = ReservationData {
2500            id,
2501            item_id,
2502            quantity,
2503            status: "pending".to_string(),
2504            reference_type: reference_type.to_string(),
2505            reference_id: reference_id.to_string(),
2506        };
2507
2508        store.reservations.insert(id, reservation.clone());
2509
2510        let js_reservation: JsReservation = (&reservation).into();
2511        serde_wasm_bindgen::to_value(&js_reservation).map_err(|e| JsValue::from_str(&e.to_string()))
2512    }
2513
2514    /// Confirm a reservation.
2515    #[wasm_bindgen(js_name = confirmReservation)]
2516    pub fn confirm_reservation(&self, reservation_id: &str) -> Result<(), JsValue> {
2517        let uuid =
2518            Uuid::parse_str(reservation_id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
2519        let mut store = self.store.borrow_mut();
2520
2521        let reservation = store
2522            .reservations
2523            .get_mut(&uuid)
2524            .ok_or_else(|| JsValue::from_str("Reservation not found"))?;
2525
2526        reservation.status = "confirmed".to_string();
2527        Ok(())
2528    }
2529
2530    /// Release a reservation.
2531    #[wasm_bindgen(js_name = releaseReservation)]
2532    pub fn release_reservation(&self, reservation_id: &str) -> Result<(), JsValue> {
2533        let uuid =
2534            Uuid::parse_str(reservation_id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
2535        let mut store = self.store.borrow_mut();
2536
2537        let reservation = store
2538            .reservations
2539            .get_mut(&uuid)
2540            .ok_or_else(|| JsValue::from_str("Reservation not found"))?;
2541
2542        if reservation.status == "released" {
2543            return Ok(());
2544        }
2545
2546        let quantity = reservation.quantity;
2547        let item_id = reservation.item_id;
2548        reservation.status = "released".to_string();
2549
2550        let balance = store
2551            .inventory_balances
2552            .get_mut(&item_id)
2553            .ok_or_else(|| JsValue::from_str("Balance not found"))?;
2554
2555        balance.allocated -= quantity;
2556        Ok(())
2557    }
2558}
2559
2560// ============================================================================
2561// Returns API
2562// ============================================================================
2563
2564/// Return processing operations.
2565#[wasm_bindgen]
2566pub struct Returns {
2567    store: StoreRef,
2568}
2569
2570#[wasm_bindgen]
2571impl Returns {
2572    /// Create a new return request.
2573    pub fn create(&self, input: JsValue) -> Result<JsValue, JsValue> {
2574        let input: CreateReturnInput =
2575            serde_wasm_bindgen::from_value(input).map_err(|e| JsValue::from_str(&e.to_string()))?;
2576
2577        let order_id = Uuid::parse_str(&input.order_id)
2578            .map_err(|_| JsValue::from_str("Invalid order UUID"))?;
2579
2580        let now = Utc::now().to_rfc3339();
2581        let id = Uuid::new_v4();
2582
2583        let data = ReturnData {
2584            id,
2585            order_id,
2586            status: "requested".to_string(),
2587            reason: input.reason,
2588            reason_details: input.reason_details,
2589            version: 1,
2590            created_at: now,
2591        };
2592
2593        let mut store = self.store.borrow_mut();
2594        store.returns.insert(id, data.clone());
2595
2596        // Create return items
2597        let items: Vec<ReturnItemData> = input
2598            .items
2599            .into_iter()
2600            .map(|i| ReturnItemData {
2601                id: Uuid::new_v4(),
2602                return_id: id,
2603                order_item_id: Uuid::parse_str(&i.order_item_id).unwrap_or_default(),
2604                quantity: i.quantity,
2605            })
2606            .collect();
2607        store.return_items.insert(id, items);
2608
2609        let js_return: JsReturn = (&data).into();
2610        serde_wasm_bindgen::to_value(&js_return).map_err(|e| JsValue::from_str(&e.to_string()))
2611    }
2612
2613    /// Get a return by ID.
2614    pub fn get(&self, id: &str) -> Result<JsValue, JsValue> {
2615        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
2616        let store = self.store.borrow();
2617
2618        match store.returns.get(&uuid) {
2619            Some(data) => {
2620                let js_return: JsReturn = data.into();
2621                serde_wasm_bindgen::to_value(&js_return)
2622                    .map_err(|e| JsValue::from_str(&e.to_string()))
2623            }
2624            None => Ok(JsValue::NULL),
2625        }
2626    }
2627
2628    /// Approve a return request.
2629    pub fn approve(&self, id: &str) -> Result<JsValue, JsValue> {
2630        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
2631        let mut store = self.store.borrow_mut();
2632
2633        let data =
2634            store.returns.get_mut(&uuid).ok_or_else(|| JsValue::from_str("Return not found"))?;
2635
2636        data.status = "approved".to_string();
2637
2638        let js_return: JsReturn = (&*data).into();
2639        serde_wasm_bindgen::to_value(&js_return).map_err(|e| JsValue::from_str(&e.to_string()))
2640    }
2641
2642    /// Reject a return request.
2643    pub fn reject(&self, id: &str, _reason: &str) -> Result<JsValue, JsValue> {
2644        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
2645        let mut store = self.store.borrow_mut();
2646
2647        let data =
2648            store.returns.get_mut(&uuid).ok_or_else(|| JsValue::from_str("Return not found"))?;
2649
2650        data.status = "rejected".to_string();
2651
2652        let js_return: JsReturn = (&*data).into();
2653        serde_wasm_bindgen::to_value(&js_return).map_err(|e| JsValue::from_str(&e.to_string()))
2654    }
2655
2656    /// List all returns.
2657    pub fn list(&self) -> Result<JsValue, JsValue> {
2658        let store = self.store.borrow();
2659        let returns: Vec<JsReturn> = store.returns.values().map(|data| data.into()).collect();
2660
2661        serde_wasm_bindgen::to_value(&returns).map_err(|e| JsValue::from_str(&e.to_string()))
2662    }
2663
2664    /// Count returns.
2665    pub fn count(&self) -> u32 {
2666        self.store.borrow().returns.len() as u32
2667    }
2668}
2669
2670// ============================================================================
2671// Payments API
2672// ============================================================================
2673
2674/// Payment processing operations.
2675#[wasm_bindgen]
2676pub struct Payments {
2677    store: StoreRef,
2678}
2679
2680#[wasm_bindgen]
2681impl Payments {
2682    /// Create a new payment.
2683    pub fn create(&self, input: JsValue) -> Result<JsValue, JsValue> {
2684        let input: CreatePaymentInput =
2685            serde_wasm_bindgen::from_value(input).map_err(|e| JsValue::from_str(&e.to_string()))?;
2686
2687        let now = Utc::now().to_rfc3339();
2688        let id = Uuid::new_v4();
2689
2690        let mut store = self.store.borrow_mut();
2691        store.next_payment_number += 1;
2692        let payment_number = format!("PAY-{}", store.next_payment_number);
2693
2694        let data = PaymentData {
2695            id,
2696            payment_number,
2697            order_id: input.order_id.as_ref().and_then(|s| Uuid::parse_str(s).ok()),
2698            customer_id: input.customer_id.as_ref().and_then(|s| Uuid::parse_str(s).ok()),
2699            amount: input.amount,
2700            currency: input.currency.unwrap_or_else(|| "USD".to_string()),
2701            status: "pending".to_string(),
2702            payment_method: input.payment_method,
2703            version: 1,
2704            created_at: now.clone(),
2705            updated_at: now,
2706        };
2707
2708        store.payments.insert(id, data.clone());
2709
2710        let js_payment: JsPayment = (&data).into();
2711        serde_wasm_bindgen::to_value(&js_payment).map_err(|e| JsValue::from_str(&e.to_string()))
2712    }
2713
2714    /// Get a payment by ID.
2715    pub fn get(&self, id: &str) -> Result<JsValue, JsValue> {
2716        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
2717        let store = self.store.borrow();
2718
2719        match store.payments.get(&uuid) {
2720            Some(data) => {
2721                let js_payment: JsPayment = data.into();
2722                serde_wasm_bindgen::to_value(&js_payment)
2723                    .map_err(|e| JsValue::from_str(&e.to_string()))
2724            }
2725            None => Ok(JsValue::NULL),
2726        }
2727    }
2728
2729    /// Complete a payment.
2730    pub fn complete(&self, id: &str) -> Result<JsValue, JsValue> {
2731        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
2732        let mut store = self.store.borrow_mut();
2733
2734        let data =
2735            store.payments.get_mut(&uuid).ok_or_else(|| JsValue::from_str("Payment not found"))?;
2736
2737        data.status = "completed".to_string();
2738        data.updated_at = Utc::now().to_rfc3339();
2739
2740        let js_payment: JsPayment = (&*data).into();
2741        serde_wasm_bindgen::to_value(&js_payment).map_err(|e| JsValue::from_str(&e.to_string()))
2742    }
2743
2744    /// Create a refund.
2745    pub fn refund(&self, input: JsValue) -> Result<JsValue, JsValue> {
2746        let input: CreateRefundInput =
2747            serde_wasm_bindgen::from_value(input).map_err(|e| JsValue::from_str(&e.to_string()))?;
2748
2749        let payment_id = Uuid::parse_str(&input.payment_id)
2750            .map_err(|_| JsValue::from_str("Invalid payment UUID"))?;
2751
2752        let now = Utc::now().to_rfc3339();
2753        let id = Uuid::new_v4();
2754
2755        let data = RefundData {
2756            id,
2757            payment_id,
2758            amount: input.amount,
2759            reason: input.reason,
2760            status: "pending".to_string(),
2761            created_at: now,
2762        };
2763
2764        self.store.borrow_mut().refunds.insert(id, data.clone());
2765
2766        let js_refund: JsRefund = (&data).into();
2767        serde_wasm_bindgen::to_value(&js_refund).map_err(|e| JsValue::from_str(&e.to_string()))
2768    }
2769
2770    /// List all payments.
2771    pub fn list(&self) -> Result<JsValue, JsValue> {
2772        let store = self.store.borrow();
2773        let payments: Vec<JsPayment> = store.payments.values().map(|data| data.into()).collect();
2774        serde_wasm_bindgen::to_value(&payments).map_err(|e| JsValue::from_str(&e.to_string()))
2775    }
2776
2777    /// Count payments.
2778    pub fn count(&self) -> u32 {
2779        self.store.borrow().payments.len() as u32
2780    }
2781}
2782
2783// ============================================================================
2784// Shipments API
2785// ============================================================================
2786
2787/// Shipment management operations.
2788#[wasm_bindgen]
2789pub struct Shipments {
2790    store: StoreRef,
2791}
2792
2793#[wasm_bindgen]
2794impl Shipments {
2795    /// Create a new shipment.
2796    pub fn create(&self, input: JsValue) -> Result<JsValue, JsValue> {
2797        let input: CreateShipmentInput =
2798            serde_wasm_bindgen::from_value(input).map_err(|e| JsValue::from_str(&e.to_string()))?;
2799
2800        let order_id = Uuid::parse_str(&input.order_id)
2801            .map_err(|_| JsValue::from_str("Invalid order UUID"))?;
2802
2803        let now = Utc::now().to_rfc3339();
2804        let id = Uuid::new_v4();
2805
2806        let mut store = self.store.borrow_mut();
2807        store.next_shipment_number += 1;
2808        let shipment_number = format!("SHP-{}", store.next_shipment_number);
2809
2810        let data = ShipmentData {
2811            id,
2812            shipment_number,
2813            order_id,
2814            carrier: input.carrier,
2815            tracking_number: input.tracking_number,
2816            status: "pending".to_string(),
2817            shipped_at: None,
2818            delivered_at: None,
2819            version: 1,
2820            created_at: now.clone(),
2821            updated_at: now,
2822        };
2823
2824        store.shipments.insert(id, data.clone());
2825
2826        let js_shipment: JsShipment = (&data).into();
2827        serde_wasm_bindgen::to_value(&js_shipment).map_err(|e| JsValue::from_str(&e.to_string()))
2828    }
2829
2830    /// Get a shipment by ID.
2831    pub fn get(&self, id: &str) -> Result<JsValue, JsValue> {
2832        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
2833        let store = self.store.borrow();
2834
2835        match store.shipments.get(&uuid) {
2836            Some(data) => {
2837                let js_shipment: JsShipment = data.into();
2838                serde_wasm_bindgen::to_value(&js_shipment)
2839                    .map_err(|e| JsValue::from_str(&e.to_string()))
2840            }
2841            None => Ok(JsValue::NULL),
2842        }
2843    }
2844
2845    /// Ship a shipment.
2846    pub fn ship(&self, id: &str) -> Result<JsValue, JsValue> {
2847        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
2848        let mut store = self.store.borrow_mut();
2849
2850        let data = store
2851            .shipments
2852            .get_mut(&uuid)
2853            .ok_or_else(|| JsValue::from_str("Shipment not found"))?;
2854
2855        let now = Utc::now().to_rfc3339();
2856        data.status = "shipped".to_string();
2857        data.shipped_at = Some(now.clone());
2858        data.updated_at = now;
2859
2860        let js_shipment: JsShipment = (&*data).into();
2861        serde_wasm_bindgen::to_value(&js_shipment).map_err(|e| JsValue::from_str(&e.to_string()))
2862    }
2863
2864    /// Mark shipment as delivered.
2865    pub fn deliver(&self, id: &str) -> Result<JsValue, JsValue> {
2866        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
2867        let mut store = self.store.borrow_mut();
2868
2869        let data = store
2870            .shipments
2871            .get_mut(&uuid)
2872            .ok_or_else(|| JsValue::from_str("Shipment not found"))?;
2873
2874        let now = Utc::now().to_rfc3339();
2875        data.status = "delivered".to_string();
2876        data.delivered_at = Some(now.clone());
2877        data.updated_at = now;
2878
2879        let js_shipment: JsShipment = (&*data).into();
2880        serde_wasm_bindgen::to_value(&js_shipment).map_err(|e| JsValue::from_str(&e.to_string()))
2881    }
2882
2883    /// List all shipments.
2884    pub fn list(&self) -> Result<JsValue, JsValue> {
2885        let store = self.store.borrow();
2886        let shipments: Vec<JsShipment> = store.shipments.values().map(|data| data.into()).collect();
2887        serde_wasm_bindgen::to_value(&shipments).map_err(|e| JsValue::from_str(&e.to_string()))
2888    }
2889
2890    /// Count shipments.
2891    pub fn count(&self) -> u32 {
2892        self.store.borrow().shipments.len() as u32
2893    }
2894}
2895
2896// ============================================================================
2897// Warranties API
2898// ============================================================================
2899
2900/// Warranty management operations.
2901#[wasm_bindgen]
2902pub struct Warranties {
2903    store: StoreRef,
2904}
2905
2906#[wasm_bindgen]
2907impl Warranties {
2908    /// Create a new warranty.
2909    pub fn create(&self, input: JsValue) -> Result<JsValue, JsValue> {
2910        let input: CreateWarrantyInput =
2911            serde_wasm_bindgen::from_value(input).map_err(|e| JsValue::from_str(&e.to_string()))?;
2912
2913        let customer_id = Uuid::parse_str(&input.customer_id)
2914            .map_err(|_| JsValue::from_str("Invalid customer UUID"))?;
2915
2916        let now = Utc::now();
2917        let duration_months = input.duration_months.unwrap_or(12);
2918        let end_date = now + chrono::Duration::days(duration_months as i64 * 30);
2919        let id = Uuid::new_v4();
2920
2921        let mut store = self.store.borrow_mut();
2922        store.next_warranty_number += 1;
2923        let warranty_number = format!("WTY-{}", store.next_warranty_number);
2924
2925        let data = WarrantyData {
2926            id,
2927            warranty_number,
2928            customer_id,
2929            product_id: input.product_id.as_ref().and_then(|s| Uuid::parse_str(s).ok()),
2930            order_id: input.order_id.as_ref().and_then(|s| Uuid::parse_str(s).ok()),
2931            status: "active".to_string(),
2932            duration_months,
2933            start_date: now.to_rfc3339(),
2934            end_date: end_date.to_rfc3339(),
2935            created_at: now.to_rfc3339(),
2936        };
2937
2938        store.warranties.insert(id, data.clone());
2939
2940        let js_warranty: JsWarranty = (&data).into();
2941        serde_wasm_bindgen::to_value(&js_warranty).map_err(|e| JsValue::from_str(&e.to_string()))
2942    }
2943
2944    /// Get a warranty by ID.
2945    pub fn get(&self, id: &str) -> Result<JsValue, JsValue> {
2946        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
2947        let store = self.store.borrow();
2948
2949        match store.warranties.get(&uuid) {
2950            Some(data) => {
2951                let js_warranty: JsWarranty = data.into();
2952                serde_wasm_bindgen::to_value(&js_warranty)
2953                    .map_err(|e| JsValue::from_str(&e.to_string()))
2954            }
2955            None => Ok(JsValue::NULL),
2956        }
2957    }
2958
2959    /// File a warranty claim.
2960    #[wasm_bindgen(js_name = createClaim)]
2961    pub fn create_claim(&self, input: JsValue) -> Result<JsValue, JsValue> {
2962        let input: CreateWarrantyClaimInput =
2963            serde_wasm_bindgen::from_value(input).map_err(|e| JsValue::from_str(&e.to_string()))?;
2964
2965        let warranty_id = Uuid::parse_str(&input.warranty_id)
2966            .map_err(|_| JsValue::from_str("Invalid warranty UUID"))?;
2967
2968        let now = Utc::now().to_rfc3339();
2969        let id = Uuid::new_v4();
2970
2971        let mut store = self.store.borrow_mut();
2972        store.next_claim_number += 1;
2973        let claim_number = format!("CLM-{}", store.next_claim_number);
2974
2975        let data = WarrantyClaimData {
2976            id,
2977            claim_number,
2978            warranty_id,
2979            issue_description: input.issue_description,
2980            status: "submitted".to_string(),
2981            resolution: None,
2982            created_at: now,
2983        };
2984
2985        store.warranty_claims.insert(id, data.clone());
2986
2987        let js_claim: JsWarrantyClaim = (&data).into();
2988        serde_wasm_bindgen::to_value(&js_claim).map_err(|e| JsValue::from_str(&e.to_string()))
2989    }
2990
2991    /// Approve a warranty claim.
2992    #[wasm_bindgen(js_name = approveClaim)]
2993    pub fn approve_claim(&self, id: &str) -> Result<JsValue, JsValue> {
2994        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
2995        let mut store = self.store.borrow_mut();
2996
2997        let data = store
2998            .warranty_claims
2999            .get_mut(&uuid)
3000            .ok_or_else(|| JsValue::from_str("Claim not found"))?;
3001
3002        data.status = "approved".to_string();
3003
3004        let js_claim: JsWarrantyClaim = (&*data).into();
3005        serde_wasm_bindgen::to_value(&js_claim).map_err(|e| JsValue::from_str(&e.to_string()))
3006    }
3007
3008    /// List all warranties.
3009    pub fn list(&self) -> Result<JsValue, JsValue> {
3010        let store = self.store.borrow();
3011        let warranties: Vec<JsWarranty> =
3012            store.warranties.values().map(|data| data.into()).collect();
3013        serde_wasm_bindgen::to_value(&warranties).map_err(|e| JsValue::from_str(&e.to_string()))
3014    }
3015
3016    /// Count warranties.
3017    pub fn count(&self) -> u32 {
3018        self.store.borrow().warranties.len() as u32
3019    }
3020}
3021
3022// ============================================================================
3023// Purchase Orders API
3024// ============================================================================
3025
3026/// Purchase order management operations.
3027#[wasm_bindgen]
3028pub struct PurchaseOrders {
3029    store: StoreRef,
3030}
3031
3032#[wasm_bindgen]
3033impl PurchaseOrders {
3034    /// Create a new supplier.
3035    #[wasm_bindgen(js_name = createSupplier)]
3036    pub fn create_supplier(&self, input: JsValue) -> Result<JsValue, JsValue> {
3037        let input: CreateSupplierInput =
3038            serde_wasm_bindgen::from_value(input).map_err(|e| JsValue::from_str(&e.to_string()))?;
3039
3040        let now = Utc::now().to_rfc3339();
3041        let id = Uuid::new_v4();
3042
3043        let mut store = self.store.borrow_mut();
3044        store.next_supplier_code += 1;
3045        let supplier_code = format!("SUP-{}", store.next_supplier_code);
3046
3047        let data = SupplierData {
3048            id,
3049            supplier_code,
3050            name: input.name,
3051            email: input.email,
3052            phone: input.phone,
3053            status: "active".to_string(),
3054            created_at: now,
3055        };
3056
3057        store.suppliers.insert(id, data.clone());
3058
3059        let js_supplier: JsSupplier = (&data).into();
3060        serde_wasm_bindgen::to_value(&js_supplier).map_err(|e| JsValue::from_str(&e.to_string()))
3061    }
3062
3063    /// Get a supplier by ID.
3064    #[wasm_bindgen(js_name = getSupplier)]
3065    pub fn get_supplier(&self, id: &str) -> Result<JsValue, JsValue> {
3066        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
3067        let store = self.store.borrow();
3068
3069        match store.suppliers.get(&uuid) {
3070            Some(data) => {
3071                let js_supplier: JsSupplier = data.into();
3072                serde_wasm_bindgen::to_value(&js_supplier)
3073                    .map_err(|e| JsValue::from_str(&e.to_string()))
3074            }
3075            None => Ok(JsValue::NULL),
3076        }
3077    }
3078
3079    /// Create a new purchase order.
3080    pub fn create(&self, input: JsValue) -> Result<JsValue, JsValue> {
3081        let input: CreatePurchaseOrderInput =
3082            serde_wasm_bindgen::from_value(input).map_err(|e| JsValue::from_str(&e.to_string()))?;
3083
3084        let supplier_id = Uuid::parse_str(&input.supplier_id)
3085            .map_err(|_| JsValue::from_str("Invalid supplier UUID"))?;
3086
3087        let now = Utc::now().to_rfc3339();
3088        let id = Uuid::new_v4();
3089
3090        let mut store = self.store.borrow_mut();
3091        store.next_po_number += 1;
3092        let po_number = format!("PO-{}", store.next_po_number);
3093
3094        let data = PurchaseOrderData {
3095            id,
3096            po_number,
3097            supplier_id,
3098            status: "draft".to_string(),
3099            total_amount: Money::zero(),
3100            currency: input.currency.unwrap_or_else(|| "USD".to_string()),
3101            created_at: now.clone(),
3102            updated_at: now,
3103        };
3104
3105        store.purchase_orders.insert(id, data.clone());
3106
3107        let js_po: JsPurchaseOrder = (&data).into();
3108        serde_wasm_bindgen::to_value(&js_po).map_err(|e| JsValue::from_str(&e.to_string()))
3109    }
3110
3111    /// Get a purchase order by ID.
3112    pub fn get(&self, id: &str) -> Result<JsValue, JsValue> {
3113        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
3114        let store = self.store.borrow();
3115
3116        match store.purchase_orders.get(&uuid) {
3117            Some(data) => {
3118                let js_po: JsPurchaseOrder = data.into();
3119                serde_wasm_bindgen::to_value(&js_po).map_err(|e| JsValue::from_str(&e.to_string()))
3120            }
3121            None => Ok(JsValue::NULL),
3122        }
3123    }
3124
3125    /// Submit a PO for approval.
3126    pub fn submit(&self, id: &str) -> Result<JsValue, JsValue> {
3127        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
3128        let mut store = self.store.borrow_mut();
3129
3130        let data = store
3131            .purchase_orders
3132            .get_mut(&uuid)
3133            .ok_or_else(|| JsValue::from_str("PO not found"))?;
3134
3135        data.status = "pending_approval".to_string();
3136        data.updated_at = Utc::now().to_rfc3339();
3137
3138        let js_po: JsPurchaseOrder = (&*data).into();
3139        serde_wasm_bindgen::to_value(&js_po).map_err(|e| JsValue::from_str(&e.to_string()))
3140    }
3141
3142    /// Approve a purchase order.
3143    pub fn approve(&self, id: &str) -> Result<JsValue, JsValue> {
3144        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
3145        let mut store = self.store.borrow_mut();
3146
3147        let data = store
3148            .purchase_orders
3149            .get_mut(&uuid)
3150            .ok_or_else(|| JsValue::from_str("PO not found"))?;
3151
3152        data.status = "approved".to_string();
3153        data.updated_at = Utc::now().to_rfc3339();
3154
3155        let js_po: JsPurchaseOrder = (&*data).into();
3156        serde_wasm_bindgen::to_value(&js_po).map_err(|e| JsValue::from_str(&e.to_string()))
3157    }
3158
3159    /// List all purchase orders.
3160    pub fn list(&self) -> Result<JsValue, JsValue> {
3161        let store = self.store.borrow();
3162        let pos: Vec<JsPurchaseOrder> =
3163            store.purchase_orders.values().map(|data| data.into()).collect();
3164        serde_wasm_bindgen::to_value(&pos).map_err(|e| JsValue::from_str(&e.to_string()))
3165    }
3166
3167    /// Count purchase orders.
3168    pub fn count(&self) -> u32 {
3169        self.store.borrow().purchase_orders.len() as u32
3170    }
3171}
3172
3173// ============================================================================
3174// Invoices API
3175// ============================================================================
3176
3177/// Invoice management operations.
3178#[wasm_bindgen]
3179pub struct Invoices {
3180    store: StoreRef,
3181}
3182
3183#[wasm_bindgen]
3184impl Invoices {
3185    /// Create a new invoice.
3186    pub fn create(&self, input: JsValue) -> Result<JsValue, JsValue> {
3187        let input: CreateInvoiceInput =
3188            serde_wasm_bindgen::from_value(input).map_err(|e| JsValue::from_str(&e.to_string()))?;
3189
3190        let customer_id = Uuid::parse_str(&input.customer_id)
3191            .map_err(|_| JsValue::from_str("Invalid customer UUID"))?;
3192
3193        let now = Utc::now().to_rfc3339();
3194        let id = Uuid::new_v4();
3195
3196        let mut store = self.store.borrow_mut();
3197        store.next_invoice_number += 1;
3198        let invoice_number = format!("INV-{}", store.next_invoice_number);
3199
3200        let tax_amount = input.tax_amount.unwrap_or_default();
3201        let total = input.subtotal + tax_amount;
3202
3203        let data = InvoiceData {
3204            id,
3205            invoice_number,
3206            customer_id,
3207            order_id: input.order_id.as_ref().and_then(|s| Uuid::parse_str(s).ok()),
3208            status: "draft".to_string(),
3209            subtotal: input.subtotal,
3210            tax_amount,
3211            total,
3212            amount_paid: Money::zero(),
3213            due_date: input.due_date,
3214            created_at: now.clone(),
3215            updated_at: now,
3216        };
3217
3218        store.invoices.insert(id, data.clone());
3219
3220        let js_invoice: JsInvoice = (&data).into();
3221        serde_wasm_bindgen::to_value(&js_invoice).map_err(|e| JsValue::from_str(&e.to_string()))
3222    }
3223
3224    /// Get an invoice by ID.
3225    pub fn get(&self, id: &str) -> Result<JsValue, JsValue> {
3226        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
3227        let store = self.store.borrow();
3228
3229        match store.invoices.get(&uuid) {
3230            Some(data) => {
3231                let js_invoice: JsInvoice = data.into();
3232                serde_wasm_bindgen::to_value(&js_invoice)
3233                    .map_err(|e| JsValue::from_str(&e.to_string()))
3234            }
3235            None => Ok(JsValue::NULL),
3236        }
3237    }
3238
3239    /// Send an invoice.
3240    pub fn send(&self, id: &str) -> Result<JsValue, JsValue> {
3241        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
3242        let mut store = self.store.borrow_mut();
3243
3244        let data =
3245            store.invoices.get_mut(&uuid).ok_or_else(|| JsValue::from_str("Invoice not found"))?;
3246
3247        data.status = "sent".to_string();
3248        data.updated_at = Utc::now().to_rfc3339();
3249
3250        let js_invoice: JsInvoice = (&*data).into();
3251        serde_wasm_bindgen::to_value(&js_invoice).map_err(|e| JsValue::from_str(&e.to_string()))
3252    }
3253
3254    /// Record a payment on an invoice.
3255    #[wasm_bindgen(js_name = recordPayment)]
3256    pub fn record_payment(&self, id: &str, amount: f64) -> Result<JsValue, JsValue> {
3257        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
3258        let mut store = self.store.borrow_mut();
3259
3260        let data =
3261            store.invoices.get_mut(&uuid).ok_or_else(|| JsValue::from_str("Invoice not found"))?;
3262
3263        let amount = Money::from_f64(amount);
3264        data.amount_paid += amount;
3265        data.updated_at = Utc::now().to_rfc3339();
3266
3267        if data.amount_paid >= data.total {
3268            data.status = "paid".to_string();
3269        } else {
3270            data.status = "partially_paid".to_string();
3271        }
3272
3273        let js_invoice: JsInvoice = (&*data).into();
3274        serde_wasm_bindgen::to_value(&js_invoice).map_err(|e| JsValue::from_str(&e.to_string()))
3275    }
3276
3277    /// List all invoices.
3278    pub fn list(&self) -> Result<JsValue, JsValue> {
3279        let store = self.store.borrow();
3280        let invoices: Vec<JsInvoice> = store.invoices.values().map(|data| data.into()).collect();
3281        serde_wasm_bindgen::to_value(&invoices).map_err(|e| JsValue::from_str(&e.to_string()))
3282    }
3283
3284    /// Count invoices.
3285    pub fn count(&self) -> u32 {
3286        self.store.borrow().invoices.len() as u32
3287    }
3288}
3289
3290// ============================================================================
3291// BOM API
3292// ============================================================================
3293
3294/// Bill of Materials management operations.
3295#[wasm_bindgen]
3296pub struct Bom {
3297    store: StoreRef,
3298}
3299
3300#[wasm_bindgen]
3301impl Bom {
3302    /// Create a new bill of materials.
3303    pub fn create(&self, input: JsValue) -> Result<JsValue, JsValue> {
3304        let input: CreateBomInput =
3305            serde_wasm_bindgen::from_value(input).map_err(|e| JsValue::from_str(&e.to_string()))?;
3306
3307        let now = Utc::now().to_rfc3339();
3308        let id = Uuid::new_v4();
3309
3310        let mut store = self.store.borrow_mut();
3311        store.next_bom_number += 1;
3312        let bom_number = format!("BOM-{}", store.next_bom_number);
3313
3314        let data = BomData {
3315            id,
3316            bom_number,
3317            sku: input.sku,
3318            name: input.name,
3319            description: input.description,
3320            status: "draft".to_string(),
3321            version: 1,
3322            created_at: now.clone(),
3323            updated_at: now,
3324        };
3325
3326        store.boms.insert(id, data.clone());
3327        store.bom_components.insert(id, Vec::new());
3328
3329        let js_bom: JsBom = (&data).into();
3330        serde_wasm_bindgen::to_value(&js_bom).map_err(|e| JsValue::from_str(&e.to_string()))
3331    }
3332
3333    /// Get a BOM by ID.
3334    pub fn get(&self, id: &str) -> Result<JsValue, JsValue> {
3335        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
3336        let store = self.store.borrow();
3337
3338        match store.boms.get(&uuid) {
3339            Some(data) => {
3340                let js_bom: JsBom = data.into();
3341                serde_wasm_bindgen::to_value(&js_bom).map_err(|e| JsValue::from_str(&e.to_string()))
3342            }
3343            None => Ok(JsValue::NULL),
3344        }
3345    }
3346
3347    /// Add a component to a BOM.
3348    #[wasm_bindgen(js_name = addComponent)]
3349    pub fn add_component(&self, bom_id: &str, input: JsValue) -> Result<JsValue, JsValue> {
3350        let bom_uuid =
3351            Uuid::parse_str(bom_id).map_err(|_| JsValue::from_str("Invalid BOM UUID"))?;
3352        let input: AddBomComponentInput =
3353            serde_wasm_bindgen::from_value(input).map_err(|e| JsValue::from_str(&e.to_string()))?;
3354
3355        let component_id = Uuid::new_v4();
3356        let component = BomComponentData {
3357            id: component_id,
3358            bom_id: bom_uuid,
3359            component_sku: input.component_sku,
3360            component_name: input.component_name,
3361            quantity: input.quantity,
3362            unit_of_measure: input.unit_of_measure.unwrap_or_else(|| "each".to_string()),
3363        };
3364
3365        let mut store = self.store.borrow_mut();
3366        store.bom_components.entry(bom_uuid).or_default().push(component.clone());
3367
3368        let js_component: JsBomComponent = (&component).into();
3369        serde_wasm_bindgen::to_value(&js_component).map_err(|e| JsValue::from_str(&e.to_string()))
3370    }
3371
3372    /// Get components for a BOM.
3373    #[wasm_bindgen(js_name = getComponents)]
3374    pub fn get_components(&self, bom_id: &str) -> Result<JsValue, JsValue> {
3375        let uuid = Uuid::parse_str(bom_id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
3376        let store = self.store.borrow();
3377
3378        let components: Vec<JsBomComponent> = store
3379            .bom_components
3380            .get(&uuid)
3381            .map(|c| c.iter().map(|data| data.into()).collect())
3382            .unwrap_or_default();
3383
3384        serde_wasm_bindgen::to_value(&components).map_err(|e| JsValue::from_str(&e.to_string()))
3385    }
3386
3387    /// Activate a BOM.
3388    pub fn activate(&self, id: &str) -> Result<JsValue, JsValue> {
3389        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
3390        let mut store = self.store.borrow_mut();
3391
3392        let data = store.boms.get_mut(&uuid).ok_or_else(|| JsValue::from_str("BOM not found"))?;
3393
3394        data.status = "active".to_string();
3395        data.updated_at = Utc::now().to_rfc3339();
3396
3397        let js_bom: JsBom = (&*data).into();
3398        serde_wasm_bindgen::to_value(&js_bom).map_err(|e| JsValue::from_str(&e.to_string()))
3399    }
3400
3401    /// List all BOMs.
3402    pub fn list(&self) -> Result<JsValue, JsValue> {
3403        let store = self.store.borrow();
3404        let boms: Vec<JsBom> = store.boms.values().map(|data| data.into()).collect();
3405        serde_wasm_bindgen::to_value(&boms).map_err(|e| JsValue::from_str(&e.to_string()))
3406    }
3407
3408    /// Count BOMs.
3409    pub fn count(&self) -> u32 {
3410        self.store.borrow().boms.len() as u32
3411    }
3412}
3413
3414// ============================================================================
3415// Work Orders API
3416// ============================================================================
3417
3418/// Work order management operations.
3419#[wasm_bindgen]
3420pub struct WorkOrders {
3421    store: StoreRef,
3422}
3423
3424#[wasm_bindgen]
3425impl WorkOrders {
3426    /// Create a new work order.
3427    pub fn create(&self, input: JsValue) -> Result<JsValue, JsValue> {
3428        let input: CreateWorkOrderInput =
3429            serde_wasm_bindgen::from_value(input).map_err(|e| JsValue::from_str(&e.to_string()))?;
3430
3431        let bom_id =
3432            Uuid::parse_str(&input.bom_id).map_err(|_| JsValue::from_str("Invalid BOM UUID"))?;
3433
3434        let now = Utc::now().to_rfc3339();
3435        let id = Uuid::new_v4();
3436
3437        let mut store = self.store.borrow_mut();
3438        store.next_work_order_number += 1;
3439        let work_order_number = format!("WO-{}", store.next_work_order_number);
3440
3441        let data = WorkOrderData {
3442            id,
3443            work_order_number,
3444            bom_id,
3445            status: "draft".to_string(),
3446            quantity_to_build: input.quantity_to_build,
3447            quantity_built: 0.0,
3448            priority: input.priority.unwrap_or_else(|| "normal".to_string()),
3449            scheduled_start: input.scheduled_start,
3450            scheduled_end: input.scheduled_end,
3451            version: 1,
3452            created_at: now.clone(),
3453            updated_at: now,
3454        };
3455
3456        store.work_orders.insert(id, data.clone());
3457
3458        let js_wo: JsWorkOrder = (&data).into();
3459        serde_wasm_bindgen::to_value(&js_wo).map_err(|e| JsValue::from_str(&e.to_string()))
3460    }
3461
3462    /// Get a work order by ID.
3463    pub fn get(&self, id: &str) -> Result<JsValue, JsValue> {
3464        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
3465        let store = self.store.borrow();
3466
3467        match store.work_orders.get(&uuid) {
3468            Some(data) => {
3469                let js_wo: JsWorkOrder = data.into();
3470                serde_wasm_bindgen::to_value(&js_wo).map_err(|e| JsValue::from_str(&e.to_string()))
3471            }
3472            None => Ok(JsValue::NULL),
3473        }
3474    }
3475
3476    /// Start a work order.
3477    pub fn start(&self, id: &str) -> Result<JsValue, JsValue> {
3478        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
3479        let mut store = self.store.borrow_mut();
3480
3481        let data = store
3482            .work_orders
3483            .get_mut(&uuid)
3484            .ok_or_else(|| JsValue::from_str("Work order not found"))?;
3485
3486        data.status = "in_progress".to_string();
3487        data.updated_at = Utc::now().to_rfc3339();
3488
3489        let js_wo: JsWorkOrder = (&*data).into();
3490        serde_wasm_bindgen::to_value(&js_wo).map_err(|e| JsValue::from_str(&e.to_string()))
3491    }
3492
3493    /// Record production output.
3494    #[wasm_bindgen(js_name = recordOutput)]
3495    pub fn record_output(&self, id: &str, quantity: f64) -> Result<JsValue, JsValue> {
3496        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
3497        let mut store = self.store.borrow_mut();
3498
3499        let data = store
3500            .work_orders
3501            .get_mut(&uuid)
3502            .ok_or_else(|| JsValue::from_str("Work order not found"))?;
3503
3504        data.quantity_built += quantity;
3505        data.updated_at = Utc::now().to_rfc3339();
3506
3507        if data.quantity_built >= data.quantity_to_build {
3508            data.status = "completed".to_string();
3509        }
3510
3511        let js_wo: JsWorkOrder = (&*data).into();
3512        serde_wasm_bindgen::to_value(&js_wo).map_err(|e| JsValue::from_str(&e.to_string()))
3513    }
3514
3515    /// Complete a work order.
3516    pub fn complete(&self, id: &str) -> Result<JsValue, JsValue> {
3517        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
3518        let mut store = self.store.borrow_mut();
3519
3520        let data = store
3521            .work_orders
3522            .get_mut(&uuid)
3523            .ok_or_else(|| JsValue::from_str("Work order not found"))?;
3524
3525        data.status = "completed".to_string();
3526        data.updated_at = Utc::now().to_rfc3339();
3527
3528        let js_wo: JsWorkOrder = (&*data).into();
3529        serde_wasm_bindgen::to_value(&js_wo).map_err(|e| JsValue::from_str(&e.to_string()))
3530    }
3531
3532    /// List all work orders.
3533    pub fn list(&self) -> Result<JsValue, JsValue> {
3534        let store = self.store.borrow();
3535        let work_orders: Vec<JsWorkOrder> =
3536            store.work_orders.values().map(|data| data.into()).collect();
3537        serde_wasm_bindgen::to_value(&work_orders).map_err(|e| JsValue::from_str(&e.to_string()))
3538    }
3539
3540    /// Count work orders.
3541    pub fn count(&self) -> u32 {
3542        self.store.borrow().work_orders.len() as u32
3543    }
3544}
3545
3546// ============================================================================
3547// Carts API
3548// ============================================================================
3549
3550/// Cart and checkout management operations.
3551#[wasm_bindgen]
3552pub struct Carts {
3553    store: StoreRef,
3554}
3555
3556#[wasm_bindgen]
3557impl Carts {
3558    /// Create a new cart.
3559    pub fn create(&self, input: JsValue) -> Result<JsValue, JsValue> {
3560        let input: CreateCartInput =
3561            serde_wasm_bindgen::from_value(input).map_err(|e| JsValue::from_str(&e.to_string()))?;
3562
3563        let customer_id = input
3564            .customer_id
3565            .map(|id| Uuid::parse_str(&id))
3566            .transpose()
3567            .map_err(|_| JsValue::from_str("Invalid customer UUID"))?;
3568
3569        let now = Utc::now().to_rfc3339();
3570        let id = Uuid::new_v4();
3571
3572        let mut store = self.store.borrow_mut();
3573        store.next_cart_number += 1;
3574        let cart_number = format!("CART-{}", store.next_cart_number);
3575
3576        let data = CartData {
3577            id,
3578            cart_number,
3579            customer_id,
3580            status: "active".to_string(),
3581            currency: input.currency.unwrap_or_else(|| "USD".to_string()),
3582            subtotal: Money::zero(),
3583            tax_amount: Money::zero(),
3584            shipping_amount: Money::zero(),
3585            discount_amount: Money::zero(),
3586            grand_total: Money::zero(),
3587            customer_email: input.customer_email,
3588            customer_name: input.customer_name,
3589            payment_method: None,
3590            payment_status: "pending".to_string(),
3591            fulfillment_type: "shipping".to_string(),
3592            shipping_method: None,
3593            coupon_code: None,
3594            notes: None,
3595            created_at: now.clone(),
3596            updated_at: now,
3597            expires_at: None,
3598        };
3599
3600        store.carts.insert(id, data.clone());
3601        store.cart_items.insert(id, Vec::new());
3602
3603        let items: Vec<JsCartItem> = Vec::new();
3604        let js_cart = JsCart {
3605            id: data.id.to_string(),
3606            cart_number: data.cart_number.clone(),
3607            customer_id: data.customer_id.map(|id| id.to_string()),
3608            status: data.status.clone(),
3609            currency: data.currency.clone(),
3610            subtotal: data.subtotal,
3611            tax_amount: data.tax_amount,
3612            shipping_amount: data.shipping_amount,
3613            discount_amount: data.discount_amount,
3614            grand_total: data.grand_total,
3615            customer_email: data.customer_email.clone(),
3616            customer_name: data.customer_name.clone(),
3617            payment_method: data.payment_method.clone(),
3618            payment_status: data.payment_status.clone(),
3619            fulfillment_type: data.fulfillment_type.clone(),
3620            shipping_method: data.shipping_method.clone(),
3621            coupon_code: data.coupon_code.clone(),
3622            notes: data.notes.clone(),
3623            item_count: 0,
3624            items,
3625            created_at: data.created_at.clone(),
3626            updated_at: data.updated_at.clone(),
3627            expires_at: data.expires_at.clone(),
3628        };
3629
3630        serde_wasm_bindgen::to_value(&js_cart).map_err(|e| JsValue::from_str(&e.to_string()))
3631    }
3632
3633    /// Get a cart by ID.
3634    pub fn get(&self, id: &str) -> Result<JsValue, JsValue> {
3635        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
3636        let store = self.store.borrow();
3637
3638        match store.carts.get(&uuid) {
3639            Some(data) => {
3640                let items: Vec<JsCartItem> = store
3641                    .cart_items
3642                    .get(&uuid)
3643                    .map(|items| items.iter().map(|i| i.into()).collect())
3644                    .unwrap_or_default();
3645
3646                let js_cart = JsCart {
3647                    id: data.id.to_string(),
3648                    cart_number: data.cart_number.clone(),
3649                    customer_id: data.customer_id.map(|id| id.to_string()),
3650                    status: data.status.clone(),
3651                    currency: data.currency.clone(),
3652                    subtotal: data.subtotal,
3653                    tax_amount: data.tax_amount,
3654                    shipping_amount: data.shipping_amount,
3655                    discount_amount: data.discount_amount,
3656                    grand_total: data.grand_total,
3657                    customer_email: data.customer_email.clone(),
3658                    customer_name: data.customer_name.clone(),
3659                    payment_method: data.payment_method.clone(),
3660                    payment_status: data.payment_status.clone(),
3661                    fulfillment_type: data.fulfillment_type.clone(),
3662                    shipping_method: data.shipping_method.clone(),
3663                    coupon_code: data.coupon_code.clone(),
3664                    notes: data.notes.clone(),
3665                    item_count: items.len(),
3666                    items,
3667                    created_at: data.created_at.clone(),
3668                    updated_at: data.updated_at.clone(),
3669                    expires_at: data.expires_at.clone(),
3670                };
3671
3672                serde_wasm_bindgen::to_value(&js_cart)
3673                    .map_err(|e| JsValue::from_str(&e.to_string()))
3674            }
3675            None => Ok(JsValue::NULL),
3676        }
3677    }
3678
3679    /// Add an item to the cart.
3680    #[wasm_bindgen(js_name = addItem)]
3681    pub fn add_item(&self, cart_id: &str, input: JsValue) -> Result<JsValue, JsValue> {
3682        let uuid = Uuid::parse_str(cart_id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
3683        let input: AddCartItemInput =
3684            serde_wasm_bindgen::from_value(input).map_err(|e| JsValue::from_str(&e.to_string()))?;
3685
3686        let mut store = self.store.borrow_mut();
3687
3688        // Check if cart exists
3689        if !store.carts.contains_key(&uuid) {
3690            return Err(JsValue::from_str("Cart not found"));
3691        }
3692
3693        let item_id = Uuid::new_v4();
3694        let total = input.unit_price * input.quantity;
3695
3696        let item = CartItemData {
3697            id: item_id,
3698            cart_id: uuid,
3699            sku: input.sku,
3700            name: input.name,
3701            description: input.description,
3702            quantity: input.quantity,
3703            unit_price: input.unit_price,
3704            total,
3705        };
3706
3707        let js_item: JsCartItem = (&item).into();
3708
3709        // Add item to cart items collection first
3710        store.cart_items.entry(uuid).or_default().push(item);
3711
3712        // Now update cart totals
3713        let cart = store.carts.get_mut(&uuid).ok_or_else(|| JsValue::from_str("Cart not found"))?;
3714        cart.subtotal += total;
3715        cart.grand_total =
3716            cart.subtotal + cart.tax_amount + cart.shipping_amount - cart.discount_amount;
3717        cart.updated_at = Utc::now().to_rfc3339();
3718
3719        serde_wasm_bindgen::to_value(&js_item).map_err(|e| JsValue::from_str(&e.to_string()))
3720    }
3721
3722    /// Remove an item from the cart.
3723    #[wasm_bindgen(js_name = removeItem)]
3724    pub fn remove_item(&self, cart_id: &str, item_id: &str) -> Result<(), JsValue> {
3725        let cart_uuid =
3726            Uuid::parse_str(cart_id).map_err(|_| JsValue::from_str("Invalid cart UUID"))?;
3727        let item_uuid =
3728            Uuid::parse_str(item_id).map_err(|_| JsValue::from_str("Invalid item UUID"))?;
3729
3730        let mut store = self.store.borrow_mut();
3731
3732        // Check if cart exists
3733        if !store.carts.contains_key(&cart_uuid) {
3734            return Err(JsValue::from_str("Cart not found"));
3735        }
3736
3737        // Remove item and get its total
3738        let item_total = if let Some(items) = store.cart_items.get_mut(&cart_uuid) {
3739            if let Some(pos) = items.iter().position(|i| i.id == item_uuid) {
3740                let item = items.remove(pos);
3741                Some(item.total)
3742            } else {
3743                None
3744            }
3745        } else {
3746            None
3747        };
3748
3749        // Update cart totals if item was removed
3750        if let Some(total) = item_total {
3751            let cart = store
3752                .carts
3753                .get_mut(&cart_uuid)
3754                .ok_or_else(|| JsValue::from_str("Cart not found"))?;
3755            cart.subtotal -= total;
3756            cart.grand_total =
3757                cart.subtotal + cart.tax_amount + cart.shipping_amount - cart.discount_amount;
3758            cart.updated_at = Utc::now().to_rfc3339();
3759        }
3760
3761        Ok(())
3762    }
3763
3764    /// Clear all items from the cart.
3765    #[wasm_bindgen(js_name = clearItems)]
3766    pub fn clear_items(&self, cart_id: &str) -> Result<(), JsValue> {
3767        let uuid = Uuid::parse_str(cart_id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
3768        let mut store = self.store.borrow_mut();
3769
3770        // Check if cart exists
3771        if !store.carts.contains_key(&uuid) {
3772            return Err(JsValue::from_str("Cart not found"));
3773        }
3774
3775        // Clear items first
3776        store.cart_items.insert(uuid, Vec::new());
3777
3778        // Now update cart totals
3779        let cart = store.carts.get_mut(&uuid).ok_or_else(|| JsValue::from_str("Cart not found"))?;
3780        cart.subtotal = Money::zero();
3781        cart.grand_total = cart.tax_amount + cart.shipping_amount - cart.discount_amount;
3782        cart.updated_at = Utc::now().to_rfc3339();
3783
3784        Ok(())
3785    }
3786
3787    /// Set payment method.
3788    #[wasm_bindgen(js_name = setPayment)]
3789    pub fn set_payment(&self, id: &str, input: JsValue) -> Result<JsValue, JsValue> {
3790        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
3791        let input: SetCartPaymentInput =
3792            serde_wasm_bindgen::from_value(input).map_err(|e| JsValue::from_str(&e.to_string()))?;
3793
3794        let mut store = self.store.borrow_mut();
3795
3796        // Check cart exists
3797        if !store.carts.contains_key(&uuid) {
3798            return Err(JsValue::from_str("Cart not found"));
3799        }
3800
3801        // Get items first (immutable borrow)
3802        let items: Vec<JsCartItem> = store
3803            .cart_items
3804            .get(&uuid)
3805            .map(|items| items.iter().map(|i| i.into()).collect())
3806            .unwrap_or_default();
3807
3808        // Now update the cart (mutable borrow)
3809        let cart = store.carts.get_mut(&uuid).ok_or_else(|| JsValue::from_str("Cart not found"))?;
3810        cart.payment_method = Some(input.payment_method);
3811        cart.updated_at = Utc::now().to_rfc3339();
3812
3813        let js_cart = JsCart {
3814            id: cart.id.to_string(),
3815            cart_number: cart.cart_number.clone(),
3816            customer_id: cart.customer_id.map(|id| id.to_string()),
3817            status: cart.status.clone(),
3818            currency: cart.currency.clone(),
3819            subtotal: cart.subtotal,
3820            tax_amount: cart.tax_amount,
3821            shipping_amount: cart.shipping_amount,
3822            discount_amount: cart.discount_amount,
3823            grand_total: cart.grand_total,
3824            customer_email: cart.customer_email.clone(),
3825            customer_name: cart.customer_name.clone(),
3826            payment_method: cart.payment_method.clone(),
3827            payment_status: cart.payment_status.clone(),
3828            fulfillment_type: cart.fulfillment_type.clone(),
3829            shipping_method: cart.shipping_method.clone(),
3830            coupon_code: cart.coupon_code.clone(),
3831            notes: cart.notes.clone(),
3832            item_count: items.len(),
3833            items,
3834            created_at: cart.created_at.clone(),
3835            updated_at: cart.updated_at.clone(),
3836            expires_at: cart.expires_at.clone(),
3837        };
3838
3839        serde_wasm_bindgen::to_value(&js_cart).map_err(|e| JsValue::from_str(&e.to_string()))
3840    }
3841
3842    /// Complete the checkout and create an order.
3843    pub fn complete(&self, id: &str) -> Result<JsValue, JsValue> {
3844        let cart_uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
3845
3846        let mut store = self.store.borrow_mut();
3847
3848        // First, extract all data we need from the cart (immutable borrow)
3849        let (_cart_status, customer_id, grand_total, currency, notes) = {
3850            let cart =
3851                store.carts.get(&cart_uuid).ok_or_else(|| JsValue::from_str("Cart not found"))?;
3852
3853            // Check cart status
3854            if cart.status != "active" && cart.status != "ready_for_payment" {
3855                return Err(JsValue::from_str("Cart is not in a valid state for checkout"));
3856            }
3857
3858            let customer_id =
3859                cart.customer_id.ok_or_else(|| JsValue::from_str("Cart has no customer"))?;
3860
3861            (
3862                cart.status.clone(),
3863                customer_id,
3864                cart.grand_total,
3865                cart.currency.clone(),
3866                cart.notes.clone(),
3867            )
3868        };
3869
3870        // Get cart items
3871        let items = store.cart_items.get(&cart_uuid).cloned().unwrap_or_default();
3872        if items.is_empty() {
3873            return Err(JsValue::from_str("Cannot complete checkout with empty cart"));
3874        }
3875
3876        // Create order
3877        let now = Utc::now().to_rfc3339();
3878        let order_id = Uuid::new_v4();
3879        store.next_order_number += 1;
3880        let order_number = format!("ORD-{}", store.next_order_number);
3881
3882        let order_items: Vec<OrderItemData> = items
3883            .iter()
3884            .map(|i| OrderItemData {
3885                id: Uuid::new_v4(),
3886                order_id,
3887                sku: i.sku.clone(),
3888                name: i.name.clone(),
3889                quantity: i.quantity,
3890                unit_price: i.unit_price,
3891                total: i.total,
3892            })
3893            .collect();
3894
3895        let order = OrderData {
3896            id: order_id,
3897            order_number: order_number.clone(),
3898            customer_id,
3899            status: "pending".to_string(),
3900            total_amount: grand_total,
3901            currency: currency.clone(),
3902            payment_status: "paid".to_string(),
3903            fulfillment_status: "unfulfilled".to_string(),
3904            tracking_number: None,
3905            notes,
3906            version: 1,
3907            created_at: now.clone(),
3908            updated_at: now,
3909        };
3910
3911        store.orders.insert(order_id, order);
3912        store.order_items.insert(order_id, order_items);
3913
3914        // Update cart status (now we can get mutable borrow)
3915        if let Some(cart) = store.carts.get_mut(&cart_uuid) {
3916            cart.status = "completed".to_string();
3917            cart.payment_status = "paid".to_string();
3918            cart.updated_at = Utc::now().to_rfc3339();
3919        }
3920
3921        let result = JsCheckoutResult {
3922            order_id: order_id.to_string(),
3923            order_number,
3924            cart_id: cart_uuid.to_string(),
3925            total_charged: grand_total,
3926            currency,
3927        };
3928
3929        serde_wasm_bindgen::to_value(&result).map_err(|e| JsValue::from_str(&e.to_string()))
3930    }
3931
3932    /// Cancel the cart.
3933    pub fn cancel(&self, id: &str) -> Result<JsValue, JsValue> {
3934        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
3935        let mut store = self.store.borrow_mut();
3936
3937        // Check cart exists
3938        if !store.carts.contains_key(&uuid) {
3939            return Err(JsValue::from_str("Cart not found"));
3940        }
3941
3942        // Get items first (immutable borrow)
3943        let items: Vec<JsCartItem> = store
3944            .cart_items
3945            .get(&uuid)
3946            .map(|items| items.iter().map(|i| i.into()).collect())
3947            .unwrap_or_default();
3948
3949        // Now update the cart (mutable borrow)
3950        let cart = store.carts.get_mut(&uuid).ok_or_else(|| JsValue::from_str("Cart not found"))?;
3951        cart.status = "cancelled".to_string();
3952        cart.updated_at = Utc::now().to_rfc3339();
3953
3954        let js_cart = JsCart {
3955            id: cart.id.to_string(),
3956            cart_number: cart.cart_number.clone(),
3957            customer_id: cart.customer_id.map(|id| id.to_string()),
3958            status: cart.status.clone(),
3959            currency: cart.currency.clone(),
3960            subtotal: cart.subtotal,
3961            tax_amount: cart.tax_amount,
3962            shipping_amount: cart.shipping_amount,
3963            discount_amount: cart.discount_amount,
3964            grand_total: cart.grand_total,
3965            customer_email: cart.customer_email.clone(),
3966            customer_name: cart.customer_name.clone(),
3967            payment_method: cart.payment_method.clone(),
3968            payment_status: cart.payment_status.clone(),
3969            fulfillment_type: cart.fulfillment_type.clone(),
3970            shipping_method: cart.shipping_method.clone(),
3971            coupon_code: cart.coupon_code.clone(),
3972            notes: cart.notes.clone(),
3973            item_count: items.len(),
3974            items,
3975            created_at: cart.created_at.clone(),
3976            updated_at: cart.updated_at.clone(),
3977            expires_at: cart.expires_at.clone(),
3978        };
3979
3980        serde_wasm_bindgen::to_value(&js_cart).map_err(|e| JsValue::from_str(&e.to_string()))
3981    }
3982
3983    /// List all carts.
3984    pub fn list(&self) -> Result<JsValue, JsValue> {
3985        let store = self.store.borrow();
3986        let carts: Vec<JsCart> = store
3987            .carts
3988            .values()
3989            .map(|data| {
3990                let items: Vec<JsCartItem> = store
3991                    .cart_items
3992                    .get(&data.id)
3993                    .map(|items| items.iter().map(|i| i.into()).collect())
3994                    .unwrap_or_default();
3995
3996                JsCart {
3997                    id: data.id.to_string(),
3998                    cart_number: data.cart_number.clone(),
3999                    customer_id: data.customer_id.map(|id| id.to_string()),
4000                    status: data.status.clone(),
4001                    currency: data.currency.clone(),
4002                    subtotal: data.subtotal,
4003                    tax_amount: data.tax_amount,
4004                    shipping_amount: data.shipping_amount,
4005                    discount_amount: data.discount_amount,
4006                    grand_total: data.grand_total,
4007                    customer_email: data.customer_email.clone(),
4008                    customer_name: data.customer_name.clone(),
4009                    payment_method: data.payment_method.clone(),
4010                    payment_status: data.payment_status.clone(),
4011                    fulfillment_type: data.fulfillment_type.clone(),
4012                    shipping_method: data.shipping_method.clone(),
4013                    coupon_code: data.coupon_code.clone(),
4014                    notes: data.notes.clone(),
4015                    item_count: items.len(),
4016                    items,
4017                    created_at: data.created_at.clone(),
4018                    updated_at: data.updated_at.clone(),
4019                    expires_at: data.expires_at.clone(),
4020                }
4021            })
4022            .collect();
4023
4024        serde_wasm_bindgen::to_value(&carts).map_err(|e| JsValue::from_str(&e.to_string()))
4025    }
4026
4027    /// Count carts.
4028    pub fn count(&self) -> u32 {
4029        self.store.borrow().carts.len() as u32
4030    }
4031
4032    /// Delete a cart.
4033    pub fn delete(&self, id: &str) -> Result<(), JsValue> {
4034        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
4035        let mut store = self.store.borrow_mut();
4036
4037        store.carts.remove(&uuid);
4038        store.cart_items.remove(&uuid);
4039
4040        Ok(())
4041    }
4042}
4043
4044// ============================================================================
4045// Subscriptions API
4046// ============================================================================
4047
4048#[wasm_bindgen]
4049pub struct Subscriptions {
4050    store: StoreRef,
4051}
4052
4053#[wasm_bindgen]
4054impl Subscriptions {
4055    // ========================================================================
4056    // Subscription Plans
4057    // ========================================================================
4058
4059    /// Create a subscription plan.
4060    #[wasm_bindgen(js_name = createPlan)]
4061    pub fn create_plan(&self, input: JsValue) -> Result<JsValue, JsValue> {
4062        #[derive(Deserialize)]
4063        #[serde(rename_all = "camelCase")]
4064        struct CreatePlanInput {
4065            code: String,
4066            name: String,
4067            description: Option<String>,
4068            billing_interval: Option<String>,
4069            billing_interval_count: Option<i32>,
4070            price: Money,
4071            currency: Option<String>,
4072            setup_fee: Option<Money>,
4073            trial_days: Option<i32>,
4074        }
4075
4076        let input: CreatePlanInput = serde_wasm_bindgen::from_value(input)
4077            .map_err(|e| JsValue::from_str(&format!("Invalid input: {}", e)))?;
4078
4079        let now = Utc::now().to_rfc3339();
4080        let mut store = self.store.borrow_mut();
4081
4082        let id = Uuid::new_v4();
4083        let plan = SubscriptionPlanData {
4084            id,
4085            code: input.code,
4086            name: input.name,
4087            description: input.description,
4088            billing_interval: input.billing_interval.unwrap_or_else(|| "monthly".to_string()),
4089            billing_interval_count: input.billing_interval_count.unwrap_or(1),
4090            price: input.price,
4091            currency: input.currency.unwrap_or_else(|| "USD".to_string()),
4092            setup_fee: input.setup_fee.unwrap_or_default(),
4093            trial_days: input.trial_days.unwrap_or(0),
4094            status: "draft".to_string(),
4095            created_at: now.clone(),
4096            updated_at: now,
4097        };
4098
4099        let js_plan: JsSubscriptionPlan = (&plan).into();
4100        store.subscription_plans.insert(id, plan);
4101
4102        serde_wasm_bindgen::to_value(&js_plan).map_err(|e| JsValue::from_str(&e.to_string()))
4103    }
4104
4105    /// Get a subscription plan by ID.
4106    #[wasm_bindgen(js_name = getPlan)]
4107    pub fn get_plan(&self, id: &str) -> Result<JsValue, JsValue> {
4108        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
4109        let store = self.store.borrow();
4110
4111        match store.subscription_plans.get(&uuid) {
4112            Some(plan) => {
4113                let js_plan: JsSubscriptionPlan = plan.into();
4114                serde_wasm_bindgen::to_value(&js_plan)
4115                    .map_err(|e| JsValue::from_str(&e.to_string()))
4116            }
4117            None => Ok(JsValue::NULL),
4118        }
4119    }
4120
4121    /// List all subscription plans.
4122    #[wasm_bindgen(js_name = listPlans)]
4123    pub fn list_plans(&self) -> Result<JsValue, JsValue> {
4124        let store = self.store.borrow();
4125        let plans: Vec<JsSubscriptionPlan> =
4126            store.subscription_plans.values().map(|p| p.into()).collect();
4127
4128        serde_wasm_bindgen::to_value(&plans).map_err(|e| JsValue::from_str(&e.to_string()))
4129    }
4130
4131    /// Activate a subscription plan.
4132    #[wasm_bindgen(js_name = activatePlan)]
4133    pub fn activate_plan(&self, id: &str) -> Result<JsValue, JsValue> {
4134        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
4135        let mut store = self.store.borrow_mut();
4136
4137        let plan = store
4138            .subscription_plans
4139            .get_mut(&uuid)
4140            .ok_or_else(|| JsValue::from_str("Plan not found"))?;
4141
4142        plan.status = "active".to_string();
4143        plan.updated_at = Utc::now().to_rfc3339();
4144
4145        let js_plan: JsSubscriptionPlan = (&*plan).into();
4146        serde_wasm_bindgen::to_value(&js_plan).map_err(|e| JsValue::from_str(&e.to_string()))
4147    }
4148
4149    /// Archive a subscription plan.
4150    #[wasm_bindgen(js_name = archivePlan)]
4151    pub fn archive_plan(&self, id: &str) -> Result<JsValue, JsValue> {
4152        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
4153        let mut store = self.store.borrow_mut();
4154
4155        let plan = store
4156            .subscription_plans
4157            .get_mut(&uuid)
4158            .ok_or_else(|| JsValue::from_str("Plan not found"))?;
4159
4160        plan.status = "archived".to_string();
4161        plan.updated_at = Utc::now().to_rfc3339();
4162
4163        let js_plan: JsSubscriptionPlan = (&*plan).into();
4164        serde_wasm_bindgen::to_value(&js_plan).map_err(|e| JsValue::from_str(&e.to_string()))
4165    }
4166
4167    // ========================================================================
4168    // Subscriptions
4169    // ========================================================================
4170
4171    /// Subscribe a customer to a plan.
4172    pub fn subscribe(&self, input: JsValue) -> Result<JsValue, JsValue> {
4173        #[derive(Deserialize)]
4174        #[serde(rename_all = "camelCase")]
4175        struct SubscribeInput {
4176            customer_id: String,
4177            plan_id: String,
4178            skip_trial: Option<bool>,
4179            price: Option<Money>,
4180        }
4181
4182        let input: SubscribeInput = serde_wasm_bindgen::from_value(input)
4183            .map_err(|e| JsValue::from_str(&format!("Invalid input: {}", e)))?;
4184
4185        let customer_id = Uuid::parse_str(&input.customer_id)
4186            .map_err(|_| JsValue::from_str("Invalid customer UUID"))?;
4187        let plan_id =
4188            Uuid::parse_str(&input.plan_id).map_err(|_| JsValue::from_str("Invalid plan UUID"))?;
4189
4190        let mut store = self.store.borrow_mut();
4191
4192        // Verify plan exists
4193        let plan = store
4194            .subscription_plans
4195            .get(&plan_id)
4196            .ok_or_else(|| JsValue::from_str("Plan not found"))?
4197            .clone();
4198
4199        let now = Utc::now();
4200        let skip_trial = input.skip_trial.unwrap_or(false);
4201        let trial_days = if skip_trial { 0 } else { plan.trial_days };
4202
4203        // Calculate periods
4204        let (status, trial_start, trial_end, period_start, period_end) = if trial_days > 0 {
4205            let trial_end_dt = now + chrono::Duration::days(trial_days as i64);
4206            (
4207                "trialing".to_string(),
4208                Some(now.to_rfc3339()),
4209                Some(trial_end_dt.to_rfc3339()),
4210                trial_end_dt.to_rfc3339(),
4211                (trial_end_dt + chrono::Duration::days(30)).to_rfc3339(),
4212            )
4213        } else {
4214            let period_end_dt = now + chrono::Duration::days(30);
4215            ("active".to_string(), None, None, now.to_rfc3339(), period_end_dt.to_rfc3339())
4216        };
4217
4218        let id = Uuid::new_v4();
4219        store.next_subscription_number += 1;
4220        let subscription_number = format!("SUB-{:06}", store.next_subscription_number);
4221
4222        let subscription = SubscriptionData {
4223            id,
4224            subscription_number,
4225            customer_id,
4226            plan_id,
4227            status,
4228            current_period_start: period_start,
4229            current_period_end: period_end,
4230            trial_start,
4231            trial_end,
4232            cancelled_at: None,
4233            cancel_at_period_end: false,
4234            pause_start: None,
4235            pause_end: None,
4236            price: input.price.unwrap_or(plan.price),
4237            currency: plan.currency,
4238            created_at: now.to_rfc3339(),
4239            updated_at: now.to_rfc3339(),
4240        };
4241
4242        // Add creation event
4243        let event = SubscriptionEventData {
4244            id: Uuid::new_v4(),
4245            subscription_id: id,
4246            event_type: "created".to_string(),
4247            description: "Subscription created".to_string(),
4248            created_at: now.to_rfc3339(),
4249        };
4250
4251        let js_sub: JsSubscription = (&subscription).into();
4252        store.subscriptions.insert(id, subscription);
4253        store.subscription_events.entry(id).or_default().push(event);
4254
4255        serde_wasm_bindgen::to_value(&js_sub).map_err(|e| JsValue::from_str(&e.to_string()))
4256    }
4257
4258    /// Get a subscription by ID.
4259    pub fn get(&self, id: &str) -> Result<JsValue, JsValue> {
4260        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
4261        let store = self.store.borrow();
4262
4263        match store.subscriptions.get(&uuid) {
4264            Some(sub) => {
4265                let js_sub: JsSubscription = sub.into();
4266                serde_wasm_bindgen::to_value(&js_sub).map_err(|e| JsValue::from_str(&e.to_string()))
4267            }
4268            None => Ok(JsValue::NULL),
4269        }
4270    }
4271
4272    /// List all subscriptions.
4273    pub fn list(&self) -> Result<JsValue, JsValue> {
4274        let store = self.store.borrow();
4275        let subs: Vec<JsSubscription> = store.subscriptions.values().map(|s| s.into()).collect();
4276
4277        serde_wasm_bindgen::to_value(&subs).map_err(|e| JsValue::from_str(&e.to_string()))
4278    }
4279
4280    /// Pause a subscription.
4281    pub fn pause(&self, id: &str, reason: Option<String>) -> Result<JsValue, JsValue> {
4282        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
4283        let mut store = self.store.borrow_mut();
4284
4285        let sub = store
4286            .subscriptions
4287            .get_mut(&uuid)
4288            .ok_or_else(|| JsValue::from_str("Subscription not found"))?;
4289
4290        if sub.status != "active" && sub.status != "trialing" {
4291            return Err(JsValue::from_str("Subscription cannot be paused in current state"));
4292        }
4293
4294        let now = Utc::now();
4295        sub.status = "paused".to_string();
4296        sub.pause_start = Some(now.to_rfc3339());
4297        sub.updated_at = now.to_rfc3339();
4298
4299        // Add pause event
4300        let event = SubscriptionEventData {
4301            id: Uuid::new_v4(),
4302            subscription_id: uuid,
4303            event_type: "paused".to_string(),
4304            description: reason
4305                .map(|r| format!("Paused: {}", r))
4306                .unwrap_or_else(|| "Paused by customer".to_string()),
4307            created_at: now.to_rfc3339(),
4308        };
4309
4310        let js_sub: JsSubscription = (&*sub).into();
4311        store.subscription_events.entry(uuid).or_default().push(event);
4312
4313        serde_wasm_bindgen::to_value(&js_sub).map_err(|e| JsValue::from_str(&e.to_string()))
4314    }
4315
4316    /// Resume a paused subscription.
4317    pub fn resume(&self, id: &str) -> Result<JsValue, JsValue> {
4318        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
4319        let mut store = self.store.borrow_mut();
4320
4321        let sub = store
4322            .subscriptions
4323            .get_mut(&uuid)
4324            .ok_or_else(|| JsValue::from_str("Subscription not found"))?;
4325
4326        if sub.status != "paused" {
4327            return Err(JsValue::from_str("Subscription is not paused"));
4328        }
4329
4330        let now = Utc::now();
4331        sub.status = "active".to_string();
4332        sub.pause_end = Some(now.to_rfc3339());
4333        sub.updated_at = now.to_rfc3339();
4334
4335        // Add resume event
4336        let event = SubscriptionEventData {
4337            id: Uuid::new_v4(),
4338            subscription_id: uuid,
4339            event_type: "resumed".to_string(),
4340            description: "Subscription resumed".to_string(),
4341            created_at: now.to_rfc3339(),
4342        };
4343
4344        let js_sub: JsSubscription = (&*sub).into();
4345        store.subscription_events.entry(uuid).or_default().push(event);
4346
4347        serde_wasm_bindgen::to_value(&js_sub).map_err(|e| JsValue::from_str(&e.to_string()))
4348    }
4349
4350    /// Cancel a subscription.
4351    pub fn cancel(
4352        &self,
4353        id: &str,
4354        at_period_end: Option<bool>,
4355        reason: Option<String>,
4356    ) -> Result<JsValue, JsValue> {
4357        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
4358        let mut store = self.store.borrow_mut();
4359
4360        let sub = store
4361            .subscriptions
4362            .get_mut(&uuid)
4363            .ok_or_else(|| JsValue::from_str("Subscription not found"))?;
4364
4365        let now = Utc::now();
4366        let at_period_end = at_period_end.unwrap_or(true);
4367
4368        if at_period_end {
4369            sub.cancel_at_period_end = true;
4370        } else {
4371            sub.status = "cancelled".to_string();
4372            sub.cancelled_at = Some(now.to_rfc3339());
4373        }
4374        sub.updated_at = now.to_rfc3339();
4375
4376        // Add cancel event
4377        let description = if at_period_end {
4378            reason.unwrap_or_else(|| "Scheduled to cancel at period end".to_string())
4379        } else {
4380            reason.unwrap_or_else(|| "Cancelled immediately".to_string())
4381        };
4382
4383        let event = SubscriptionEventData {
4384            id: Uuid::new_v4(),
4385            subscription_id: uuid,
4386            event_type: "cancelled".to_string(),
4387            description,
4388            created_at: now.to_rfc3339(),
4389        };
4390
4391        let js_sub: JsSubscription = (&*sub).into();
4392        store.subscription_events.entry(uuid).or_default().push(event);
4393
4394        serde_wasm_bindgen::to_value(&js_sub).map_err(|e| JsValue::from_str(&e.to_string()))
4395    }
4396
4397    /// Skip the next billing cycle.
4398    #[wasm_bindgen(js_name = skipNextCycle)]
4399    pub fn skip_next_cycle(&self, id: &str, reason: Option<String>) -> Result<JsValue, JsValue> {
4400        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
4401        let mut store = self.store.borrow_mut();
4402
4403        let sub = store
4404            .subscriptions
4405            .get_mut(&uuid)
4406            .ok_or_else(|| JsValue::from_str("Subscription not found"))?;
4407
4408        let now = Utc::now();
4409        // Extend the period by 30 days (simplified)
4410        if let Ok(period_end) = chrono::DateTime::parse_from_rfc3339(&sub.current_period_end) {
4411            let new_end = period_end + chrono::Duration::days(30);
4412            sub.current_period_end = new_end.to_rfc3339();
4413        }
4414        sub.updated_at = now.to_rfc3339();
4415
4416        // Add skip event
4417        let event = SubscriptionEventData {
4418            id: Uuid::new_v4(),
4419            subscription_id: uuid,
4420            event_type: "billing_skipped".to_string(),
4421            description: reason.unwrap_or_else(|| "Billing cycle skipped".to_string()),
4422            created_at: now.to_rfc3339(),
4423        };
4424
4425        let js_sub: JsSubscription = (&*sub).into();
4426        store.subscription_events.entry(uuid).or_default().push(event);
4427
4428        serde_wasm_bindgen::to_value(&js_sub).map_err(|e| JsValue::from_str(&e.to_string()))
4429    }
4430
4431    // ========================================================================
4432    // Billing Cycles
4433    // ========================================================================
4434
4435    /// Create a billing cycle for a subscription.
4436    #[wasm_bindgen(js_name = createBillingCycle)]
4437    pub fn create_billing_cycle(&self, subscription_id: &str) -> Result<JsValue, JsValue> {
4438        let sub_uuid =
4439            Uuid::parse_str(subscription_id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
4440        let mut store = self.store.borrow_mut();
4441
4442        let sub = store
4443            .subscriptions
4444            .get(&sub_uuid)
4445            .ok_or_else(|| JsValue::from_str("Subscription not found"))?
4446            .clone();
4447
4448        let now = Utc::now();
4449        let id = Uuid::new_v4();
4450        store.next_billing_cycle_number += 1;
4451        let cycle_number = format!("BC-{:06}", store.next_billing_cycle_number);
4452
4453        let cycle = BillingCycleData {
4454            id,
4455            cycle_number,
4456            subscription_id: sub_uuid,
4457            status: "pending".to_string(),
4458            period_start: sub.current_period_start.clone(),
4459            period_end: sub.current_period_end.clone(),
4460            amount: sub.price,
4461            currency: sub.currency,
4462            payment_id: None,
4463            invoice_id: None,
4464            created_at: now.to_rfc3339(),
4465            updated_at: now.to_rfc3339(),
4466        };
4467
4468        let js_cycle: JsBillingCycle = (&cycle).into();
4469        store.billing_cycles.insert(id, cycle);
4470
4471        serde_wasm_bindgen::to_value(&js_cycle).map_err(|e| JsValue::from_str(&e.to_string()))
4472    }
4473
4474    /// List billing cycles for a subscription.
4475    #[wasm_bindgen(js_name = listBillingCycles)]
4476    pub fn list_billing_cycles(&self, subscription_id: Option<String>) -> Result<JsValue, JsValue> {
4477        let store = self.store.borrow();
4478        let sub_uuid = subscription_id.as_ref().and_then(|s| Uuid::parse_str(s).ok());
4479
4480        let cycles: Vec<JsBillingCycle> = store
4481            .billing_cycles
4482            .values()
4483            .filter(|c| sub_uuid.is_none_or(|id| c.subscription_id == id))
4484            .map(|c| c.into())
4485            .collect();
4486
4487        serde_wasm_bindgen::to_value(&cycles).map_err(|e| JsValue::from_str(&e.to_string()))
4488    }
4489
4490    /// Get a billing cycle by ID.
4491    #[wasm_bindgen(js_name = getBillingCycle)]
4492    pub fn get_billing_cycle(&self, id: &str) -> Result<JsValue, JsValue> {
4493        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
4494        let store = self.store.borrow();
4495
4496        match store.billing_cycles.get(&uuid) {
4497            Some(cycle) => {
4498                let js_cycle: JsBillingCycle = cycle.into();
4499                serde_wasm_bindgen::to_value(&js_cycle)
4500                    .map_err(|e| JsValue::from_str(&e.to_string()))
4501            }
4502            None => Ok(JsValue::NULL),
4503        }
4504    }
4505
4506    // ========================================================================
4507    // Events
4508    // ========================================================================
4509
4510    /// Get events for a subscription.
4511    #[wasm_bindgen(js_name = getEvents)]
4512    pub fn get_events(&self, subscription_id: &str) -> Result<JsValue, JsValue> {
4513        let uuid =
4514            Uuid::parse_str(subscription_id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
4515        let store = self.store.borrow();
4516
4517        let events: Vec<JsSubscriptionEvent> = store
4518            .subscription_events
4519            .get(&uuid)
4520            .map(|evts| evts.iter().map(|e| e.into()).collect())
4521            .unwrap_or_default();
4522
4523        serde_wasm_bindgen::to_value(&events).map_err(|e| JsValue::from_str(&e.to_string()))
4524    }
4525
4526    /// Count subscription plans.
4527    #[wasm_bindgen(js_name = countPlans)]
4528    pub fn count_plans(&self) -> u32 {
4529        self.store.borrow().subscription_plans.len() as u32
4530    }
4531
4532    /// Count subscriptions.
4533    #[wasm_bindgen(js_name = countSubscriptions)]
4534    pub fn count_subscriptions(&self) -> u32 {
4535        self.store.borrow().subscriptions.len() as u32
4536    }
4537}
4538
4539// ============================================================================
4540// Promotions API
4541// ============================================================================
4542
4543/// Promotions and discounts management.
4544#[wasm_bindgen]
4545pub struct Promotions {
4546    store: StoreRef,
4547}
4548
4549impl Default for Promotions {
4550    fn default() -> Self {
4551        Self::new()
4552    }
4553}
4554
4555#[wasm_bindgen]
4556impl Promotions {
4557    /// Create a new Promotions instance (typically from Commerce.promotions()).
4558    #[wasm_bindgen(constructor)]
4559    pub fn new() -> Promotions {
4560        Promotions { store: Rc::new(RefCell::new(Store::default())) }
4561    }
4562
4563    pub(crate) fn with_store(store: StoreRef) -> Promotions {
4564        Promotions { store }
4565    }
4566
4567    // ========================================================================
4568    // Promotions CRUD
4569    // ========================================================================
4570
4571    /// Create a new promotion.
4572    #[wasm_bindgen(js_name = createPromotion)]
4573    pub fn create_promotion(&self, input: JsValue) -> Result<JsValue, JsValue> {
4574        let input: CreatePromotionInput =
4575            serde_wasm_bindgen::from_value(input).map_err(|e| JsValue::from_str(&e.to_string()))?;
4576
4577        let mut store = self.store.borrow_mut();
4578        let now = Utc::now();
4579        let id = Uuid::new_v4();
4580
4581        store.next_promotion_code_number += 1;
4582        let code =
4583            input.code.unwrap_or_else(|| format!("PROMO-{:06}", store.next_promotion_code_number));
4584
4585        let promo = PromotionData {
4586            id,
4587            code,
4588            name: input.name,
4589            description: input.description,
4590            promotion_type: input.promotion_type.unwrap_or_else(|| "percentage_off".to_string()),
4591            trigger: input.trigger.unwrap_or_else(|| "automatic".to_string()),
4592            target: input.target.unwrap_or_else(|| "order".to_string()),
4593            stacking: input.stacking.unwrap_or_else(|| "stackable".to_string()),
4594            status: "draft".to_string(),
4595            percentage_off: input.percentage_off,
4596            fixed_amount_off: input.fixed_amount_off,
4597            max_discount_amount: input.max_discount_amount,
4598            buy_quantity: input.buy_quantity,
4599            get_quantity: input.get_quantity,
4600            starts_at: input.starts_at.unwrap_or_else(|| now.to_rfc3339()),
4601            ends_at: input.ends_at,
4602            total_usage_limit: input.total_usage_limit,
4603            per_customer_limit: input.per_customer_limit,
4604            usage_count: 0,
4605            currency: input.currency.unwrap_or_else(|| "USD".to_string()),
4606            priority: input.priority.unwrap_or(0),
4607            created_at: now.to_rfc3339(),
4608            updated_at: now.to_rfc3339(),
4609        };
4610
4611        let js_promo: JsPromotion = (&promo).into();
4612        store.promotions.insert(id, promo);
4613
4614        serde_wasm_bindgen::to_value(&js_promo).map_err(|e| JsValue::from_str(&e.to_string()))
4615    }
4616
4617    /// Get a promotion by ID.
4618    #[wasm_bindgen(js_name = getPromotion)]
4619    pub fn get_promotion(&self, id: &str) -> Result<JsValue, JsValue> {
4620        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
4621        let store = self.store.borrow();
4622
4623        match store.promotions.get(&uuid) {
4624            Some(promo) => {
4625                let js_promo: JsPromotion = promo.into();
4626                serde_wasm_bindgen::to_value(&js_promo)
4627                    .map_err(|e| JsValue::from_str(&e.to_string()))
4628            }
4629            None => Ok(JsValue::NULL),
4630        }
4631    }
4632
4633    /// Get a promotion by its code.
4634    #[wasm_bindgen(js_name = getPromotionByCode)]
4635    pub fn get_promotion_by_code(&self, code: &str) -> Result<JsValue, JsValue> {
4636        let store = self.store.borrow();
4637
4638        match store.promotions.values().find(|p| p.code == code) {
4639            Some(promo) => {
4640                let js_promo: JsPromotion = promo.into();
4641                serde_wasm_bindgen::to_value(&js_promo)
4642                    .map_err(|e| JsValue::from_str(&e.to_string()))
4643            }
4644            None => Ok(JsValue::NULL),
4645        }
4646    }
4647
4648    /// List all promotions.
4649    #[wasm_bindgen(js_name = listPromotions)]
4650    pub fn list_promotions(
4651        &self,
4652        status: Option<String>,
4653        is_active: Option<bool>,
4654    ) -> Result<JsValue, JsValue> {
4655        let store = self.store.borrow();
4656
4657        let promos: Vec<JsPromotion> = store
4658            .promotions
4659            .values()
4660            .filter(|p| {
4661                let status_match = status.as_ref().is_none_or(|s| &p.status == s);
4662                let active_match = is_active.is_none_or(|active| {
4663                    if active { p.status == "active" } else { p.status != "active" }
4664                });
4665                status_match && active_match
4666            })
4667            .map(|p| p.into())
4668            .collect();
4669
4670        serde_wasm_bindgen::to_value(&promos).map_err(|e| JsValue::from_str(&e.to_string()))
4671    }
4672
4673    /// Update a promotion.
4674    #[wasm_bindgen(js_name = updatePromotion)]
4675    pub fn update_promotion(&self, id: &str, input: JsValue) -> Result<JsValue, JsValue> {
4676        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
4677        let input: UpdatePromotionInput =
4678            serde_wasm_bindgen::from_value(input).map_err(|e| JsValue::from_str(&e.to_string()))?;
4679
4680        let mut store = self.store.borrow_mut();
4681        let now = Utc::now();
4682
4683        let promo = store
4684            .promotions
4685            .get_mut(&uuid)
4686            .ok_or_else(|| JsValue::from_str("Promotion not found"))?;
4687
4688        if let Some(name) = input.name {
4689            promo.name = name;
4690        }
4691        if let Some(desc) = input.description {
4692            promo.description = Some(desc);
4693        }
4694        if let Some(status) = input.status {
4695            promo.status = status;
4696        }
4697        if let Some(pct) = input.percentage_off {
4698            promo.percentage_off = Some(pct);
4699        }
4700        if let Some(fixed) = input.fixed_amount_off {
4701            promo.fixed_amount_off = Some(fixed);
4702        }
4703        if let Some(max) = input.max_discount_amount {
4704            promo.max_discount_amount = Some(max);
4705        }
4706        if let Some(starts) = input.starts_at {
4707            promo.starts_at = starts;
4708        }
4709        if let Some(ends) = input.ends_at {
4710            promo.ends_at = Some(ends);
4711        }
4712        if let Some(limit) = input.total_usage_limit {
4713            promo.total_usage_limit = Some(limit);
4714        }
4715        if let Some(limit) = input.per_customer_limit {
4716            promo.per_customer_limit = Some(limit);
4717        }
4718        if let Some(priority) = input.priority {
4719            promo.priority = priority;
4720        }
4721        promo.updated_at = now.to_rfc3339();
4722
4723        let js_promo: JsPromotion = (&*promo).into();
4724        serde_wasm_bindgen::to_value(&js_promo).map_err(|e| JsValue::from_str(&e.to_string()))
4725    }
4726
4727    /// Delete a promotion.
4728    #[wasm_bindgen(js_name = deletePromotion)]
4729    pub fn delete_promotion(&self, id: &str) -> Result<bool, JsValue> {
4730        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
4731        let mut store = self.store.borrow_mut();
4732
4733        Ok(store.promotions.remove(&uuid).is_some())
4734    }
4735
4736    /// Activate a promotion.
4737    #[wasm_bindgen(js_name = activatePromotion)]
4738    pub fn activate_promotion(&self, id: &str) -> Result<JsValue, JsValue> {
4739        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
4740        let mut store = self.store.borrow_mut();
4741        let now = Utc::now();
4742
4743        let promo = store
4744            .promotions
4745            .get_mut(&uuid)
4746            .ok_or_else(|| JsValue::from_str("Promotion not found"))?;
4747
4748        promo.status = "active".to_string();
4749        promo.updated_at = now.to_rfc3339();
4750
4751        let js_promo: JsPromotion = (&*promo).into();
4752        serde_wasm_bindgen::to_value(&js_promo).map_err(|e| JsValue::from_str(&e.to_string()))
4753    }
4754
4755    /// Deactivate (pause) a promotion.
4756    #[wasm_bindgen(js_name = deactivatePromotion)]
4757    pub fn deactivate_promotion(&self, id: &str) -> Result<JsValue, JsValue> {
4758        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
4759        let mut store = self.store.borrow_mut();
4760        let now = Utc::now();
4761
4762        let promo = store
4763            .promotions
4764            .get_mut(&uuid)
4765            .ok_or_else(|| JsValue::from_str("Promotion not found"))?;
4766
4767        promo.status = "paused".to_string();
4768        promo.updated_at = now.to_rfc3339();
4769
4770        let js_promo: JsPromotion = (&*promo).into();
4771        serde_wasm_bindgen::to_value(&js_promo).map_err(|e| JsValue::from_str(&e.to_string()))
4772    }
4773
4774    /// Get all active promotions.
4775    #[wasm_bindgen(js_name = getActivePromotions)]
4776    pub fn get_active_promotions(&self) -> Result<JsValue, JsValue> {
4777        self.list_promotions(Some("active".to_string()), None)
4778    }
4779
4780    // ========================================================================
4781    // Coupon Codes
4782    // ========================================================================
4783
4784    /// Create a coupon code for a promotion.
4785    #[wasm_bindgen(js_name = createCoupon)]
4786    pub fn create_coupon(&self, input: JsValue) -> Result<JsValue, JsValue> {
4787        let input: CreateCouponInput =
4788            serde_wasm_bindgen::from_value(input).map_err(|e| JsValue::from_str(&e.to_string()))?;
4789
4790        let promotion_id = Uuid::parse_str(&input.promotion_id)
4791            .map_err(|_| JsValue::from_str("Invalid promotion UUID"))?;
4792
4793        let store_ref = self.store.borrow();
4794        if !store_ref.promotions.contains_key(&promotion_id) {
4795            return Err(JsValue::from_str("Promotion not found"));
4796        }
4797        drop(store_ref);
4798
4799        let mut store = self.store.borrow_mut();
4800        let now = Utc::now();
4801        let id = Uuid::new_v4();
4802
4803        let coupon = CouponData {
4804            id,
4805            promotion_id,
4806            code: input.code,
4807            status: "active".to_string(),
4808            usage_limit: input.usage_limit,
4809            per_customer_limit: input.per_customer_limit,
4810            usage_count: 0,
4811            starts_at: input.starts_at,
4812            ends_at: input.ends_at,
4813            created_at: now.to_rfc3339(),
4814            updated_at: now.to_rfc3339(),
4815        };
4816
4817        let js_coupon: JsCoupon = (&coupon).into();
4818        store.coupons.insert(id, coupon);
4819
4820        serde_wasm_bindgen::to_value(&js_coupon).map_err(|e| JsValue::from_str(&e.to_string()))
4821    }
4822
4823    /// Get a coupon by ID.
4824    #[wasm_bindgen(js_name = getCoupon)]
4825    pub fn get_coupon(&self, id: &str) -> Result<JsValue, JsValue> {
4826        let uuid = Uuid::parse_str(id).map_err(|_| JsValue::from_str("Invalid UUID"))?;
4827        let store = self.store.borrow();
4828
4829        match store.coupons.get(&uuid) {
4830            Some(coupon) => {
4831                let js_coupon: JsCoupon = coupon.into();
4832                serde_wasm_bindgen::to_value(&js_coupon)
4833                    .map_err(|e| JsValue::from_str(&e.to_string()))
4834            }
4835            None => Ok(JsValue::NULL),
4836        }
4837    }
4838
4839    /// Get a coupon by its code.
4840    #[wasm_bindgen(js_name = getCouponByCode)]
4841    pub fn get_coupon_by_code(&self, code: &str) -> Result<JsValue, JsValue> {
4842        let store = self.store.borrow();
4843
4844        match store.coupons.values().find(|c| c.code == code) {
4845            Some(coupon) => {
4846                let js_coupon: JsCoupon = coupon.into();
4847                serde_wasm_bindgen::to_value(&js_coupon)
4848                    .map_err(|e| JsValue::from_str(&e.to_string()))
4849            }
4850            None => Ok(JsValue::NULL),
4851        }
4852    }
4853
4854    /// List coupons.
4855    #[wasm_bindgen(js_name = listCoupons)]
4856    pub fn list_coupons(&self, promotion_id: Option<String>) -> Result<JsValue, JsValue> {
4857        let store = self.store.borrow();
4858        let promo_uuid = promotion_id.as_ref().and_then(|s| Uuid::parse_str(s).ok());
4859
4860        let coupons: Vec<JsCoupon> = store
4861            .coupons
4862            .values()
4863            .filter(|c| promo_uuid.is_none_or(|id| c.promotion_id == id))
4864            .map(|c| c.into())
4865            .collect();
4866
4867        serde_wasm_bindgen::to_value(&coupons).map_err(|e| JsValue::from_str(&e.to_string()))
4868    }
4869
4870    /// Validate a coupon code.
4871    #[wasm_bindgen(js_name = validateCoupon)]
4872    pub fn validate_coupon(&self, code: &str) -> Result<JsValue, JsValue> {
4873        let store = self.store.borrow();
4874
4875        match store.coupons.values().find(|c| c.code == code) {
4876            Some(coupon) => {
4877                // Check status
4878                if coupon.status != "active" {
4879                    return Ok(JsValue::NULL);
4880                }
4881
4882                // Check usage limits
4883                if let Some(limit) = coupon.usage_limit {
4884                    if coupon.usage_count >= limit {
4885                        return Ok(JsValue::NULL);
4886                    }
4887                }
4888
4889                let js_coupon: JsCoupon = coupon.into();
4890                serde_wasm_bindgen::to_value(&js_coupon)
4891                    .map_err(|e| JsValue::from_str(&e.to_string()))
4892            }
4893            None => Ok(JsValue::NULL),
4894        }
4895    }
4896
4897    // ========================================================================
4898    // Apply Promotions
4899    // ========================================================================
4900
4901    /// Apply promotions to a cart (simplified in-memory calculation).
4902    #[wasm_bindgen(js_name = applyPromotions)]
4903    pub fn apply_promotions(&self, input: JsValue) -> Result<JsValue, JsValue> {
4904        let input: ApplyPromotionsInput =
4905            serde_wasm_bindgen::from_value(input).map_err(|e| JsValue::from_str(&e.to_string()))?;
4906
4907        let store = self.store.borrow();
4908        let coupon_codes = input.coupon_codes.unwrap_or_default();
4909        let subtotal = input.subtotal;
4910        let shipping = input.shipping_amount.unwrap_or_default();
4911
4912        let mut total_discount = Money::zero();
4913        let mut shipping_discount = Money::zero();
4914        let mut applied_promotions = Vec::new();
4915
4916        // Find applicable promotions
4917        for promo in store.promotions.values() {
4918            if promo.status != "active" {
4919                continue;
4920            }
4921
4922            // Check if triggered by coupon
4923            let triggered = if promo.trigger == "coupon_code" {
4924                // Check if any coupon for this promotion is in coupon_codes
4925                store.coupons.values().any(|c| {
4926                    c.promotion_id == promo.id
4927                        && c.status == "active"
4928                        && coupon_codes.contains(&c.code)
4929                })
4930            } else {
4931                // Automatic trigger
4932                true
4933            };
4934
4935            if !triggered {
4936                continue;
4937            }
4938
4939            // Calculate discount
4940            let discount = if let Some(pct) = promo.percentage_off {
4941                subtotal.mul_rate(pct)
4942            } else if let Some(fixed) = promo.fixed_amount_off {
4943                fixed
4944            } else {
4945                Money::zero()
4946            };
4947
4948            // Apply max discount cap
4949            let final_discount = if let Some(max) = promo.max_discount_amount {
4950                std::cmp::min(discount, max)
4951            } else {
4952                discount
4953            };
4954
4955            if final_discount > Money::zero() {
4956                // Check if free shipping
4957                if promo.promotion_type == "free_shipping" {
4958                    shipping_discount += shipping;
4959                } else {
4960                    total_discount += final_discount;
4961                }
4962
4963                let coupon_code = store
4964                    .coupons
4965                    .values()
4966                    .find(|c| c.promotion_id == promo.id && coupon_codes.contains(&c.code))
4967                    .map(|c| c.code.clone());
4968
4969                applied_promotions.push(JsAppliedPromotion {
4970                    promotion_id: promo.id.to_string(),
4971                    promotion_name: promo.name.clone(),
4972                    coupon_code,
4973                    discount_amount: final_discount,
4974                    discount_type: promo.promotion_type.clone(),
4975                });
4976            }
4977        }
4978
4979        let result = JsApplyPromotionsResult {
4980            original_subtotal: subtotal,
4981            total_discount,
4982            discounted_subtotal: std::cmp::max(subtotal - total_discount, Money::zero()),
4983            original_shipping: shipping,
4984            shipping_discount,
4985            final_shipping: std::cmp::max(shipping - shipping_discount, Money::zero()),
4986            grand_total: std::cmp::max(
4987                subtotal - total_discount + shipping - shipping_discount,
4988                Money::zero(),
4989            ),
4990            applied_promotions,
4991        };
4992
4993        serde_wasm_bindgen::to_value(&result).map_err(|e| JsValue::from_str(&e.to_string()))
4994    }
4995
4996    /// Record promotion usage.
4997    #[wasm_bindgen(js_name = recordUsage)]
4998    #[allow(clippy::too_many_arguments)]
4999    pub fn record_usage(
5000        &self,
5001        promotion_id: &str,
5002        coupon_id: Option<String>,
5003        customer_id: Option<String>,
5004        order_id: Option<String>,
5005        cart_id: Option<String>,
5006        discount_amount: f64,
5007        currency: &str,
5008    ) -> Result<JsValue, JsValue> {
5009        let promo_uuid = Uuid::parse_str(promotion_id)
5010            .map_err(|_| JsValue::from_str("Invalid promotion UUID"))?;
5011
5012        let mut store = self.store.borrow_mut();
5013        let now = Utc::now();
5014
5015        // Increment promotion usage
5016        if let Some(promo) = store.promotions.get_mut(&promo_uuid) {
5017            promo.usage_count += 1;
5018        }
5019
5020        // Increment coupon usage
5021        if let Some(ref coupon_id_str) = coupon_id {
5022            if let Ok(coupon_uuid) = Uuid::parse_str(coupon_id_str) {
5023                if let Some(coupon) = store.coupons.get_mut(&coupon_uuid) {
5024                    coupon.usage_count += 1;
5025                }
5026            }
5027        }
5028
5029        let discount_amount = Money::from_f64(discount_amount);
5030        let usage = PromotionUsageData {
5031            id: Uuid::new_v4(),
5032            promotion_id: promo_uuid,
5033            coupon_id: coupon_id.and_then(|s| Uuid::parse_str(&s).ok()),
5034            customer_id: customer_id.and_then(|s| Uuid::parse_str(&s).ok()),
5035            order_id: order_id.and_then(|s| Uuid::parse_str(&s).ok()),
5036            cart_id: cart_id.and_then(|s| Uuid::parse_str(&s).ok()),
5037            discount_amount,
5038            currency: currency.to_string(),
5039            used_at: now.to_rfc3339(),
5040        };
5041
5042        let js_usage: JsPromotionUsage = (&usage).into();
5043        store.promotion_usages.push(usage);
5044
5045        serde_wasm_bindgen::to_value(&js_usage).map_err(|e| JsValue::from_str(&e.to_string()))
5046    }
5047
5048    /// Count promotions.
5049    #[wasm_bindgen(js_name = countPromotions)]
5050    pub fn count_promotions(&self) -> u32 {
5051        self.store.borrow().promotions.len() as u32
5052    }
5053
5054    /// Count coupons.
5055    #[wasm_bindgen(js_name = countCoupons)]
5056    pub fn count_coupons(&self) -> u32 {
5057        self.store.borrow().coupons.len() as u32
5058    }
5059}
5060
5061// ============================================================================
5062// Tax API
5063// ============================================================================
5064
5065// --- Internal Data Types ---
5066
5067#[derive(Clone)]
5068struct TaxJurisdictionData {
5069    id: Uuid,
5070    parent_id: Option<Uuid>,
5071    name: String,
5072    code: String,
5073    level: String,
5074    country_code: String,
5075    state_code: Option<String>,
5076    county: Option<String>,
5077    city: Option<String>,
5078    postal_codes: Vec<String>,
5079    active: bool,
5080    created_at: String,
5081    updated_at: String,
5082}
5083
5084#[derive(Clone)]
5085struct TaxRateData {
5086    id: Uuid,
5087    jurisdiction_id: Uuid,
5088    tax_type: String,
5089    product_category: String,
5090    rate: f64,
5091    name: String,
5092    description: Option<String>,
5093    is_compound: bool,
5094    priority: i32,
5095    threshold_min: Option<Money>,
5096    threshold_max: Option<Money>,
5097    fixed_amount: Option<Money>,
5098    effective_from: String,
5099    effective_to: Option<String>,
5100    active: bool,
5101    created_at: String,
5102    updated_at: String,
5103}
5104
5105#[derive(Clone)]
5106struct TaxExemptionData {
5107    id: Uuid,
5108    customer_id: Uuid,
5109    exemption_type: String,
5110    certificate_number: Option<String>,
5111    issuing_authority: Option<String>,
5112    jurisdiction_ids: Vec<Uuid>,
5113    exempt_categories: Vec<String>,
5114    effective_from: String,
5115    expires_at: Option<String>,
5116    verified: bool,
5117    verified_at: Option<String>,
5118    notes: Option<String>,
5119    active: bool,
5120    created_at: String,
5121    updated_at: String,
5122}
5123
5124#[derive(Clone)]
5125struct TaxSettingsData {
5126    id: Uuid,
5127    enabled: bool,
5128    calculation_method: String,
5129    compound_method: String,
5130    tax_shipping: bool,
5131    tax_handling: bool,
5132    tax_gift_wrap: bool,
5133    default_product_category: String,
5134    rounding_mode: String,
5135    decimal_places: i32,
5136    validate_addresses: bool,
5137    tax_provider: Option<String>,
5138    created_at: String,
5139    updated_at: String,
5140}
5141
5142// --- JS Return Types ---
5143
5144#[derive(Serialize, Deserialize, Clone)]
5145#[serde(rename_all = "camelCase")]
5146struct JsTaxJurisdiction {
5147    id: String,
5148    parent_id: Option<String>,
5149    name: String,
5150    code: String,
5151    level: String,
5152    country_code: String,
5153    state_code: Option<String>,
5154    county: Option<String>,
5155    city: Option<String>,
5156    postal_codes: Vec<String>,
5157    active: bool,
5158    created_at: String,
5159    updated_at: String,
5160}
5161
5162impl From<&TaxJurisdictionData> for JsTaxJurisdiction {
5163    fn from(d: &TaxJurisdictionData) -> Self {
5164        Self {
5165            id: d.id.to_string(),
5166            parent_id: d.parent_id.map(|u| u.to_string()),
5167            name: d.name.clone(),
5168            code: d.code.clone(),
5169            level: d.level.clone(),
5170            country_code: d.country_code.clone(),
5171            state_code: d.state_code.clone(),
5172            county: d.county.clone(),
5173            city: d.city.clone(),
5174            postal_codes: d.postal_codes.clone(),
5175            active: d.active,
5176            created_at: d.created_at.clone(),
5177            updated_at: d.updated_at.clone(),
5178        }
5179    }
5180}
5181
5182#[derive(Serialize, Deserialize, Clone)]
5183#[serde(rename_all = "camelCase")]
5184struct JsTaxRate {
5185    id: String,
5186    jurisdiction_id: String,
5187    tax_type: String,
5188    product_category: String,
5189    rate: f64,
5190    name: String,
5191    description: Option<String>,
5192    is_compound: bool,
5193    priority: i32,
5194    threshold_min: Option<Money>,
5195    threshold_max: Option<Money>,
5196    fixed_amount: Option<Money>,
5197    effective_from: String,
5198    effective_to: Option<String>,
5199    active: bool,
5200    created_at: String,
5201    updated_at: String,
5202}
5203
5204impl From<&TaxRateData> for JsTaxRate {
5205    fn from(d: &TaxRateData) -> Self {
5206        Self {
5207            id: d.id.to_string(),
5208            jurisdiction_id: d.jurisdiction_id.to_string(),
5209            tax_type: d.tax_type.clone(),
5210            product_category: d.product_category.clone(),
5211            rate: d.rate,
5212            name: d.name.clone(),
5213            description: d.description.clone(),
5214            is_compound: d.is_compound,
5215            priority: d.priority,
5216            threshold_min: d.threshold_min,
5217            threshold_max: d.threshold_max,
5218            fixed_amount: d.fixed_amount,
5219            effective_from: d.effective_from.clone(),
5220            effective_to: d.effective_to.clone(),
5221            active: d.active,
5222            created_at: d.created_at.clone(),
5223            updated_at: d.updated_at.clone(),
5224        }
5225    }
5226}
5227
5228#[derive(Serialize, Deserialize, Clone)]
5229#[serde(rename_all = "camelCase")]
5230struct JsTaxExemption {
5231    id: String,
5232    customer_id: String,
5233    exemption_type: String,
5234    certificate_number: Option<String>,
5235    issuing_authority: Option<String>,
5236    jurisdiction_ids: Vec<String>,
5237    exempt_categories: Vec<String>,
5238    effective_from: String,
5239    expires_at: Option<String>,
5240    verified: bool,
5241    verified_at: Option<String>,
5242    notes: Option<String>,
5243    active: bool,
5244    created_at: String,
5245    updated_at: String,
5246}
5247
5248impl From<&TaxExemptionData> for JsTaxExemption {
5249    fn from(d: &TaxExemptionData) -> Self {
5250        Self {
5251            id: d.id.to_string(),
5252            customer_id: d.customer_id.to_string(),
5253            exemption_type: d.exemption_type.clone(),
5254            certificate_number: d.certificate_number.clone(),
5255            issuing_authority: d.issuing_authority.clone(),
5256            jurisdiction_ids: d.jurisdiction_ids.iter().map(|u| u.to_string()).collect(),
5257            exempt_categories: d.exempt_categories.clone(),
5258            effective_from: d.effective_from.clone(),
5259            expires_at: d.expires_at.clone(),
5260            verified: d.verified,
5261            verified_at: d.verified_at.clone(),
5262            notes: d.notes.clone(),
5263            active: d.active,
5264            created_at: d.created_at.clone(),
5265            updated_at: d.updated_at.clone(),
5266        }
5267    }
5268}
5269
5270#[derive(Serialize, Deserialize, Clone)]
5271#[serde(rename_all = "camelCase")]
5272struct JsTaxSettings {
5273    id: String,
5274    enabled: bool,
5275    calculation_method: String,
5276    compound_method: String,
5277    tax_shipping: bool,
5278    tax_handling: bool,
5279    tax_gift_wrap: bool,
5280    default_product_category: String,
5281    rounding_mode: String,
5282    decimal_places: i32,
5283    validate_addresses: bool,
5284    tax_provider: Option<String>,
5285    created_at: String,
5286    updated_at: String,
5287}
5288
5289impl From<&TaxSettingsData> for JsTaxSettings {
5290    fn from(d: &TaxSettingsData) -> Self {
5291        Self {
5292            id: d.id.to_string(),
5293            enabled: d.enabled,
5294            calculation_method: d.calculation_method.clone(),
5295            compound_method: d.compound_method.clone(),
5296            tax_shipping: d.tax_shipping,
5297            tax_handling: d.tax_handling,
5298            tax_gift_wrap: d.tax_gift_wrap,
5299            default_product_category: d.default_product_category.clone(),
5300            rounding_mode: d.rounding_mode.clone(),
5301            decimal_places: d.decimal_places,
5302            validate_addresses: d.validate_addresses,
5303            tax_provider: d.tax_provider.clone(),
5304            created_at: d.created_at.clone(),
5305            updated_at: d.updated_at.clone(),
5306        }
5307    }
5308}
5309
5310#[derive(Serialize, Deserialize, Clone)]
5311#[serde(rename_all = "camelCase")]
5312struct JsTaxCalculationResult {
5313    id: String,
5314    total_tax: Money,
5315    subtotal: Money,
5316    total: Money,
5317    shipping_tax: Money,
5318    tax_breakdown: Vec<JsTaxBreakdown>,
5319    line_item_taxes: Vec<JsLineItemTax>,
5320    exemptions_applied: bool,
5321    calculated_at: String,
5322    is_estimate: bool,
5323}
5324
5325#[derive(Serialize, Deserialize, Clone)]
5326#[serde(rename_all = "camelCase")]
5327struct JsTaxBreakdown {
5328    jurisdiction_id: String,
5329    jurisdiction_name: String,
5330    tax_type: String,
5331    rate_name: String,
5332    rate: f64,
5333    taxable_amount: Money,
5334    tax_amount: Money,
5335    is_compound: bool,
5336}
5337
5338#[derive(Serialize, Deserialize, Clone)]
5339#[serde(rename_all = "camelCase")]
5340struct JsLineItemTax {
5341    line_item_id: String,
5342    taxable_amount: Money,
5343    tax_amount: Money,
5344    effective_rate: f64,
5345    is_exempt: bool,
5346    exemption_reason: Option<String>,
5347}
5348
5349#[derive(Serialize, Deserialize, Clone)]
5350#[serde(rename_all = "camelCase")]
5351struct JsUsStateTaxInfo {
5352    state_code: String,
5353    state_name: String,
5354    state_rate: f64,
5355    has_local_taxes: bool,
5356    origin_based: bool,
5357    tax_shipping: bool,
5358    tax_clothing: bool,
5359    tax_food: bool,
5360    tax_digital: bool,
5361}
5362
5363#[derive(Serialize, Deserialize, Clone)]
5364#[serde(rename_all = "camelCase")]
5365struct JsEuVatInfo {
5366    country_code: String,
5367    country_name: String,
5368    standard_rate: f64,
5369    reduced_rate: Option<f64>,
5370    super_reduced_rate: Option<f64>,
5371    parking_rate: Option<f64>,
5372}
5373
5374#[derive(Serialize, Deserialize, Clone)]
5375#[serde(rename_all = "camelCase")]
5376struct JsCanadianTaxInfo {
5377    province_code: String,
5378    province_name: String,
5379    gst_rate: f64,
5380    pst_rate: Option<f64>,
5381    hst_rate: Option<f64>,
5382    qst_rate: Option<f64>,
5383    total_rate: f64,
5384}
5385
5386// --- Input Types ---
5387
5388#[derive(Deserialize)]
5389#[serde(rename_all = "camelCase")]
5390struct CreateJurisdictionInput {
5391    parent_id: Option<String>,
5392    name: String,
5393    code: String,
5394    level: Option<String>,
5395    country_code: String,
5396    state_code: Option<String>,
5397    county: Option<String>,
5398    city: Option<String>,
5399    postal_codes: Option<Vec<String>>,
5400}
5401
5402#[derive(Deserialize)]
5403#[serde(rename_all = "camelCase")]
5404struct CreateTaxRateInput {
5405    jurisdiction_id: String,
5406    tax_type: Option<String>,
5407    product_category: Option<String>,
5408    rate: f64,
5409    name: String,
5410    description: Option<String>,
5411    is_compound: Option<bool>,
5412    priority: Option<i32>,
5413    threshold_min: Option<Money>,
5414    threshold_max: Option<Money>,
5415    fixed_amount: Option<Money>,
5416    effective_from: String,
5417    effective_to: Option<String>,
5418}
5419
5420#[derive(Deserialize)]
5421#[serde(rename_all = "camelCase")]
5422struct CreateExemptionInput {
5423    customer_id: String,
5424    exemption_type: String,
5425    certificate_number: Option<String>,
5426    issuing_authority: Option<String>,
5427    jurisdiction_ids: Option<Vec<String>>,
5428    exempt_categories: Option<Vec<String>>,
5429    effective_from: String,
5430    expires_at: Option<String>,
5431    notes: Option<String>,
5432}
5433
5434#[derive(Deserialize)]
5435#[serde(rename_all = "camelCase")]
5436struct TaxCalculationInput {
5437    line_items: Vec<TaxLineItemInput>,
5438    shipping_address: TaxAddressInput,
5439    customer_id: Option<String>,
5440    shipping_amount: Option<Money>,
5441    currency: Option<String>,
5442}
5443
5444#[derive(Deserialize)]
5445#[serde(rename_all = "camelCase")]
5446struct TaxLineItemInput {
5447    id: String,
5448    quantity: f64,
5449    unit_price: Money,
5450    discount_amount: Option<Money>,
5451    tax_category: Option<String>,
5452}
5453
5454#[derive(Deserialize)]
5455#[serde(rename_all = "camelCase")]
5456struct TaxAddressInput {
5457    line1: Option<String>,
5458    line2: Option<String>,
5459    city: Option<String>,
5460    state: Option<String>,
5461    postal_code: Option<String>,
5462    country: String,
5463}
5464
5465#[derive(Deserialize)]
5466#[serde(rename_all = "camelCase")]
5467struct UpdateTaxSettingsInput {
5468    enabled: Option<bool>,
5469    calculation_method: Option<String>,
5470    compound_method: Option<String>,
5471    tax_shipping: Option<bool>,
5472    tax_handling: Option<bool>,
5473    tax_gift_wrap: Option<bool>,
5474    default_product_category: Option<String>,
5475    rounding_mode: Option<String>,
5476    decimal_places: Option<i32>,
5477    validate_addresses: Option<bool>,
5478    tax_provider: Option<String>,
5479}
5480
5481// --- Tax Struct ---
5482
5483#[wasm_bindgen]
5484pub struct Tax {
5485    store: StoreRef,
5486}
5487
5488#[wasm_bindgen]
5489impl Tax {
5490    pub(crate) fn with_store(store: StoreRef) -> Tax {
5491        Tax { store }
5492    }
5493
5494    // ========================================================================
5495    // Jurisdiction Operations
5496    // ========================================================================
5497
5498    /// Create a tax jurisdiction.
5499    #[wasm_bindgen(js_name = createJurisdiction)]
5500    pub fn create_jurisdiction(&self, input: JsValue) -> Result<JsValue, JsValue> {
5501        let input: CreateJurisdictionInput =
5502            serde_wasm_bindgen::from_value(input).map_err(|e| JsValue::from_str(&e.to_string()))?;
5503
5504        let now = Utc::now();
5505        let id = Uuid::new_v4();
5506
5507        let data = TaxJurisdictionData {
5508            id,
5509            parent_id: input.parent_id.and_then(|s| Uuid::parse_str(&s).ok()),
5510            name: input.name,
5511            code: input.code,
5512            level: input.level.unwrap_or_else(|| "country".to_string()),
5513            country_code: input.country_code,
5514            state_code: input.state_code,
5515            county: input.county,
5516            city: input.city,
5517            postal_codes: input.postal_codes.unwrap_or_default(),
5518            active: true,
5519            created_at: now.to_rfc3339(),
5520            updated_at: now.to_rfc3339(),
5521        };
5522
5523        let js_jurisdiction: JsTaxJurisdiction = (&data).into();
5524        self.store.borrow_mut().tax_jurisdictions.insert(id, data);
5525
5526        serde_wasm_bindgen::to_value(&js_jurisdiction)
5527            .map_err(|e| JsValue::from_str(&e.to_string()))
5528    }
5529
5530    /// Get a jurisdiction by ID.
5531    #[wasm_bindgen(js_name = getJurisdiction)]
5532    pub fn get_jurisdiction(&self, id: &str) -> Result<JsValue, JsValue> {
5533        let uuid = Uuid::parse_str(id).map_err(|e| JsValue::from_str(&e.to_string()))?;
5534        let store = self.store.borrow();
5535
5536        match store.tax_jurisdictions.get(&uuid) {
5537            Some(data) => {
5538                let js: JsTaxJurisdiction = data.into();
5539                serde_wasm_bindgen::to_value(&js).map_err(|e| JsValue::from_str(&e.to_string()))
5540            }
5541            None => Ok(JsValue::NULL),
5542        }
5543    }
5544
5545    /// Get a jurisdiction by code.
5546    #[wasm_bindgen(js_name = getJurisdictionByCode)]
5547    pub fn get_jurisdiction_by_code(&self, code: &str) -> Result<JsValue, JsValue> {
5548        let store = self.store.borrow();
5549
5550        match store.tax_jurisdictions.values().find(|j| j.code == code) {
5551            Some(data) => {
5552                let js: JsTaxJurisdiction = data.into();
5553                serde_wasm_bindgen::to_value(&js).map_err(|e| JsValue::from_str(&e.to_string()))
5554            }
5555            None => Ok(JsValue::NULL),
5556        }
5557    }
5558
5559    /// List all jurisdictions.
5560    #[wasm_bindgen(js_name = listJurisdictions)]
5561    pub fn list_jurisdictions(
5562        &self,
5563        country_code: Option<String>,
5564        level: Option<String>,
5565    ) -> Result<JsValue, JsValue> {
5566        let store = self.store.borrow();
5567        let jurisdictions: Vec<JsTaxJurisdiction> = store
5568            .tax_jurisdictions
5569            .values()
5570            .filter(|j| {
5571                let country_match = country_code.as_ref().is_none_or(|c| &j.country_code == c);
5572                let level_match = level.as_ref().is_none_or(|l| &j.level == l);
5573                country_match && level_match
5574            })
5575            .map(|d| d.into())
5576            .collect();
5577
5578        serde_wasm_bindgen::to_value(&jurisdictions).map_err(|e| JsValue::from_str(&e.to_string()))
5579    }
5580
5581    // ========================================================================
5582    // Tax Rate Operations
5583    // ========================================================================
5584
5585    /// Create a tax rate.
5586    #[wasm_bindgen(js_name = createRate)]
5587    pub fn create_rate(&self, input: JsValue) -> Result<JsValue, JsValue> {
5588        let input: CreateTaxRateInput =
5589            serde_wasm_bindgen::from_value(input).map_err(|e| JsValue::from_str(&e.to_string()))?;
5590
5591        let jurisdiction_id = Uuid::parse_str(&input.jurisdiction_id)
5592            .map_err(|e| JsValue::from_str(&e.to_string()))?;
5593
5594        let now = Utc::now();
5595        let id = Uuid::new_v4();
5596
5597        let data = TaxRateData {
5598            id,
5599            jurisdiction_id,
5600            tax_type: input.tax_type.unwrap_or_else(|| "sales_tax".to_string()),
5601            product_category: input.product_category.unwrap_or_else(|| "standard".to_string()),
5602            rate: input.rate,
5603            name: input.name,
5604            description: input.description,
5605            is_compound: input.is_compound.unwrap_or(false),
5606            priority: input.priority.unwrap_or(0),
5607            threshold_min: input.threshold_min,
5608            threshold_max: input.threshold_max,
5609            fixed_amount: input.fixed_amount,
5610            effective_from: input.effective_from,
5611            effective_to: input.effective_to,
5612            active: true,
5613            created_at: now.to_rfc3339(),
5614            updated_at: now.to_rfc3339(),
5615        };
5616
5617        let js_rate: JsTaxRate = (&data).into();
5618        self.store.borrow_mut().tax_rates.insert(id, data);
5619
5620        serde_wasm_bindgen::to_value(&js_rate).map_err(|e| JsValue::from_str(&e.to_string()))
5621    }
5622
5623    /// Get a rate by ID.
5624    #[wasm_bindgen(js_name = getRate)]
5625    pub fn get_rate(&self, id: &str) -> Result<JsValue, JsValue> {
5626        let uuid = Uuid::parse_str(id).map_err(|e| JsValue::from_str(&e.to_string()))?;
5627        let store = self.store.borrow();
5628
5629        match store.tax_rates.get(&uuid) {
5630            Some(data) => {
5631                let js: JsTaxRate = data.into();
5632                serde_wasm_bindgen::to_value(&js).map_err(|e| JsValue::from_str(&e.to_string()))
5633            }
5634            None => Ok(JsValue::NULL),
5635        }
5636    }
5637
5638    /// List tax rates.
5639    #[wasm_bindgen(js_name = listRates)]
5640    pub fn list_rates(&self, jurisdiction_id: Option<String>) -> Result<JsValue, JsValue> {
5641        let store = self.store.borrow();
5642        let filter_id = jurisdiction_id.and_then(|s| Uuid::parse_str(&s).ok());
5643
5644        let rates: Vec<JsTaxRate> = store
5645            .tax_rates
5646            .values()
5647            .filter(|r| filter_id.is_none_or(|id| r.jurisdiction_id == id))
5648            .map(|d| d.into())
5649            .collect();
5650
5651        serde_wasm_bindgen::to_value(&rates).map_err(|e| JsValue::from_str(&e.to_string()))
5652    }
5653
5654    // ========================================================================
5655    // Exemption Operations
5656    // ========================================================================
5657
5658    /// Create a tax exemption.
5659    #[wasm_bindgen(js_name = createExemption)]
5660    pub fn create_exemption(&self, input: JsValue) -> Result<JsValue, JsValue> {
5661        let input: CreateExemptionInput =
5662            serde_wasm_bindgen::from_value(input).map_err(|e| JsValue::from_str(&e.to_string()))?;
5663
5664        let customer_id =
5665            Uuid::parse_str(&input.customer_id).map_err(|e| JsValue::from_str(&e.to_string()))?;
5666
5667        let now = Utc::now();
5668        let id = Uuid::new_v4();
5669
5670        let data = TaxExemptionData {
5671            id,
5672            customer_id,
5673            exemption_type: input.exemption_type,
5674            certificate_number: input.certificate_number,
5675            issuing_authority: input.issuing_authority,
5676            jurisdiction_ids: input
5677                .jurisdiction_ids
5678                .unwrap_or_default()
5679                .into_iter()
5680                .filter_map(|s| Uuid::parse_str(&s).ok())
5681                .collect(),
5682            exempt_categories: input.exempt_categories.unwrap_or_default(),
5683            effective_from: input.effective_from,
5684            expires_at: input.expires_at,
5685            verified: false,
5686            verified_at: None,
5687            notes: input.notes,
5688            active: true,
5689            created_at: now.to_rfc3339(),
5690            updated_at: now.to_rfc3339(),
5691        };
5692
5693        let js_exemption: JsTaxExemption = (&data).into();
5694        self.store.borrow_mut().tax_exemptions.insert(id, data);
5695
5696        serde_wasm_bindgen::to_value(&js_exemption).map_err(|e| JsValue::from_str(&e.to_string()))
5697    }
5698
5699    /// Get an exemption by ID.
5700    #[wasm_bindgen(js_name = getExemption)]
5701    pub fn get_exemption(&self, id: &str) -> Result<JsValue, JsValue> {
5702        let uuid = Uuid::parse_str(id).map_err(|e| JsValue::from_str(&e.to_string()))?;
5703        let store = self.store.borrow();
5704
5705        match store.tax_exemptions.get(&uuid) {
5706            Some(data) => {
5707                let js: JsTaxExemption = data.into();
5708                serde_wasm_bindgen::to_value(&js).map_err(|e| JsValue::from_str(&e.to_string()))
5709            }
5710            None => Ok(JsValue::NULL),
5711        }
5712    }
5713
5714    /// Get exemptions for a customer.
5715    #[wasm_bindgen(js_name = getCustomerExemptions)]
5716    pub fn get_customer_exemptions(&self, customer_id: &str) -> Result<JsValue, JsValue> {
5717        let customer_uuid =
5718            Uuid::parse_str(customer_id).map_err(|e| JsValue::from_str(&e.to_string()))?;
5719
5720        let store = self.store.borrow();
5721        let exemptions: Vec<JsTaxExemption> = store
5722            .tax_exemptions
5723            .values()
5724            .filter(|e| e.customer_id == customer_uuid && e.active)
5725            .map(|d| d.into())
5726            .collect();
5727
5728        serde_wasm_bindgen::to_value(&exemptions).map_err(|e| JsValue::from_str(&e.to_string()))
5729    }
5730
5731    /// Check if a customer is tax exempt.
5732    #[wasm_bindgen(js_name = customerIsExempt)]
5733    pub fn customer_is_exempt(&self, customer_id: &str) -> Result<bool, JsValue> {
5734        let customer_uuid =
5735            Uuid::parse_str(customer_id).map_err(|e| JsValue::from_str(&e.to_string()))?;
5736
5737        let store = self.store.borrow();
5738        let has_exemption =
5739            store.tax_exemptions.values().any(|e| e.customer_id == customer_uuid && e.active);
5740
5741        Ok(has_exemption)
5742    }
5743
5744    // ========================================================================
5745    // Tax Calculation
5746    // ========================================================================
5747
5748    /// Calculate tax for a transaction.
5749    #[wasm_bindgen(js_name = calculate)]
5750    pub fn calculate(&self, input: JsValue) -> Result<JsValue, JsValue> {
5751        let input: TaxCalculationInput =
5752            serde_wasm_bindgen::from_value(input).map_err(|e| JsValue::from_str(&e.to_string()))?;
5753
5754        let store = self.store.borrow();
5755        let now = Utc::now();
5756
5757        // Find applicable rates based on address
5758        let mut applicable_rates: Vec<&TaxRateData> = store
5759            .tax_rates
5760            .values()
5761            .filter(|r| {
5762                // Simple matching - in a real implementation this would be more sophisticated
5763                if let Some(jurisdiction) = store.tax_jurisdictions.get(&r.jurisdiction_id) {
5764                    let country_match = jurisdiction.country_code == input.shipping_address.country;
5765                    let state_match =
5766                        input.shipping_address.state.as_ref().is_none_or(|s| {
5767                            jurisdiction.state_code.as_ref().is_none_or(|js| js == s)
5768                        });
5769                    country_match && state_match && r.active
5770                } else {
5771                    false
5772                }
5773            })
5774            .collect();
5775        applicable_rates.sort_by_key(|r| r.priority);
5776
5777        let shipping_amount = input.shipping_amount.unwrap_or_default();
5778        let has_shipping = input.shipping_amount.is_some();
5779        let default_category = store
5780            .tax_settings
5781            .as_ref()
5782            .map(|s| s.default_product_category.as_str())
5783            .unwrap_or("standard");
5784
5785        struct TaxBreakdownAccum {
5786            jurisdiction_id: Uuid,
5787            jurisdiction_name: String,
5788            tax_type: String,
5789            rate_name: String,
5790            rate: f64,
5791            taxable_amount: Money,
5792            tax_amount: Money,
5793            is_compound: bool,
5794        }
5795
5796        let rate_base = |taxable: Money, min: Option<Money>, max: Option<Money>| -> Option<Money> {
5797            if taxable <= Money::zero() {
5798                return None;
5799            }
5800            if let Some(min) = min {
5801                if taxable < min {
5802                    return None;
5803                }
5804            }
5805            let capped = match max {
5806                Some(max) if taxable > max => max,
5807                _ => taxable,
5808            };
5809            if capped <= Money::zero() { None } else { Some(capped) }
5810        };
5811
5812        // Calculate line item taxes
5813        let mut subtotal = Money::zero();
5814        let mut total_tax = Money::zero();
5815        let mut line_item_taxes = Vec::new();
5816        let mut tax_breakdown_map: std::collections::HashMap<Uuid, TaxBreakdownAccum> =
5817            std::collections::HashMap::new();
5818
5819        for item in &input.line_items {
5820            let mut line_total =
5821                item.unit_price.mul_rate(item.quantity) - item.discount_amount.unwrap_or_default();
5822            if line_total < Money::zero() {
5823                line_total = Money::zero();
5824            }
5825            subtotal += line_total;
5826
5827            let mut line_tax = Money::zero();
5828            let item_category = item.tax_category.as_deref().unwrap_or(default_category);
5829
5830            for rate in applicable_rates.iter().filter(|r| r.product_category == item_category) {
5831                let Some(capped_base) =
5832                    rate_base(line_total, rate.threshold_min, rate.threshold_max)
5833                else {
5834                    continue;
5835                };
5836                let taxable_amount = if rate.fixed_amount.is_some() {
5837                    capped_base
5838                } else if rate.is_compound {
5839                    capped_base + line_tax
5840                } else {
5841                    capped_base
5842                };
5843                let rate_tax = if let Some(fixed) = rate.fixed_amount {
5844                    fixed
5845                } else {
5846                    taxable_amount.mul_rate(rate.rate)
5847                };
5848                line_tax += rate_tax;
5849                total_tax += rate_tax;
5850
5851                if let Some(j) = store.tax_jurisdictions.get(&rate.jurisdiction_id) {
5852                    let entry =
5853                        tax_breakdown_map.entry(rate.id).or_insert_with(|| TaxBreakdownAccum {
5854                            jurisdiction_id: j.id,
5855                            jurisdiction_name: j.name.clone(),
5856                            tax_type: rate.tax_type.clone(),
5857                            rate_name: rate.name.clone(),
5858                            rate: rate.rate,
5859                            taxable_amount: Money::zero(),
5860                            tax_amount: Money::zero(),
5861                            is_compound: rate.is_compound,
5862                        });
5863                    entry.taxable_amount += taxable_amount;
5864                    entry.tax_amount += rate_tax;
5865                }
5866            }
5867
5868            let effective_rate = if line_total > Money::zero() {
5869                line_tax.to_f64() / line_total.to_f64()
5870            } else {
5871                0.0
5872            };
5873
5874            line_item_taxes.push(JsLineItemTax {
5875                line_item_id: item.id.clone(),
5876                taxable_amount: line_total,
5877                tax_amount: line_tax,
5878                effective_rate,
5879                is_exempt: false,
5880                exemption_reason: None,
5881            });
5882        }
5883
5884        // Handle shipping tax
5885        let mut shipping_tax = Money::zero();
5886        if has_shipping {
5887            let settings = store.tax_settings.as_ref();
5888            if settings.is_none_or(|s| s.tax_shipping) {
5889                let mut shipping_taxable =
5890                    if shipping_amount > Money::zero() { shipping_amount } else { Money::zero() };
5891                if shipping_taxable < Money::zero() {
5892                    shipping_taxable = Money::zero();
5893                }
5894                for rate in applicable_rates.iter().filter(|r| r.product_category == "standard") {
5895                    let Some(capped_base) =
5896                        rate_base(shipping_taxable, rate.threshold_min, rate.threshold_max)
5897                    else {
5898                        continue;
5899                    };
5900                    let taxable_amount = if rate.fixed_amount.is_some() {
5901                        capped_base
5902                    } else if rate.is_compound {
5903                        capped_base + shipping_tax
5904                    } else {
5905                        capped_base
5906                    };
5907                    let rate_tax = if let Some(fixed) = rate.fixed_amount {
5908                        fixed
5909                    } else {
5910                        taxable_amount.mul_rate(rate.rate)
5911                    };
5912                    shipping_tax += rate_tax;
5913                    total_tax += rate_tax;
5914
5915                    if let Some(j) = store.tax_jurisdictions.get(&rate.jurisdiction_id) {
5916                        let entry =
5917                            tax_breakdown_map.entry(rate.id).or_insert_with(|| TaxBreakdownAccum {
5918                                jurisdiction_id: j.id,
5919                                jurisdiction_name: j.name.clone(),
5920                                tax_type: rate.tax_type.clone(),
5921                                rate_name: rate.name.clone(),
5922                                rate: rate.rate,
5923                                taxable_amount: Money::zero(),
5924                                tax_amount: Money::zero(),
5925                                is_compound: rate.is_compound,
5926                            });
5927                        entry.taxable_amount += taxable_amount;
5928                        entry.tax_amount += rate_tax;
5929                    }
5930                }
5931            }
5932        }
5933
5934        let tax_breakdown: Vec<JsTaxBreakdown> = tax_breakdown_map
5935            .into_values()
5936            .map(|b| JsTaxBreakdown {
5937                jurisdiction_id: b.jurisdiction_id.to_string(),
5938                jurisdiction_name: b.jurisdiction_name,
5939                tax_type: b.tax_type,
5940                rate_name: b.rate_name,
5941                rate: b.rate,
5942                taxable_amount: b.taxable_amount,
5943                tax_amount: b.tax_amount,
5944                is_compound: b.is_compound,
5945            })
5946            .collect();
5947
5948        let result = JsTaxCalculationResult {
5949            id: Uuid::new_v4().to_string(),
5950            total_tax,
5951            subtotal,
5952            total: subtotal + total_tax + shipping_amount,
5953            shipping_tax,
5954            tax_breakdown,
5955            line_item_taxes,
5956            exemptions_applied: false,
5957            calculated_at: now.to_rfc3339(),
5958            is_estimate: true,
5959        };
5960
5961        serde_wasm_bindgen::to_value(&result).map_err(|e| JsValue::from_str(&e.to_string()))
5962    }
5963
5964    /// Get the effective tax rate for an address.
5965    #[wasm_bindgen(js_name = getEffectiveRate)]
5966    pub fn get_effective_rate(&self, country: &str, state: Option<String>) -> f64 {
5967        let store = self.store.borrow();
5968
5969        store
5970            .tax_rates
5971            .values()
5972            .filter(|r| {
5973                if let Some(j) = store.tax_jurisdictions.get(&r.jurisdiction_id) {
5974                    let country_match = j.country_code == country;
5975                    let state_match = state
5976                        .as_ref()
5977                        .is_none_or(|s| j.state_code.as_ref().is_none_or(|js| js == s));
5978                    country_match && state_match && r.active
5979                } else {
5980                    false
5981                }
5982            })
5983            .filter(|r| !r.is_compound)
5984            .map(|r| r.rate)
5985            .sum()
5986    }
5987
5988    // ========================================================================
5989    // Settings Operations
5990    // ========================================================================
5991
5992    /// Get tax settings.
5993    #[wasm_bindgen(js_name = getSettings)]
5994    pub fn get_settings(&self) -> Result<JsValue, JsValue> {
5995        let store = self.store.borrow();
5996
5997        let settings = store
5998            .tax_settings
5999            .as_ref()
6000            .map(|s| {
6001                let js: JsTaxSettings = s.into();
6002                js
6003            })
6004            .unwrap_or_else(|| {
6005                let now = Utc::now();
6006                JsTaxSettings {
6007                    id: Uuid::new_v4().to_string(),
6008                    enabled: true,
6009                    calculation_method: "exclusive".to_string(),
6010                    compound_method: "combined".to_string(),
6011                    tax_shipping: true,
6012                    tax_handling: true,
6013                    tax_gift_wrap: true,
6014                    default_product_category: "standard".to_string(),
6015                    rounding_mode: "half_up".to_string(),
6016                    decimal_places: 2,
6017                    validate_addresses: false,
6018                    tax_provider: None,
6019                    created_at: now.to_rfc3339(),
6020                    updated_at: now.to_rfc3339(),
6021                }
6022            });
6023
6024        serde_wasm_bindgen::to_value(&settings).map_err(|e| JsValue::from_str(&e.to_string()))
6025    }
6026
6027    /// Update tax settings.
6028    #[wasm_bindgen(js_name = updateSettings)]
6029    pub fn update_settings(&self, input: JsValue) -> Result<JsValue, JsValue> {
6030        let input: UpdateTaxSettingsInput =
6031            serde_wasm_bindgen::from_value(input).map_err(|e| JsValue::from_str(&e.to_string()))?;
6032
6033        let now = Utc::now();
6034        let mut store = self.store.borrow_mut();
6035
6036        let settings = store.tax_settings.get_or_insert_with(|| TaxSettingsData {
6037            id: Uuid::new_v4(),
6038            enabled: true,
6039            calculation_method: "exclusive".to_string(),
6040            compound_method: "combined".to_string(),
6041            tax_shipping: true,
6042            tax_handling: true,
6043            tax_gift_wrap: true,
6044            default_product_category: "standard".to_string(),
6045            rounding_mode: "half_up".to_string(),
6046            decimal_places: 2,
6047            validate_addresses: false,
6048            tax_provider: None,
6049            created_at: now.to_rfc3339(),
6050            updated_at: now.to_rfc3339(),
6051        });
6052
6053        if let Some(v) = input.enabled {
6054            settings.enabled = v;
6055        }
6056        if let Some(v) = input.calculation_method {
6057            settings.calculation_method = v;
6058        }
6059        if let Some(v) = input.compound_method {
6060            settings.compound_method = v;
6061        }
6062        if let Some(v) = input.tax_shipping {
6063            settings.tax_shipping = v;
6064        }
6065        if let Some(v) = input.tax_handling {
6066            settings.tax_handling = v;
6067        }
6068        if let Some(v) = input.tax_gift_wrap {
6069            settings.tax_gift_wrap = v;
6070        }
6071        if let Some(v) = input.default_product_category {
6072            settings.default_product_category = v;
6073        }
6074        if let Some(v) = input.rounding_mode {
6075            settings.rounding_mode = v;
6076        }
6077        if let Some(v) = input.decimal_places {
6078            settings.decimal_places = v;
6079        }
6080        if let Some(v) = input.validate_addresses {
6081            settings.validate_addresses = v;
6082        }
6083        if let Some(v) = input.tax_provider {
6084            settings.tax_provider = Some(v);
6085        }
6086        settings.updated_at = now.to_rfc3339();
6087
6088        let js_settings: JsTaxSettings = (&*settings).into();
6089        serde_wasm_bindgen::to_value(&js_settings).map_err(|e| JsValue::from_str(&e.to_string()))
6090    }
6091
6092    /// Enable or disable tax calculation.
6093    #[wasm_bindgen(js_name = setEnabled)]
6094    pub fn set_enabled(&self, enabled: bool) -> Result<JsValue, JsValue> {
6095        let now = Utc::now();
6096        let mut store = self.store.borrow_mut();
6097
6098        let settings = store.tax_settings.get_or_insert_with(|| TaxSettingsData {
6099            id: Uuid::new_v4(),
6100            enabled: true,
6101            calculation_method: "exclusive".to_string(),
6102            compound_method: "combined".to_string(),
6103            tax_shipping: true,
6104            tax_handling: true,
6105            tax_gift_wrap: true,
6106            default_product_category: "standard".to_string(),
6107            rounding_mode: "half_up".to_string(),
6108            decimal_places: 2,
6109            validate_addresses: false,
6110            tax_provider: None,
6111            created_at: now.to_rfc3339(),
6112            updated_at: now.to_rfc3339(),
6113        });
6114
6115        settings.enabled = enabled;
6116        settings.updated_at = now.to_rfc3339();
6117
6118        let js_settings: JsTaxSettings = (&*settings).into();
6119        serde_wasm_bindgen::to_value(&js_settings).map_err(|e| JsValue::from_str(&e.to_string()))
6120    }
6121
6122    /// Check if tax calculation is enabled.
6123    #[wasm_bindgen(js_name = isEnabled)]
6124    pub fn is_enabled(&self) -> bool {
6125        self.store.borrow().tax_settings.as_ref().is_none_or(|s| s.enabled)
6126    }
6127
6128    // ========================================================================
6129    // Helper Methods
6130    // ========================================================================
6131
6132    /// Get US state tax information.
6133    #[wasm_bindgen(js_name = getUsStateInfo)]
6134    pub fn get_us_state_info(state_code: &str) -> Result<JsValue, JsValue> {
6135        // US state tax rates (simplified)
6136        let info = match state_code.to_uppercase().as_str() {
6137            "CA" => Some(JsUsStateTaxInfo {
6138                state_code: "CA".to_string(),
6139                state_name: "California".to_string(),
6140                state_rate: 0.0725,
6141                has_local_taxes: true,
6142                origin_based: true,
6143                tax_shipping: false,
6144                tax_clothing: true,
6145                tax_food: false,
6146                tax_digital: false,
6147            }),
6148            "TX" => Some(JsUsStateTaxInfo {
6149                state_code: "TX".to_string(),
6150                state_name: "Texas".to_string(),
6151                state_rate: 0.0625,
6152                has_local_taxes: true,
6153                origin_based: true,
6154                tax_shipping: true,
6155                tax_clothing: true,
6156                tax_food: false,
6157                tax_digital: true,
6158            }),
6159            "NY" => Some(JsUsStateTaxInfo {
6160                state_code: "NY".to_string(),
6161                state_name: "New York".to_string(),
6162                state_rate: 0.04,
6163                has_local_taxes: true,
6164                origin_based: false,
6165                tax_shipping: true,
6166                tax_clothing: false,
6167                tax_food: false,
6168                tax_digital: true,
6169            }),
6170            "FL" => Some(JsUsStateTaxInfo {
6171                state_code: "FL".to_string(),
6172                state_name: "Florida".to_string(),
6173                state_rate: 0.06,
6174                has_local_taxes: true,
6175                origin_based: false,
6176                tax_shipping: true,
6177                tax_clothing: true,
6178                tax_food: false,
6179                tax_digital: true,
6180            }),
6181            "DE" | "MT" | "NH" | "OR" => Some(JsUsStateTaxInfo {
6182                state_code: state_code.to_uppercase(),
6183                state_name: match state_code.to_uppercase().as_str() {
6184                    "DE" => "Delaware",
6185                    "MT" => "Montana",
6186                    "NH" => "New Hampshire",
6187                    "OR" => "Oregon",
6188                    _ => state_code,
6189                }
6190                .to_string(),
6191                state_rate: 0.0,
6192                has_local_taxes: false,
6193                origin_based: false,
6194                tax_shipping: false,
6195                tax_clothing: false,
6196                tax_food: false,
6197                tax_digital: false,
6198            }),
6199            _ => None,
6200        };
6201
6202        match info {
6203            Some(i) => {
6204                serde_wasm_bindgen::to_value(&i).map_err(|e| JsValue::from_str(&e.to_string()))
6205            }
6206            None => Ok(JsValue::NULL),
6207        }
6208    }
6209
6210    /// Get EU VAT information.
6211    #[wasm_bindgen(js_name = getEuVatInfo)]
6212    pub fn get_eu_vat_info(country_code: &str) -> Result<JsValue, JsValue> {
6213        let info = match country_code.to_uppercase().as_str() {
6214            "DE" => Some(JsEuVatInfo {
6215                country_code: "DE".to_string(),
6216                country_name: "Germany".to_string(),
6217                standard_rate: 0.19,
6218                reduced_rate: Some(0.07),
6219                super_reduced_rate: None,
6220                parking_rate: None,
6221            }),
6222            "FR" => Some(JsEuVatInfo {
6223                country_code: "FR".to_string(),
6224                country_name: "France".to_string(),
6225                standard_rate: 0.20,
6226                reduced_rate: Some(0.10),
6227                super_reduced_rate: Some(0.055),
6228                parking_rate: None,
6229            }),
6230            "GB" => Some(JsEuVatInfo {
6231                country_code: "GB".to_string(),
6232                country_name: "United Kingdom".to_string(),
6233                standard_rate: 0.20,
6234                reduced_rate: Some(0.05),
6235                super_reduced_rate: None,
6236                parking_rate: None,
6237            }),
6238            "IT" => Some(JsEuVatInfo {
6239                country_code: "IT".to_string(),
6240                country_name: "Italy".to_string(),
6241                standard_rate: 0.22,
6242                reduced_rate: Some(0.10),
6243                super_reduced_rate: Some(0.04),
6244                parking_rate: None,
6245            }),
6246            "ES" => Some(JsEuVatInfo {
6247                country_code: "ES".to_string(),
6248                country_name: "Spain".to_string(),
6249                standard_rate: 0.21,
6250                reduced_rate: Some(0.10),
6251                super_reduced_rate: Some(0.04),
6252                parking_rate: None,
6253            }),
6254            _ => None,
6255        };
6256
6257        match info {
6258            Some(i) => {
6259                serde_wasm_bindgen::to_value(&i).map_err(|e| JsValue::from_str(&e.to_string()))
6260            }
6261            None => Ok(JsValue::NULL),
6262        }
6263    }
6264
6265    /// Get Canadian tax information.
6266    #[wasm_bindgen(js_name = getCanadianTaxInfo)]
6267    pub fn get_canadian_tax_info(province_code: &str) -> Result<JsValue, JsValue> {
6268        let gst = 0.05;
6269        let info = match province_code.to_uppercase().as_str() {
6270            "ON" => Some(JsCanadianTaxInfo {
6271                province_code: "ON".to_string(),
6272                province_name: "Ontario".to_string(),
6273                gst_rate: 0.0,
6274                pst_rate: None,
6275                hst_rate: Some(0.13),
6276                qst_rate: None,
6277                total_rate: 0.13,
6278            }),
6279            "BC" => Some(JsCanadianTaxInfo {
6280                province_code: "BC".to_string(),
6281                province_name: "British Columbia".to_string(),
6282                gst_rate: gst,
6283                pst_rate: Some(0.07),
6284                hst_rate: None,
6285                qst_rate: None,
6286                total_rate: 0.12,
6287            }),
6288            "QC" => Some(JsCanadianTaxInfo {
6289                province_code: "QC".to_string(),
6290                province_name: "Quebec".to_string(),
6291                gst_rate: gst,
6292                pst_rate: None,
6293                hst_rate: None,
6294                qst_rate: Some(0.09975),
6295                total_rate: 0.14975,
6296            }),
6297            "AB" => Some(JsCanadianTaxInfo {
6298                province_code: "AB".to_string(),
6299                province_name: "Alberta".to_string(),
6300                gst_rate: gst,
6301                pst_rate: None,
6302                hst_rate: None,
6303                qst_rate: None,
6304                total_rate: gst,
6305            }),
6306            _ => None,
6307        };
6308
6309        match info {
6310            Some(i) => {
6311                serde_wasm_bindgen::to_value(&i).map_err(|e| JsValue::from_str(&e.to_string()))
6312            }
6313            None => Ok(JsValue::NULL),
6314        }
6315    }
6316
6317    /// Check if a country is in the EU.
6318    #[wasm_bindgen(js_name = isEuCountry)]
6319    pub fn is_eu_country(country_code: &str) -> bool {
6320        const EU_MEMBERS: &[&str] = &[
6321            "AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", "HU", "IE",
6322            "IT", "LV", "LT", "LU", "MT", "NL", "PL", "PT", "RO", "SK", "SI", "ES", "SE",
6323        ];
6324        EU_MEMBERS.contains(&country_code.to_uppercase().as_str())
6325    }
6326
6327    /// Count jurisdictions.
6328    #[wasm_bindgen(js_name = countJurisdictions)]
6329    pub fn count_jurisdictions(&self) -> u32 {
6330        self.store.borrow().tax_jurisdictions.len() as u32
6331    }
6332
6333    /// Count tax rates.
6334    #[wasm_bindgen(js_name = countRates)]
6335    pub fn count_rates(&self) -> u32 {
6336        self.store.borrow().tax_rates.len() as u32
6337    }
6338
6339    /// Count exemptions.
6340    #[wasm_bindgen(js_name = countExemptions)]
6341    pub fn count_exemptions(&self) -> u32 {
6342        self.store.borrow().tax_exemptions.len() as u32
6343    }
6344}