@thorprovider/adapters 5.0.10 → 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 +9 -9
- package/.turbo/turbo-test.log +564 -39
- package/AGENTS.md +1 -1
- package/dist/.tsbuildinfo +1 -1
- 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 -1
- 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
|
@@ -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 {
|
|
@@ -160,6 +183,7 @@ const productFixtures: 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',
|
|
@@ -425,7 +452,7 @@ function buildProducts(currencyCode: string): Product[] {
|
|
|
425
452
|
productType: product.productType,
|
|
426
453
|
vendor: product.vendor,
|
|
427
454
|
tags: product.tags,
|
|
428
|
-
createdAt:
|
|
455
|
+
createdAt: product.createdAt,
|
|
429
456
|
updatedAt: FIXTURE_TIMESTAMP,
|
|
430
457
|
metadata: {
|
|
431
458
|
categoryHandle: product.categoryHandle,
|
|
@@ -517,6 +544,167 @@ function paginateProducts(products: Product[], skip?: number, first?: number): P
|
|
|
517
544
|
return products.slice(start, end);
|
|
518
545
|
}
|
|
519
546
|
|
|
547
|
+
/**
|
|
548
|
+
* Canonical sort keys the mock catalog can truthfully apply (E2 task 2.5).
|
|
549
|
+
*
|
|
550
|
+
* Mirrors the Medusa advertisement: `featured` (stable fixture order, the
|
|
551
|
+
* canonical default), `name` (title ascending), and `newest` (fixture-supplied
|
|
552
|
+
* `createdAt` descending). No global price or rating ordering is advertised.
|
|
553
|
+
*/
|
|
554
|
+
export const MOCK_CATALOG_SORT_KEYS: readonly CatalogSortKey[] = [
|
|
555
|
+
'featured',
|
|
556
|
+
'name',
|
|
557
|
+
'newest',
|
|
558
|
+
];
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* The mock catalog's demonstration endpoint identity.
|
|
562
|
+
*
|
|
563
|
+
* Explicitly mock-scoped so a support descriptor can never be mistaken for a
|
|
564
|
+
* live Medusa endpoint. The mock is the basic fixture listing; advanced facets
|
|
565
|
+
* are held.
|
|
566
|
+
*/
|
|
567
|
+
const MOCK_CATALOG_ENDPOINT = 'mock://catalog/products';
|
|
568
|
+
|
|
569
|
+
/** Canonical default page size, matching the Medusa configured bound. */
|
|
570
|
+
const MOCK_CATALOG_PAGE_SIZE = 50;
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Effective category scope for a canonical query.
|
|
574
|
+
*
|
|
575
|
+
* Mirrors the Medusa contract: the route `categoryScope` is always retained and
|
|
576
|
+
* the `categories` filter narrows within it. Returns `undefined` when no
|
|
577
|
+
* category filter is requested, `[]` for an explicit empty intersection (a valid
|
|
578
|
+
* zero-result query), or the handles to resolve against the fixtures.
|
|
579
|
+
*/
|
|
580
|
+
function resolveEffectiveCategoryHandles(
|
|
581
|
+
query: CatalogQuery
|
|
582
|
+
): string[] | undefined {
|
|
583
|
+
const { categoryScope, categories } = query;
|
|
584
|
+
if (categoryScope === undefined) return categories;
|
|
585
|
+
if (categories === undefined) return [categoryScope];
|
|
586
|
+
return categories.filter((handle) => handle === categoryScope);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/** Canonical query fields that require advanced filtering the mock does not implement. */
|
|
590
|
+
function collectRequestedAdvancedFields(query: CatalogQuery): string[] {
|
|
591
|
+
const fields: string[] = [];
|
|
592
|
+
if (query.priceMin !== undefined) fields.push('priceMin');
|
|
593
|
+
if (query.priceMax !== undefined) fields.push('priceMax');
|
|
594
|
+
if (query.inStock !== undefined) fields.push('inStock');
|
|
595
|
+
for (const name of Object.keys(query.attributes ?? {})) {
|
|
596
|
+
fields.push(`attr.${name}`);
|
|
597
|
+
}
|
|
598
|
+
return fields;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/** Map a requested advanced field name to the canonical facet id it needs. */
|
|
602
|
+
function advancedFieldToFacet(field: string): CatalogFacetId {
|
|
603
|
+
if (field === 'priceMin' || field === 'priceMax') return 'price';
|
|
604
|
+
return field as CatalogFacetId;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Filters a successful (or explicitly-empty) canonical result reports as
|
|
609
|
+
* applied. The mock implements only the basic filters (text/categories/
|
|
610
|
+
* collections); requested advanced filters never reach a success path.
|
|
611
|
+
*/
|
|
612
|
+
function buildAppliedCatalogFilters(
|
|
613
|
+
query: CatalogQuery,
|
|
614
|
+
effectiveCategoryHandles: string[] | undefined
|
|
615
|
+
): CatalogAppliedFilters {
|
|
616
|
+
return {
|
|
617
|
+
...(query.text ? { text: query.text } : {}),
|
|
618
|
+
...(effectiveCategoryHandles !== undefined
|
|
619
|
+
? { categories: effectiveCategoryHandles }
|
|
620
|
+
: {}),
|
|
621
|
+
...(query.collections !== undefined
|
|
622
|
+
? { collections: query.collections }
|
|
623
|
+
: {}),
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** Basic fields the mock listing actually accepts, recorded on the demonstration. */
|
|
628
|
+
function buildBasicDemonstratedQuery(query: CatalogQuery): CatalogQuery {
|
|
629
|
+
return {
|
|
630
|
+
...(query.text !== undefined ? { text: query.text } : {}),
|
|
631
|
+
...(query.categoryScope !== undefined
|
|
632
|
+
? { categoryScope: query.categoryScope }
|
|
633
|
+
: {}),
|
|
634
|
+
...(query.categories !== undefined ? { categories: query.categories } : {}),
|
|
635
|
+
...(query.collections !== undefined
|
|
636
|
+
? { collections: query.collections }
|
|
637
|
+
: {}),
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Truthful facet-support descriptor for the mock.
|
|
643
|
+
*
|
|
644
|
+
* `categories`/`collections` are offered with the mock listing as their
|
|
645
|
+
* demonstration source; `price`/`inStock` are held with a real reason because
|
|
646
|
+
* the mock does not implement advanced filtering. No count or range is
|
|
647
|
+
* synthesized and the endpoint is explicitly mock-scoped.
|
|
648
|
+
*/
|
|
649
|
+
function buildMockFacetSupport(query: CatalogQuery): FacetSupport {
|
|
650
|
+
const appliedQuery = buildBasicDemonstratedQuery(query);
|
|
651
|
+
const scopeField =
|
|
652
|
+
query.categoryScope !== undefined ? { scope: query.categoryScope } : {};
|
|
653
|
+
|
|
654
|
+
return {
|
|
655
|
+
offered: ['categories', 'collections'],
|
|
656
|
+
held: ['price', 'inStock'],
|
|
657
|
+
demonstrations: {
|
|
658
|
+
categories: {
|
|
659
|
+
endpoint: MOCK_CATALOG_ENDPOINT,
|
|
660
|
+
appliedQuery,
|
|
661
|
+
...scopeField,
|
|
662
|
+
},
|
|
663
|
+
collections: {
|
|
664
|
+
endpoint: MOCK_CATALOG_ENDPOINT,
|
|
665
|
+
appliedQuery,
|
|
666
|
+
...scopeField,
|
|
667
|
+
},
|
|
668
|
+
},
|
|
669
|
+
heldReasons: {
|
|
670
|
+
price: `${MOCK_CATALOG_ENDPOINT} is the mock basic listing; price filtering is not implemented in mock mode`,
|
|
671
|
+
inStock: `${MOCK_CATALOG_ENDPOINT} is the mock basic listing; stock filtering is not implemented in mock mode`,
|
|
672
|
+
},
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/** Build a typed `unsupported-query` outcome naming the offending fields. */
|
|
677
|
+
function unsupportedMockCatalogOutcome(fields: string[]): CatalogQueryOutcome {
|
|
678
|
+
return {
|
|
679
|
+
ok: false,
|
|
680
|
+
error: {
|
|
681
|
+
code: 'unsupported-query',
|
|
682
|
+
fields,
|
|
683
|
+
message: `Unsupported catalog query in mock mode: ${fields.join(', ')}`,
|
|
684
|
+
repair: { remove: fields },
|
|
685
|
+
},
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Apply the canonical ordering globally, before pagination.
|
|
691
|
+
*
|
|
692
|
+
* `featured` preserves the stable fixture order; `name` sorts by title
|
|
693
|
+
* ascending; `newest` sorts by the fixture-supplied `createdAt` descending.
|
|
694
|
+
*/
|
|
695
|
+
function sortMockCatalogProducts(
|
|
696
|
+
products: Product[],
|
|
697
|
+
sortKey?: CatalogSortKey
|
|
698
|
+
): Product[] {
|
|
699
|
+
const sorted = [...products];
|
|
700
|
+
if (sortKey === 'name') {
|
|
701
|
+
sorted.sort((left, right) => left.title.localeCompare(right.title));
|
|
702
|
+
} else if (sortKey === 'newest') {
|
|
703
|
+
sorted.sort((left, right) => right.createdAt.localeCompare(left.createdAt));
|
|
704
|
+
}
|
|
705
|
+
return sorted;
|
|
706
|
+
}
|
|
707
|
+
|
|
520
708
|
function resolveVariant(currencyCode: string, merchandiseId: string): {
|
|
521
709
|
product: Product;
|
|
522
710
|
variant: Product['variants'][number];
|
|
@@ -595,11 +783,22 @@ function buildExternalCart(state: MockCartState, currencyCode: string, siteUrl:
|
|
|
595
783
|
checkoutUrl: `${siteUrl}/checkout?cart=${state.id}`,
|
|
596
784
|
cost: {
|
|
597
785
|
subtotalAmount: createMoney(subtotal, regionCurrency),
|
|
598
|
-
|
|
786
|
+
discountAmount: createMoney(discount, regionCurrency),
|
|
599
787
|
totalTaxAmount: createMoney(totalTax, regionCurrency),
|
|
788
|
+
shippingAmount: createMoney(shipping, regionCurrency),
|
|
789
|
+
totalAmount: createMoney(total, regionCurrency),
|
|
600
790
|
},
|
|
601
791
|
lines,
|
|
602
792
|
totalQuantity: lines.reduce((sum, line) => sum + line.quantity, 0),
|
|
793
|
+
salesChannelId: MOCK_SALES_CHANNEL_ID,
|
|
794
|
+
regionId: state.regionId,
|
|
795
|
+
currencyCode: regionCurrency.toUpperCase(),
|
|
796
|
+
selectedShippingMethodId: state.shippingMethodId,
|
|
797
|
+
// The mock method id IS the shipping option id (the value returned by
|
|
798
|
+
// `getMockShippingMethods` and accepted by `addShippingMethod`), so the
|
|
799
|
+
// canonical option identity mirrors the real adapter's `so_` field and the
|
|
800
|
+
// checkout UI can highlight the selected option.
|
|
801
|
+
selectedShippingOptionId: state.shippingMethodId,
|
|
603
802
|
completedAt: state.completedAt,
|
|
604
803
|
};
|
|
605
804
|
}
|
|
@@ -674,6 +873,118 @@ export function getMockProducts(options?: {
|
|
|
674
873
|
return paginateProducts(sorted, options?.skip, options?.first);
|
|
675
874
|
}
|
|
676
875
|
|
|
876
|
+
/**
|
|
877
|
+
* Canonical catalog query for the mock provider — E2 `storefront-catalog-search-filters`
|
|
878
|
+
* task 2.5.
|
|
879
|
+
*
|
|
880
|
+
* A faithful stand-in for the Medusa canonical contract so mock mode exercises
|
|
881
|
+
* the same behavior:
|
|
882
|
+
*
|
|
883
|
+
* - The route `categoryScope` is always retained and intersected with the
|
|
884
|
+
* `categories` filter; an explicit empty intersection (or a requested
|
|
885
|
+
* category/collection handle that matches no fixture) is a valid zero-result
|
|
886
|
+
* query, never an unfiltered request.
|
|
887
|
+
* - Requested category/collection handles resolve against the fixtures by id or
|
|
888
|
+
* handle; text matching is case-insensitive over title, description, and tags.
|
|
889
|
+
* - Ordering is global, before pagination: `featured` preserves the stable
|
|
890
|
+
* fixture order, `name` sorts by title ascending, and `newest` sorts by the
|
|
891
|
+
* fixture-supplied `createdAt` descending. Only the advertised
|
|
892
|
+
* `MOCK_CATALOG_SORT_KEYS` are accepted; any other sort is a typed
|
|
893
|
+
* `unsupported-query`.
|
|
894
|
+
* - `total` is the full matching fixture count (never the page length) and is
|
|
895
|
+
* present whenever a listing is returned.
|
|
896
|
+
* - Advanced filters (price/stock/attributes) are held and rejected as a typed
|
|
897
|
+
* `unsupported-query` **before** any zero-result short-circuit, so a requested
|
|
898
|
+
* filter is never silently dropped and a partial result is never labelled
|
|
899
|
+
* success. The attached `facetSupport` marks its demonstration endpoint as
|
|
900
|
+
* mock-scoped and never claims Medusa support.
|
|
901
|
+
*/
|
|
902
|
+
export function getMockCatalogProducts(
|
|
903
|
+
query: CatalogQuery,
|
|
904
|
+
options: { currencyCode?: string; regionId?: string } = {}
|
|
905
|
+
): CatalogQueryOutcome {
|
|
906
|
+
if (
|
|
907
|
+
query.sort !== undefined &&
|
|
908
|
+
!MOCK_CATALOG_SORT_KEYS.includes(query.sort)
|
|
909
|
+
) {
|
|
910
|
+
return unsupportedMockCatalogOutcome(['sort']);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const region = getRegionFixture(options.regionId);
|
|
914
|
+
const currencyCode = options.currencyCode ?? region.currencyCode;
|
|
915
|
+
const allProducts = buildProducts(currencyCode);
|
|
916
|
+
const facetSupport = buildMockFacetSupport(query);
|
|
917
|
+
|
|
918
|
+
// Reject every requested advanced field before the zero-result short-circuit,
|
|
919
|
+
// so an unsupported filter is never hidden behind an empty intersection.
|
|
920
|
+
const requestedAdvanced = collectRequestedAdvancedFields(query);
|
|
921
|
+
const heldAdvanced = requestedAdvanced.filter(
|
|
922
|
+
(field) => !facetSupport.offered.includes(advancedFieldToFacet(field))
|
|
923
|
+
);
|
|
924
|
+
if (heldAdvanced.length > 0) {
|
|
925
|
+
return unsupportedMockCatalogOutcome(heldAdvanced);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
const effectiveCategoryHandles = resolveEffectiveCategoryHandles(query);
|
|
929
|
+
const emptyIntersection =
|
|
930
|
+
effectiveCategoryHandles !== undefined &&
|
|
931
|
+
effectiveCategoryHandles.length === 0;
|
|
932
|
+
const categoryFilterEmpty =
|
|
933
|
+
effectiveCategoryHandles !== undefined &&
|
|
934
|
+
!effectiveCategoryHandles.some((handle) =>
|
|
935
|
+
categoryFixtures.some(
|
|
936
|
+
(category) => category.handle === handle || category.id === handle
|
|
937
|
+
)
|
|
938
|
+
);
|
|
939
|
+
const collectionFilterEmpty =
|
|
940
|
+
query.collections !== undefined &&
|
|
941
|
+
!query.collections.some((handle) =>
|
|
942
|
+
collectionFixtures.some(
|
|
943
|
+
(collection) =>
|
|
944
|
+
collection.handle === handle || collection.id === handle
|
|
945
|
+
)
|
|
946
|
+
);
|
|
947
|
+
|
|
948
|
+
if (emptyIntersection || categoryFilterEmpty || collectionFilterEmpty) {
|
|
949
|
+
return {
|
|
950
|
+
ok: true,
|
|
951
|
+
results: {
|
|
952
|
+
products: [],
|
|
953
|
+
appliedFilters: buildAppliedCatalogFilters(
|
|
954
|
+
query,
|
|
955
|
+
effectiveCategoryHandles
|
|
956
|
+
),
|
|
957
|
+
facetSupport,
|
|
958
|
+
},
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
const filtered = filterProducts(allProducts, {
|
|
963
|
+
query: query.text,
|
|
964
|
+
categoryIds: effectiveCategoryHandles,
|
|
965
|
+
collectionIds: query.collections,
|
|
966
|
+
});
|
|
967
|
+
const sorted = sortMockCatalogProducts(filtered, query.sort);
|
|
968
|
+
|
|
969
|
+
const pageSize = query.pageSize ?? MOCK_CATALOG_PAGE_SIZE;
|
|
970
|
+
const page = query.page ?? 1;
|
|
971
|
+
const offset = (page - 1) * pageSize;
|
|
972
|
+
|
|
973
|
+
return {
|
|
974
|
+
ok: true,
|
|
975
|
+
results: {
|
|
976
|
+
products: sorted.slice(offset, offset + pageSize),
|
|
977
|
+
// The full matching fixture count, never the page length.
|
|
978
|
+
total: filtered.length,
|
|
979
|
+
appliedFilters: buildAppliedCatalogFilters(
|
|
980
|
+
query,
|
|
981
|
+
effectiveCategoryHandles
|
|
982
|
+
),
|
|
983
|
+
facetSupport,
|
|
984
|
+
},
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
|
|
677
988
|
export function getMockProduct(handle: string, regionId?: string): Product | undefined {
|
|
678
989
|
const region = getRegionFixture(regionId);
|
|
679
990
|
return buildProducts(region.currencyCode).find(
|
|
@@ -813,6 +1124,17 @@ export function getMockPaymentMethods(currencyCode: string): PaymentMethod[] {
|
|
|
813
1124
|
requiresSetup: false,
|
|
814
1125
|
description: 'Mock cash payment option for checkout prototypes.',
|
|
815
1126
|
},
|
|
1127
|
+
{
|
|
1128
|
+
// The single approved manual TEST provider used by the real Medusa
|
|
1129
|
+
// backend, mirrored in mock mode so the guest TEST purchase path is
|
|
1130
|
+
// exercisable without a backend.
|
|
1131
|
+
id: 'pp_system_default',
|
|
1132
|
+
name: 'Manual TEST payment',
|
|
1133
|
+
type: 'cash_on_delivery',
|
|
1134
|
+
isEnabled: true,
|
|
1135
|
+
requiresSetup: false,
|
|
1136
|
+
description: 'Manual system TEST provider (bookkeeping only; not external funds).',
|
|
1137
|
+
},
|
|
816
1138
|
];
|
|
817
1139
|
}
|
|
818
1140
|
|
|
@@ -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';
|