@thorprovider/adapters 5.0.9 → 5.0.11
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 +897 -0
- package/.turbo/turbo-type-check.log +0 -0
- package/AGENTS.md +1 -1
- package/dist/.tsbuildinfo +1 -0
- package/dist/index.d.mts +348 -11
- package/dist/index.d.ts +348 -11
- package/dist/index.js +1393 -496
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1386 -498
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/publish.log +93 -0
- package/src/core/errors.ts +31 -0
- package/src/factory/index.ts +16 -6
- package/src/factory/provider-types.test.ts +32 -0
- package/src/factory/provider-types.ts +176 -0
- package/src/index.ts +6 -4
- package/src/providers/index.ts +1 -1
- package/src/providers/medusa/available-filters.test.ts +280 -0
- package/src/providers/medusa/catalog-facet-support.test.ts +715 -0
- package/src/providers/medusa/catalog-query.test.ts +422 -0
- package/src/providers/medusa/catalog-sort.test.ts +175 -0
- package/src/providers/medusa/index.test.ts +480 -0
- package/src/providers/medusa/index.ts +1490 -518
- package/src/providers/medusa/live-smoke.test.ts +68 -0
- package/src/providers/medusa/transforms.test.ts +177 -0
- package/src/providers/medusa/transforms.ts +145 -66
- package/src/providers/mock/catalog-query.test.ts +355 -0
- package/src/providers/mock/index.test.ts +47 -0
- package/src/providers/mock/index.ts +43 -1
- package/src/providers/mock/mock.ts +324 -2
- package/src/types/completion.ts +57 -0
- package/src/types/index.ts +7 -0
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { describe, expect, it, beforeAll } from 'vitest'
|
|
25
|
+
import Medusa from '@medusajs/js-sdk'
|
|
25
26
|
import { MedusaProvider } from './index'
|
|
26
27
|
import { MedusaAdminClient } from './admin'
|
|
27
28
|
|
|
@@ -357,6 +358,7 @@ if (!runLive) {
|
|
|
357
358
|
// --------------------------------------------------------------------------
|
|
358
359
|
// Group E: Admin Standard Medusa Endpoints (via SDK with Basic auth)
|
|
359
360
|
// --------------------------------------------------------------------------
|
|
361
|
+
// (See the separate change-B acceptance block below.)
|
|
360
362
|
describe('Group E: Admin Standard Medusa Endpoints', () => {
|
|
361
363
|
let provider: MedusaProvider;
|
|
362
364
|
|
|
@@ -410,3 +412,69 @@ if (!runLive) {
|
|
|
410
412
|
})
|
|
411
413
|
});
|
|
412
414
|
}
|
|
415
|
+
|
|
416
|
+
// --------------------------------------------------------------------------
|
|
417
|
+
// Change B acceptance — native pagination-bound probes + directed product
|
|
418
|
+
// lookup. Runs independently of MEDUSA_SMOKE_TEST; gated by
|
|
419
|
+
// MEDUSA_B_ACCEPTANCE=1. Requires a running backend and the acceptance fixture
|
|
420
|
+
// env (never printed): MEDUSA_BASE_URL, MEDUSA_PUBLISHABLE_KEY,
|
|
421
|
+
// MEDUSA_REGION_ID, MEDUSA_SALES_CHANNEL_ID, B_ACCEPTANCE_PRODUCT_ID,
|
|
422
|
+
// B_ACCEPTANCE_PRODUCT_HANDLE.
|
|
423
|
+
// --------------------------------------------------------------------------
|
|
424
|
+
const runBAcceptance = process.env.MEDUSA_B_ACCEPTANCE === '1'
|
|
425
|
+
|
|
426
|
+
describe('Medusa change-B acceptance — pagination bound + directed lookup', () => {
|
|
427
|
+
it.skipIf(!runBAcceptance)(
|
|
428
|
+
'accepts native listing bounds 1/50/100/200/1000 (configured bound, not a claimed max)',
|
|
429
|
+
async () => {
|
|
430
|
+
const baseUrl = requiredEnv('MEDUSA_BASE_URL')
|
|
431
|
+
const publishableKey = requiredEnv('MEDUSA_PUBLISHABLE_KEY')
|
|
432
|
+
const regionId = requiredEnv('MEDUSA_REGION_ID')
|
|
433
|
+
const salesChannelId = requiredEnv('MEDUSA_SALES_CHANNEL_ID')
|
|
434
|
+
|
|
435
|
+
const sdk = new Medusa({ baseUrl, publishableKey })
|
|
436
|
+
for (const limit of [1, 50, 100, 200, 1000]) {
|
|
437
|
+
const { products, count } = await sdk.store.product.list({
|
|
438
|
+
limit,
|
|
439
|
+
region_id: regionId,
|
|
440
|
+
sales_channel_id: [salesChannelId],
|
|
441
|
+
fields: '+variants.calculated_price',
|
|
442
|
+
} as any)
|
|
443
|
+
expect(Array.isArray(products)).toBe(true)
|
|
444
|
+
expect(products.length).toBeLessThanOrEqual(limit)
|
|
445
|
+
expect(typeof count === 'number' || count === undefined).toBe(true)
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
it.skipIf(!runBAcceptance)(
|
|
451
|
+
'resolves the acceptance product by handle and by id with real variant prices',
|
|
452
|
+
async () => {
|
|
453
|
+
const baseUrl = requiredEnv('MEDUSA_BASE_URL')
|
|
454
|
+
const publishableKey = requiredEnv('MEDUSA_PUBLISHABLE_KEY')
|
|
455
|
+
const regionId = requiredEnv('MEDUSA_REGION_ID')
|
|
456
|
+
const productId = requiredEnv('B_ACCEPTANCE_PRODUCT_ID')
|
|
457
|
+
const handle = requiredEnv('B_ACCEPTANCE_PRODUCT_HANDLE')
|
|
458
|
+
|
|
459
|
+
const provider = new MedusaProvider({
|
|
460
|
+
baseUrl,
|
|
461
|
+
publishableKey,
|
|
462
|
+
regionId,
|
|
463
|
+
currencyCode: 'usd',
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
const byHandle = await provider.getProduct(handle)
|
|
467
|
+
expect(byHandle?.handle).toBe(handle)
|
|
468
|
+
expect(byHandle?.variants.length).toBeGreaterThanOrEqual(2)
|
|
469
|
+
|
|
470
|
+
const byId = await provider.getProductById(productId)
|
|
471
|
+
expect(byId?.id).toBe(productId)
|
|
472
|
+
|
|
473
|
+
const amounts = (byHandle?.variants ?? [])
|
|
474
|
+
.map((v) => v.price.amount)
|
|
475
|
+
.filter((amount) => amount !== '')
|
|
476
|
+
expect(amounts).toContain('11.11')
|
|
477
|
+
expect(amounts).toContain('22.22')
|
|
478
|
+
},
|
|
479
|
+
)
|
|
480
|
+
})
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
transformProduct,
|
|
4
|
+
transformProductPrice,
|
|
5
|
+
transformProductVariant,
|
|
6
|
+
} from './transforms'
|
|
7
|
+
|
|
8
|
+
const BASE_VARIANT = {
|
|
9
|
+
id: 'variant_s',
|
|
10
|
+
title: 'Small',
|
|
11
|
+
sku: 'B-ACC-S',
|
|
12
|
+
manage_inventory: false,
|
|
13
|
+
inventory_quantity: 5,
|
|
14
|
+
calculated_price: { calculated_amount: 11.11, currency_code: 'usd' },
|
|
15
|
+
options: [{ option: { title: 'Size' }, value: 'Small' }],
|
|
16
|
+
thumbnail: 'https://example.com/small.png',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('transformProductPrice (change B, decision 6)', () => {
|
|
20
|
+
it('preserves a valid zero in major units', () => {
|
|
21
|
+
expect(transformProductPrice(0, 'usd')).toEqual({
|
|
22
|
+
amount: '0.00',
|
|
23
|
+
currencyCode: 'USD',
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('carries an absent price as an empty amount, never a fabricated zero', () => {
|
|
28
|
+
expect(transformProductPrice(undefined, 'usd')).toEqual({
|
|
29
|
+
amount: '',
|
|
30
|
+
currencyCode: 'USD',
|
|
31
|
+
})
|
|
32
|
+
expect(transformProductPrice(null, 'usd').amount).toBe('')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('rejects a non-finite amount before display', () => {
|
|
36
|
+
expect(transformProductPrice(Number.NaN, 'usd').amount).toBe('')
|
|
37
|
+
expect(transformProductPrice(Number.POSITIVE_INFINITY, 'usd').amount).toBe('')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('never hardcodes USD: an unresolved currency stays empty', () => {
|
|
41
|
+
expect(transformProductPrice(11.11, undefined)).toEqual({
|
|
42
|
+
amount: '11.11',
|
|
43
|
+
currencyCode: '',
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('transformProductVariant', () => {
|
|
49
|
+
it('maps real options, availability, SKU and the backend currency', () => {
|
|
50
|
+
const variant = transformProductVariant(BASE_VARIANT)
|
|
51
|
+
expect(variant.id).toBe('variant_s')
|
|
52
|
+
expect(variant.selectedOptions).toEqual([{ name: 'Size', value: 'Small' }])
|
|
53
|
+
expect(variant.availableForSale).toBe(true)
|
|
54
|
+
expect(variant.sku).toBe('B-ACC-S')
|
|
55
|
+
expect(variant.price).toEqual({ amount: '11.11', currencyCode: 'USD' })
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('keeps stock availability when the price is unresolved (price is separate)', () => {
|
|
59
|
+
const variant = transformProductVariant({
|
|
60
|
+
...BASE_VARIANT,
|
|
61
|
+
calculated_price: undefined,
|
|
62
|
+
prices: [],
|
|
63
|
+
})
|
|
64
|
+
expect(variant.availableForSale).toBe(true)
|
|
65
|
+
expect(variant.price.amount).toBe('')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('preserves a valid zero price', () => {
|
|
69
|
+
const variant = transformProductVariant({
|
|
70
|
+
...BASE_VARIANT,
|
|
71
|
+
calculated_price: { calculated_amount: 0, currency_code: 'eur' },
|
|
72
|
+
})
|
|
73
|
+
expect(variant.price).toEqual({ amount: '0.00', currencyCode: 'EUR' })
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('omits SKU when the merchant does not supply one (optional descriptive)', () => {
|
|
77
|
+
const variant = transformProductVariant({ ...BASE_VARIANT, sku: undefined })
|
|
78
|
+
expect(variant.sku).toBeUndefined()
|
|
79
|
+
expect(variant.id).toBe('variant_s')
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe('transformProduct', () => {
|
|
84
|
+
it('builds the price range from resolvable variant prices', () => {
|
|
85
|
+
const product = transformProduct({
|
|
86
|
+
id: 'prod_1',
|
|
87
|
+
handle: 'b-acceptance-product',
|
|
88
|
+
title: 'B Acceptance Product',
|
|
89
|
+
description: '',
|
|
90
|
+
thumbnail: 'https://example.com/p.png',
|
|
91
|
+
images: [],
|
|
92
|
+
options: [],
|
|
93
|
+
variants: [
|
|
94
|
+
BASE_VARIANT,
|
|
95
|
+
{
|
|
96
|
+
...BASE_VARIANT,
|
|
97
|
+
id: 'variant_l',
|
|
98
|
+
title: 'Large',
|
|
99
|
+
calculated_price: { calculated_amount: 22.22, currency_code: 'usd' },
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
})
|
|
103
|
+
expect(product.priceRange.minVariantPrice).toEqual({
|
|
104
|
+
amount: '11.11',
|
|
105
|
+
currencyCode: 'USD',
|
|
106
|
+
})
|
|
107
|
+
expect(product.priceRange.maxVariantPrice).toEqual({
|
|
108
|
+
amount: '22.22',
|
|
109
|
+
currencyCode: 'USD',
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('maps a present thumbnail and images to featuredImage and images', () => {
|
|
114
|
+
const product = transformProduct({
|
|
115
|
+
id: 'prod_media',
|
|
116
|
+
handle: 'with-media',
|
|
117
|
+
title: 'With Media',
|
|
118
|
+
description: '',
|
|
119
|
+
thumbnail: 'https://example.com/cover.png',
|
|
120
|
+
images: [
|
|
121
|
+
{ url: 'https://example.com/a.png' },
|
|
122
|
+
{ url: 'https://example.com/b.png' },
|
|
123
|
+
],
|
|
124
|
+
options: [],
|
|
125
|
+
variants: [],
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
expect(product.featuredImage).toEqual({
|
|
129
|
+
url: 'https://example.com/cover.png',
|
|
130
|
+
altText: 'With Media',
|
|
131
|
+
width: 800,
|
|
132
|
+
height: 800,
|
|
133
|
+
})
|
|
134
|
+
expect(product.images?.map((img) => img.url)).toEqual([
|
|
135
|
+
'https://example.com/a.png',
|
|
136
|
+
'https://example.com/b.png',
|
|
137
|
+
])
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('omits featuredImage and images when the backend supplies neither (never an empty src)', () => {
|
|
141
|
+
const product = transformProduct({
|
|
142
|
+
id: 'prod_no_media',
|
|
143
|
+
handle: 'no-media',
|
|
144
|
+
title: 'No Media',
|
|
145
|
+
description: '',
|
|
146
|
+
thumbnail: undefined,
|
|
147
|
+
images: undefined,
|
|
148
|
+
options: [],
|
|
149
|
+
variants: [],
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
expect(product.featuredImage).toBeUndefined()
|
|
153
|
+
expect(product.images).toEqual([])
|
|
154
|
+
expect(product.images?.some((img) => img.url === '')).toBe(false)
|
|
155
|
+
expect(product.featuredImage?.url).toBeUndefined()
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('does not fabricate a zero range when no variant is priced', () => {
|
|
159
|
+
const product = transformProduct({
|
|
160
|
+
id: 'prod_1',
|
|
161
|
+
handle: 'sin-precio',
|
|
162
|
+
title: 'Sin precio',
|
|
163
|
+
description: '',
|
|
164
|
+
images: [],
|
|
165
|
+
options: [],
|
|
166
|
+
variants: [
|
|
167
|
+
{
|
|
168
|
+
...BASE_VARIANT,
|
|
169
|
+
calculated_price: undefined,
|
|
170
|
+
prices: [],
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
})
|
|
174
|
+
expect(product.priceRange.minVariantPrice.amount).toBe('')
|
|
175
|
+
expect(product.priceRange.maxVariantPrice.amount).toBe('')
|
|
176
|
+
})
|
|
177
|
+
})
|
|
@@ -49,6 +49,44 @@ function transformMoney(amount: number | null | undefined, currencyCode: string
|
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Backend-authoritative money (change B, decision 9).
|
|
54
|
+
*
|
|
55
|
+
* A finite backend amount is carried through unchanged, including a legitimate
|
|
56
|
+
* zero; a missing or non-finite amount becomes an empty `amount` string instead
|
|
57
|
+
* of an invented zero, so the runtime boundary can surface the backend gap
|
|
58
|
+
* rather than fabricate a total. The currency comes from the backend and is
|
|
59
|
+
* never defaulted to a hardcoded `USD`.
|
|
60
|
+
*/
|
|
61
|
+
function transformAmount(
|
|
62
|
+
amount: number | null | undefined,
|
|
63
|
+
currencyCode?: string | null,
|
|
64
|
+
): Money {
|
|
65
|
+
const currencyCodeUpper = (currencyCode ?? '').toUpperCase();
|
|
66
|
+
if (typeof amount !== 'number' || !Number.isFinite(amount)) {
|
|
67
|
+
return { amount: '', currencyCode: currencyCodeUpper };
|
|
68
|
+
}
|
|
69
|
+
return { amount: amount.toFixed(2), currencyCode: currencyCodeUpper };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Product price projection (change B, decision 6).
|
|
74
|
+
*
|
|
75
|
+
* Unlike cart/order money, a product variant price is NOT fabricated: an
|
|
76
|
+
* absent or non-finite backend amount is carried as an empty `amount` string
|
|
77
|
+
* so the runtime boundary (`parseMoney`) can reject it and mark the variant
|
|
78
|
+
* unavailable instead of showing a fake zero. A backend-resolved valid zero is
|
|
79
|
+
* preserved as `"0.00"`. The currency comes from the backend or the configured
|
|
80
|
+
* context — never a hardcoded `USD` (an unresolved currency stays empty and is
|
|
81
|
+
* likewise rejected downstream).
|
|
82
|
+
*/
|
|
83
|
+
export function transformProductPrice(
|
|
84
|
+
amount: number | null | undefined,
|
|
85
|
+
currencyCode?: string | null,
|
|
86
|
+
): Money {
|
|
87
|
+
return transformAmount(amount, currencyCode);
|
|
88
|
+
}
|
|
89
|
+
|
|
52
90
|
/**
|
|
53
91
|
* Transform Medusa image to unified Image
|
|
54
92
|
*/
|
|
@@ -126,43 +164,51 @@ export function transformProductOption(option: any): ProductOption {
|
|
|
126
164
|
*/
|
|
127
165
|
export function transformProductVariant(
|
|
128
166
|
variant: any,
|
|
129
|
-
currencyCode
|
|
167
|
+
currencyCode?: string
|
|
130
168
|
): ProductVariant {
|
|
131
169
|
let priceAmount: number | null | undefined;
|
|
132
|
-
let priceCurrency: string = currencyCode;
|
|
133
|
-
|
|
170
|
+
let priceCurrency: string | undefined = currencyCode;
|
|
171
|
+
|
|
134
172
|
// Priority 1: Use calculated_price if available (from region-based pricing)
|
|
135
173
|
if (variant.calculated_price) {
|
|
136
174
|
priceAmount = variant.calculated_price.calculated_amount;
|
|
137
|
-
priceCurrency = variant.calculated_price.currency_code
|
|
138
|
-
}
|
|
139
|
-
// Priority 2: Fallback to prices array
|
|
175
|
+
priceCurrency = variant.calculated_price.currency_code ?? currencyCode;
|
|
176
|
+
}
|
|
177
|
+
// Priority 2: Fallback to prices array (match the context currency, else the first)
|
|
140
178
|
else if (variant.prices && variant.prices.length > 0) {
|
|
141
|
-
const
|
|
179
|
+
const target = currencyCode?.toLowerCase();
|
|
180
|
+
const price =
|
|
181
|
+
(target
|
|
182
|
+
? variant.prices.find((p: any) => p.currency_code === target)
|
|
183
|
+
: undefined) ?? variant.prices[0];
|
|
142
184
|
priceAmount = price?.amount;
|
|
143
|
-
priceCurrency = price?.currency_code
|
|
185
|
+
priceCurrency = price?.currency_code ?? currencyCode;
|
|
144
186
|
}
|
|
145
|
-
|
|
187
|
+
|
|
146
188
|
// Determine if variant is available for sale
|
|
147
189
|
// Following Medusa docs: https://docs.medusajs.com/resources/storefront-development/products/inventory
|
|
148
190
|
// A variant is in stock if:
|
|
149
191
|
// 1. manage_inventory is false (inventory not tracked) - always in stock
|
|
150
192
|
// 2. inventory_quantity > 0 (has stock)
|
|
151
193
|
// 3. allow_backorder is true (can order even without stock)
|
|
194
|
+
// NOTE: this is stock availability only. Price availability is a separate
|
|
195
|
+
// concern resolved by the runtime boundary; an unpriced variant keeps its
|
|
196
|
+
// stock-derived availability and carries an empty amount.
|
|
152
197
|
const isAvailable = variant.manage_inventory === false
|
|
153
198
|
? true
|
|
154
199
|
: (variant.inventory_quantity ?? 0) > 0 || variant.allow_backorder === true;
|
|
155
|
-
|
|
200
|
+
|
|
156
201
|
return {
|
|
157
202
|
id: variant.id!,
|
|
158
203
|
title: variant.title ?? 'Default',
|
|
204
|
+
// SKU is optional descriptive data only — never API/purchase identity.
|
|
159
205
|
sku: variant.sku ?? undefined,
|
|
160
206
|
availableForSale: isAvailable,
|
|
161
207
|
selectedOptions: variant.options?.map((opt: any) => ({
|
|
162
208
|
name: (opt as any).option?.title ?? '',
|
|
163
209
|
value: opt.value ?? '',
|
|
164
210
|
})) ?? [],
|
|
165
|
-
price:
|
|
211
|
+
price: transformProductPrice(priceAmount, priceCurrency),
|
|
166
212
|
compareAtPrice: undefined, // Medusa doesn't have compare-at pricing
|
|
167
213
|
quantityAvailable: variant.manage_inventory ? variant.inventory_quantity : undefined,
|
|
168
214
|
image: variant.thumbnail
|
|
@@ -176,14 +222,22 @@ export function transformProductVariant(
|
|
|
176
222
|
*/
|
|
177
223
|
export function transformProduct(
|
|
178
224
|
product: any,
|
|
179
|
-
currencyCode
|
|
225
|
+
currencyCode?: string
|
|
180
226
|
): Product {
|
|
181
227
|
const variants = product.variants?.map((v: any) => transformProductVariant(v, currencyCode)) ?? [];
|
|
182
|
-
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const
|
|
228
|
+
|
|
229
|
+
// Price range from resolvable variant prices only. An unresolved price is
|
|
230
|
+
// carried as an empty amount (never fabricated as zero); if no variant is
|
|
231
|
+
// priced, the range is unresolved too and the runtime marks it unavailable.
|
|
232
|
+
const validPrices = variants
|
|
233
|
+
.map((v: ProductVariant) => v.price)
|
|
234
|
+
.filter((money: Money) => money.amount !== '' && Number.isFinite(Number(money.amount)))
|
|
235
|
+
.map((money: Money) => Number(money.amount));
|
|
236
|
+
const rangeCurrency =
|
|
237
|
+
variants.find((v: ProductVariant) => v.price.currencyCode)?.price.currencyCode ??
|
|
238
|
+
(currencyCode ?? '').toUpperCase();
|
|
239
|
+
const minPrice = validPrices.length > 0 ? Math.min(...validPrices) : undefined;
|
|
240
|
+
const maxPrice = validPrices.length > 0 ? Math.max(...validPrices) : undefined;
|
|
187
241
|
|
|
188
242
|
return {
|
|
189
243
|
id: product.id!,
|
|
@@ -206,10 +260,16 @@ export function transformProduct(
|
|
|
206
260
|
|
|
207
261
|
priceRange: {
|
|
208
262
|
// Build Money directly from already-resolved major-unit prices.
|
|
209
|
-
// Do NOT
|
|
210
|
-
// and do NOT
|
|
211
|
-
minVariantPrice: {
|
|
212
|
-
|
|
263
|
+
// Do NOT multiply by 100 (that was a compensatory hack for the old /100
|
|
264
|
+
// bug) and do NOT fabricate a zero for an unresolved price.
|
|
265
|
+
minVariantPrice: {
|
|
266
|
+
amount: minPrice !== undefined ? minPrice.toFixed(2) : '',
|
|
267
|
+
currencyCode: rangeCurrency,
|
|
268
|
+
},
|
|
269
|
+
maxVariantPrice: {
|
|
270
|
+
amount: maxPrice !== undefined ? maxPrice.toFixed(2) : '',
|
|
271
|
+
currencyCode: rangeCurrency,
|
|
272
|
+
},
|
|
213
273
|
},
|
|
214
274
|
|
|
215
275
|
collections: product.collection ? [{
|
|
@@ -250,47 +310,30 @@ function resolveMedusaCartItemCurrencyCode(item: any, cartCurrencyCode?: string)
|
|
|
250
310
|
}
|
|
251
311
|
|
|
252
312
|
/**
|
|
253
|
-
* Resolve the
|
|
254
|
-
*
|
|
313
|
+
* Resolve the authoritative line total a Medusa cart line reports.
|
|
314
|
+
*
|
|
315
|
+
* Only backend-supplied totals are used (`total` after discounts, then
|
|
316
|
+
* `item_total`, then `subtotal`); the adapter never multiplies a unit price by
|
|
317
|
+
* a quantity to invent a line total. A legitimate `0` is preserved and an
|
|
318
|
+
* absent total is returned as `undefined` so the money transform carries an
|
|
319
|
+
* empty amount instead of fabricating a value.
|
|
255
320
|
*/
|
|
256
|
-
function
|
|
257
|
-
const
|
|
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
|
-
];
|
|
321
|
+
function resolveAuthoritativeLineTotal(item: any): number | undefined {
|
|
322
|
+
const candidates = [item?.total, item?.item_total, item?.subtotal];
|
|
279
323
|
|
|
280
|
-
for (const candidate of
|
|
281
|
-
if (typeof candidate === 'number' && Number.isFinite(candidate)
|
|
282
|
-
return candidate
|
|
324
|
+
for (const candidate of candidates) {
|
|
325
|
+
if (typeof candidate === 'number' && Number.isFinite(candidate)) {
|
|
326
|
+
return candidate;
|
|
283
327
|
}
|
|
284
328
|
}
|
|
285
329
|
|
|
286
|
-
return
|
|
330
|
+
return undefined;
|
|
287
331
|
}
|
|
288
332
|
|
|
289
333
|
export function transformCartItem(item: any, cartCurrencyCode?: string): CartItem {
|
|
290
334
|
const variant = item.variant;
|
|
291
335
|
const product = variant?.product;
|
|
292
336
|
const currencyCode = resolveMedusaCartItemCurrencyCode(item, cartCurrencyCode);
|
|
293
|
-
const totalCents = resolveMedusaCartItemTotal(item);
|
|
294
337
|
|
|
295
338
|
// Medusa v2 store cart items return flat fields (product_title, variant_title,
|
|
296
339
|
// product_handle, thumbnail) instead of a nested variant.product object.
|
|
@@ -305,7 +348,9 @@ export function transformCartItem(item: any, cartCurrencyCode?: string): CartIte
|
|
|
305
348
|
id: item.id,
|
|
306
349
|
quantity: item.quantity ?? 1,
|
|
307
350
|
cost: {
|
|
308
|
-
totalAmount:
|
|
351
|
+
totalAmount: transformAmount(resolveAuthoritativeLineTotal(item), currencyCode),
|
|
352
|
+
// Backend unit price, carried so the display never derives it locally.
|
|
353
|
+
unitPrice: transformAmount(item?.unit_price, currencyCode),
|
|
309
354
|
},
|
|
310
355
|
merchandise: {
|
|
311
356
|
id: merchandiseId,
|
|
@@ -326,26 +371,53 @@ export function transformCartItem(item: any, cartCurrencyCode?: string): CartIte
|
|
|
326
371
|
}
|
|
327
372
|
|
|
328
373
|
/**
|
|
329
|
-
* Transform Medusa Cart to unified Cart
|
|
374
|
+
* Transform Medusa Cart to unified Cart.
|
|
375
|
+
*
|
|
376
|
+
* Totals are backend-authoritative (change B, decision 9): subtotal, discount,
|
|
377
|
+
* tax, shipping and total are carried straight from the cart response, a
|
|
378
|
+
* legitimate zero is preserved, and no amount is derived locally.
|
|
379
|
+
*
|
|
380
|
+
* `subtotalAmount` prefers the backend's `item_subtotal` (the sum of the line
|
|
381
|
+
* items, excluding shipping and tax). Medusa's native `subtotal` also folds in
|
|
382
|
+
* `shipping_subtotal`; presenting that as the commercial subtotal would
|
|
383
|
+
* double-count shipping. When `item_subtotal` is absent the native `subtotal`
|
|
384
|
+
* is used unchanged, preserving backward compatibility.
|
|
385
|
+
*
|
|
386
|
+
* The cart's sales channel, region and currency are carried so the runtime can
|
|
387
|
+
* enforce the validated single-store context, and `completed_at` is carried so
|
|
388
|
+
* a completed cart can be reconciled.
|
|
330
389
|
*/
|
|
331
390
|
export function transformCart(cart: any): Cart {
|
|
332
|
-
const currencyCode = cart.currency_code
|
|
333
|
-
|
|
391
|
+
const currencyCode = cart.currency_code;
|
|
392
|
+
|
|
334
393
|
return {
|
|
335
394
|
id: cart.id,
|
|
336
395
|
checkoutUrl: `/checkout/${cart.id}`, // Medusa doesn't have a direct checkout URL
|
|
337
396
|
cost: {
|
|
338
|
-
subtotalAmount:
|
|
339
|
-
|
|
340
|
-
|
|
397
|
+
subtotalAmount: transformAmount(
|
|
398
|
+
typeof cart.item_subtotal === 'number' ? cart.item_subtotal : cart.subtotal,
|
|
399
|
+
currencyCode,
|
|
400
|
+
),
|
|
401
|
+
discountAmount: transformAmount(cart.discount_total, currencyCode),
|
|
402
|
+
totalTaxAmount: transformAmount(cart.tax_total, currencyCode),
|
|
403
|
+
shippingAmount: transformAmount(cart.shipping_total, currencyCode),
|
|
404
|
+
totalAmount: transformAmount(cart.total, currencyCode),
|
|
341
405
|
},
|
|
342
406
|
lines: cart.items?.map((item: any) => transformCartItem(item, currencyCode)) ?? [],
|
|
343
407
|
totalQuantity: cart.items?.reduce((sum: any, item: any) => sum + (item.quantity ?? 0), 0) ?? 0,
|
|
408
|
+
salesChannelId: cart.sales_channel_id ?? undefined,
|
|
409
|
+
regionId: cart.region_id ?? undefined,
|
|
410
|
+
currencyCode:
|
|
411
|
+
typeof currencyCode === 'string' ? currencyCode.toUpperCase() : undefined,
|
|
412
|
+
selectedShippingMethodId: cart.shipping_methods?.[0]?.id ?? undefined,
|
|
413
|
+
selectedShippingOptionId:
|
|
414
|
+
cart.shipping_methods?.[0]?.shipping_option_id ?? undefined,
|
|
344
415
|
completedAt: cart.completed_at
|
|
345
416
|
? (typeof cart.completed_at === 'string'
|
|
346
417
|
? cart.completed_at
|
|
347
418
|
: cart.completed_at?.toISOString())
|
|
348
419
|
: undefined,
|
|
420
|
+
metadata: cart.metadata ?? undefined,
|
|
349
421
|
};
|
|
350
422
|
}
|
|
351
423
|
|
|
@@ -477,8 +549,8 @@ export function transformOrderItem(item: any, currencyCode: string): OrderItem {
|
|
|
477
549
|
title: item.product_title ?? item.title ?? '',
|
|
478
550
|
variantTitle: item.variant_title ?? variant?.title ?? undefined,
|
|
479
551
|
quantity: item.quantity ?? 1,
|
|
480
|
-
price:
|
|
481
|
-
total:
|
|
552
|
+
price: transformAmount(item.unit_price, currencyCode),
|
|
553
|
+
total: transformAmount(item.total, currencyCode),
|
|
482
554
|
sku: variant?.sku ?? undefined,
|
|
483
555
|
image: product?.thumbnail || item.thumbnail
|
|
484
556
|
? transformImage(product?.thumbnail || item.thumbnail, item.product_title ?? '')
|
|
@@ -556,12 +628,19 @@ export function transformOrder(order: any): Order {
|
|
|
556
628
|
.filter((item: any) => item != null) // Filter out null/undefined items
|
|
557
629
|
.map((item: any) => transformOrderItem(item, currencyCode)) ?? [],
|
|
558
630
|
|
|
559
|
-
// Pricing
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
631
|
+
// Pricing — backend-authoritative; a legitimate zero is preserved and no
|
|
632
|
+
// amount is derived locally. `subtotal` prefers the backend's
|
|
633
|
+
// `item_subtotal` (lines only, excluding shipping/tax); Medusa's native
|
|
634
|
+
// `subtotal` folds in `shipping_subtotal`, so falling back to it is only
|
|
635
|
+
// for installs that do not supply `item_subtotal`.
|
|
636
|
+
subtotal: transformAmount(
|
|
637
|
+
typeof order.item_subtotal === 'number' ? order.item_subtotal : order.subtotal,
|
|
638
|
+
currencyCode,
|
|
639
|
+
),
|
|
640
|
+
total: transformAmount(order.total, currencyCode),
|
|
641
|
+
tax: transformAmount(order.tax_total, currencyCode),
|
|
642
|
+
shipping: transformAmount(order.shipping_total, currencyCode),
|
|
643
|
+
discount: transformAmount(order.discount_total, currencyCode),
|
|
565
644
|
|
|
566
645
|
// Addresses (transformed to camelCase)
|
|
567
646
|
shippingAddress: order.shipping_address
|