Skip to main content

stateset_dotnet/
lib.rs

1//! C FFI bindings for StateSet Embedded Commerce (.NET/C#)
2//!
3//! This crate provides C-compatible bindings for .NET P/Invoke integration.
4
5use rust_decimal::Decimal;
6use stateset_core::{
7    AccountType, BackorderPriority, InspectionType, LocationType, OrderStatus, ReturnReason,
8    ShippingCarrier, WarehouseType,
9};
10use stateset_embedded::{
11    AddCartItem,
12    AnalyticsQuery,
13    Commerce as RustCommerce,
14    CreateBackorder,
15    CreateBill,
16    CreateBillItem,
17    CreateCart,
18    CreateCreditAccount,
19    CreateCustomer,
20    CreateGlAccount,
21    CreateInspection,
22    CreateInventoryItem,
23    CreateLocation,
24    CreateLot,
25    CreateOrder,
26    CreatePayment,
27    CreateProduct,
28    CreateProductVariant,
29    CreateReturn,
30    CreateReturnItem,
31    CreateSerialNumber,
32    CreateShipment,
33    // New modules
34    CreateWarehouse,
35    CustomerFilter,
36    OrderFilter,
37    PaymentMethodType,
38    ProductFilter,
39    SetItemCost,
40    TimePeriod,
41};
42use std::ffi::{CStr, CString, c_char, c_double, c_int};
43use std::ptr;
44use std::sync::{Arc, Mutex};
45
46// =============================================================================
47// Handle Management
48// =============================================================================
49
50type CommerceHandle = Arc<Mutex<RustCommerce>>;
51
52fn create_handle(commerce: RustCommerce) -> *mut CommerceHandle {
53    let handle: CommerceHandle = Arc::new(Mutex::new(commerce));
54    Box::into_raw(Box::new(handle))
55}
56
57fn get_handle(ptr: *mut CommerceHandle) -> Option<CommerceHandle> {
58    if ptr.is_null() {
59        return None;
60    }
61    Some(unsafe { (*ptr).clone() })
62}
63
64fn use_handle<F, R>(ptr: *mut CommerceHandle, f: F) -> Result<R, String>
65where
66    F: FnOnce(&RustCommerce) -> Result<R, String>,
67{
68    let handle = get_handle(ptr).ok_or("Null handle")?;
69    let guard = handle.lock().map_err(|e| format!("Lock failed: {}", e))?;
70    f(&guard)
71}
72
73// =============================================================================
74// Helper Functions
75// =============================================================================
76
77fn cstr_to_string(s: *const c_char) -> Option<String> {
78    if s.is_null() {
79        return None;
80    }
81    unsafe { CStr::from_ptr(s).to_str().ok().map(|s| s.to_string()) }
82}
83
84fn string_to_cstr(s: String) -> *mut c_char {
85    match CString::new(s) {
86        Ok(cstr) => cstr.into_raw(),
87        Err(_) => ptr::null_mut(),
88    }
89}
90
91fn to_json_cstr<T: serde::Serialize>(value: &T) -> *mut c_char {
92    match serde_json::to_string(value) {
93        Ok(json) => string_to_cstr(json),
94        Err(_) => ptr::null_mut(),
95    }
96}
97
98// =============================================================================
99// Memory Management
100// =============================================================================
101
102/// Free a string allocated by Rust
103/// # Safety
104/// The pointer must have been allocated by this library
105#[unsafe(no_mangle)]
106pub unsafe extern "C" fn stateset_string_free(s: *mut c_char) {
107    if !s.is_null() {
108        drop(unsafe { CString::from_raw(s) });
109    }
110}
111
112// =============================================================================
113// Commerce Lifecycle
114// =============================================================================
115
116/// Create a new Commerce instance
117/// Returns a handle pointer, or null on error
118#[unsafe(no_mangle)]
119pub extern "C" fn stateset_commerce_new(db_path: *const c_char) -> *mut CommerceHandle {
120    let path = match cstr_to_string(db_path) {
121        Some(p) => p,
122        None => return ptr::null_mut(),
123    };
124
125    match RustCommerce::new(&path) {
126        Ok(commerce) => create_handle(commerce),
127        Err(_) => ptr::null_mut(),
128    }
129}
130
131/// Destroy a Commerce instance
132/// # Safety
133/// The handle must have been created by `stateset_commerce_new`
134#[unsafe(no_mangle)]
135pub unsafe extern "C" fn stateset_commerce_free(handle: *mut CommerceHandle) {
136    if !handle.is_null() {
137        drop(unsafe { Box::from_raw(handle) });
138    }
139}
140
141/// Get the last error message (returns null if no error)
142/// Caller must free the returned string
143#[unsafe(no_mangle)]
144pub const extern "C" fn stateset_get_last_error() -> *mut c_char {
145    // For now, return null - could implement thread-local error storage
146    ptr::null_mut()
147}
148
149// =============================================================================
150// Customers API
151// =============================================================================
152
153/// Create a customer
154/// Returns JSON string (caller must free with `stateset_string_free`)
155#[unsafe(no_mangle)]
156pub extern "C" fn stateset_customer_create(
157    handle: *mut CommerceHandle,
158    email: *const c_char,
159    first_name: *const c_char,
160    last_name: *const c_char,
161    phone: *const c_char,
162) -> *mut c_char {
163    let email_str = cstr_to_string(email).unwrap_or_default();
164    let first_name_str = cstr_to_string(first_name).unwrap_or_default();
165    let last_name_str = cstr_to_string(last_name).unwrap_or_default();
166    let phone_str = cstr_to_string(phone);
167
168    let result = use_handle(handle, |commerce| {
169        commerce
170            .customers()
171            .create(CreateCustomer {
172                email: email_str,
173                first_name: first_name_str,
174                last_name: last_name_str,
175                phone: phone_str,
176                ..Default::default()
177            })
178            .map_err(|e| e.to_string())
179    });
180
181    match result {
182        Ok(customer) => to_json_cstr(&customer),
183        Err(_) => ptr::null_mut(),
184    }
185}
186
187/// Get a customer by ID
188/// Returns JSON string (caller must free)
189#[unsafe(no_mangle)]
190pub extern "C" fn stateset_customer_get(
191    handle: *mut CommerceHandle,
192    id: *const c_char,
193) -> *mut c_char {
194    let id_str = match cstr_to_string(id) {
195        Some(s) => s,
196        None => return ptr::null_mut(),
197    };
198
199    let uuid = match uuid::Uuid::parse_str(&id_str) {
200        Ok(u) => u,
201        Err(_) => return ptr::null_mut(),
202    };
203
204    let result = use_handle(handle, |commerce| {
205        commerce.customers().get(uuid.into()).map_err(|e| e.to_string())
206    });
207
208    match result {
209        Ok(Some(customer)) => to_json_cstr(&customer),
210        _ => ptr::null_mut(),
211    }
212}
213
214/// List all customers
215/// Returns JSON array string (caller must free)
216#[unsafe(no_mangle)]
217pub extern "C" fn stateset_customer_list(handle: *mut CommerceHandle) -> *mut c_char {
218    let result = use_handle(handle, |commerce| {
219        commerce.customers().list(CustomerFilter::default()).map_err(|e| e.to_string())
220    });
221
222    match result {
223        Ok(customers) => to_json_cstr(&customers),
224        Err(_) => ptr::null_mut(),
225    }
226}
227
228/// Delete a customer by ID
229/// Returns 1 on success, 0 on failure
230#[unsafe(no_mangle)]
231pub extern "C" fn stateset_customer_delete(
232    handle: *mut CommerceHandle,
233    id: *const c_char,
234) -> c_int {
235    let id_str = match cstr_to_string(id) {
236        Some(s) => s,
237        None => return 0,
238    };
239
240    let uuid = match uuid::Uuid::parse_str(&id_str) {
241        Ok(u) => u,
242        Err(_) => return 0,
243    };
244
245    let result = use_handle(handle, |commerce| {
246        commerce.customers().delete(uuid.into()).map_err(|e| e.to_string())
247    });
248
249    match result {
250        Ok(_) => 1,
251        Err(_) => 0,
252    }
253}
254
255/// Count customers
256/// Returns count or -1 on error
257#[unsafe(no_mangle)]
258pub extern "C" fn stateset_customer_count(handle: *mut CommerceHandle) -> c_int {
259    let result = use_handle(handle, |commerce| {
260        commerce.customers().list(CustomerFilter::default()).map_err(|e| e.to_string())
261    });
262
263    match result {
264        Ok(customers) => customers.len() as c_int,
265        Err(_) => -1,
266    }
267}
268
269// =============================================================================
270// Products API
271// =============================================================================
272
273/// Create a product
274/// Returns JSON string (caller must free)
275#[unsafe(no_mangle)]
276pub extern "C" fn stateset_product_create(
277    handle: *mut CommerceHandle,
278    name: *const c_char,
279    sku: *const c_char,
280    price: c_double,
281    description: *const c_char,
282) -> *mut c_char {
283    let name_str = cstr_to_string(name).unwrap_or_default();
284    let sku_str = cstr_to_string(sku).unwrap_or_default();
285    let desc_str = cstr_to_string(description);
286    let price_decimal = Decimal::try_from(price).unwrap_or_default();
287
288    let result = use_handle(handle, |commerce| {
289        commerce
290            .products()
291            .create(CreateProduct {
292                name: name_str,
293                description: desc_str,
294                variants: Some(vec![CreateProductVariant {
295                    sku: sku_str,
296                    price: price_decimal,
297                    is_default: Some(true),
298                    ..Default::default()
299                }]),
300                ..Default::default()
301            })
302            .map_err(|e| e.to_string())
303    });
304
305    match result {
306        Ok(product) => to_json_cstr(&product),
307        Err(_) => ptr::null_mut(),
308    }
309}
310
311/// Get a product by ID
312/// Returns JSON string (caller must free)
313#[unsafe(no_mangle)]
314pub extern "C" fn stateset_product_get(
315    handle: *mut CommerceHandle,
316    id: *const c_char,
317) -> *mut c_char {
318    let id_str = match cstr_to_string(id) {
319        Some(s) => s,
320        None => return ptr::null_mut(),
321    };
322
323    let uuid = match uuid::Uuid::parse_str(&id_str) {
324        Ok(u) => u,
325        Err(_) => return ptr::null_mut(),
326    };
327
328    let result = use_handle(handle, |commerce| {
329        commerce.products().get(uuid.into()).map_err(|e| e.to_string())
330    });
331
332    match result {
333        Ok(Some(product)) => to_json_cstr(&product),
334        _ => ptr::null_mut(),
335    }
336}
337
338/// List all products
339/// Returns JSON array string (caller must free)
340#[unsafe(no_mangle)]
341pub extern "C" fn stateset_product_list(handle: *mut CommerceHandle) -> *mut c_char {
342    let result = use_handle(handle, |commerce| {
343        commerce.products().list(ProductFilter::default()).map_err(|e| e.to_string())
344    });
345
346    match result {
347        Ok(products) => to_json_cstr(&products),
348        Err(_) => ptr::null_mut(),
349    }
350}
351
352// =============================================================================
353// Orders API
354// =============================================================================
355
356/// Create an order
357/// `items_json` should be a JSON array of order items
358/// Returns JSON string (caller must free)
359#[unsafe(no_mangle)]
360pub extern "C" fn stateset_order_create(
361    handle: *mut CommerceHandle,
362    customer_id: *const c_char,
363    items_json: *const c_char,
364    currency: *const c_char,
365) -> *mut c_char {
366    let customer_id_str = match cstr_to_string(customer_id) {
367        Some(s) => s,
368        None => return ptr::null_mut(),
369    };
370    let items_str = cstr_to_string(items_json).unwrap_or_default();
371    let currency_str = cstr_to_string(currency).unwrap_or_else(|| "USD".to_string());
372
373    let customer_uuid = match uuid::Uuid::parse_str(&customer_id_str) {
374        Ok(u) => u,
375        Err(_) => return ptr::null_mut(),
376    };
377
378    let items: Vec<stateset_embedded::CreateOrderItem> = match serde_json::from_str(&items_str) {
379        Ok(i) => i,
380        Err(_) => return ptr::null_mut(),
381    };
382
383    let result = use_handle(handle, |commerce| {
384        commerce
385            .orders()
386            .create(CreateOrder {
387                customer_id: customer_uuid.into(),
388                items,
389                currency: Some(currency_str),
390                ..Default::default()
391            })
392            .map_err(|e| e.to_string())
393    });
394
395    match result {
396        Ok(order) => to_json_cstr(&order),
397        Err(_) => ptr::null_mut(),
398    }
399}
400
401/// Get an order by ID
402/// Returns JSON string (caller must free)
403#[unsafe(no_mangle)]
404pub extern "C" fn stateset_order_get(
405    handle: *mut CommerceHandle,
406    id: *const c_char,
407) -> *mut c_char {
408    let id_str = match cstr_to_string(id) {
409        Some(s) => s,
410        None => return ptr::null_mut(),
411    };
412
413    let uuid = match uuid::Uuid::parse_str(&id_str) {
414        Ok(u) => u,
415        Err(_) => return ptr::null_mut(),
416    };
417
418    let result = use_handle(handle, |commerce| {
419        commerce.orders().get(uuid.into()).map_err(|e| e.to_string())
420    });
421
422    match result {
423        Ok(Some(order)) => to_json_cstr(&order),
424        _ => ptr::null_mut(),
425    }
426}
427
428/// List all orders
429/// Returns JSON array string (caller must free)
430#[unsafe(no_mangle)]
431pub extern "C" fn stateset_order_list(handle: *mut CommerceHandle) -> *mut c_char {
432    let result = use_handle(handle, |commerce| {
433        commerce.orders().list(OrderFilter::default()).map_err(|e| e.to_string())
434    });
435
436    match result {
437        Ok(orders) => to_json_cstr(&orders),
438        Err(_) => ptr::null_mut(),
439    }
440}
441
442/// Update order status
443/// status: "pending", "confirmed", "processing", "shipped", "delivered", "cancelled", "refunded"
444/// Returns JSON string (caller must free)
445#[unsafe(no_mangle)]
446pub extern "C" fn stateset_order_update_status(
447    handle: *mut CommerceHandle,
448    id: *const c_char,
449    status: *const c_char,
450) -> *mut c_char {
451    let id_str = match cstr_to_string(id) {
452        Some(s) => s,
453        None => return ptr::null_mut(),
454    };
455    let status_str = cstr_to_string(status).unwrap_or_default();
456
457    let uuid = match uuid::Uuid::parse_str(&id_str) {
458        Ok(u) => u,
459        Err(_) => return ptr::null_mut(),
460    };
461
462    let order_status = match status_str.to_lowercase().as_str() {
463        "pending" => OrderStatus::Pending,
464        "confirmed" => OrderStatus::Confirmed,
465        "processing" => OrderStatus::Processing,
466        "shipped" => OrderStatus::Shipped,
467        "delivered" => OrderStatus::Delivered,
468        "cancelled" => OrderStatus::Cancelled,
469        "refunded" => OrderStatus::Refunded,
470        _ => return ptr::null_mut(),
471    };
472
473    let result = use_handle(handle, |commerce| {
474        commerce.orders().update_status(uuid.into(), order_status).map_err(|e| e.to_string())
475    });
476
477    match result {
478        Ok(order) => to_json_cstr(&order),
479        Err(_) => ptr::null_mut(),
480    }
481}
482
483// =============================================================================
484// Inventory API
485// =============================================================================
486
487/// Create an inventory item
488/// Returns JSON string (caller must free)
489#[unsafe(no_mangle)]
490pub extern "C" fn stateset_inventory_create_item(
491    handle: *mut CommerceHandle,
492    sku: *const c_char,
493    name: *const c_char,
494    initial_quantity: c_double,
495) -> *mut c_char {
496    let sku_str = cstr_to_string(sku).unwrap_or_default();
497    let name_str = cstr_to_string(name).unwrap_or_default();
498    let qty = Decimal::try_from(initial_quantity).ok();
499
500    let result = use_handle(handle, |commerce| {
501        commerce
502            .inventory()
503            .create_item(CreateInventoryItem {
504                sku: sku_str,
505                name: name_str,
506                initial_quantity: qty,
507                ..Default::default()
508            })
509            .map_err(|e| e.to_string())
510    });
511
512    match result {
513        Ok(item) => to_json_cstr(&item),
514        Err(_) => ptr::null_mut(),
515    }
516}
517
518/// Adjust inventory quantity
519/// Returns 1 on success, 0 on failure
520#[unsafe(no_mangle)]
521pub extern "C" fn stateset_inventory_adjust(
522    handle: *mut CommerceHandle,
523    sku: *const c_char,
524    quantity_delta: c_double,
525    reason: *const c_char,
526) -> c_int {
527    let sku_str = cstr_to_string(sku).unwrap_or_default();
528    let reason_str = cstr_to_string(reason).unwrap_or_else(|| "adjustment".to_string());
529    let delta = Decimal::try_from(quantity_delta).unwrap_or_default();
530
531    let result = use_handle(handle, |commerce| {
532        commerce.inventory().adjust(&sku_str, delta, &reason_str).map_err(|e| e.to_string())
533    });
534
535    match result {
536        Ok(_) => 1,
537        Err(_) => 0,
538    }
539}
540
541/// Get stock level for SKU
542/// Returns JSON string (caller must free)
543#[unsafe(no_mangle)]
544pub extern "C" fn stateset_inventory_get_level(
545    handle: *mut CommerceHandle,
546    sku: *const c_char,
547) -> *mut c_char {
548    let sku_str = cstr_to_string(sku).unwrap_or_default();
549
550    let result = use_handle(handle, |commerce| {
551        commerce.inventory().get_stock(&sku_str).map_err(|e| e.to_string())
552    });
553
554    match result {
555        Ok(Some(level)) => to_json_cstr(&level),
556        _ => ptr::null_mut(),
557    }
558}
559
560// =============================================================================
561// Carts API
562// =============================================================================
563
564/// Create a cart
565/// `customer_id` can be null for anonymous carts
566/// Returns JSON string (caller must free)
567#[unsafe(no_mangle)]
568pub extern "C" fn stateset_cart_create(
569    handle: *mut CommerceHandle,
570    customer_id: *const c_char,
571    currency: *const c_char,
572) -> *mut c_char {
573    let customer_id_str = cstr_to_string(customer_id);
574    let currency_str = cstr_to_string(currency);
575
576    let customer_uuid = customer_id_str
577        .and_then(|s| if s.is_empty() { None } else { uuid::Uuid::parse_str(&s).ok() });
578
579    let result = use_handle(handle, |commerce| {
580        commerce
581            .carts()
582            .create(CreateCart {
583                customer_id: customer_uuid.map(Into::into),
584                currency: currency_str,
585                ..Default::default()
586            })
587            .map_err(|e| e.to_string())
588    });
589
590    match result {
591        Ok(cart) => to_json_cstr(&cart),
592        Err(_) => ptr::null_mut(),
593    }
594}
595
596/// Add item to cart
597/// Returns JSON string (caller must free)
598#[unsafe(no_mangle)]
599pub extern "C" fn stateset_cart_add_item(
600    handle: *mut CommerceHandle,
601    cart_id: *const c_char,
602    variant_id: *const c_char,
603    quantity: c_int,
604) -> *mut c_char {
605    let cart_id_str = match cstr_to_string(cart_id) {
606        Some(s) => s,
607        None => return ptr::null_mut(),
608    };
609    let variant_id_str = match cstr_to_string(variant_id) {
610        Some(s) => s,
611        None => return ptr::null_mut(),
612    };
613
614    let cart_uuid = match uuid::Uuid::parse_str(&cart_id_str) {
615        Ok(u) => u,
616        Err(_) => return ptr::null_mut(),
617    };
618
619    let variant_uuid = match uuid::Uuid::parse_str(&variant_id_str) {
620        Ok(u) => u,
621        Err(_) => return ptr::null_mut(),
622    };
623
624    let result = use_handle(handle, |commerce| {
625        commerce
626            .carts()
627            .add_item(
628                cart_uuid.into(),
629                AddCartItem { variant_id: Some(variant_uuid), quantity, ..Default::default() },
630            )
631            .map_err(|e| e.to_string())
632    });
633
634    match result {
635        Ok(cart) => to_json_cstr(&cart),
636        Err(_) => ptr::null_mut(),
637    }
638}
639
640/// Get cart by ID
641/// Returns JSON string (caller must free)
642#[unsafe(no_mangle)]
643pub extern "C" fn stateset_cart_get(
644    handle: *mut CommerceHandle,
645    cart_id: *const c_char,
646) -> *mut c_char {
647    let cart_id_str = match cstr_to_string(cart_id) {
648        Some(s) => s,
649        None => return ptr::null_mut(),
650    };
651
652    let cart_uuid = match uuid::Uuid::parse_str(&cart_id_str) {
653        Ok(u) => u,
654        Err(_) => return ptr::null_mut(),
655    };
656
657    let result = use_handle(handle, |commerce| {
658        commerce.carts().get(cart_uuid.into()).map_err(|e| e.to_string())
659    });
660
661    match result {
662        Ok(Some(cart)) => to_json_cstr(&cart),
663        _ => ptr::null_mut(),
664    }
665}
666
667// =============================================================================
668// Returns API
669// =============================================================================
670
671/// Create a return
672/// reason: "defective", "`wrong_item`", "`not_as_described`", "`changed_mind`", "damaged", "other"
673/// Returns JSON string (caller must free)
674#[unsafe(no_mangle)]
675pub extern "C" fn stateset_return_create(
676    handle: *mut CommerceHandle,
677    order_id: *const c_char,
678    reason: *const c_char,
679    notes: *const c_char,
680) -> *mut c_char {
681    let order_id_str = match cstr_to_string(order_id) {
682        Some(s) => s,
683        None => return ptr::null_mut(),
684    };
685    let reason_str = cstr_to_string(reason).unwrap_or_default();
686    let notes_str = cstr_to_string(notes);
687
688    let order_uuid = match uuid::Uuid::parse_str(&order_id_str) {
689        Ok(u) => u,
690        Err(_) => return ptr::null_mut(),
691    };
692
693    let return_reason = match reason_str.to_lowercase().as_str() {
694        "defective" => ReturnReason::Defective,
695        "wrong_item" | "wrongitem" => ReturnReason::WrongItem,
696        "not_as_described" | "notasdescribed" => ReturnReason::NotAsDescribed,
697        "changed_mind" | "changedmind" => ReturnReason::ChangedMind,
698        "damaged" => ReturnReason::Damaged,
699        _ => ReturnReason::Other,
700    };
701
702    let result = use_handle(handle, |commerce| {
703        let order = commerce.orders().get(order_uuid.into()).map_err(|e| e.to_string())?;
704        let order = order.ok_or_else(|| format!("Order not found: {}", order_uuid))?;
705        let items: Vec<CreateReturnItem> = order
706            .items
707            .iter()
708            .map(|item| CreateReturnItem {
709                order_item_id: item.id,
710                quantity: item.quantity,
711                condition: None,
712            })
713            .collect();
714        if items.is_empty() {
715            return Err("Return must have at least one item".to_string());
716        }
717        commerce
718            .returns()
719            .create(CreateReturn {
720                order_id: order_uuid.into(),
721                reason: return_reason,
722                notes: notes_str,
723                items,
724                ..Default::default()
725            })
726            .map_err(|e| e.to_string())
727    });
728
729    match result {
730        Ok(ret) => to_json_cstr(&ret),
731        Err(_) => ptr::null_mut(),
732    }
733}
734
735/// List all returns
736/// Returns JSON array string (caller must free)
737#[unsafe(no_mangle)]
738pub extern "C" fn stateset_return_list(handle: *mut CommerceHandle) -> *mut c_char {
739    let result = use_handle(handle, |commerce| {
740        commerce.returns().list(Default::default()).map_err(|e| e.to_string())
741    });
742
743    match result {
744        Ok(returns) => to_json_cstr(&returns),
745        Err(_) => ptr::null_mut(),
746    }
747}
748
749// =============================================================================
750// Payments API
751// =============================================================================
752
753/// Create a payment
754/// method: "`credit_card`", "`debit_card`", "`bank_transfer`", "paypal", "`apple_pay`", "`google_pay`", "crypto"
755/// Returns JSON string (caller must free)
756#[unsafe(no_mangle)]
757pub extern "C" fn stateset_payment_create(
758    handle: *mut CommerceHandle,
759    order_id: *const c_char,
760    amount: c_double,
761    currency: *const c_char,
762    method: *const c_char,
763) -> *mut c_char {
764    let order_id_str = match cstr_to_string(order_id) {
765        Some(s) => s,
766        None => return ptr::null_mut(),
767    };
768    let currency_str = cstr_to_string(currency).unwrap_or_else(|| "USD".to_string());
769    let method_str = cstr_to_string(method).unwrap_or_default();
770    let amount_decimal = Decimal::try_from(amount).unwrap_or_default();
771
772    let order_uuid = match uuid::Uuid::parse_str(&order_id_str) {
773        Ok(u) => u,
774        Err(_) => return ptr::null_mut(),
775    };
776
777    let payment_method = match method_str.to_lowercase().as_str() {
778        "credit_card" | "creditcard" => PaymentMethodType::CreditCard,
779        "debit_card" | "debitcard" => PaymentMethodType::DebitCard,
780        "bank_transfer" | "banktransfer" => PaymentMethodType::BankTransfer,
781        "paypal" => PaymentMethodType::PayPal,
782        "apple_pay" | "applepay" => PaymentMethodType::ApplePay,
783        "google_pay" | "googlepay" => PaymentMethodType::GooglePay,
784        "crypto" => PaymentMethodType::Crypto,
785        _ => PaymentMethodType::Other,
786    };
787
788    let result = use_handle(handle, |commerce| {
789        commerce
790            .payments()
791            .create(CreatePayment {
792                order_id: Some(order_uuid.into()),
793                amount: amount_decimal,
794                currency: Some(currency_str),
795                payment_method,
796                ..Default::default()
797            })
798            .map_err(|e| e.to_string())
799    });
800
801    match result {
802        Ok(payment) => to_json_cstr(&payment),
803        Err(_) => ptr::null_mut(),
804    }
805}
806
807// =============================================================================
808// Analytics API
809// =============================================================================
810
811/// Get sales summary
812/// period: "today", "week", "month", "quarter", "year", "all"
813/// Returns JSON string (caller must free)
814#[unsafe(no_mangle)]
815pub extern "C" fn stateset_analytics_sales_summary(
816    handle: *mut CommerceHandle,
817    period: *const c_char,
818) -> *mut c_char {
819    let period_str = cstr_to_string(period).unwrap_or_else(|| "month".to_string());
820
821    let time_period = match period_str.to_lowercase().as_str() {
822        "today" => TimePeriod::Today,
823        "week" | "last_7_days" => TimePeriod::Last7Days,
824        "month" | "this_month" => TimePeriod::ThisMonth,
825        "quarter" | "this_quarter" => TimePeriod::ThisQuarter,
826        "year" | "this_year" => TimePeriod::ThisYear,
827        "all" | "all_time" => TimePeriod::AllTime,
828        _ => TimePeriod::ThisMonth,
829    };
830
831    let result = use_handle(handle, |commerce| {
832        commerce
833            .analytics()
834            .sales_summary(AnalyticsQuery { period: Some(time_period), ..Default::default() })
835            .map_err(|e| e.to_string())
836    });
837
838    match result {
839        Ok(summary) => to_json_cstr(&summary),
840        Err(_) => ptr::null_mut(),
841    }
842}
843
844/// Get top products
845/// Returns JSON array string (caller must free)
846#[unsafe(no_mangle)]
847pub extern "C" fn stateset_analytics_top_products(
848    handle: *mut CommerceHandle,
849    limit: c_int,
850) -> *mut c_char {
851    let result = use_handle(handle, |commerce| {
852        commerce
853            .analytics()
854            .top_products(AnalyticsQuery { limit: Some(limit as u32), ..Default::default() })
855            .map_err(|e| e.to_string())
856    });
857
858    match result {
859        Ok(products) => to_json_cstr(&products),
860        Err(_) => ptr::null_mut(),
861    }
862}
863
864/// Get top customers
865/// Returns JSON array string (caller must free)
866#[unsafe(no_mangle)]
867pub extern "C" fn stateset_analytics_top_customers(
868    handle: *mut CommerceHandle,
869    limit: c_int,
870) -> *mut c_char {
871    let result = use_handle(handle, |commerce| {
872        commerce
873            .analytics()
874            .top_customers(AnalyticsQuery { limit: Some(limit as u32), ..Default::default() })
875            .map_err(|e| e.to_string())
876    });
877
878    match result {
879        Ok(customers) => to_json_cstr(&customers),
880        Err(_) => ptr::null_mut(),
881    }
882}
883
884// =============================================================================
885// Shipments API
886// =============================================================================
887
888/// Create a shipment
889/// Returns JSON string (caller must free)
890#[unsafe(no_mangle)]
891pub extern "C" fn stateset_shipment_create(
892    handle: *mut CommerceHandle,
893    order_id: *const c_char,
894    recipient_name: *const c_char,
895    shipping_address: *const c_char,
896    carrier: *const c_char,
897) -> *mut c_char {
898    let order_id_str = match cstr_to_string(order_id) {
899        Some(s) => s,
900        None => return ptr::null_mut(),
901    };
902    let recipient_name_str = cstr_to_string(recipient_name).unwrap_or_default();
903    let shipping_address_str = cstr_to_string(shipping_address).unwrap_or_default();
904    let carrier_str = cstr_to_string(carrier);
905
906    let order_uuid = match uuid::Uuid::parse_str(&order_id_str) {
907        Ok(u) => u,
908        Err(_) => return ptr::null_mut(),
909    };
910
911    let shipping_carrier = carrier_str.map(|c| match c.to_lowercase().as_str() {
912        "ups" => ShippingCarrier::Ups,
913        "fedex" => ShippingCarrier::FedEx,
914        "usps" => ShippingCarrier::Usps,
915        "dhl" => ShippingCarrier::Dhl,
916        _ => ShippingCarrier::Other,
917    });
918
919    let result = use_handle(handle, |commerce| {
920        commerce
921            .shipments()
922            .create(CreateShipment {
923                order_id: order_uuid.into(),
924                recipient_name: recipient_name_str,
925                shipping_address: shipping_address_str,
926                carrier: shipping_carrier,
927                ..Default::default()
928            })
929            .map_err(|e| e.to_string())
930    });
931
932    match result {
933        Ok(shipment) => to_json_cstr(&shipment),
934        Err(_) => ptr::null_mut(),
935    }
936}
937
938/// Get a shipment by ID
939/// Returns JSON string (caller must free)
940#[unsafe(no_mangle)]
941pub extern "C" fn stateset_shipment_get(
942    handle: *mut CommerceHandle,
943    id: *const c_char,
944) -> *mut c_char {
945    let id_str = match cstr_to_string(id) {
946        Some(s) => s,
947        None => return ptr::null_mut(),
948    };
949
950    let uuid = match uuid::Uuid::parse_str(&id_str) {
951        Ok(u) => u,
952        Err(_) => return ptr::null_mut(),
953    };
954
955    let result = use_handle(handle, |commerce| {
956        commerce.shipments().get(uuid.into()).map_err(|e| e.to_string())
957    });
958
959    match result {
960        Ok(Some(shipment)) => to_json_cstr(&shipment),
961        _ => ptr::null_mut(),
962    }
963}
964
965/// List all shipments
966/// Returns JSON array string (caller must free)
967#[unsafe(no_mangle)]
968pub extern "C" fn stateset_shipment_list(handle: *mut CommerceHandle) -> *mut c_char {
969    let result = use_handle(handle, |commerce| {
970        commerce.shipments().list(Default::default()).map_err(|e| e.to_string())
971    });
972
973    match result {
974        Ok(shipments) => to_json_cstr(&shipments),
975        Err(_) => ptr::null_mut(),
976    }
977}
978
979/// Ship a shipment (hand off to carrier)
980/// Returns JSON string (caller must free)
981#[unsafe(no_mangle)]
982pub extern "C" fn stateset_shipment_ship(
983    handle: *mut CommerceHandle,
984    id: *const c_char,
985    tracking_number: *const c_char,
986) -> *mut c_char {
987    let id_str = match cstr_to_string(id) {
988        Some(s) => s,
989        None => return ptr::null_mut(),
990    };
991    let tracking = cstr_to_string(tracking_number);
992
993    let uuid = match uuid::Uuid::parse_str(&id_str) {
994        Ok(u) => u,
995        Err(_) => return ptr::null_mut(),
996    };
997
998    let result = use_handle(handle, |commerce| {
999        commerce.shipments().ship(uuid.into(), tracking).map_err(|e| e.to_string())
1000    });
1001
1002    match result {
1003        Ok(shipment) => to_json_cstr(&shipment),
1004        Err(_) => ptr::null_mut(),
1005    }
1006}
1007
1008/// Mark shipment as delivered
1009/// Returns JSON string (caller must free)
1010#[unsafe(no_mangle)]
1011pub extern "C" fn stateset_shipment_deliver(
1012    handle: *mut CommerceHandle,
1013    id: *const c_char,
1014) -> *mut c_char {
1015    let id_str = match cstr_to_string(id) {
1016        Some(s) => s,
1017        None => return ptr::null_mut(),
1018    };
1019
1020    let uuid = match uuid::Uuid::parse_str(&id_str) {
1021        Ok(u) => u,
1022        Err(_) => return ptr::null_mut(),
1023    };
1024
1025    let result = use_handle(handle, |commerce| {
1026        commerce.shipments().mark_delivered(uuid.into()).map_err(|e| e.to_string())
1027    });
1028
1029    match result {
1030        Ok(shipment) => to_json_cstr(&shipment),
1031        Err(_) => ptr::null_mut(),
1032    }
1033}
1034
1035/// Cancel a shipment
1036/// Returns JSON string (caller must free)
1037#[unsafe(no_mangle)]
1038pub extern "C" fn stateset_shipment_cancel(
1039    handle: *mut CommerceHandle,
1040    id: *const c_char,
1041) -> *mut c_char {
1042    let id_str = match cstr_to_string(id) {
1043        Some(s) => s,
1044        None => return ptr::null_mut(),
1045    };
1046
1047    let uuid = match uuid::Uuid::parse_str(&id_str) {
1048        Ok(u) => u,
1049        Err(_) => return ptr::null_mut(),
1050    };
1051
1052    let result = use_handle(handle, |commerce| {
1053        commerce.shipments().cancel(uuid.into()).map_err(|e| e.to_string())
1054    });
1055
1056    match result {
1057        Ok(shipment) => to_json_cstr(&shipment),
1058        Err(_) => ptr::null_mut(),
1059    }
1060}
1061
1062// =============================================================================
1063// Warehouse Module
1064// =============================================================================
1065
1066/// Create a warehouse
1067/// Returns JSON string (caller must free)
1068#[unsafe(no_mangle)]
1069pub extern "C" fn stateset_warehouse_create(
1070    handle: *mut CommerceHandle,
1071    code: *const c_char,
1072    name: *const c_char,
1073) -> *mut c_char {
1074    let code_str = match cstr_to_string(code) {
1075        Some(s) => s,
1076        None => return ptr::null_mut(),
1077    };
1078    let name_str = match cstr_to_string(name) {
1079        Some(s) => s,
1080        None => return ptr::null_mut(),
1081    };
1082
1083    let result = use_handle(handle, |commerce| {
1084        commerce
1085            .warehouse()
1086            .create_warehouse(CreateWarehouse {
1087                code: code_str,
1088                name: name_str,
1089                warehouse_type: WarehouseType::Distribution,
1090                ..Default::default()
1091            })
1092            .map_err(|e| e.to_string())
1093    });
1094
1095    match result {
1096        Ok(wh) => to_json_cstr(&wh),
1097        Err(_) => ptr::null_mut(),
1098    }
1099}
1100
1101/// List warehouses
1102/// Returns JSON array string (caller must free)
1103#[unsafe(no_mangle)]
1104pub extern "C" fn stateset_warehouse_list(handle: *mut CommerceHandle) -> *mut c_char {
1105    let result = use_handle(handle, |commerce| {
1106        commerce.warehouse().list_warehouses(Default::default()).map_err(|e| e.to_string())
1107    });
1108
1109    match result {
1110        Ok(list) => to_json_cstr(&list),
1111        Err(_) => ptr::null_mut(),
1112    }
1113}
1114
1115/// Create a warehouse location
1116/// Returns JSON string (caller must free)
1117#[unsafe(no_mangle)]
1118pub extern "C" fn stateset_warehouse_create_location(
1119    handle: *mut CommerceHandle,
1120    warehouse_id: *const c_char,
1121    zone: *const c_char,
1122    aisle: *const c_char,
1123    rack: *const c_char,
1124    bin: *const c_char,
1125) -> *mut c_char {
1126    let wh_id = match cstr_to_string(warehouse_id).and_then(|s| s.parse().ok()) {
1127        Some(id) => id,
1128        None => return ptr::null_mut(),
1129    };
1130
1131    let result = use_handle(handle, |commerce| {
1132        commerce
1133            .warehouse()
1134            .create_location(CreateLocation {
1135                warehouse_id: wh_id,
1136                location_type: LocationType::Pick,
1137                zone: cstr_to_string(zone),
1138                aisle: cstr_to_string(aisle),
1139                rack: cstr_to_string(rack),
1140                bin: cstr_to_string(bin),
1141                ..Default::default()
1142            })
1143            .map_err(|e| e.to_string())
1144    });
1145
1146    match result {
1147        Ok(loc) => to_json_cstr(&loc),
1148        Err(_) => ptr::null_mut(),
1149    }
1150}
1151
1152// =============================================================================
1153// Quality Module
1154// =============================================================================
1155
1156/// Create a quality inspection
1157/// Returns JSON string (caller must free)
1158#[unsafe(no_mangle)]
1159pub extern "C" fn stateset_quality_create_inspection(
1160    handle: *mut CommerceHandle,
1161    reference_type: *const c_char,
1162    reference_id: *const c_char,
1163    sku: *const c_char,
1164) -> *mut c_char {
1165    let ref_type = cstr_to_string(reference_type).unwrap_or_else(|| "purchase_order".to_string());
1166    let ref_id = match cstr_to_string(reference_id).and_then(|s| s.parse().ok()) {
1167        Some(id) => id,
1168        None => return ptr::null_mut(),
1169    };
1170    let sku_str = match cstr_to_string(sku) {
1171        Some(s) => s,
1172        None => return ptr::null_mut(),
1173    };
1174
1175    let result = use_handle(handle, |commerce| {
1176        commerce
1177            .quality()
1178            .create_inspection(CreateInspection {
1179                inspection_type: InspectionType::Incoming,
1180                reference_type: ref_type,
1181                reference_id: ref_id,
1182                inspector_id: None,
1183                scheduled_at: None,
1184                notes: None,
1185                items: vec![stateset_embedded::CreateInspectionItem {
1186                    sku: sku_str,
1187                    lot_number: None,
1188                    serial_number: None,
1189                    quantity_to_inspect: rust_decimal::Decimal::ONE,
1190                }],
1191            })
1192            .map_err(|e| e.to_string())
1193    });
1194
1195    match result {
1196        Ok(insp) => to_json_cstr(&insp),
1197        Err(_) => ptr::null_mut(),
1198    }
1199}
1200
1201// =============================================================================
1202// Lots Module
1203// =============================================================================
1204
1205/// Create a lot
1206/// Returns JSON string (caller must free)
1207#[unsafe(no_mangle)]
1208pub extern "C" fn stateset_lots_create(
1209    handle: *mut CommerceHandle,
1210    lot_number: *const c_char,
1211    sku: *const c_char,
1212    quantity: c_double,
1213) -> *mut c_char {
1214    let sku_str = match cstr_to_string(sku) {
1215        Some(s) => s,
1216        None => return ptr::null_mut(),
1217    };
1218
1219    let result = use_handle(handle, |commerce| {
1220        commerce
1221            .lots()
1222            .create(CreateLot {
1223                lot_number: cstr_to_string(lot_number),
1224                sku: sku_str,
1225                quantity: Decimal::try_from(quantity).unwrap_or_default(),
1226                ..Default::default()
1227            })
1228            .map_err(|e| e.to_string())
1229    });
1230
1231    match result {
1232        Ok(lot) => to_json_cstr(&lot),
1233        Err(_) => ptr::null_mut(),
1234    }
1235}
1236
1237/// List lots
1238/// Returns JSON array string (caller must free)
1239#[unsafe(no_mangle)]
1240pub extern "C" fn stateset_lots_list(handle: *mut CommerceHandle) -> *mut c_char {
1241    let result = use_handle(handle, |commerce| {
1242        commerce.lots().list(Default::default()).map_err(|e| e.to_string())
1243    });
1244
1245    match result {
1246        Ok(list) => to_json_cstr(&list),
1247        Err(_) => ptr::null_mut(),
1248    }
1249}
1250
1251// =============================================================================
1252// Serials Module
1253// =============================================================================
1254
1255/// Create a serial number
1256/// Returns JSON string (caller must free)
1257#[unsafe(no_mangle)]
1258pub extern "C" fn stateset_serials_create(
1259    handle: *mut CommerceHandle,
1260    serial: *const c_char,
1261    sku: *const c_char,
1262) -> *mut c_char {
1263    let sku_str = match cstr_to_string(sku) {
1264        Some(s) => s,
1265        None => return ptr::null_mut(),
1266    };
1267
1268    let result = use_handle(handle, |commerce| {
1269        commerce
1270            .serials()
1271            .create(CreateSerialNumber {
1272                serial: cstr_to_string(serial),
1273                sku: sku_str,
1274                ..Default::default()
1275            })
1276            .map_err(|e| e.to_string())
1277    });
1278
1279    match result {
1280        Ok(sn) => to_json_cstr(&sn),
1281        Err(_) => ptr::null_mut(),
1282    }
1283}
1284
1285/// List serial numbers
1286/// Returns JSON array string (caller must free)
1287#[unsafe(no_mangle)]
1288pub extern "C" fn stateset_serials_list(handle: *mut CommerceHandle) -> *mut c_char {
1289    let result = use_handle(handle, |commerce| {
1290        commerce.serials().list(Default::default()).map_err(|e| e.to_string())
1291    });
1292
1293    match result {
1294        Ok(list) => to_json_cstr(&list),
1295        Err(_) => ptr::null_mut(),
1296    }
1297}
1298
1299// =============================================================================
1300// Accounts Payable Module
1301// =============================================================================
1302
1303/// Create a bill
1304/// Returns JSON string (caller must free)
1305#[unsafe(no_mangle)]
1306pub extern "C" fn stateset_ap_create_bill(
1307    handle: *mut CommerceHandle,
1308    supplier_id: *const c_char,
1309    description: *const c_char,
1310    amount: c_double,
1311) -> *mut c_char {
1312    let sup_id = match cstr_to_string(supplier_id).and_then(|s| s.parse().ok()) {
1313        Some(id) => id,
1314        None => return ptr::null_mut(),
1315    };
1316
1317    let result = use_handle(handle, |commerce| {
1318        commerce
1319            .accounts_payable()
1320            .create_bill(CreateBill {
1321                supplier_id: sup_id,
1322                items: vec![CreateBillItem {
1323                    description: cstr_to_string(description).unwrap_or_default(),
1324                    quantity: Decimal::ONE,
1325                    unit_price: Decimal::try_from(amount).unwrap_or_default(),
1326                    ..Default::default()
1327                }],
1328                ..Default::default()
1329            })
1330            .map_err(|e| e.to_string())
1331    });
1332
1333    match result {
1334        Ok(bill) => to_json_cstr(&bill),
1335        Err(_) => ptr::null_mut(),
1336    }
1337}
1338
1339/// List bills
1340/// Returns JSON array string (caller must free)
1341#[unsafe(no_mangle)]
1342pub extern "C" fn stateset_ap_list_bills(handle: *mut CommerceHandle) -> *mut c_char {
1343    let result = use_handle(handle, |commerce| {
1344        commerce.accounts_payable().list_bills(Default::default()).map_err(|e| e.to_string())
1345    });
1346
1347    match result {
1348        Ok(list) => to_json_cstr(&list),
1349        Err(_) => ptr::null_mut(),
1350    }
1351}
1352
1353// =============================================================================
1354// Accounts Receivable Module
1355// =============================================================================
1356
1357/// Get AR aging summary
1358/// Returns JSON string (caller must free)
1359#[unsafe(no_mangle)]
1360pub extern "C" fn stateset_ar_aging_summary(handle: *mut CommerceHandle) -> *mut c_char {
1361    let result = use_handle(handle, |commerce| {
1362        commerce.accounts_receivable().get_aging_summary().map_err(|e| e.to_string())
1363    });
1364
1365    match result {
1366        Ok(summary) => to_json_cstr(&summary),
1367        Err(_) => ptr::null_mut(),
1368    }
1369}
1370
1371/// Get customer AR aging
1372/// Returns JSON string (caller must free)
1373#[unsafe(no_mangle)]
1374pub extern "C" fn stateset_ar_customer_aging(
1375    handle: *mut CommerceHandle,
1376    customer_id: *const c_char,
1377) -> *mut c_char {
1378    let cust_id = match cstr_to_string(customer_id).and_then(|s| s.parse().ok()) {
1379        Some(id) => id,
1380        None => return ptr::null_mut(),
1381    };
1382
1383    let result = use_handle(handle, |commerce| {
1384        commerce.accounts_receivable().get_customer_aging(cust_id).map_err(|e| e.to_string())
1385    });
1386
1387    match result {
1388        Ok(aging) => to_json_cstr(&aging),
1389        Err(_) => ptr::null_mut(),
1390    }
1391}
1392
1393// =============================================================================
1394// Cost Accounting Module
1395// =============================================================================
1396
1397/// Set item cost
1398/// Returns JSON string (caller must free)
1399#[unsafe(no_mangle)]
1400pub extern "C" fn stateset_cost_set_item_cost(
1401    handle: *mut CommerceHandle,
1402    sku: *const c_char,
1403    standard_cost: c_double,
1404) -> *mut c_char {
1405    let sku_str = match cstr_to_string(sku) {
1406        Some(s) => s,
1407        None => return ptr::null_mut(),
1408    };
1409
1410    let result = use_handle(handle, |commerce| {
1411        commerce
1412            .cost_accounting()
1413            .set_item_cost(SetItemCost {
1414                sku: sku_str,
1415                standard_cost: Some(Decimal::try_from(standard_cost).unwrap_or_default()),
1416                ..Default::default()
1417            })
1418            .map_err(|e| e.to_string())
1419    });
1420
1421    match result {
1422        Ok(cost) => to_json_cstr(&cost),
1423        Err(_) => ptr::null_mut(),
1424    }
1425}
1426
1427/// Get item cost
1428/// Returns JSON string (caller must free)
1429#[unsafe(no_mangle)]
1430pub extern "C" fn stateset_cost_get_item_cost(
1431    handle: *mut CommerceHandle,
1432    sku: *const c_char,
1433) -> *mut c_char {
1434    let sku_str = match cstr_to_string(sku) {
1435        Some(s) => s,
1436        None => return ptr::null_mut(),
1437    };
1438
1439    let result = use_handle(handle, |commerce| {
1440        commerce.cost_accounting().get_item_cost(&sku_str).map_err(|e| e.to_string())
1441    });
1442
1443    match result {
1444        Ok(Some(cost)) => to_json_cstr(&cost),
1445        _ => ptr::null_mut(),
1446    }
1447}
1448
1449// =============================================================================
1450// Credit Module
1451// =============================================================================
1452
1453/// Create a credit account
1454/// Returns JSON string (caller must free)
1455#[unsafe(no_mangle)]
1456pub extern "C" fn stateset_credit_create_account(
1457    handle: *mut CommerceHandle,
1458    customer_id: *const c_char,
1459    credit_limit: c_double,
1460) -> *mut c_char {
1461    let cust_id = match cstr_to_string(customer_id).and_then(|s| s.parse().ok()) {
1462        Some(id) => id,
1463        None => return ptr::null_mut(),
1464    };
1465
1466    let result = use_handle(handle, |commerce| {
1467        commerce
1468            .credit()
1469            .create_credit_account(CreateCreditAccount {
1470                customer_id: cust_id,
1471                credit_limit: Decimal::try_from(credit_limit).unwrap_or_default(),
1472                ..Default::default()
1473            })
1474            .map_err(|e| e.to_string())
1475    });
1476
1477    match result {
1478        Ok(acct) => to_json_cstr(&acct),
1479        Err(_) => ptr::null_mut(),
1480    }
1481}
1482
1483// =============================================================================
1484// Backorder Module
1485// =============================================================================
1486
1487/// Create a backorder
1488/// Returns JSON string (caller must free)
1489#[unsafe(no_mangle)]
1490pub extern "C" fn stateset_backorder_create(
1491    handle: *mut CommerceHandle,
1492    order_id: *const c_char,
1493    customer_id: *const c_char,
1494    sku: *const c_char,
1495    quantity: c_double,
1496) -> *mut c_char {
1497    let ord_id = match cstr_to_string(order_id).and_then(|s| s.parse().ok()) {
1498        Some(id) => id,
1499        None => return ptr::null_mut(),
1500    };
1501    let cust_id = match cstr_to_string(customer_id).and_then(|s| s.parse().ok()) {
1502        Some(id) => id,
1503        None => return ptr::null_mut(),
1504    };
1505    let sku_str = match cstr_to_string(sku) {
1506        Some(s) => s,
1507        None => return ptr::null_mut(),
1508    };
1509
1510    let result = use_handle(handle, |commerce| {
1511        commerce
1512            .backorder()
1513            .create_backorder(CreateBackorder {
1514                order_id: ord_id,
1515                customer_id: cust_id,
1516                sku: sku_str,
1517                quantity: Decimal::try_from(quantity).unwrap_or_default(),
1518                priority: Some(BackorderPriority::Normal),
1519                order_line_id: None,
1520                expected_date: None,
1521                promised_date: None,
1522                source_location_id: None,
1523                notes: None,
1524            })
1525            .map_err(|e| e.to_string())
1526    });
1527
1528    match result {
1529        Ok(bo) => to_json_cstr(&bo),
1530        Err(_) => ptr::null_mut(),
1531    }
1532}
1533
1534/// List backorders
1535/// Returns JSON array string (caller must free)
1536#[unsafe(no_mangle)]
1537pub extern "C" fn stateset_backorder_list(handle: *mut CommerceHandle) -> *mut c_char {
1538    let result = use_handle(handle, |commerce| {
1539        commerce.backorder().list_backorders(Default::default()).map_err(|e| e.to_string())
1540    });
1541
1542    match result {
1543        Ok(list) => to_json_cstr(&list),
1544        Err(_) => ptr::null_mut(),
1545    }
1546}
1547
1548// =============================================================================
1549// General Ledger Module
1550// =============================================================================
1551
1552/// Create a GL account
1553/// Returns JSON string (caller must free)
1554#[unsafe(no_mangle)]
1555pub extern "C" fn stateset_gl_create_account(
1556    handle: *mut CommerceHandle,
1557    account_number: *const c_char,
1558    name: *const c_char,
1559    account_type: *const c_char,
1560) -> *mut c_char {
1561    let num = match cstr_to_string(account_number) {
1562        Some(s) => s,
1563        None => return ptr::null_mut(),
1564    };
1565    let name_str = match cstr_to_string(name) {
1566        Some(s) => s,
1567        None => return ptr::null_mut(),
1568    };
1569    let acct_type = match cstr_to_string(account_type).as_deref() {
1570        Some("asset") => AccountType::Asset,
1571        Some("liability") => AccountType::Liability,
1572        Some("equity") => AccountType::Equity,
1573        Some("revenue") => AccountType::Revenue,
1574        Some("expense") => AccountType::Expense,
1575        _ => AccountType::Asset,
1576    };
1577
1578    let result = use_handle(handle, |commerce| {
1579        commerce
1580            .general_ledger()
1581            .create_account(CreateGlAccount {
1582                account_number: num,
1583                name: name_str,
1584                account_type: acct_type,
1585                description: None,
1586                account_sub_type: None,
1587                parent_account_id: None,
1588                is_header: None,
1589                is_posting: Some(true),
1590                currency: None,
1591            })
1592            .map_err(|e| e.to_string())
1593    });
1594
1595    match result {
1596        Ok(acct) => to_json_cstr(&acct),
1597        Err(_) => ptr::null_mut(),
1598    }
1599}
1600
1601/// List GL accounts
1602/// Returns JSON array string (caller must free)
1603#[unsafe(no_mangle)]
1604pub extern "C" fn stateset_gl_list_accounts(handle: *mut CommerceHandle) -> *mut c_char {
1605    let result = use_handle(handle, |commerce| {
1606        commerce.general_ledger().list_accounts(Default::default()).map_err(|e| e.to_string())
1607    });
1608
1609    match result {
1610        Ok(list) => to_json_cstr(&list),
1611        Err(_) => ptr::null_mut(),
1612    }
1613}