@thorprovider/adapters 5.0.10 → 5.0.12
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/AGENTS.md +1 -1
- package/dist/index.d.mts +376 -16
- package/dist/index.d.ts +376 -16
- package/dist/index.js +1687 -528
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1680 -530
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
- 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/auth.test.ts +148 -25
- package/src/providers/medusa/auth.ts +129 -16
- 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/customer-orders.test.ts +142 -0
- package/src/providers/medusa/index.test.ts +480 -0
- package/src/providers/medusa/index.ts +1592 -538
- package/src/providers/medusa/live-smoke.test.ts +68 -0
- package/src/providers/medusa/transforms.test.ts +222 -0
- package/src/providers/medusa/transforms.ts +152 -66
- package/src/providers/mock/catalog-query.test.ts +491 -0
- package/src/providers/mock/index.test.ts +66 -0
- package/src/providers/mock/index.ts +43 -1
- package/src/providers/mock/mock.ts +513 -3
- package/src/types/completion.ts +57 -0
- package/src/types/index.ts +7 -0
- package/.turbo/turbo-build.log +0 -18
- package/.turbo/turbo-test.log +0 -372
- package/.turbo/turbo-type-check.log +0 -0
- package/dist/.tsbuildinfo +0 -1
- package/publish.log +0 -93
|
@@ -18,10 +18,25 @@ import type {
|
|
|
18
18
|
StockStatus,
|
|
19
19
|
StockValidation,
|
|
20
20
|
} from '@thorprovider/types';
|
|
21
|
+
import type {
|
|
22
|
+
CatalogAppliedFilters,
|
|
23
|
+
CatalogFacetId,
|
|
24
|
+
CatalogQuery,
|
|
25
|
+
CatalogQueryOutcome,
|
|
26
|
+
CatalogSortKey,
|
|
27
|
+
FacetSupport,
|
|
28
|
+
} from '@thorprovider/types';
|
|
21
29
|
|
|
22
30
|
const FIXTURE_TIMESTAMP = '2026-01-01T00:00:00.000Z';
|
|
23
31
|
const DEFAULT_SITE_URL = 'https://mock.thorcommerce.local';
|
|
24
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Stable sales channel the fixture cart is scoped to. Exported so a host can
|
|
35
|
+
* build a matching `StoreContext` for the mock data source; the runtime
|
|
36
|
+
* context check compares against it like a real backend channel.
|
|
37
|
+
*/
|
|
38
|
+
export const MOCK_SALES_CHANNEL_ID = 'mock-sales-channel';
|
|
39
|
+
|
|
25
40
|
interface MockRegionFixture {
|
|
26
41
|
id: string;
|
|
27
42
|
name: string;
|
|
@@ -69,6 +84,14 @@ interface MockProductFixture {
|
|
|
69
84
|
tags: string[];
|
|
70
85
|
options: Array<{ id: string; name: string; values: string[] }>;
|
|
71
86
|
variants: MockVariantFixture[];
|
|
87
|
+
/**
|
|
88
|
+
* Fixture-supplied creation timestamp (ISO-8601).
|
|
89
|
+
*
|
|
90
|
+
* Declared explicitly per product rather than derived from the id, so the
|
|
91
|
+
* canonical `newest` ordering is truthful and deterministic. Products keep
|
|
92
|
+
* distinct values so global ordering before pagination is observable.
|
|
93
|
+
*/
|
|
94
|
+
createdAt: string;
|
|
72
95
|
}
|
|
73
96
|
|
|
74
97
|
export interface MockCartState {
|
|
@@ -156,10 +179,11 @@ const categoryFixtures: MockCategoryFixture[] = [
|
|
|
156
179
|
},
|
|
157
180
|
];
|
|
158
181
|
|
|
159
|
-
const
|
|
182
|
+
const heroProductFixtures: MockProductFixture[] = [
|
|
160
183
|
{
|
|
161
184
|
id: 'mock-product-aurora-jacket',
|
|
162
185
|
handle: 'aurora-jacket',
|
|
186
|
+
createdAt: '2026-01-05T00:00:00.000Z',
|
|
163
187
|
title: 'Aurora Quilted Jacket',
|
|
164
188
|
description: 'Lightweight quilted outerwear with tonal hardware and a soft structured collar.',
|
|
165
189
|
imageLabel: 'Aurora Jacket',
|
|
@@ -201,6 +225,7 @@ const productFixtures: MockProductFixture[] = [
|
|
|
201
225
|
{
|
|
202
226
|
id: 'mock-product-solis-dress',
|
|
203
227
|
handle: 'solis-dress',
|
|
228
|
+
createdAt: '2026-01-03T00:00:00.000Z',
|
|
204
229
|
title: 'Solis Pleated Dress',
|
|
205
230
|
description: 'A fluid pleated dress with a clean neckline designed for hero storytelling.',
|
|
206
231
|
imageLabel: 'Solis Dress',
|
|
@@ -240,6 +265,7 @@ const productFixtures: MockProductFixture[] = [
|
|
|
240
265
|
{
|
|
241
266
|
id: 'mock-product-helios-lamp',
|
|
242
267
|
handle: 'helios-lamp',
|
|
268
|
+
createdAt: '2026-01-04T00:00:00.000Z',
|
|
243
269
|
title: 'Helios Table Lamp',
|
|
244
270
|
description: 'Warm ambient lamp with sculptural proportions for lifestyle and home grids.',
|
|
245
271
|
imageLabel: 'Helios Lamp',
|
|
@@ -263,6 +289,7 @@ const productFixtures: MockProductFixture[] = [
|
|
|
263
289
|
{
|
|
264
290
|
id: 'mock-product-luna-bag',
|
|
265
291
|
handle: 'luna-bag',
|
|
292
|
+
createdAt: '2026-01-02T00:00:00.000Z',
|
|
266
293
|
title: 'Luna Crossbody Bag',
|
|
267
294
|
description: 'Compact everyday bag for accessory rails, checkout add-ons, and bundle modules.',
|
|
268
295
|
imageLabel: 'Luna Bag',
|
|
@@ -285,6 +312,180 @@ const productFixtures: MockProductFixture[] = [
|
|
|
285
312
|
},
|
|
286
313
|
];
|
|
287
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Deterministic blueprint for a generated demo category.
|
|
317
|
+
*
|
|
318
|
+
* Every field is a fixed literal so the factory below produces the same ids,
|
|
319
|
+
* handles, titles, prices, and ordering on every load (no `Math.random`, no
|
|
320
|
+
* clock reads). `names` are unique within the category and the `noun`
|
|
321
|
+
* distinguishes categories, so `${name} ${noun}` yields globally distinct
|
|
322
|
+
* titles and `${name}-${noun}` yields globally unique handles.
|
|
323
|
+
*/
|
|
324
|
+
interface MockProductBlueprint {
|
|
325
|
+
categoryHandle: string;
|
|
326
|
+
noun: string;
|
|
327
|
+
productType: string;
|
|
328
|
+
vendor: string;
|
|
329
|
+
tags: string[];
|
|
330
|
+
variantName: string;
|
|
331
|
+
variantValues: string[];
|
|
332
|
+
names: string[];
|
|
333
|
+
description: string;
|
|
334
|
+
basePrice: number;
|
|
335
|
+
baseHue: number;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Fixed per-category pools for the generated demo products.
|
|
340
|
+
*
|
|
341
|
+
* Copy deliberately avoids the substrings "light" and "helios" so the existing
|
|
342
|
+
* CU-09 text-search assertions (which pin the only "light"/"HELIOS" matches to
|
|
343
|
+
* the fixture heroes) keep holding.
|
|
344
|
+
*/
|
|
345
|
+
const generatedBlueprints: MockProductBlueprint[] = [
|
|
346
|
+
{
|
|
347
|
+
categoryHandle: 'jackets',
|
|
348
|
+
noun: 'Jacket',
|
|
349
|
+
productType: 'outerwear',
|
|
350
|
+
vendor: 'Thor Atelier',
|
|
351
|
+
tags: ['outerwear', 'layer'],
|
|
352
|
+
variantName: 'Size',
|
|
353
|
+
variantValues: ['S', 'M'],
|
|
354
|
+
names: ['Alpine', 'Boreal', 'Cinder', 'Drift', 'Ember', 'Fjord', 'Harbor', 'Meridian', 'Nomad', 'Tundra', 'Vantage'],
|
|
355
|
+
description: 'A considered layering piece in a brushed weave.',
|
|
356
|
+
basePrice: 118,
|
|
357
|
+
baseHue: 210,
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
categoryHandle: 'dresses',
|
|
361
|
+
noun: 'Dress',
|
|
362
|
+
productType: 'dress',
|
|
363
|
+
vendor: 'Thor Atelier',
|
|
364
|
+
tags: ['dress', 'editorial'],
|
|
365
|
+
variantName: 'Size',
|
|
366
|
+
variantValues: ['S', 'M'],
|
|
367
|
+
names: ['Amber', 'Camille', 'Dahlia', 'Elara', 'Iris', 'Lumen', 'Nova', 'Rosalind', 'Vera'],
|
|
368
|
+
description: 'Cut from a fluid cloth for editorial styling.',
|
|
369
|
+
basePrice: 142,
|
|
370
|
+
baseHue: 330,
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
categoryHandle: 'home',
|
|
374
|
+
noun: 'Lamp',
|
|
375
|
+
productType: 'decor',
|
|
376
|
+
vendor: 'Thor Home',
|
|
377
|
+
tags: ['home', 'decor', 'interior'],
|
|
378
|
+
variantName: 'Finish',
|
|
379
|
+
variantValues: ['Brass'],
|
|
380
|
+
names: ['Atlas', 'Copper', 'Dune', 'Onyx', 'Quill', 'Slate', 'Terra', 'Verdant'],
|
|
381
|
+
description: 'A calm sculptural accent for modern interiors.',
|
|
382
|
+
basePrice: 74,
|
|
383
|
+
baseHue: 40,
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
categoryHandle: 'accessories',
|
|
387
|
+
noun: 'Bag',
|
|
388
|
+
productType: 'bag',
|
|
389
|
+
vendor: 'Thor Atelier',
|
|
390
|
+
tags: ['accessory', 'everyday'],
|
|
391
|
+
variantName: 'Color',
|
|
392
|
+
variantValues: ['Juniper'],
|
|
393
|
+
names: ['Cove', 'Fable', 'Juniper', 'Kestrel', 'Marlowe', 'Piper', 'Rune'],
|
|
394
|
+
description: 'An everyday carry with a textured finish.',
|
|
395
|
+
basePrice: 68,
|
|
396
|
+
baseHue: 280,
|
|
397
|
+
},
|
|
398
|
+
];
|
|
399
|
+
|
|
400
|
+
/** Oldest anchor for generated `createdAt` values; all stay before the heroes. */
|
|
401
|
+
const GENERATED_CREATED_AT_BASE = Date.parse('2025-12-30T00:00:00.000Z');
|
|
402
|
+
|
|
403
|
+
/** Deterministic collection order the generated products are assigned across. */
|
|
404
|
+
const GENERATED_COLLECTION_POOL = ['new-arrivals', 'best-sellers', 'studio-essentials'];
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Build the generated demo products, one pool per seeded category.
|
|
408
|
+
*
|
|
409
|
+
* Deterministic by construction: the trailing `globalIndex` drives collection
|
|
410
|
+
* membership, pricing step, stock, hue, and `createdAt` so the whole seed is a
|
|
411
|
+
* pure function of fixed literals.
|
|
412
|
+
*/
|
|
413
|
+
function buildGeneratedProductFixtures(): MockProductFixture[] {
|
|
414
|
+
const fixtures: MockProductFixture[] = [];
|
|
415
|
+
let globalIndex = 0;
|
|
416
|
+
|
|
417
|
+
for (const blueprint of generatedBlueprints) {
|
|
418
|
+
const noun = blueprint.noun.toLowerCase();
|
|
419
|
+
|
|
420
|
+
blueprint.names.forEach((name, localIndex) => {
|
|
421
|
+
const handle = `${name.toLowerCase()}-${noun}`;
|
|
422
|
+
const title = `${name} ${blueprint.noun}`;
|
|
423
|
+
const price = blueprint.basePrice + localIndex * 7 + (localIndex % 3) * 0.5;
|
|
424
|
+
const quantityAvailable = (globalIndex * 5) % 17;
|
|
425
|
+
|
|
426
|
+
const primaryCollection =
|
|
427
|
+
GENERATED_COLLECTION_POOL[globalIndex % GENERATED_COLLECTION_POOL.length];
|
|
428
|
+
const collectionHandles = [primaryCollection];
|
|
429
|
+
if (globalIndex % 5 === 0) {
|
|
430
|
+
const secondary =
|
|
431
|
+
GENERATED_COLLECTION_POOL[(globalIndex + 1) % GENERATED_COLLECTION_POOL.length];
|
|
432
|
+
if (secondary !== primaryCollection) {
|
|
433
|
+
collectionHandles.push(secondary);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const createdAt = new Date(GENERATED_CREATED_AT_BASE - globalIndex * 86_400_000).toISOString();
|
|
438
|
+
|
|
439
|
+
const variants: MockVariantFixture[] = blueprint.variantValues.map((value, variantIndex) => ({
|
|
440
|
+
id: `mock-variant-${handle}-${value.toLowerCase()}`,
|
|
441
|
+
title: value,
|
|
442
|
+
price,
|
|
443
|
+
quantityAvailable: Math.max(0, quantityAvailable - variantIndex),
|
|
444
|
+
selectedOptions: [{ name: blueprint.variantName, value }],
|
|
445
|
+
}));
|
|
446
|
+
|
|
447
|
+
fixtures.push({
|
|
448
|
+
id: `mock-product-${handle}`,
|
|
449
|
+
handle,
|
|
450
|
+
title,
|
|
451
|
+
description: `${title} - ${blueprint.description}`,
|
|
452
|
+
imageLabel: title,
|
|
453
|
+
imageHue: (blueprint.baseHue + localIndex * 13) % 360,
|
|
454
|
+
collectionHandles,
|
|
455
|
+
categoryHandle: blueprint.categoryHandle,
|
|
456
|
+
productType: blueprint.productType,
|
|
457
|
+
vendor: blueprint.vendor,
|
|
458
|
+
tags: [...blueprint.tags],
|
|
459
|
+
options: [
|
|
460
|
+
{
|
|
461
|
+
id: `${handle}-${blueprint.variantName.toLowerCase()}`,
|
|
462
|
+
name: blueprint.variantName,
|
|
463
|
+
values: [...blueprint.variantValues],
|
|
464
|
+
},
|
|
465
|
+
],
|
|
466
|
+
variants,
|
|
467
|
+
createdAt,
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
globalIndex += 1;
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return fixtures;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* The full mock product seed.
|
|
479
|
+
*
|
|
480
|
+
* The four fixture heroes stay first so the canonical `featured` order and the
|
|
481
|
+
* home hero (`slice(0, 4)`) remain representative; the generated demo products
|
|
482
|
+
* follow and populate every category with a credible multi-item grid.
|
|
483
|
+
*/
|
|
484
|
+
const productFixtures: MockProductFixture[] = [
|
|
485
|
+
...heroProductFixtures,
|
|
486
|
+
...buildGeneratedProductFixtures(),
|
|
487
|
+
];
|
|
488
|
+
|
|
288
489
|
const stockLocationFixture: StockLocation = {
|
|
289
490
|
id: 'mock-stock-primary',
|
|
290
491
|
name: 'Mock Main Warehouse',
|
|
@@ -422,10 +623,24 @@ function buildProducts(currencyCode: string): Product[] {
|
|
|
422
623
|
title: collection.title,
|
|
423
624
|
handle: collection.handle,
|
|
424
625
|
})),
|
|
626
|
+
categories: (() => {
|
|
627
|
+
const category = categoryFixtures.find(
|
|
628
|
+
(item) => item.handle === product.categoryHandle,
|
|
629
|
+
);
|
|
630
|
+
if (!category) return undefined;
|
|
631
|
+
return [
|
|
632
|
+
{
|
|
633
|
+
id: category.id,
|
|
634
|
+
name: category.name,
|
|
635
|
+
handle: category.handle,
|
|
636
|
+
parentCategoryId: category.parentCategoryId,
|
|
637
|
+
},
|
|
638
|
+
];
|
|
639
|
+
})(),
|
|
425
640
|
productType: product.productType,
|
|
426
641
|
vendor: product.vendor,
|
|
427
642
|
tags: product.tags,
|
|
428
|
-
createdAt:
|
|
643
|
+
createdAt: product.createdAt,
|
|
429
644
|
updatedAt: FIXTURE_TIMESTAMP,
|
|
430
645
|
metadata: {
|
|
431
646
|
categoryHandle: product.categoryHandle,
|
|
@@ -517,6 +732,167 @@ function paginateProducts(products: Product[], skip?: number, first?: number): P
|
|
|
517
732
|
return products.slice(start, end);
|
|
518
733
|
}
|
|
519
734
|
|
|
735
|
+
/**
|
|
736
|
+
* Canonical sort keys the mock catalog can truthfully apply (E2 task 2.5).
|
|
737
|
+
*
|
|
738
|
+
* Mirrors the Medusa advertisement: `featured` (stable fixture order, the
|
|
739
|
+
* canonical default), `name` (title ascending), and `newest` (fixture-supplied
|
|
740
|
+
* `createdAt` descending). No global price or rating ordering is advertised.
|
|
741
|
+
*/
|
|
742
|
+
export const MOCK_CATALOG_SORT_KEYS: readonly CatalogSortKey[] = [
|
|
743
|
+
'featured',
|
|
744
|
+
'name',
|
|
745
|
+
'newest',
|
|
746
|
+
];
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* The mock catalog's demonstration endpoint identity.
|
|
750
|
+
*
|
|
751
|
+
* Explicitly mock-scoped so a support descriptor can never be mistaken for a
|
|
752
|
+
* live Medusa endpoint. The mock is the basic fixture listing; advanced facets
|
|
753
|
+
* are held.
|
|
754
|
+
*/
|
|
755
|
+
const MOCK_CATALOG_ENDPOINT = 'mock://catalog/products';
|
|
756
|
+
|
|
757
|
+
/** Canonical default page size, matching the Medusa configured bound. */
|
|
758
|
+
const MOCK_CATALOG_PAGE_SIZE = 50;
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Effective category scope for a canonical query.
|
|
762
|
+
*
|
|
763
|
+
* Mirrors the Medusa contract: the route `categoryScope` is always retained and
|
|
764
|
+
* the `categories` filter narrows within it. Returns `undefined` when no
|
|
765
|
+
* category filter is requested, `[]` for an explicit empty intersection (a valid
|
|
766
|
+
* zero-result query), or the handles to resolve against the fixtures.
|
|
767
|
+
*/
|
|
768
|
+
function resolveEffectiveCategoryHandles(
|
|
769
|
+
query: CatalogQuery
|
|
770
|
+
): string[] | undefined {
|
|
771
|
+
const { categoryScope, categories } = query;
|
|
772
|
+
if (categoryScope === undefined) return categories;
|
|
773
|
+
if (categories === undefined) return [categoryScope];
|
|
774
|
+
return categories.filter((handle) => handle === categoryScope);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/** Canonical query fields that require advanced filtering the mock does not implement. */
|
|
778
|
+
function collectRequestedAdvancedFields(query: CatalogQuery): string[] {
|
|
779
|
+
const fields: string[] = [];
|
|
780
|
+
if (query.priceMin !== undefined) fields.push('priceMin');
|
|
781
|
+
if (query.priceMax !== undefined) fields.push('priceMax');
|
|
782
|
+
if (query.inStock !== undefined) fields.push('inStock');
|
|
783
|
+
for (const name of Object.keys(query.attributes ?? {})) {
|
|
784
|
+
fields.push(`attr.${name}`);
|
|
785
|
+
}
|
|
786
|
+
return fields;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/** Map a requested advanced field name to the canonical facet id it needs. */
|
|
790
|
+
function advancedFieldToFacet(field: string): CatalogFacetId {
|
|
791
|
+
if (field === 'priceMin' || field === 'priceMax') return 'price';
|
|
792
|
+
return field as CatalogFacetId;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Filters a successful (or explicitly-empty) canonical result reports as
|
|
797
|
+
* applied. The mock implements only the basic filters (text/categories/
|
|
798
|
+
* collections); requested advanced filters never reach a success path.
|
|
799
|
+
*/
|
|
800
|
+
function buildAppliedCatalogFilters(
|
|
801
|
+
query: CatalogQuery,
|
|
802
|
+
effectiveCategoryHandles: string[] | undefined
|
|
803
|
+
): CatalogAppliedFilters {
|
|
804
|
+
return {
|
|
805
|
+
...(query.text ? { text: query.text } : {}),
|
|
806
|
+
...(effectiveCategoryHandles !== undefined
|
|
807
|
+
? { categories: effectiveCategoryHandles }
|
|
808
|
+
: {}),
|
|
809
|
+
...(query.collections !== undefined
|
|
810
|
+
? { collections: query.collections }
|
|
811
|
+
: {}),
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/** Basic fields the mock listing actually accepts, recorded on the demonstration. */
|
|
816
|
+
function buildBasicDemonstratedQuery(query: CatalogQuery): CatalogQuery {
|
|
817
|
+
return {
|
|
818
|
+
...(query.text !== undefined ? { text: query.text } : {}),
|
|
819
|
+
...(query.categoryScope !== undefined
|
|
820
|
+
? { categoryScope: query.categoryScope }
|
|
821
|
+
: {}),
|
|
822
|
+
...(query.categories !== undefined ? { categories: query.categories } : {}),
|
|
823
|
+
...(query.collections !== undefined
|
|
824
|
+
? { collections: query.collections }
|
|
825
|
+
: {}),
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Truthful facet-support descriptor for the mock.
|
|
831
|
+
*
|
|
832
|
+
* `categories`/`collections` are offered with the mock listing as their
|
|
833
|
+
* demonstration source; `price`/`inStock` are held with a real reason because
|
|
834
|
+
* the mock does not implement advanced filtering. No count or range is
|
|
835
|
+
* synthesized and the endpoint is explicitly mock-scoped.
|
|
836
|
+
*/
|
|
837
|
+
function buildMockFacetSupport(query: CatalogQuery): FacetSupport {
|
|
838
|
+
const appliedQuery = buildBasicDemonstratedQuery(query);
|
|
839
|
+
const scopeField =
|
|
840
|
+
query.categoryScope !== undefined ? { scope: query.categoryScope } : {};
|
|
841
|
+
|
|
842
|
+
return {
|
|
843
|
+
offered: ['categories', 'collections'],
|
|
844
|
+
held: ['price', 'inStock'],
|
|
845
|
+
demonstrations: {
|
|
846
|
+
categories: {
|
|
847
|
+
endpoint: MOCK_CATALOG_ENDPOINT,
|
|
848
|
+
appliedQuery,
|
|
849
|
+
...scopeField,
|
|
850
|
+
},
|
|
851
|
+
collections: {
|
|
852
|
+
endpoint: MOCK_CATALOG_ENDPOINT,
|
|
853
|
+
appliedQuery,
|
|
854
|
+
...scopeField,
|
|
855
|
+
},
|
|
856
|
+
},
|
|
857
|
+
heldReasons: {
|
|
858
|
+
price: `${MOCK_CATALOG_ENDPOINT} is the mock basic listing; price filtering is not implemented in mock mode`,
|
|
859
|
+
inStock: `${MOCK_CATALOG_ENDPOINT} is the mock basic listing; stock filtering is not implemented in mock mode`,
|
|
860
|
+
},
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/** Build a typed `unsupported-query` outcome naming the offending fields. */
|
|
865
|
+
function unsupportedMockCatalogOutcome(fields: string[]): CatalogQueryOutcome {
|
|
866
|
+
return {
|
|
867
|
+
ok: false,
|
|
868
|
+
error: {
|
|
869
|
+
code: 'unsupported-query',
|
|
870
|
+
fields,
|
|
871
|
+
message: `Unsupported catalog query in mock mode: ${fields.join(', ')}`,
|
|
872
|
+
repair: { remove: fields },
|
|
873
|
+
},
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Apply the canonical ordering globally, before pagination.
|
|
879
|
+
*
|
|
880
|
+
* `featured` preserves the stable fixture order; `name` sorts by title
|
|
881
|
+
* ascending; `newest` sorts by the fixture-supplied `createdAt` descending.
|
|
882
|
+
*/
|
|
883
|
+
function sortMockCatalogProducts(
|
|
884
|
+
products: Product[],
|
|
885
|
+
sortKey?: CatalogSortKey
|
|
886
|
+
): Product[] {
|
|
887
|
+
const sorted = [...products];
|
|
888
|
+
if (sortKey === 'name') {
|
|
889
|
+
sorted.sort((left, right) => left.title.localeCompare(right.title));
|
|
890
|
+
} else if (sortKey === 'newest') {
|
|
891
|
+
sorted.sort((left, right) => right.createdAt.localeCompare(left.createdAt));
|
|
892
|
+
}
|
|
893
|
+
return sorted;
|
|
894
|
+
}
|
|
895
|
+
|
|
520
896
|
function resolveVariant(currencyCode: string, merchandiseId: string): {
|
|
521
897
|
product: Product;
|
|
522
898
|
variant: Product['variants'][number];
|
|
@@ -595,11 +971,22 @@ function buildExternalCart(state: MockCartState, currencyCode: string, siteUrl:
|
|
|
595
971
|
checkoutUrl: `${siteUrl}/checkout?cart=${state.id}`,
|
|
596
972
|
cost: {
|
|
597
973
|
subtotalAmount: createMoney(subtotal, regionCurrency),
|
|
598
|
-
|
|
974
|
+
discountAmount: createMoney(discount, regionCurrency),
|
|
599
975
|
totalTaxAmount: createMoney(totalTax, regionCurrency),
|
|
976
|
+
shippingAmount: createMoney(shipping, regionCurrency),
|
|
977
|
+
totalAmount: createMoney(total, regionCurrency),
|
|
600
978
|
},
|
|
601
979
|
lines,
|
|
602
980
|
totalQuantity: lines.reduce((sum, line) => sum + line.quantity, 0),
|
|
981
|
+
salesChannelId: MOCK_SALES_CHANNEL_ID,
|
|
982
|
+
regionId: state.regionId,
|
|
983
|
+
currencyCode: regionCurrency.toUpperCase(),
|
|
984
|
+
selectedShippingMethodId: state.shippingMethodId,
|
|
985
|
+
// The mock method id IS the shipping option id (the value returned by
|
|
986
|
+
// `getMockShippingMethods` and accepted by `addShippingMethod`), so the
|
|
987
|
+
// canonical option identity mirrors the real adapter's `so_` field and the
|
|
988
|
+
// checkout UI can highlight the selected option.
|
|
989
|
+
selectedShippingOptionId: state.shippingMethodId,
|
|
603
990
|
completedAt: state.completedAt,
|
|
604
991
|
};
|
|
605
992
|
}
|
|
@@ -674,6 +1061,118 @@ export function getMockProducts(options?: {
|
|
|
674
1061
|
return paginateProducts(sorted, options?.skip, options?.first);
|
|
675
1062
|
}
|
|
676
1063
|
|
|
1064
|
+
/**
|
|
1065
|
+
* Canonical catalog query for the mock provider — E2 `storefront-catalog-search-filters`
|
|
1066
|
+
* task 2.5.
|
|
1067
|
+
*
|
|
1068
|
+
* A faithful stand-in for the Medusa canonical contract so mock mode exercises
|
|
1069
|
+
* the same behavior:
|
|
1070
|
+
*
|
|
1071
|
+
* - The route `categoryScope` is always retained and intersected with the
|
|
1072
|
+
* `categories` filter; an explicit empty intersection (or a requested
|
|
1073
|
+
* category/collection handle that matches no fixture) is a valid zero-result
|
|
1074
|
+
* query, never an unfiltered request.
|
|
1075
|
+
* - Requested category/collection handles resolve against the fixtures by id or
|
|
1076
|
+
* handle; text matching is case-insensitive over title, description, and tags.
|
|
1077
|
+
* - Ordering is global, before pagination: `featured` preserves the stable
|
|
1078
|
+
* fixture order, `name` sorts by title ascending, and `newest` sorts by the
|
|
1079
|
+
* fixture-supplied `createdAt` descending. Only the advertised
|
|
1080
|
+
* `MOCK_CATALOG_SORT_KEYS` are accepted; any other sort is a typed
|
|
1081
|
+
* `unsupported-query`.
|
|
1082
|
+
* - `total` is the full matching fixture count (never the page length) and is
|
|
1083
|
+
* present whenever a listing is returned.
|
|
1084
|
+
* - Advanced filters (price/stock/attributes) are held and rejected as a typed
|
|
1085
|
+
* `unsupported-query` **before** any zero-result short-circuit, so a requested
|
|
1086
|
+
* filter is never silently dropped and a partial result is never labelled
|
|
1087
|
+
* success. The attached `facetSupport` marks its demonstration endpoint as
|
|
1088
|
+
* mock-scoped and never claims Medusa support.
|
|
1089
|
+
*/
|
|
1090
|
+
export function getMockCatalogProducts(
|
|
1091
|
+
query: CatalogQuery,
|
|
1092
|
+
options: { currencyCode?: string; regionId?: string } = {}
|
|
1093
|
+
): CatalogQueryOutcome {
|
|
1094
|
+
if (
|
|
1095
|
+
query.sort !== undefined &&
|
|
1096
|
+
!MOCK_CATALOG_SORT_KEYS.includes(query.sort)
|
|
1097
|
+
) {
|
|
1098
|
+
return unsupportedMockCatalogOutcome(['sort']);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
const region = getRegionFixture(options.regionId);
|
|
1102
|
+
const currencyCode = options.currencyCode ?? region.currencyCode;
|
|
1103
|
+
const allProducts = buildProducts(currencyCode);
|
|
1104
|
+
const facetSupport = buildMockFacetSupport(query);
|
|
1105
|
+
|
|
1106
|
+
// Reject every requested advanced field before the zero-result short-circuit,
|
|
1107
|
+
// so an unsupported filter is never hidden behind an empty intersection.
|
|
1108
|
+
const requestedAdvanced = collectRequestedAdvancedFields(query);
|
|
1109
|
+
const heldAdvanced = requestedAdvanced.filter(
|
|
1110
|
+
(field) => !facetSupport.offered.includes(advancedFieldToFacet(field))
|
|
1111
|
+
);
|
|
1112
|
+
if (heldAdvanced.length > 0) {
|
|
1113
|
+
return unsupportedMockCatalogOutcome(heldAdvanced);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
const effectiveCategoryHandles = resolveEffectiveCategoryHandles(query);
|
|
1117
|
+
const emptyIntersection =
|
|
1118
|
+
effectiveCategoryHandles !== undefined &&
|
|
1119
|
+
effectiveCategoryHandles.length === 0;
|
|
1120
|
+
const categoryFilterEmpty =
|
|
1121
|
+
effectiveCategoryHandles !== undefined &&
|
|
1122
|
+
!effectiveCategoryHandles.some((handle) =>
|
|
1123
|
+
categoryFixtures.some(
|
|
1124
|
+
(category) => category.handle === handle || category.id === handle
|
|
1125
|
+
)
|
|
1126
|
+
);
|
|
1127
|
+
const collectionFilterEmpty =
|
|
1128
|
+
query.collections !== undefined &&
|
|
1129
|
+
!query.collections.some((handle) =>
|
|
1130
|
+
collectionFixtures.some(
|
|
1131
|
+
(collection) =>
|
|
1132
|
+
collection.handle === handle || collection.id === handle
|
|
1133
|
+
)
|
|
1134
|
+
);
|
|
1135
|
+
|
|
1136
|
+
if (emptyIntersection || categoryFilterEmpty || collectionFilterEmpty) {
|
|
1137
|
+
return {
|
|
1138
|
+
ok: true,
|
|
1139
|
+
results: {
|
|
1140
|
+
products: [],
|
|
1141
|
+
appliedFilters: buildAppliedCatalogFilters(
|
|
1142
|
+
query,
|
|
1143
|
+
effectiveCategoryHandles
|
|
1144
|
+
),
|
|
1145
|
+
facetSupport,
|
|
1146
|
+
},
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
const filtered = filterProducts(allProducts, {
|
|
1151
|
+
query: query.text,
|
|
1152
|
+
categoryIds: effectiveCategoryHandles,
|
|
1153
|
+
collectionIds: query.collections,
|
|
1154
|
+
});
|
|
1155
|
+
const sorted = sortMockCatalogProducts(filtered, query.sort);
|
|
1156
|
+
|
|
1157
|
+
const pageSize = query.pageSize ?? MOCK_CATALOG_PAGE_SIZE;
|
|
1158
|
+
const page = query.page ?? 1;
|
|
1159
|
+
const offset = (page - 1) * pageSize;
|
|
1160
|
+
|
|
1161
|
+
return {
|
|
1162
|
+
ok: true,
|
|
1163
|
+
results: {
|
|
1164
|
+
products: sorted.slice(offset, offset + pageSize),
|
|
1165
|
+
// The full matching fixture count, never the page length.
|
|
1166
|
+
total: filtered.length,
|
|
1167
|
+
appliedFilters: buildAppliedCatalogFilters(
|
|
1168
|
+
query,
|
|
1169
|
+
effectiveCategoryHandles
|
|
1170
|
+
),
|
|
1171
|
+
facetSupport,
|
|
1172
|
+
},
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
|
|
677
1176
|
export function getMockProduct(handle: string, regionId?: string): Product | undefined {
|
|
678
1177
|
const region = getRegionFixture(regionId);
|
|
679
1178
|
return buildProducts(region.currencyCode).find(
|
|
@@ -813,6 +1312,17 @@ export function getMockPaymentMethods(currencyCode: string): PaymentMethod[] {
|
|
|
813
1312
|
requiresSetup: false,
|
|
814
1313
|
description: 'Mock cash payment option for checkout prototypes.',
|
|
815
1314
|
},
|
|
1315
|
+
{
|
|
1316
|
+
// The single approved manual TEST provider used by the real Medusa
|
|
1317
|
+
// backend, mirrored in mock mode so the guest TEST purchase path is
|
|
1318
|
+
// exercisable without a backend.
|
|
1319
|
+
id: 'pp_system_default',
|
|
1320
|
+
name: 'Manual TEST payment',
|
|
1321
|
+
type: 'cash_on_delivery',
|
|
1322
|
+
isEnabled: true,
|
|
1323
|
+
requiresSetup: false,
|
|
1324
|
+
description: 'Manual system TEST provider (bookkeeping only; not external funds).',
|
|
1325
|
+
},
|
|
816
1326
|
];
|
|
817
1327
|
}
|
|
818
1328
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @thorprovider/adapters — Cart completion contract
|
|
3
|
+
*
|
|
4
|
+
* Completion of a backend cart is discriminated (change B, decision 11): a
|
|
5
|
+
* native complete may return an order, a cart carrying a structured error, or
|
|
6
|
+
* an ambiguous/unresolved result whose outcome must be reconciled before any
|
|
7
|
+
* retry. This type is provider-agnostic and lives in adapters (not
|
|
8
|
+
* `@thorprovider/types`, which stays type-only and frozen).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Cart, Order } from '@thorprovider/types';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Structured completion error. `raw` carries the backend's own error object
|
|
15
|
+
* when one was supplied so the discriminator and details are preserved.
|
|
16
|
+
*/
|
|
17
|
+
export interface CartCompletionError {
|
|
18
|
+
/** Human-readable message. */
|
|
19
|
+
message: string;
|
|
20
|
+
|
|
21
|
+
/** Stable error code when the backend or transport supplies one. */
|
|
22
|
+
code?: string;
|
|
23
|
+
|
|
24
|
+
/** HTTP/transport status when one is known. */
|
|
25
|
+
status?: number;
|
|
26
|
+
|
|
27
|
+
/** Backend error details, carried through unchanged. */
|
|
28
|
+
raw?: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The discriminated result of completing a cart.
|
|
33
|
+
*
|
|
34
|
+
* - `order` — the cart was completed and an order was placed.
|
|
35
|
+
* - `cart` — the backend returned a cart with a structured error (the native
|
|
36
|
+
* HTTP 200 `type: "cart"` discriminator is preserved).
|
|
37
|
+
* - `unresolved` — the mutation's outcome is unknown (timeout/transport/5xx).
|
|
38
|
+
* Never blindly retry: reconcile the committed cart before another attempt.
|
|
39
|
+
*/
|
|
40
|
+
export type CartCompletionResult =
|
|
41
|
+
| { type: 'order'; order: Order }
|
|
42
|
+
| { type: 'cart'; cart: Cart; error: CartCompletionError }
|
|
43
|
+
| { type: 'unresolved'; error: CartCompletionError };
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Optional capability a provider may implement to expose the discriminated
|
|
47
|
+
* completion and reconciliation protocol. Providers that do not implement it
|
|
48
|
+
* still satisfy `CommerceProvider`; the runtime falls back to
|
|
49
|
+
* `completeCheckout` for them.
|
|
50
|
+
*/
|
|
51
|
+
export interface CartCompletionCapable {
|
|
52
|
+
/**
|
|
53
|
+
* Complete the cart once, preserving the native discriminator. Must not
|
|
54
|
+
* retry an ambiguous mutation.
|
|
55
|
+
*/
|
|
56
|
+
completeCart?(cartId: string): Promise<CartCompletionResult>;
|
|
57
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -22,3 +22,10 @@ export type {
|
|
|
22
22
|
UIDiscountCode,
|
|
23
23
|
UIPaymentMethod,
|
|
24
24
|
} from './ui-responses';
|
|
25
|
+
|
|
26
|
+
// Cart completion contract (discriminated native completion)
|
|
27
|
+
export type {
|
|
28
|
+
CartCompletionResult,
|
|
29
|
+
CartCompletionError,
|
|
30
|
+
CartCompletionCapable,
|
|
31
|
+
} from './completion';
|
package/.turbo/turbo-build.log
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
CLI Building entry: {"index":"src/index.ts"}
|
|
2
|
-
CLI Using tsconfig: tsconfig.json
|
|
3
|
-
CLI tsup v8.5.1
|
|
4
|
-
CLI Using tsup config: /home/nelson/dev/active/thorprovider/repos/thorcommerce/packages/adapters/tsup.config.ts
|
|
5
|
-
CLI Target: esnext
|
|
6
|
-
CLI Cleaning output folder
|
|
7
|
-
CJS Build start
|
|
8
|
-
ESM Build start
|
|
9
|
-
ESM dist/index.mjs 343.86 KB
|
|
10
|
-
ESM dist/index.mjs.map 702.37 KB
|
|
11
|
-
ESM ⚡️ Build success in 150ms
|
|
12
|
-
CJS dist/index.js 348.70 KB
|
|
13
|
-
CJS dist/index.js.map 702.75 KB
|
|
14
|
-
CJS ⚡️ Build success in 151ms
|
|
15
|
-
DTS Build start
|
|
16
|
-
DTS ⚡️ Build success in 11687ms
|
|
17
|
-
DTS dist/index.d.ts 99.44 KB
|
|
18
|
-
DTS dist/index.d.mts 99.44 KB
|