@thorprovider/adapters 4.1.0 → 4.2.0

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.
@@ -0,0 +1,917 @@
1
+ import type {
2
+ Cart,
3
+ CartItem,
4
+ Collection,
5
+ Country,
6
+ FulfillmentOption,
7
+ Image,
8
+ InventoryLevel,
9
+ Money,
10
+ Order,
11
+ PaymentMethod,
12
+ Product,
13
+ ProductCategory,
14
+ Region,
15
+ SelectedOption,
16
+ ShippingMethod,
17
+ StockLocation,
18
+ StockStatus,
19
+ StockValidation,
20
+ } from '@thorprovider/types';
21
+
22
+ const FIXTURE_TIMESTAMP = '2026-01-01T00:00:00.000Z';
23
+ const DEFAULT_SITE_URL = 'https://mock.thorcommerce.local';
24
+
25
+ interface MockRegionFixture {
26
+ id: string;
27
+ name: string;
28
+ currencyCode: string;
29
+ locale?: string;
30
+ taxRate?: number;
31
+ countries: Country[];
32
+ }
33
+
34
+ interface MockCollectionFixture {
35
+ id: string;
36
+ handle: string;
37
+ title: string;
38
+ description: string;
39
+ }
40
+
41
+ interface MockCategoryFixture {
42
+ id: string;
43
+ handle: string;
44
+ name: string;
45
+ description: string;
46
+ parentCategoryId?: string;
47
+ }
48
+
49
+ interface MockVariantFixture {
50
+ id: string;
51
+ title: string;
52
+ price: number;
53
+ compareAtPrice?: number;
54
+ quantityAvailable: number;
55
+ selectedOptions: SelectedOption[];
56
+ }
57
+
58
+ interface MockProductFixture {
59
+ id: string;
60
+ handle: string;
61
+ title: string;
62
+ description: string;
63
+ imageLabel: string;
64
+ imageHue: number;
65
+ collectionHandles: string[];
66
+ categoryHandle: string;
67
+ productType: string;
68
+ vendor: string;
69
+ tags: string[];
70
+ options: Array<{ id: string; name: string; values: string[] }>;
71
+ variants: MockVariantFixture[];
72
+ }
73
+
74
+ export interface MockCartState {
75
+ id: string;
76
+ regionId?: string;
77
+ lines: Array<{
78
+ id: string;
79
+ merchandiseId: string;
80
+ quantity: number;
81
+ }>;
82
+ shippingMethodId?: string;
83
+ paymentProviderId?: string;
84
+ discountCode?: string;
85
+ completedAt?: string;
86
+ }
87
+
88
+ const regionFixtures: MockRegionFixture[] = [
89
+ {
90
+ id: 'mock-region-us',
91
+ name: 'United States',
92
+ currencyCode: 'USD',
93
+ locale: 'en-US',
94
+ taxRate: 0.08,
95
+ countries: [{ code: 'US', name: 'United States' }],
96
+ },
97
+ {
98
+ id: 'mock-region-eu',
99
+ name: 'Europe',
100
+ currencyCode: 'EUR',
101
+ locale: 'en-ES',
102
+ taxRate: 0.21,
103
+ countries: [
104
+ { code: 'ES', name: 'Spain' },
105
+ { code: 'FR', name: 'France' },
106
+ { code: 'DE', name: 'Germany' },
107
+ ],
108
+ },
109
+ ];
110
+
111
+ const collectionFixtures: MockCollectionFixture[] = [
112
+ {
113
+ id: 'mock-collection-new-arrivals',
114
+ handle: 'new-arrivals',
115
+ title: 'New Arrivals',
116
+ description: 'Fresh mock catalog items for layout and merchandising flows.',
117
+ },
118
+ {
119
+ id: 'mock-collection-best-sellers',
120
+ handle: 'best-sellers',
121
+ title: 'Best Sellers',
122
+ description: 'High-performing mock products used to design hero grids and featured sections.',
123
+ },
124
+ {
125
+ id: 'mock-collection-studio-essentials',
126
+ handle: 'studio-essentials',
127
+ title: 'Studio Essentials',
128
+ description: 'A balanced fixture set for mock storefront navigation and detail pages.',
129
+ },
130
+ ];
131
+
132
+ const categoryFixtures: MockCategoryFixture[] = [
133
+ {
134
+ id: 'mock-category-jackets',
135
+ handle: 'jackets',
136
+ name: 'Jackets',
137
+ description: 'Outerwear and layering pieces.',
138
+ },
139
+ {
140
+ id: 'mock-category-dresses',
141
+ handle: 'dresses',
142
+ name: 'Dresses',
143
+ description: 'Editorial silhouettes for seasonal collections.',
144
+ },
145
+ {
146
+ id: 'mock-category-home',
147
+ handle: 'home',
148
+ name: 'Home',
149
+ description: 'Decor and living objects for lifestyle layouts.',
150
+ },
151
+ {
152
+ id: 'mock-category-accessories',
153
+ handle: 'accessories',
154
+ name: 'Accessories',
155
+ description: 'Complementary products for cart and bundle flows.',
156
+ },
157
+ ];
158
+
159
+ const productFixtures: MockProductFixture[] = [
160
+ {
161
+ id: 'mock-product-aurora-jacket',
162
+ handle: 'aurora-jacket',
163
+ title: 'Aurora Quilted Jacket',
164
+ description: 'Lightweight quilted outerwear with tonal hardware and a soft structured collar.',
165
+ imageLabel: 'Aurora Jacket',
166
+ imageHue: 222,
167
+ collectionHandles: ['new-arrivals', 'best-sellers'],
168
+ categoryHandle: 'jackets',
169
+ productType: 'outerwear',
170
+ vendor: 'Thor Atelier',
171
+ tags: ['outerwear', 'quilted', 'winter'],
172
+ options: [
173
+ { id: 'aurora-size', name: 'Size', values: ['S', 'M', 'L'] },
174
+ { id: 'aurora-color', name: 'Color', values: ['Midnight'] },
175
+ ],
176
+ variants: [
177
+ {
178
+ id: 'mock-variant-aurora-s',
179
+ title: 'S / Midnight',
180
+ price: 129,
181
+ compareAtPrice: 159,
182
+ quantityAvailable: 8,
183
+ selectedOptions: [
184
+ { name: 'Size', value: 'S' },
185
+ { name: 'Color', value: 'Midnight' },
186
+ ],
187
+ },
188
+ {
189
+ id: 'mock-variant-aurora-m',
190
+ title: 'M / Midnight',
191
+ price: 129,
192
+ compareAtPrice: 159,
193
+ quantityAvailable: 12,
194
+ selectedOptions: [
195
+ { name: 'Size', value: 'M' },
196
+ { name: 'Color', value: 'Midnight' },
197
+ ],
198
+ },
199
+ ],
200
+ },
201
+ {
202
+ id: 'mock-product-solis-dress',
203
+ handle: 'solis-dress',
204
+ title: 'Solis Pleated Dress',
205
+ description: 'A fluid pleated dress with a clean neckline designed for hero storytelling.',
206
+ imageLabel: 'Solis Dress',
207
+ imageHue: 18,
208
+ collectionHandles: ['new-arrivals', 'studio-essentials'],
209
+ categoryHandle: 'dresses',
210
+ productType: 'dress',
211
+ vendor: 'Thor Atelier',
212
+ tags: ['dress', 'occasion', 'pleated'],
213
+ options: [
214
+ { id: 'solis-size', name: 'Size', values: ['XS', 'S', 'M'] },
215
+ { id: 'solis-color', name: 'Color', values: ['Clay'] },
216
+ ],
217
+ variants: [
218
+ {
219
+ id: 'mock-variant-solis-xs',
220
+ title: 'XS / Clay',
221
+ price: 164,
222
+ quantityAvailable: 4,
223
+ selectedOptions: [
224
+ { name: 'Size', value: 'XS' },
225
+ { name: 'Color', value: 'Clay' },
226
+ ],
227
+ },
228
+ {
229
+ id: 'mock-variant-solis-s',
230
+ title: 'S / Clay',
231
+ price: 164,
232
+ quantityAvailable: 9,
233
+ selectedOptions: [
234
+ { name: 'Size', value: 'S' },
235
+ { name: 'Color', value: 'Clay' },
236
+ ],
237
+ },
238
+ ],
239
+ },
240
+ {
241
+ id: 'mock-product-helios-lamp',
242
+ handle: 'helios-lamp',
243
+ title: 'Helios Table Lamp',
244
+ description: 'Warm ambient lamp with sculptural proportions for lifestyle and home grids.',
245
+ imageLabel: 'Helios Lamp',
246
+ imageHue: 45,
247
+ collectionHandles: ['best-sellers', 'studio-essentials'],
248
+ categoryHandle: 'home',
249
+ productType: 'lighting',
250
+ vendor: 'Thor Home',
251
+ tags: ['home', 'lighting', 'decor'],
252
+ options: [{ id: 'helios-finish', name: 'Finish', values: ['Brass'] }],
253
+ variants: [
254
+ {
255
+ id: 'mock-variant-helios-brass',
256
+ title: 'Brass',
257
+ price: 96,
258
+ quantityAvailable: 15,
259
+ selectedOptions: [{ name: 'Finish', value: 'Brass' }],
260
+ },
261
+ ],
262
+ },
263
+ {
264
+ id: 'mock-product-luna-bag',
265
+ handle: 'luna-bag',
266
+ title: 'Luna Crossbody Bag',
267
+ description: 'Compact everyday bag for accessory rails, checkout add-ons, and bundle modules.',
268
+ imageLabel: 'Luna Bag',
269
+ imageHue: 306,
270
+ collectionHandles: ['studio-essentials'],
271
+ categoryHandle: 'accessories',
272
+ productType: 'bag',
273
+ vendor: 'Thor Atelier',
274
+ tags: ['bag', 'accessory', 'everyday'],
275
+ options: [{ id: 'luna-color', name: 'Color', values: ['Berry'] }],
276
+ variants: [
277
+ {
278
+ id: 'mock-variant-luna-berry',
279
+ title: 'Berry',
280
+ price: 82,
281
+ quantityAvailable: 18,
282
+ selectedOptions: [{ name: 'Color', value: 'Berry' }],
283
+ },
284
+ ],
285
+ },
286
+ ];
287
+
288
+ const stockLocationFixture: StockLocation = {
289
+ id: 'mock-stock-primary',
290
+ name: 'Mock Main Warehouse',
291
+ address: {
292
+ address1: '100 Mock Avenue',
293
+ city: 'Design City',
294
+ countryCode: 'US',
295
+ postalCode: '10001',
296
+ province: 'NY',
297
+ },
298
+ metadata: {
299
+ timezone: 'America/New_York',
300
+ },
301
+ };
302
+
303
+ const cartStore = new Map<string, MockCartState>();
304
+ const orderStore = new Map<string, Order>();
305
+
306
+ let sequence = 0;
307
+
308
+ function nextId(prefix: string): string {
309
+ sequence += 1;
310
+ return `${prefix}-${sequence}`;
311
+ }
312
+
313
+ function createMoney(amount: number, currencyCode: string): Money {
314
+ return {
315
+ amount: amount.toFixed(2),
316
+ currencyCode,
317
+ };
318
+ }
319
+
320
+ function createImage(label: string, hue: number): Image {
321
+ const svg = `
322
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 1200" role="img" aria-label="${label}">
323
+ <defs>
324
+ <linearGradient id="g" x1="0" x2="1" y1="0" y2="1">
325
+ <stop offset="0%" stop-color="hsl(${hue}, 72%, 72%)" />
326
+ <stop offset="100%" stop-color="hsl(${(hue + 48) % 360}, 62%, 42%)" />
327
+ </linearGradient>
328
+ </defs>
329
+ <rect width="1200" height="1200" fill="url(#g)" rx="48" />
330
+ <circle cx="910" cy="280" r="180" fill="rgba(255,255,255,0.22)" />
331
+ <text x="90" y="990" fill="white" font-family="Arial, sans-serif" font-size="96" font-weight="700">${label}</text>
332
+ </svg>
333
+ `.trim();
334
+
335
+ return {
336
+ url: `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svg)}`,
337
+ altText: label,
338
+ width: 1200,
339
+ height: 1200,
340
+ };
341
+ }
342
+
343
+ function getRegionFixture(regionId?: string): MockRegionFixture {
344
+ if (!regionId) {
345
+ return regionFixtures[0];
346
+ }
347
+
348
+ return regionFixtures.find((region) => region.id === regionId) ?? regionFixtures[0];
349
+ }
350
+
351
+ function createProductImageSet(label: string, hue: number): { featuredImage: Image; images: Image[] } {
352
+ const featuredImage = createImage(label, hue);
353
+
354
+ return {
355
+ featuredImage,
356
+ images: [featuredImage, createImage(`${label} Detail`, (hue + 18) % 360)],
357
+ };
358
+ }
359
+
360
+ function buildCollections(): Collection[] {
361
+ return collectionFixtures.map((collection) => ({
362
+ id: collection.id,
363
+ handle: collection.handle,
364
+ title: collection.title,
365
+ description: collection.description,
366
+ path: `/collections/${collection.handle}`,
367
+ updatedAt: FIXTURE_TIMESTAMP,
368
+ }));
369
+ }
370
+
371
+ function buildCategories(): ProductCategory[] {
372
+ return categoryFixtures.map((category) => ({
373
+ id: category.id,
374
+ handle: category.handle,
375
+ name: category.name,
376
+ description: category.description,
377
+ parentCategoryId: category.parentCategoryId,
378
+ productCount: productFixtures.filter((product) => product.categoryHandle === category.handle).length,
379
+ image: createImage(category.name, 180).url,
380
+ }));
381
+ }
382
+
383
+ function buildProducts(currencyCode: string): Product[] {
384
+ const collections = buildCollections();
385
+
386
+ return productFixtures.map((product) => {
387
+ const { featuredImage, images } = createProductImageSet(product.imageLabel, product.imageHue);
388
+ const variants = product.variants.map((variant) => ({
389
+ id: variant.id,
390
+ title: variant.title,
391
+ availableForSale: variant.quantityAvailable > 0,
392
+ selectedOptions: variant.selectedOptions,
393
+ price: createMoney(variant.price, currencyCode),
394
+ compareAtPrice: variant.compareAtPrice
395
+ ? createMoney(variant.compareAtPrice, currencyCode)
396
+ : undefined,
397
+ quantityAvailable: variant.quantityAvailable,
398
+ image: featuredImage,
399
+ }));
400
+
401
+ const prices = product.variants.map((variant) => variant.price);
402
+
403
+ return {
404
+ id: product.id,
405
+ handle: product.handle,
406
+ title: product.title,
407
+ description: product.description,
408
+ availableForSale: variants.some((variant) => variant.availableForSale),
409
+ options: product.options,
410
+ variants,
411
+ featuredImage,
412
+ images,
413
+ priceRange: {
414
+ minVariantPrice: createMoney(Math.min(...prices), currencyCode),
415
+ maxVariantPrice: createMoney(Math.max(...prices), currencyCode),
416
+ },
417
+ collections: product.collectionHandles
418
+ .map((handle) => collections.find((collection) => collection.handle === handle))
419
+ .filter((collection): collection is Collection => Boolean(collection))
420
+ .map((collection) => ({
421
+ id: collection.id,
422
+ title: collection.title,
423
+ handle: collection.handle,
424
+ })),
425
+ productType: product.productType,
426
+ vendor: product.vendor,
427
+ tags: product.tags,
428
+ createdAt: FIXTURE_TIMESTAMP,
429
+ updatedAt: FIXTURE_TIMESTAMP,
430
+ metadata: {
431
+ categoryHandle: product.categoryHandle,
432
+ collectionHandles: product.collectionHandles,
433
+ },
434
+ };
435
+ });
436
+ }
437
+
438
+ function filterProducts(products: Product[], options: {
439
+ query?: string;
440
+ categoryIds?: string[];
441
+ collectionIds?: string[];
442
+ }): Product[] {
443
+ const query = options.query?.trim().toLowerCase();
444
+ const categoryIds = new Set(options.categoryIds ?? []);
445
+ const collectionIds = new Set(options.collectionIds ?? []);
446
+
447
+ return products.filter((product) => {
448
+ if (query) {
449
+ const matchesQuery = [product.title, product.description, ...(product.tags ?? [])]
450
+ .join(' ')
451
+ .toLowerCase()
452
+ .includes(query);
453
+
454
+ if (!matchesQuery) {
455
+ return false;
456
+ }
457
+ }
458
+
459
+ if (categoryIds.size > 0) {
460
+ const categoryHandle = String(product.metadata?.categoryHandle ?? '');
461
+ const category = categoryFixtures.find((item) => item.handle === categoryHandle);
462
+ const matchesCategory = category
463
+ ? categoryIds.has(category.id) || categoryIds.has(category.handle)
464
+ : false;
465
+
466
+ if (!matchesCategory) {
467
+ return false;
468
+ }
469
+ }
470
+
471
+ if (collectionIds.size > 0) {
472
+ const productCollections = (product.collections ?? []).map((collection) => collection.handle);
473
+ const matchesCollection = (product.collections ?? []).some(
474
+ (collection) => collectionIds.has(collection.id) || collectionIds.has(collection.handle)
475
+ ) || productCollections.some((handle) => collectionIds.has(handle));
476
+
477
+ if (!matchesCollection) {
478
+ return false;
479
+ }
480
+ }
481
+
482
+ return true;
483
+ });
484
+ }
485
+
486
+ function sortProducts(products: Product[], sortKey?: string, reverse?: boolean): Product[] {
487
+ const sorted = [...products];
488
+
489
+ switch (sortKey) {
490
+ case 'price':
491
+ case 'price-asc':
492
+ case 'price-desc':
493
+ sorted.sort(
494
+ (left, right) =>
495
+ Number(left.priceRange.minVariantPrice.amount) - Number(right.priceRange.minVariantPrice.amount)
496
+ );
497
+ break;
498
+ case 'created_at':
499
+ case 'createdAt':
500
+ case 'newest':
501
+ sorted.sort((left, right) => left.createdAt.localeCompare(right.createdAt));
502
+ break;
503
+ case 'title':
504
+ case 'name':
505
+ default:
506
+ sorted.sort((left, right) => left.title.localeCompare(right.title));
507
+ break;
508
+ }
509
+
510
+ const shouldReverse = reverse || sortKey === 'price-desc' || sortKey === 'newest';
511
+ return shouldReverse ? sorted.reverse() : sorted;
512
+ }
513
+
514
+ function paginateProducts(products: Product[], skip?: number, first?: number): Product[] {
515
+ const start = Math.max(0, skip ?? 0);
516
+ const end = first ? start + first : undefined;
517
+ return products.slice(start, end);
518
+ }
519
+
520
+ function resolveVariant(currencyCode: string, merchandiseId: string): {
521
+ product: Product;
522
+ variant: Product['variants'][number];
523
+ } | undefined {
524
+ const products = buildProducts(currencyCode);
525
+
526
+ for (const product of products) {
527
+ const variant = product.variants.find((item) => item.id === merchandiseId);
528
+ if (variant) {
529
+ return { product, variant };
530
+ }
531
+ }
532
+
533
+ return undefined;
534
+ }
535
+
536
+ function getShippingPrice(methodId: string | undefined): number {
537
+ switch (methodId) {
538
+ case 'mock-shipping-express':
539
+ return 18;
540
+ case 'mock-shipping-standard':
541
+ return 8;
542
+ default:
543
+ return 0;
544
+ }
545
+ }
546
+
547
+ function buildExternalCart(state: MockCartState, currencyCode: string, siteUrl: string): Cart {
548
+ const region = getRegionFixture(state.regionId);
549
+ const regionCurrency = region.currencyCode || currencyCode;
550
+
551
+ const lines: CartItem[] = state.lines.flatMap((line) => {
552
+ const resolved = resolveVariant(regionCurrency, line.merchandiseId);
553
+ if (!resolved || !resolved.product.featuredImage) {
554
+ return [];
555
+ }
556
+
557
+ const lineTotal = Number(resolved.variant.price.amount) * line.quantity;
558
+ const stockStatus: StockStatus = resolved.variant.quantityAvailable && resolved.variant.quantityAvailable > 0
559
+ ? 'in_stock'
560
+ : 'out_of_stock';
561
+
562
+ return [{
563
+ id: line.id,
564
+ quantity: line.quantity,
565
+ stockStatus,
566
+ cost: {
567
+ totalAmount: createMoney(lineTotal, regionCurrency),
568
+ },
569
+ merchandise: {
570
+ id: resolved.variant.id,
571
+ title: resolved.variant.title,
572
+ selectedOptions: resolved.variant.selectedOptions,
573
+ product: {
574
+ id: resolved.product.id,
575
+ handle: resolved.product.handle,
576
+ title: resolved.product.title,
577
+ featuredImage: resolved.product.featuredImage,
578
+ },
579
+ },
580
+ }];
581
+ });
582
+
583
+ const subtotal = lines.reduce(
584
+ (sum, line) => sum + Number(line.cost.totalAmount.amount),
585
+ 0
586
+ );
587
+ const shipping = getShippingPrice(state.shippingMethodId);
588
+ const discount = state.discountCode ? subtotal * 0.1 : 0;
589
+ const taxableBase = Math.max(subtotal + shipping - discount, 0);
590
+ const totalTax = region.taxRate ? taxableBase * region.taxRate : 0;
591
+ const total = taxableBase + totalTax;
592
+
593
+ return {
594
+ id: state.id,
595
+ checkoutUrl: `${siteUrl}/checkout?cart=${state.id}`,
596
+ cost: {
597
+ subtotalAmount: createMoney(subtotal, regionCurrency),
598
+ totalAmount: createMoney(total, regionCurrency),
599
+ totalTaxAmount: createMoney(totalTax, regionCurrency),
600
+ },
601
+ lines,
602
+ totalQuantity: lines.reduce((sum, line) => sum + line.quantity, 0),
603
+ completedAt: state.completedAt,
604
+ };
605
+ }
606
+
607
+ function buildOrderFromCart(state: MockCartState, currencyCode: string, siteUrl: string): Order {
608
+ const cart = buildExternalCart(state, currencyCode, siteUrl);
609
+
610
+ return {
611
+ id: `mock-order-${state.id}`,
612
+ orderNumber: `${1000 + sequence}`,
613
+ email: 'mock-customer@thorprovider.dev',
614
+ status: 'completed',
615
+ paymentStatus: state.paymentProviderId ? 'paid' : 'pending',
616
+ fulfillmentStatus: state.shippingMethodId ? 'fulfilled' : 'unfulfilled',
617
+ items: cart.lines.map((line) => ({
618
+ id: line.id ?? nextId('mock-order-line'),
619
+ title: line.merchandise.product.title,
620
+ variantTitle: line.merchandise.title,
621
+ quantity: line.quantity,
622
+ price: line.merchandise.product.featuredImage
623
+ ? createMoney(Number(line.cost.totalAmount.amount) / Math.max(line.quantity, 1), cart.cost.totalAmount.currencyCode)
624
+ : createMoney(0, cart.cost.totalAmount.currencyCode),
625
+ total: line.cost.totalAmount,
626
+ productId: line.merchandise.product.id,
627
+ variantId: line.merchandise.id,
628
+ image: line.merchandise.product.featuredImage,
629
+ })),
630
+ subtotal: cart.cost.subtotalAmount,
631
+ total: cart.cost.totalAmount,
632
+ tax: cart.cost.totalTaxAmount,
633
+ shipping: createMoney(getShippingPrice(state.shippingMethodId), cart.cost.totalAmount.currencyCode),
634
+ discount: createMoney(state.discountCode ? Number(cart.cost.subtotalAmount.amount) * 0.1 : 0, cart.cost.totalAmount.currencyCode),
635
+ createdAt: FIXTURE_TIMESTAMP,
636
+ updatedAt: FIXTURE_TIMESTAMP,
637
+ completedAt: state.completedAt,
638
+ metadata: {
639
+ provider: 'mock',
640
+ checkoutUrl: cart.checkoutUrl,
641
+ },
642
+ };
643
+ }
644
+
645
+ export function getMockCollections(): Collection[] {
646
+ return buildCollections();
647
+ }
648
+
649
+ export function getMockCategories(): ProductCategory[] {
650
+ return buildCategories();
651
+ }
652
+
653
+ export function getMockProducts(options?: {
654
+ currencyCode?: string;
655
+ query?: string;
656
+ sortKey?: string;
657
+ reverse?: boolean;
658
+ first?: number;
659
+ skip?: number;
660
+ categoryIds?: string[];
661
+ collectionIds?: string[];
662
+ regionId?: string;
663
+ }): Product[] {
664
+ const region = getRegionFixture(options?.regionId);
665
+ const currencyCode = options?.currencyCode ?? region.currencyCode;
666
+ const products = buildProducts(currencyCode);
667
+ const filtered = filterProducts(products, {
668
+ query: options?.query,
669
+ categoryIds: options?.categoryIds,
670
+ collectionIds: options?.collectionIds,
671
+ });
672
+ const sorted = sortProducts(filtered, options?.sortKey, options?.reverse);
673
+
674
+ return paginateProducts(sorted, options?.skip, options?.first);
675
+ }
676
+
677
+ export function getMockProduct(handle: string, regionId?: string): Product | undefined {
678
+ const region = getRegionFixture(regionId);
679
+ return buildProducts(region.currencyCode).find(
680
+ (product) => product.handle === handle || product.id === handle
681
+ );
682
+ }
683
+
684
+ export function getMockProductRecommendations(productId: string, regionId?: string): Product[] {
685
+ const product = getMockProduct(productId, regionId)
686
+ ?? buildProducts(getRegionFixture(regionId).currencyCode).find((item) => item.id === productId);
687
+
688
+ if (!product) {
689
+ return [];
690
+ }
691
+
692
+ const collectionHandles = new Set((product.collections ?? []).map((collection) => collection.handle));
693
+
694
+ return buildProducts(getRegionFixture(regionId).currencyCode)
695
+ .filter((candidate) => candidate.id !== product.id)
696
+ .filter((candidate) =>
697
+ (candidate.collections ?? []).some((collection) => collectionHandles.has(collection.handle))
698
+ )
699
+ .slice(0, 4);
700
+ }
701
+
702
+ export function getMockCollection(handle: string): Collection | undefined {
703
+ return buildCollections().find(
704
+ (collection) => collection.handle === handle || collection.id === handle
705
+ );
706
+ }
707
+
708
+ export function getMockCategory(idOrHandle: string): ProductCategory | undefined {
709
+ return buildCategories().find(
710
+ (category) => category.handle === idOrHandle || category.id === idOrHandle
711
+ );
712
+ }
713
+
714
+ export function getMockRegions(): Array<{
715
+ id: string;
716
+ name: string;
717
+ currency_code: string;
718
+ countries: Array<{ id: string; iso_2: string; name: string; display_name?: string }>;
719
+ }> {
720
+ return regionFixtures.map((region) => ({
721
+ id: region.id,
722
+ name: region.name,
723
+ currency_code: region.currencyCode,
724
+ countries: region.countries.map((country) => ({
725
+ id: `country-${country.code.toLowerCase()}`,
726
+ iso_2: country.code.toLowerCase(),
727
+ name: country.name,
728
+ display_name: country.name,
729
+ })),
730
+ }));
731
+ }
732
+
733
+ export function getMockAdminRegions(): Region[] {
734
+ return regionFixtures.map((region) => ({
735
+ id: region.id,
736
+ name: region.name,
737
+ countryCodes: region.countries.map((country) => country.code),
738
+ currency: region.currencyCode,
739
+ taxRate: region.taxRate,
740
+ isEnabled: true,
741
+ }));
742
+ }
743
+
744
+ export function getMockRegionCountries(regionId: string): Country[] {
745
+ const region = regionFixtures.find((item) => item.id === regionId);
746
+ return region?.countries ?? [];
747
+ }
748
+
749
+ export function createMockCart(regionId?: string): MockCartState {
750
+ const cart: MockCartState = {
751
+ id: nextId('mock-cart'),
752
+ regionId: regionId ?? regionFixtures[0].id,
753
+ lines: [],
754
+ };
755
+
756
+ cartStore.set(cart.id, cart);
757
+ return cart;
758
+ }
759
+
760
+ export function getMockCartState(cartId: string): MockCartState | undefined {
761
+ return cartStore.get(cartId);
762
+ }
763
+
764
+ export function saveMockCartState(cart: MockCartState): MockCartState {
765
+ cartStore.set(cart.id, cart);
766
+ return cart;
767
+ }
768
+
769
+ export function buildMockCart(cart: MockCartState, currencyCode?: string, siteUrl?: string): Cart {
770
+ return buildExternalCart(
771
+ cart,
772
+ currencyCode ?? getRegionFixture(cart.regionId).currencyCode,
773
+ siteUrl ?? DEFAULT_SITE_URL
774
+ );
775
+ }
776
+
777
+ export function getMockShippingMethods(regionId?: string): ShippingMethod[] {
778
+ const currencyCode = getRegionFixture(regionId).currencyCode;
779
+
780
+ return [
781
+ {
782
+ id: 'mock-shipping-standard',
783
+ name: 'Standard Shipping',
784
+ description: 'Fixture shipping option for checkout and cart layouts.',
785
+ price: createMoney(8, currencyCode),
786
+ estimatedDays: '3-5 business days',
787
+ },
788
+ {
789
+ id: 'mock-shipping-express',
790
+ name: 'Express Shipping',
791
+ description: 'Faster fixture shipping option for premium checkout flows.',
792
+ price: createMoney(18, currencyCode),
793
+ estimatedDays: '1-2 business days',
794
+ },
795
+ ];
796
+ }
797
+
798
+ export function getMockPaymentMethods(currencyCode: string): PaymentMethod[] {
799
+ return [
800
+ {
801
+ id: 'mock-card',
802
+ name: 'Mock Card',
803
+ type: 'card',
804
+ isEnabled: true,
805
+ requiresSetup: true,
806
+ description: `Fixture card checkout for ${currencyCode}.`,
807
+ },
808
+ {
809
+ id: 'mock-cash',
810
+ name: 'Cash on Delivery',
811
+ type: 'cash_on_delivery',
812
+ isEnabled: true,
813
+ requiresSetup: false,
814
+ description: 'Mock cash payment option for checkout prototypes.',
815
+ },
816
+ ];
817
+ }
818
+
819
+ export function getMockStockLocations(): StockLocation[] {
820
+ return [stockLocationFixture];
821
+ }
822
+
823
+ export function getMockInventoryLevels(variantId: string): InventoryLevel[] {
824
+ for (const region of regionFixtures) {
825
+ const resolved = resolveVariant(region.currencyCode, variantId);
826
+ if (resolved) {
827
+ return [{
828
+ locationId: stockLocationFixture.id,
829
+ locationName: stockLocationFixture.name,
830
+ availableQuantity: resolved.variant.quantityAvailable ?? 0,
831
+ reservedQuantity: 0,
832
+ stockedQuantity: resolved.variant.quantityAvailable ?? 0,
833
+ }];
834
+ }
835
+ }
836
+
837
+ return [];
838
+ }
839
+
840
+ export function validateMockCartStock(cart: MockCartState): StockValidation {
841
+ const currencyCode = getRegionFixture(cart.regionId).currencyCode;
842
+ const unavailableItems = cart.lines.flatMap((line) => {
843
+ const resolved = resolveVariant(currencyCode, line.merchandiseId);
844
+ if (!resolved) {
845
+ return [];
846
+ }
847
+
848
+ const availableQuantity = resolved.variant.quantityAvailable ?? 0;
849
+
850
+ if (line.quantity <= availableQuantity) {
851
+ return [];
852
+ }
853
+
854
+ return [{
855
+ variantId: resolved.variant.id,
856
+ productName: resolved.product.title,
857
+ requestedQuantity: line.quantity,
858
+ availableQuantity,
859
+ }];
860
+ });
861
+
862
+ return {
863
+ allAvailable: unavailableItems.length === 0,
864
+ unavailableItems,
865
+ };
866
+ }
867
+
868
+ export function getMockFulfillmentOptions(regionId?: string): FulfillmentOption[] {
869
+ return getMockShippingMethods(regionId).map((method) => ({
870
+ id: method.id,
871
+ providerId: 'mock-fulfillment',
872
+ name: method.name,
873
+ description: method.description,
874
+ price: method.price,
875
+ estimatedDays: method.estimatedDays,
876
+ stockLocation: stockLocationFixture,
877
+ }));
878
+ }
879
+
880
+ export function completeMockCheckout(cart: MockCartState, currencyCode?: string, siteUrl?: string): Order {
881
+ const completedAt = new Date().toISOString();
882
+ const nextCart = {
883
+ ...cart,
884
+ completedAt,
885
+ };
886
+
887
+ cartStore.set(nextCart.id, nextCart);
888
+
889
+ const order = buildOrderFromCart(
890
+ nextCart,
891
+ currencyCode ?? getRegionFixture(nextCart.regionId).currencyCode,
892
+ siteUrl ?? DEFAULT_SITE_URL
893
+ );
894
+
895
+ orderStore.set(order.id, order);
896
+ return order;
897
+ }
898
+
899
+ export function getMockAdminOrders(): { orders: Order[]; count: number } {
900
+ const orders = Array.from(orderStore.values()).sort((left, right) =>
901
+ right.createdAt.localeCompare(left.createdAt)
902
+ );
903
+
904
+ return {
905
+ orders,
906
+ count: orders.length,
907
+ };
908
+ }
909
+
910
+ export function getMockOrder(orderId: string): Order | undefined {
911
+ return orderStore.get(orderId);
912
+ }
913
+
914
+ export function saveMockOrder(order: Order): Order {
915
+ orderStore.set(order.id, order);
916
+ return order;
917
+ }