1#![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#[wasm_bindgen(start)]
32pub fn init() {
33 console_error_panic_hook::set_once();
34}
35
36#[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 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: HashMap<Uuid, CartData>,
67 cart_items: HashMap<Uuid, Vec<CartItemData>>,
68 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: HashMap<Uuid, PromotionData>,
75 coupons: HashMap<Uuid, CouponData>,
76 promotion_usages: Vec<PromotionUsageData>,
77 tax_jurisdictions: HashMap<Uuid, TaxJurisdictionData>,
79 tax_rates: HashMap<Uuid, TaxRateData>,
80 tax_exemptions: HashMap<Uuid, TaxExemptionData>,
81 tax_settings: Option<TaxSettingsData>,
82 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#[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#[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#[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#[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#[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#[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#[wasm_bindgen]
1824pub struct Commerce {
1825 store: StoreRef,
1826}
1827
1828#[wasm_bindgen]
1829impl Commerce {
1830 #[wasm_bindgen(constructor)]
1832 pub fn new() -> Commerce {
1833 Commerce { store: Rc::new(RefCell::new(Store::default())) }
1834 }
1835
1836 #[wasm_bindgen(getter)]
1838 pub fn customers(&self) -> Customers {
1839 Customers { store: Rc::clone(&self.store) }
1840 }
1841
1842 #[wasm_bindgen(getter)]
1844 pub fn orders(&self) -> Orders {
1845 Orders { store: Rc::clone(&self.store) }
1846 }
1847
1848 #[wasm_bindgen(getter)]
1850 pub fn products(&self) -> Products {
1851 Products { store: Rc::clone(&self.store) }
1852 }
1853
1854 #[wasm_bindgen(getter)]
1856 pub fn inventory(&self) -> Inventory {
1857 Inventory { store: Rc::clone(&self.store) }
1858 }
1859
1860 #[wasm_bindgen(getter)]
1862 pub fn returns(&self) -> Returns {
1863 Returns { store: Rc::clone(&self.store) }
1864 }
1865
1866 #[wasm_bindgen(getter)]
1868 pub fn payments(&self) -> Payments {
1869 Payments { store: Rc::clone(&self.store) }
1870 }
1871
1872 #[wasm_bindgen(getter)]
1874 pub fn shipments(&self) -> Shipments {
1875 Shipments { store: Rc::clone(&self.store) }
1876 }
1877
1878 #[wasm_bindgen(getter)]
1880 pub fn warranties(&self) -> Warranties {
1881 Warranties { store: Rc::clone(&self.store) }
1882 }
1883
1884 #[wasm_bindgen(getter, js_name = purchaseOrders)]
1886 pub fn purchase_orders(&self) -> PurchaseOrders {
1887 PurchaseOrders { store: Rc::clone(&self.store) }
1888 }
1889
1890 #[wasm_bindgen(getter)]
1892 pub fn invoices(&self) -> Invoices {
1893 Invoices { store: Rc::clone(&self.store) }
1894 }
1895
1896 #[wasm_bindgen(getter)]
1898 pub fn bom(&self) -> Bom {
1899 Bom { store: Rc::clone(&self.store) }
1900 }
1901
1902 #[wasm_bindgen(getter, js_name = workOrders)]
1904 pub fn work_orders(&self) -> WorkOrders {
1905 WorkOrders { store: Rc::clone(&self.store) }
1906 }
1907
1908 #[wasm_bindgen(getter)]
1910 pub fn carts(&self) -> Carts {
1911 Carts { store: Rc::clone(&self.store) }
1912 }
1913
1914 #[wasm_bindgen(getter)]
1916 pub fn subscriptions(&self) -> Subscriptions {
1917 Subscriptions { store: Rc::clone(&self.store) }
1918 }
1919
1920 #[wasm_bindgen(getter)]
1922 pub fn promotions(&self) -> Promotions {
1923 Promotions { store: Rc::clone(&self.store) }
1924 }
1925
1926 #[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#[wasm_bindgen]
1945pub struct Customers {
1946 store: StoreRef,
1947}
1948
1949#[wasm_bindgen]
1950impl Customers {
1951 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 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 #[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 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 pub fn count(&self) -> u32 {
2017 self.store.borrow().customers.len() as u32
2018 }
2019}
2020
2021#[wasm_bindgen]
2027pub struct Orders {
2028 store: StoreRef,
2029}
2030
2031#[wasm_bindgen]
2032impl Orders {
2033 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 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 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 #[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 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 #[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 {
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 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 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 {
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 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 pub fn cancel(&self, id: &str) -> Result<JsValue, JsValue> {
2267 self.update_status(id, "cancelled")
2268 }
2269
2270 pub fn count(&self) -> u32 {
2272 self.store.borrow().orders.len() as u32
2273 }
2274}
2275
2276#[wasm_bindgen]
2282pub struct Products {
2283 store: StoreRef,
2284}
2285
2286#[wasm_bindgen]
2287impl Products {
2288 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 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 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 #[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 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 pub fn count(&self) -> u32 {
2371 self.store.borrow().products.len() as u32
2372 }
2373}
2374
2375#[wasm_bindgen]
2381pub struct Inventory {
2382 store: StoreRef,
2383}
2384
2385#[wasm_bindgen]
2386impl Inventory {
2387 #[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 #[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 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 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 #[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 #[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#[wasm_bindgen]
2566pub struct Returns {
2567 store: StoreRef,
2568}
2569
2570#[wasm_bindgen]
2571impl Returns {
2572 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 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 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 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 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 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 pub fn count(&self) -> u32 {
2666 self.store.borrow().returns.len() as u32
2667 }
2668}
2669
2670#[wasm_bindgen]
2676pub struct Payments {
2677 store: StoreRef,
2678}
2679
2680#[wasm_bindgen]
2681impl Payments {
2682 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 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 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 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 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 pub fn count(&self) -> u32 {
2779 self.store.borrow().payments.len() as u32
2780 }
2781}
2782
2783#[wasm_bindgen]
2789pub struct Shipments {
2790 store: StoreRef,
2791}
2792
2793#[wasm_bindgen]
2794impl Shipments {
2795 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 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 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 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 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 pub fn count(&self) -> u32 {
2892 self.store.borrow().shipments.len() as u32
2893 }
2894}
2895
2896#[wasm_bindgen]
2902pub struct Warranties {
2903 store: StoreRef,
2904}
2905
2906#[wasm_bindgen]
2907impl Warranties {
2908 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 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 #[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 #[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 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 pub fn count(&self) -> u32 {
3018 self.store.borrow().warranties.len() as u32
3019 }
3020}
3021
3022#[wasm_bindgen]
3028pub struct PurchaseOrders {
3029 store: StoreRef,
3030}
3031
3032#[wasm_bindgen]
3033impl PurchaseOrders {
3034 #[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 #[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 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 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 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 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 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 pub fn count(&self) -> u32 {
3169 self.store.borrow().purchase_orders.len() as u32
3170 }
3171}
3172
3173#[wasm_bindgen]
3179pub struct Invoices {
3180 store: StoreRef,
3181}
3182
3183#[wasm_bindgen]
3184impl Invoices {
3185 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 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 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 #[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 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 pub fn count(&self) -> u32 {
3286 self.store.borrow().invoices.len() as u32
3287 }
3288}
3289
3290#[wasm_bindgen]
3296pub struct Bom {
3297 store: StoreRef,
3298}
3299
3300#[wasm_bindgen]
3301impl Bom {
3302 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 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 #[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 #[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 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 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 pub fn count(&self) -> u32 {
3410 self.store.borrow().boms.len() as u32
3411 }
3412}
3413
3414#[wasm_bindgen]
3420pub struct WorkOrders {
3421 store: StoreRef,
3422}
3423
3424#[wasm_bindgen]
3425impl WorkOrders {
3426 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 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 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 #[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 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 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 pub fn count(&self) -> u32 {
3542 self.store.borrow().work_orders.len() as u32
3543 }
3544}
3545
3546#[wasm_bindgen]
3552pub struct Carts {
3553 store: StoreRef,
3554}
3555
3556#[wasm_bindgen]
3557impl Carts {
3558 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 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 #[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 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 store.cart_items.entry(uuid).or_default().push(item);
3711
3712 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 #[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 if !store.carts.contains_key(&cart_uuid) {
3734 return Err(JsValue::from_str("Cart not found"));
3735 }
3736
3737 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 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 #[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 if !store.carts.contains_key(&uuid) {
3772 return Err(JsValue::from_str("Cart not found"));
3773 }
3774
3775 store.cart_items.insert(uuid, Vec::new());
3777
3778 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 #[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 if !store.carts.contains_key(&uuid) {
3798 return Err(JsValue::from_str("Cart not found"));
3799 }
3800
3801 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 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 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 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 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 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 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 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 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 if !store.carts.contains_key(&uuid) {
3939 return Err(JsValue::from_str("Cart not found"));
3940 }
3941
3942 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 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 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 pub fn count(&self) -> u32 {
4029 self.store.borrow().carts.len() as u32
4030 }
4031
4032 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#[wasm_bindgen]
4049pub struct Subscriptions {
4050 store: StoreRef,
4051}
4052
4053#[wasm_bindgen]
4054impl Subscriptions {
4055 #[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 #[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 #[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 #[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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 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 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 #[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 #[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 #[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 #[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 #[wasm_bindgen(js_name = countPlans)]
4528 pub fn count_plans(&self) -> u32 {
4529 self.store.borrow().subscription_plans.len() as u32
4530 }
4531
4532 #[wasm_bindgen(js_name = countSubscriptions)]
4534 pub fn count_subscriptions(&self) -> u32 {
4535 self.store.borrow().subscriptions.len() as u32
4536 }
4537}
4538
4539#[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 if coupon.status != "active" {
4879 return Ok(JsValue::NULL);
4880 }
4881
4882 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 #[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 for promo in store.promotions.values() {
4918 if promo.status != "active" {
4919 continue;
4920 }
4921
4922 let triggered = if promo.trigger == "coupon_code" {
4924 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 true
4933 };
4934
4935 if !triggered {
4936 continue;
4937 }
4938
4939 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 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 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 #[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 if let Some(promo) = store.promotions.get_mut(&promo_uuid) {
5017 promo.usage_count += 1;
5018 }
5019
5020 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 #[wasm_bindgen(js_name = countPromotions)]
5050 pub fn count_promotions(&self) -> u32 {
5051 self.store.borrow().promotions.len() as u32
5052 }
5053
5054 #[wasm_bindgen(js_name = countCoupons)]
5056 pub fn count_coupons(&self) -> u32 {
5057 self.store.borrow().coupons.len() as u32
5058 }
5059}
5060
5061#[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#[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#[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#[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 let mut applicable_rates: Vec<&TaxRateData> = store
5759 .tax_rates
5760 .values()
5761 .filter(|r| {
5762 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 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 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 #[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 #[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 #[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 #[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 #[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 #[wasm_bindgen(js_name = getUsStateInfo)]
6134 pub fn get_us_state_info(state_code: &str) -> Result<JsValue, JsValue> {
6135 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 #[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 #[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 #[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 #[wasm_bindgen(js_name = countJurisdictions)]
6329 pub fn count_jurisdictions(&self) -> u32 {
6330 self.store.borrow().tax_jurisdictions.len() as u32
6331 }
6332
6333 #[wasm_bindgen(js_name = countRates)]
6335 pub fn count_rates(&self) -> u32 {
6336 self.store.borrow().tax_rates.len() as u32
6337 }
6338
6339 #[wasm_bindgen(js_name = countExemptions)]
6341 pub fn count_exemptions(&self) -> u32 {
6342 self.store.borrow().tax_exemptions.len() as u32
6343 }
6344}