@thorprovider/adapters 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/.turbo/turbo-build.log +18 -0
  2. package/.turbo/turbo-test.log +1 -0
  3. package/.turbo/turbo-type-check.log +0 -0
  4. package/AGENTS.md +1 -0
  5. package/CHANGELOG.md +271 -0
  6. package/README.md +634 -0
  7. package/dist/.tsbuildinfo +1 -0
  8. package/dist/index.d.mts +2524 -0
  9. package/dist/index.d.ts +2524 -0
  10. package/dist/index.js +8136 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/index.mjs +8045 -0
  13. package/dist/index.mjs.map +1 -0
  14. package/docs/RETRY_SOLUTION_SUMMARY.md +55 -0
  15. package/docs/authentication.md +165 -0
  16. package/docs/capabilities-system.md +143 -0
  17. package/docs/category-customization.md +309 -0
  18. package/docs/fetch-reliability-solution.md +399 -0
  19. package/docs/getting-started.md +154 -0
  20. package/docs/multi-tenant-channel-scoping.md +225 -0
  21. package/docs/profile-management.md +147 -0
  22. package/docs/providers-overview.md +85 -0
  23. package/docs/services-overview.md +135 -0
  24. package/docs/update-documentation.md +113 -0
  25. package/package.json +43 -0
  26. package/publish.log +93 -0
  27. package/quick-reference.md +169 -0
  28. package/src/admin/AdminAuthService.ts +88 -0
  29. package/src/admin/AdminSessionService.ts +91 -0
  30. package/src/admin/MetricsService.ts +145 -0
  31. package/src/admin/index.ts +13 -0
  32. package/src/backend/BackendMetadataAdapter.ts +88 -0
  33. package/src/backend/factory.ts +107 -0
  34. package/src/backend/index.ts +12 -0
  35. package/src/backend/medusa/MedusaMetadataAdapter.ts +139 -0
  36. package/src/core/errors.js +161 -0
  37. package/src/core/errors.ts +132 -0
  38. package/src/core/fetch-utils.js +240 -0
  39. package/src/core/fetch-utils.ts +227 -0
  40. package/src/core/index.ts +8 -0
  41. package/src/core/logger.js +63 -0
  42. package/src/core/logger.ts +37 -0
  43. package/src/factory/index.ts +102 -0
  44. package/src/index.ts +205 -0
  45. package/src/lib/money-transform.ts +80 -0
  46. package/src/metadata/channel-scope-validation.js +136 -0
  47. package/src/metadata/channel-scope-validation.ts +151 -0
  48. package/src/metadata/channel-scope.js +174 -0
  49. package/src/metadata/channel-scope.ts +215 -0
  50. package/src/metadata/index.js +31 -0
  51. package/src/metadata/index.ts +37 -0
  52. package/src/providers/bigcommerce/bigcommerce.ts +6 -0
  53. package/src/providers/bigcommerce/index.ts +315 -0
  54. package/src/providers/bigcommerce/transforms.ts +227 -0
  55. package/src/providers/index.ts +22 -0
  56. package/src/providers/magento/index.ts +387 -0
  57. package/src/providers/magento/magento.ts +5 -0
  58. package/src/providers/magento/queries.ts +267 -0
  59. package/src/providers/magento/transforms.ts +249 -0
  60. package/src/providers/medusa/auth.js +601 -0
  61. package/src/providers/medusa/auth.ts +496 -0
  62. package/src/providers/medusa/fulfillment-planner.ts +225 -0
  63. package/src/providers/medusa/index.js +2485 -0
  64. package/src/providers/medusa/index.ts +4239 -0
  65. package/src/providers/medusa/medusa.ts +8 -0
  66. package/src/providers/medusa/payment.js +265 -0
  67. package/src/providers/medusa/payment.ts +269 -0
  68. package/src/providers/medusa/transforms.js +505 -0
  69. package/src/providers/medusa/transforms.ts +625 -0
  70. package/src/providers/shopify/index.ts +444 -0
  71. package/src/providers/shopify/mutations.ts +102 -0
  72. package/src/providers/shopify/queries.ts +324 -0
  73. package/src/providers/shopify/shopify.ts +7 -0
  74. package/src/providers/shopify/transforms.ts +270 -0
  75. package/src/providers/spree/index.ts +344 -0
  76. package/src/providers/spree/spree.ts +5 -0
  77. package/src/providers/spree/transforms.ts +254 -0
  78. package/src/providers/woocommerce/index.ts +310 -0
  79. package/src/providers/woocommerce/transforms.ts +254 -0
  80. package/src/providers/woocommerce/woocommerce.ts +5 -0
  81. package/src/resilience/README.md +299 -0
  82. package/src/resilience/circuit-breaker.ts +222 -0
  83. package/src/resilience/index.ts +10 -0
  84. package/src/types/capabilities.types.js +141 -0
  85. package/src/types/capabilities.types.ts +366 -0
  86. package/src/types/index.ts +24 -0
  87. package/src/types/provider.js +9 -0
  88. package/src/types/provider.ts +9 -0
  89. package/src/types/ui-responses.ts +261 -0
  90. package/src/utils/city-coordinates.ts +40 -0
  91. package/src/utils/geolocation.ts +52 -0
  92. package/src/utils/payment-utils.js +152 -0
  93. package/src/utils/payment-utils.ts +203 -0
  94. package/tsconfig.json +15 -0
  95. package/tsup.config.ts +14 -0
@@ -0,0 +1,625 @@
1
+ /**
2
+ * @thorprovider/adapters v2.0
3
+ * Medusa Provider - Transform functions
4
+ *
5
+ * These functions convert Medusa-specific types to our unified types
6
+ */
7
+
8
+ // Note: We use 'any' for Medusa types because the SDK returns different types
9
+ // than what's in @medusajs/types (StoreProduct vs ProductDTO)
10
+
11
+ import type {
12
+ Product,
13
+ ProductVariant,
14
+ ProductOption,
15
+ Cart,
16
+ CartItem,
17
+ Collection,
18
+ ProductCategory,
19
+ Money,
20
+ Image,
21
+ StockLocation,
22
+ InventoryLevel,
23
+ FulfillmentOption,
24
+ Order,
25
+ OrderItem,
26
+ OrderStatus,
27
+ PaymentStatus,
28
+ FulfillmentStatus,
29
+ Address,
30
+ Customer,
31
+ StorefrontConfig,
32
+ } from '@thorprovider/types';
33
+
34
+ /**
35
+ * Transform Medusa Money to unified Money
36
+ *
37
+ * Medusa v2 stores and returns monetary amounts in MAJOR units (dollars, euros, etc.).
38
+ * Reference: https://docs.medusajs.com/resources/commerce-modules/pricing/concepts
39
+ * "The Price data model has an `amount` property...It's stored as an integer in major units.
40
+ * For example, $20.00 is stored as 20, and $20.5 is stored as 20.5."
41
+ *
42
+ * Money.amount in @thorprovider/types is a decimal string in major units (e.g., "45.00").
43
+ * No division by 100 is needed — the value comes through as-is.
44
+ */
45
+ function transformMoney(amount: number | null | undefined, currencyCode: string = 'USD'): Money {
46
+ return {
47
+ amount: (amount ?? 0).toFixed(2), // Medusa v2 sends major units (dollars/euros), not cents
48
+ currencyCode: currencyCode.toUpperCase(),
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Transform Medusa image to unified Image
54
+ */
55
+ function transformImage(url: string, alt: string = ''): Image {
56
+ return {
57
+ url,
58
+ altText: alt,
59
+ width: 800,
60
+ height: 800,
61
+ };
62
+ }
63
+
64
+ // ============================================
65
+ // Inventory & Fulfillment Transforms
66
+ // ============================================
67
+
68
+ export function transformStockLocation(raw: any): StockLocation {
69
+ return {
70
+ id: raw.id,
71
+ name: raw.name,
72
+ address: raw.address
73
+ ? {
74
+ address1: raw.address.address_1,
75
+ address2: raw.address.address_2,
76
+ city: raw.address.city,
77
+ countryCode: raw.address.country_code,
78
+ postalCode: raw.address.postal_code,
79
+ province: raw.address.province,
80
+ }
81
+ : undefined,
82
+ metadata: raw.metadata,
83
+ };
84
+ }
85
+
86
+ export function transformInventoryLevel(raw: any, locationName: string): InventoryLevel {
87
+ return {
88
+ locationId: raw.location_id,
89
+ locationName,
90
+ availableQuantity: raw.available_quantity ?? 0,
91
+ reservedQuantity: raw.reserved_quantity ?? 0,
92
+ incomingQuantity: raw.incoming_quantity,
93
+ stockedQuantity: raw.stocked_quantity ?? 0,
94
+ };
95
+ }
96
+
97
+ export function transformFulfillmentOption(
98
+ shippingOption: any,
99
+ location: StockLocation,
100
+ currencyCode: string,
101
+ ): FulfillmentOption {
102
+ return {
103
+ id: shippingOption.id,
104
+ providerId: shippingOption.provider_id,
105
+ name: shippingOption.name,
106
+ description: shippingOption.description,
107
+ price: transformMoney(shippingOption.amount, currencyCode),
108
+ estimatedDays: shippingOption.metadata?.estimated_days,
109
+ stockLocation: location,
110
+ };
111
+ }
112
+
113
+ /**
114
+ * Transform Medusa ProductOption to unified ProductOption
115
+ */
116
+ export function transformProductOption(option: any): ProductOption {
117
+ return {
118
+ id: option.id!,
119
+ name: option.title ?? '',
120
+ values: option.values?.map((v: any) => v.value ?? '') ?? [],
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Transform Medusa ProductVariant to unified ProductVariant
126
+ */
127
+ export function transformProductVariant(
128
+ variant: any,
129
+ currencyCode: string = 'USD'
130
+ ): ProductVariant {
131
+ let priceAmount: number | null | undefined;
132
+ let priceCurrency: string = currencyCode;
133
+
134
+ // Priority 1: Use calculated_price if available (from region-based pricing)
135
+ if (variant.calculated_price) {
136
+ priceAmount = variant.calculated_price.calculated_amount;
137
+ priceCurrency = variant.calculated_price.currency_code || currencyCode;
138
+ }
139
+ // Priority 2: Fallback to prices array
140
+ else if (variant.prices && variant.prices.length > 0) {
141
+ const price = variant.prices.find((p: any) => p.currency_code === currencyCode.toLowerCase());
142
+ priceAmount = price?.amount;
143
+ priceCurrency = price?.currency_code || currencyCode;
144
+ }
145
+
146
+ // Determine if variant is available for sale
147
+ // Following Medusa docs: https://docs.medusajs.com/resources/storefront-development/products/inventory
148
+ // A variant is in stock if:
149
+ // 1. manage_inventory is false (inventory not tracked) - always in stock
150
+ // 2. inventory_quantity > 0 (has stock)
151
+ // 3. allow_backorder is true (can order even without stock)
152
+ const isAvailable = variant.manage_inventory === false
153
+ ? true
154
+ : (variant.inventory_quantity ?? 0) > 0 || variant.allow_backorder === true;
155
+
156
+ return {
157
+ id: variant.id!,
158
+ title: variant.title ?? 'Default',
159
+ sku: variant.sku ?? undefined,
160
+ availableForSale: isAvailable,
161
+ selectedOptions: variant.options?.map((opt: any) => ({
162
+ name: (opt as any).option?.title ?? '',
163
+ value: opt.value ?? '',
164
+ })) ?? [],
165
+ price: transformMoney(priceAmount, priceCurrency),
166
+ compareAtPrice: undefined, // Medusa doesn't have compare-at pricing
167
+ quantityAvailable: variant.manage_inventory ? variant.inventory_quantity : undefined,
168
+ image: variant.thumbnail
169
+ ? transformImage(variant.thumbnail, variant.title ?? '')
170
+ : undefined,
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Transform Medusa Product to unified Product
176
+ */
177
+ export function transformProduct(
178
+ product: any,
179
+ currencyCode: string = 'USD'
180
+ ): Product {
181
+ const variants = product.variants?.map((v: any) => transformProductVariant(v, currencyCode)) ?? [];
182
+
183
+ // Calculate price range
184
+ const prices = variants.map((v: any) => parseFloat(v.price.amount));
185
+ const minPrice = Math.min(...prices);
186
+ const maxPrice = Math.max(...prices);
187
+
188
+ return {
189
+ id: product.id!,
190
+ handle: product.handle ?? '',
191
+ title: product.title ?? '',
192
+ description: product.description ?? '',
193
+ descriptionHtml: product.description ?? '',
194
+ // Medusa v2 /store/products API doesn't return 'status' field (it's null)
195
+ // Products returned by Store API are already published, so we check if any variant is available
196
+ availableForSale: variants.length > 0 && variants.some((v: ProductVariant) => v.availableForSale),
197
+
198
+ options: product.options?.map(transformProductOption) ?? [],
199
+ variants,
200
+
201
+ featuredImage: product.thumbnail
202
+ ? transformImage(product.thumbnail, product.title ?? '')
203
+ : undefined,
204
+
205
+ images: product.images?.map((img: any) => transformImage(img.url ?? '', product.title ?? '')) ?? [],
206
+
207
+ priceRange: {
208
+ // Build Money directly from already-resolved major-unit prices.
209
+ // Do NOT pass through transformMoney (which would call toFixed on already-correct values)
210
+ // and do NOT multiply by 100 (that was a compensatory hack for the old /100 bug).
211
+ minVariantPrice: { amount: (Number.isFinite(minPrice) ? minPrice : 0).toFixed(2), currencyCode: currencyCode.toUpperCase() },
212
+ maxVariantPrice: { amount: (Number.isFinite(maxPrice) ? maxPrice : 0).toFixed(2), currencyCode: currencyCode.toUpperCase() },
213
+ },
214
+
215
+ collections: product.collection ? [{
216
+ id: product.collection.id,
217
+ title: product.collection.title ?? '',
218
+ handle: product.collection.handle ?? '',
219
+ }] : undefined,
220
+
221
+ productType: product.type?.value ?? undefined,
222
+ vendor: undefined, // Medusa doesn't have vendor field
223
+ tags: product.tags?.map((t: any) => t.value ?? '') ?? [],
224
+
225
+ seo: {
226
+ title: product.title ?? '',
227
+ description: product.description ?? '',
228
+ },
229
+
230
+ createdAt: typeof product.created_at === 'string'
231
+ ? product.created_at
232
+ : product.created_at?.toISOString() ?? new Date().toISOString(),
233
+ updatedAt: typeof product.updated_at === 'string'
234
+ ? product.updated_at
235
+ : product.updated_at?.toISOString() ?? new Date().toISOString(),
236
+
237
+ metadata: product.metadata ?? undefined,
238
+ };
239
+ }
240
+
241
+ /**
242
+ * Transform Medusa LineItem to unified CartItem
243
+ */
244
+ function resolveMedusaCartItemCurrencyCode(item: any, cartCurrencyCode?: string): string {
245
+ return (
246
+ item?.cart?.currency_code ??
247
+ cartCurrencyCode ??
248
+ 'usd'
249
+ );
250
+ }
251
+
252
+ /**
253
+ * Resolve the total amount for a Medusa cart item.
254
+ * All amounts from Medusa v2 are in major units (dollars, euros).
255
+ */
256
+ function resolveMedusaCartItemTotal(item: any): number {
257
+ const quantity = item?.quantity ?? 1;
258
+ const safeQuantity = typeof quantity === 'number' && quantity > 0 ? quantity : 1;
259
+
260
+ const totalCandidates = [
261
+ item?.total,
262
+ item?.item_total,
263
+ item?.original_total,
264
+ item?.subtotal,
265
+ item?.item_subtotal,
266
+ item?.original_subtotal,
267
+ ];
268
+
269
+ for (const candidate of totalCandidates) {
270
+ if (typeof candidate === 'number' && Number.isFinite(candidate) && candidate > 0) {
271
+ return candidate;
272
+ }
273
+ }
274
+
275
+ const unitCandidates = [
276
+ item?.unit_price,
277
+ item?.original_unit_price,
278
+ ];
279
+
280
+ for (const candidate of unitCandidates) {
281
+ if (typeof candidate === 'number' && Number.isFinite(candidate) && candidate > 0) {
282
+ return candidate * safeQuantity;
283
+ }
284
+ }
285
+
286
+ return 0;
287
+ }
288
+
289
+ export function transformCartItem(item: any, cartCurrencyCode?: string): CartItem {
290
+ const variant = item.variant;
291
+ const product = variant?.product;
292
+ const currencyCode = resolveMedusaCartItemCurrencyCode(item, cartCurrencyCode);
293
+ const totalCents = resolveMedusaCartItemTotal(item);
294
+
295
+ return {
296
+ id: item.id,
297
+ quantity: item.quantity ?? 1,
298
+ cost: {
299
+ totalAmount: transformMoney(totalCents, currencyCode),
300
+ },
301
+ merchandise: {
302
+ id: variant?.id ?? '',
303
+ title: variant?.title ?? '',
304
+ selectedOptions: variant?.options?.map((opt: any) => ({
305
+ name: (opt as any).option?.title ?? '',
306
+ value: opt.value ?? '',
307
+ })) ?? [],
308
+ product: {
309
+ id: product?.id ?? '',
310
+ handle: product?.handle ?? '',
311
+ title: product?.title ?? '',
312
+ featuredImage: product?.thumbnail
313
+ ? transformImage(product.thumbnail, product.title ?? '')
314
+ : transformImage('', ''),
315
+ },
316
+ },
317
+ };
318
+ }
319
+
320
+ /**
321
+ * Transform Medusa Cart to unified Cart
322
+ */
323
+ export function transformCart(cart: any): Cart {
324
+ const currencyCode = cart.currency_code ?? 'usd';
325
+
326
+ return {
327
+ id: cart.id,
328
+ checkoutUrl: `/checkout/${cart.id}`, // Medusa doesn't have a direct checkout URL
329
+ cost: {
330
+ subtotalAmount: transformMoney(cart.subtotal, currencyCode),
331
+ totalAmount: transformMoney(cart.total, currencyCode),
332
+ totalTaxAmount: transformMoney(cart.tax_total, currencyCode),
333
+ },
334
+ lines: cart.items?.map((item: any) => transformCartItem(item, currencyCode)) ?? [],
335
+ totalQuantity: cart.items?.reduce((sum: any, item: any) => sum + (item.quantity ?? 0), 0) ?? 0,
336
+ completedAt: cart.completed_at
337
+ ? (typeof cart.completed_at === 'string'
338
+ ? cart.completed_at
339
+ : cart.completed_at?.toISOString())
340
+ : undefined,
341
+ };
342
+ }
343
+
344
+ /**
345
+ * Transform Medusa Collection to unified Collection
346
+ */
347
+ export function transformCollection(collection: any): Collection {
348
+ return {
349
+ id: collection.id!,
350
+ handle: collection.handle!,
351
+ title: collection.title ?? '',
352
+ description: collection.metadata?.description as string ?? '',
353
+ seo: {
354
+ title: collection.title ?? '',
355
+ description: collection.metadata?.description as string ?? '',
356
+ },
357
+ path: `/collections/${collection.handle}`,
358
+ updatedAt: typeof collection.updated_at === 'string'
359
+ ? collection.updated_at
360
+ : collection.updated_at?.toISOString() ?? new Date().toISOString(),
361
+ };
362
+ }
363
+
364
+ /**
365
+ * Transform Medusa ProductCategory to unified ProductCategory
366
+ */
367
+ export function transformCategory(category: any): ProductCategory {
368
+ return {
369
+ id: category.id,
370
+ name: category.name ?? '',
371
+ handle: category.handle ?? '',
372
+ description: category.description ?? undefined,
373
+ parentCategoryId: category.parent_category_id ?? undefined,
374
+ productCount: category.product_count ?? undefined,
375
+ image: category.metadata?.image as string ?? undefined,
376
+ metadata: category.metadata ?? undefined,
377
+ };
378
+ }
379
+
380
+ /**
381
+ * Transform Medusa order status to unified OrderStatus
382
+ */
383
+ function transformOrderStatus(status: string): OrderStatus {
384
+ // Medusa order statuses: pending, completed, archived, canceled, requires_action, draft
385
+ switch (status) {
386
+ case 'pending':
387
+ case 'requires_action':
388
+ case 'draft':
389
+ return 'pending';
390
+ case 'completed':
391
+ return 'completed';
392
+ case 'canceled':
393
+ return 'cancelled';
394
+ case 'archived':
395
+ // Archived could be completed or refunded, default to completed
396
+ return 'completed';
397
+ default:
398
+ return 'processing';
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Transform Medusa payment status to unified PaymentStatus
404
+ */
405
+ function transformPaymentStatus(status: string | undefined): PaymentStatus {
406
+ // Medusa payment statuses: not_paid, awaiting, captured, partially_refunded, refunded, canceled, requires_action
407
+ if (!status || status === 'not_paid' || status === 'requires_action') {
408
+ return 'pending';
409
+ }
410
+
411
+ if (status === 'awaiting') {
412
+ return 'authorized';
413
+ }
414
+
415
+ if (status === 'captured') {
416
+ return 'paid';
417
+ }
418
+
419
+ if (status === 'partially_refunded') {
420
+ return 'partially_refunded';
421
+ }
422
+
423
+ if (status === 'refunded') {
424
+ return 'refunded';
425
+ }
426
+
427
+ return 'pending';
428
+ }
429
+
430
+ /**
431
+ * Transform Medusa fulfillment status to unified FulfillmentStatus
432
+ */
433
+ function transformFulfillmentStatus(status: string | undefined): FulfillmentStatus {
434
+ // Medusa fulfillment statuses: not_fulfilled, partially_fulfilled, fulfilled,
435
+ // partially_shipped, shipped, partially_returned, returned, canceled, requires_action
436
+ if (!status || status === 'not_fulfilled' || status === 'canceled') {
437
+ return 'unfulfilled';
438
+ }
439
+
440
+ if (status === 'partially_fulfilled' || status === 'partially_shipped') {
441
+ return 'partially_fulfilled';
442
+ }
443
+
444
+ if (status === 'fulfilled' || status === 'shipped') {
445
+ return 'fulfilled';
446
+ }
447
+
448
+ if (status === 'returned' || status === 'partially_returned') {
449
+ return 'returned';
450
+ }
451
+
452
+ return 'unfulfilled';
453
+ }
454
+
455
+ /**
456
+ * Transform Medusa LineItem to unified OrderItem
457
+ */
458
+ export function transformOrderItem(item: any, currencyCode: string): OrderItem {
459
+ // Defensive checks for null/undefined item or variant
460
+ if (!item) {
461
+ throw new Error('Invalid order item: item is null or undefined');
462
+ }
463
+
464
+ const variant = item.variant ?? null;
465
+ const product = variant?.product;
466
+
467
+ return {
468
+ id: item.id,
469
+ title: item.product_title ?? item.title ?? '',
470
+ variantTitle: item.variant_title ?? variant?.title ?? undefined,
471
+ quantity: item.quantity ?? 1,
472
+ price: transformMoney(item.unit_price, currencyCode),
473
+ total: transformMoney(item.total, currencyCode),
474
+ sku: variant?.sku ?? undefined,
475
+ image: product?.thumbnail || item.thumbnail
476
+ ? transformImage(product?.thumbnail || item.thumbnail, item.product_title ?? '')
477
+ : undefined,
478
+ productId: item.product_id ?? product?.id ?? undefined,
479
+ variantId: item.variant_id ?? variant?.id ?? undefined,
480
+ };
481
+ }
482
+
483
+ /**
484
+ * Transform Medusa Address (snake_case) to unified Address (camelCase)
485
+ */
486
+ export function transformAddress(address: any): Address {
487
+ return {
488
+ id: address.id ?? undefined,
489
+ firstName: address.first_name ?? '',
490
+ lastName: address.last_name ?? '',
491
+ company: address.company ?? undefined,
492
+ address1: address.address_1 ?? '',
493
+ address2: address.address_2 ?? undefined,
494
+ city: address.city ?? '',
495
+ province: address.province ?? '',
496
+ postalCode: address.postal_code ?? '',
497
+ countryCode: address.country_code ?? '',
498
+ phone: address.phone ?? undefined,
499
+ isDefault: address.is_default ?? address.is_default_shipping ?? undefined,
500
+ };
501
+ }
502
+
503
+ /**
504
+ * Transform Medusa Customer (snake_case) to unified Customer (camelCase)
505
+ */
506
+ export function transformCustomer(customer: any, email?: string): Customer {
507
+ return {
508
+ id: customer.id,
509
+ email: customer.email ?? email ?? '',
510
+ firstName: customer.first_name ?? '',
511
+ lastName: customer.last_name ?? '',
512
+ phone: customer.phone ?? undefined,
513
+ hasAccount: customer.has_account ?? false,
514
+ createdAt: typeof customer.created_at === 'string'
515
+ ? customer.created_at
516
+ : customer.created_at?.toISOString() ?? undefined,
517
+ updatedAt: typeof customer.updated_at === 'string'
518
+ ? customer.updated_at
519
+ : customer.updated_at?.toISOString() ?? undefined,
520
+ metadata: customer.metadata ?? undefined,
521
+ };
522
+ }
523
+
524
+ /**
525
+ * Transform Medusa Order to unified Order
526
+ */
527
+ export function transformOrder(order: any): Order {
528
+ const currencyCode = order.currency_code ?? 'usd';
529
+
530
+ return {
531
+ id: order.id,
532
+ orderNumber: order.display_id?.toString() ?? order.id,
533
+ email: order.email ?? '',
534
+
535
+ // Status (3 separate statuses)
536
+ status: transformOrderStatus(order.status),
537
+ paymentStatus: transformPaymentStatus(order.payment_status),
538
+ fulfillmentStatus: transformFulfillmentStatus(order.fulfillment_status),
539
+
540
+ // Customer
541
+ customerId: order.customer_id ?? undefined,
542
+ customer: order.customer
543
+ ? transformCustomer(order.customer, order.email)
544
+ : undefined,
545
+
546
+ // Items
547
+ items: (order.items ?? [])
548
+ .filter((item: any) => item != null) // Filter out null/undefined items
549
+ .map((item: any) => transformOrderItem(item, currencyCode)) ?? [],
550
+
551
+ // Pricing
552
+ subtotal: transformMoney(order.subtotal, currencyCode),
553
+ total: transformMoney(order.total, currencyCode),
554
+ tax: transformMoney(order.tax_total, currencyCode),
555
+ shipping: transformMoney(order.shipping_total, currencyCode),
556
+ discount: transformMoney(order.discount_total, currencyCode),
557
+
558
+ // Addresses (transformed to camelCase)
559
+ shippingAddress: order.shipping_address
560
+ ? transformAddress(order.shipping_address)
561
+ : undefined,
562
+ billingAddress: order.billing_address
563
+ ? transformAddress(order.billing_address)
564
+ : undefined,
565
+
566
+ // Dates
567
+ createdAt: typeof order.created_at === 'string'
568
+ ? order.created_at
569
+ : order.created_at?.toISOString() ?? new Date().toISOString(),
570
+ updatedAt: typeof order.updated_at === 'string'
571
+ ? order.updated_at
572
+ : order.updated_at?.toISOString() ?? new Date().toISOString(),
573
+ completedAt: order.completed_at
574
+ ? (typeof order.completed_at === 'string'
575
+ ? order.completed_at
576
+ : order.completed_at?.toISOString())
577
+ : undefined,
578
+ cancelledAt: order.canceled_at
579
+ ? (typeof order.canceled_at === 'string'
580
+ ? order.canceled_at
581
+ : order.canceled_at?.toISOString())
582
+ : undefined,
583
+
584
+ // Platform-specific metadata
585
+ metadata: order.metadata ?? undefined,
586
+ };
587
+ }
588
+
589
+ // ============================================
590
+ // Storefront Configuration Transforms
591
+ // ============================================
592
+
593
+ /**
594
+ * Transform backend `storefront_config` (snake_case) to `StorefrontConfig` (camelCase).
595
+ *
596
+ * Backend response shape:
597
+ * ```json
598
+ * {
599
+ * "id": "sfc_01J...",
600
+ * "sales_channel_id": "sc_01J...",
601
+ * "theme_accent_color": "indigo",
602
+ * "logo_url": "https://...",
603
+ * "currency_code": "EUR",
604
+ * "seo_defaults": { "title": "...", "description": "...", "ogImage": "..." },
605
+ * "created_at": "...",
606
+ * "updated_at": "..."
607
+ * }
608
+ * ```
609
+ */
610
+ export function transformStorefrontConfig(raw: any): StorefrontConfig {
611
+ return {
612
+ id: raw.id ?? '',
613
+ salesChannelId: raw.sales_channel_id ?? '',
614
+ themeAccentColor: raw.theme_accent_color ?? '',
615
+ logoUrl: raw.logo_url ?? '',
616
+ currencyCode: raw.currency_code ?? 'USD',
617
+ seoDefaults: {
618
+ title: raw.seo_defaults?.title ?? '',
619
+ description: raw.seo_defaults?.description ?? '',
620
+ ogImage: raw.seo_defaults?.ogImage ?? raw.seo_defaults?.og_image,
621
+ },
622
+ createdAt: raw.created_at ?? '',
623
+ updatedAt: raw.updated_at ?? '',
624
+ };
625
+ }