@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.
- package/.turbo/turbo-build.log +18 -0
- package/.turbo/turbo-test.log +1 -0
- package/.turbo/turbo-type-check.log +0 -0
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +271 -0
- package/README.md +634 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/index.d.mts +2524 -0
- package/dist/index.d.ts +2524 -0
- package/dist/index.js +8136 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +8045 -0
- package/dist/index.mjs.map +1 -0
- package/docs/RETRY_SOLUTION_SUMMARY.md +55 -0
- package/docs/authentication.md +165 -0
- package/docs/capabilities-system.md +143 -0
- package/docs/category-customization.md +309 -0
- package/docs/fetch-reliability-solution.md +399 -0
- package/docs/getting-started.md +154 -0
- package/docs/multi-tenant-channel-scoping.md +225 -0
- package/docs/profile-management.md +147 -0
- package/docs/providers-overview.md +85 -0
- package/docs/services-overview.md +135 -0
- package/docs/update-documentation.md +113 -0
- package/package.json +43 -0
- package/publish.log +93 -0
- package/quick-reference.md +169 -0
- package/src/admin/AdminAuthService.ts +88 -0
- package/src/admin/AdminSessionService.ts +91 -0
- package/src/admin/MetricsService.ts +145 -0
- package/src/admin/index.ts +13 -0
- package/src/backend/BackendMetadataAdapter.ts +88 -0
- package/src/backend/factory.ts +107 -0
- package/src/backend/index.ts +12 -0
- package/src/backend/medusa/MedusaMetadataAdapter.ts +139 -0
- package/src/core/errors.js +161 -0
- package/src/core/errors.ts +132 -0
- package/src/core/fetch-utils.js +240 -0
- package/src/core/fetch-utils.ts +227 -0
- package/src/core/index.ts +8 -0
- package/src/core/logger.js +63 -0
- package/src/core/logger.ts +37 -0
- package/src/factory/index.ts +102 -0
- package/src/index.ts +205 -0
- package/src/lib/money-transform.ts +80 -0
- package/src/metadata/channel-scope-validation.js +136 -0
- package/src/metadata/channel-scope-validation.ts +151 -0
- package/src/metadata/channel-scope.js +174 -0
- package/src/metadata/channel-scope.ts +215 -0
- package/src/metadata/index.js +31 -0
- package/src/metadata/index.ts +37 -0
- package/src/providers/bigcommerce/bigcommerce.ts +6 -0
- package/src/providers/bigcommerce/index.ts +315 -0
- package/src/providers/bigcommerce/transforms.ts +227 -0
- package/src/providers/index.ts +22 -0
- package/src/providers/magento/index.ts +387 -0
- package/src/providers/magento/magento.ts +5 -0
- package/src/providers/magento/queries.ts +267 -0
- package/src/providers/magento/transforms.ts +249 -0
- package/src/providers/medusa/auth.js +601 -0
- package/src/providers/medusa/auth.ts +496 -0
- package/src/providers/medusa/fulfillment-planner.ts +225 -0
- package/src/providers/medusa/index.js +2485 -0
- package/src/providers/medusa/index.ts +4239 -0
- package/src/providers/medusa/medusa.ts +8 -0
- package/src/providers/medusa/payment.js +265 -0
- package/src/providers/medusa/payment.ts +269 -0
- package/src/providers/medusa/transforms.js +505 -0
- package/src/providers/medusa/transforms.ts +625 -0
- package/src/providers/shopify/index.ts +444 -0
- package/src/providers/shopify/mutations.ts +102 -0
- package/src/providers/shopify/queries.ts +324 -0
- package/src/providers/shopify/shopify.ts +7 -0
- package/src/providers/shopify/transforms.ts +270 -0
- package/src/providers/spree/index.ts +344 -0
- package/src/providers/spree/spree.ts +5 -0
- package/src/providers/spree/transforms.ts +254 -0
- package/src/providers/woocommerce/index.ts +310 -0
- package/src/providers/woocommerce/transforms.ts +254 -0
- package/src/providers/woocommerce/woocommerce.ts +5 -0
- package/src/resilience/README.md +299 -0
- package/src/resilience/circuit-breaker.ts +222 -0
- package/src/resilience/index.ts +10 -0
- package/src/types/capabilities.types.js +141 -0
- package/src/types/capabilities.types.ts +366 -0
- package/src/types/index.ts +24 -0
- package/src/types/provider.js +9 -0
- package/src/types/provider.ts +9 -0
- package/src/types/ui-responses.ts +261 -0
- package/src/utils/city-coordinates.ts +40 -0
- package/src/utils/geolocation.ts +52 -0
- package/src/utils/payment-utils.js +152 -0
- package/src/utils/payment-utils.ts +203 -0
- package/tsconfig.json +15 -0
- 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
|
+
}
|