@thorprovider/adapters 4.2.0 → 4.3.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.
- package/CHANGELOG.md +6 -0
- package/PROVIDER_IMPLEMENTATION_ANALYSIS.md +659 -0
- package/README.md +19 -0
- package/dist/index.d.mts +249 -2
- package/dist/index.d.ts +249 -2
- package/dist/index.js +1450 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1448 -2
- package/dist/index.mjs.map +1 -1
- package/docs/RETRY_SOLUTION_SUMMARY.md +26 -11
- package/docs/authentication.md +115 -62
- package/docs/capabilities-system.md +34 -2
- package/docs/category-customization.md +37 -29
- package/docs/fetch-reliability-solution.md +127 -278
- package/docs/getting-started.md +43 -29
- package/docs/profile-management.md +68 -38
- package/docs/providers-overview.md +14 -0
- package/docs/services-overview.md +20 -2
- package/package.json +10 -6
- package/quick-reference.md +34 -0
- package/src/factory/index.ts +4 -0
- package/src/index.ts +3 -0
- package/src/providers/index.ts +3 -0
- package/src/providers/medusa/admin.test.ts +404 -0
- package/src/providers/medusa/admin.ts +442 -0
- package/src/providers/medusa/auth.test.ts +137 -0
- package/src/providers/medusa/auth.ts +96 -1
- package/src/providers/medusa/index.ts +21 -1
- package/src/providers/shopify/auth.ts +6 -0
- package/src/providers/stelorder/client.test-helpers.ts +3 -0
- package/src/providers/stelorder/client.test.ts +58 -0
- package/src/providers/stelorder/client.ts +326 -0
- package/src/providers/stelorder/index.test.ts +48 -0
- package/src/providers/stelorder/index.ts +685 -0
- package/src/providers/stelorder/live-smoke.test.ts +54 -0
- package/src/providers/stelorder/transforms.test.ts +103 -0
- package/src/providers/stelorder/transforms.ts +631 -0
- package/src/resilience/README.md +70 -48
- package/src/types/capabilities.types.ts +51 -0
- package/vitest.config.ts +13 -0
- package/vitest.setup.ts +6 -0
- package/.turbo/turbo-build.log +0 -18
- package/dist/.tsbuildinfo +0 -1
- package/src/core/errors.js +0 -161
- package/src/core/fetch-utils.js +0 -240
- package/src/core/logger.js +0 -63
- package/src/metadata/channel-scope-validation.js +0 -136
- package/src/metadata/channel-scope.js +0 -174
- package/src/metadata/index.js +0 -31
- package/src/providers/medusa/auth.js +0 -601
- package/src/providers/medusa/index.js +0 -2485
- package/src/providers/medusa/payment.js +0 -265
- package/src/providers/medusa/transforms.js +0 -505
- package/src/types/capabilities.types.js +0 -141
- package/src/types/provider.js +0 -9
- package/src/utils/payment-utils.js +0 -152
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BackendCapabilities,
|
|
3
|
+
Cart,
|
|
4
|
+
CartLineInput,
|
|
5
|
+
CartLineUpdate,
|
|
6
|
+
Collection,
|
|
7
|
+
CollectionProductsOptions,
|
|
8
|
+
Country,
|
|
9
|
+
Customer,
|
|
10
|
+
FulfillmentOption,
|
|
11
|
+
GetCategoriesOptions,
|
|
12
|
+
GetCollectionsOptions,
|
|
13
|
+
GetCustomersOptions,
|
|
14
|
+
GetOrdersOptions,
|
|
15
|
+
GetProductsOptions,
|
|
16
|
+
InventoryLevel,
|
|
17
|
+
Order,
|
|
18
|
+
PaymentMethod,
|
|
19
|
+
PaymentMethodsOptions,
|
|
20
|
+
Product,
|
|
21
|
+
ProductCategory,
|
|
22
|
+
ShippingMethod,
|
|
23
|
+
StockLocation,
|
|
24
|
+
StockValidation,
|
|
25
|
+
StorefrontContext,
|
|
26
|
+
} from '@thorprovider/types';
|
|
27
|
+
import type { CommerceProvider } from '../../types/provider';
|
|
28
|
+
import { STELORDER_CAPABILITIES } from '../../types/capabilities.types';
|
|
29
|
+
import { ProviderAPIError } from '../../core/errors';
|
|
30
|
+
import { createLogger } from '../../core/logger';
|
|
31
|
+
import { StelOrderClient, type StelOrderProviderConfig } from './client';
|
|
32
|
+
import {
|
|
33
|
+
buildStelHandle,
|
|
34
|
+
transformStelCategory,
|
|
35
|
+
transformStelCategoryToCollection,
|
|
36
|
+
transformStelClient,
|
|
37
|
+
transformStelDeliveryOption,
|
|
38
|
+
transformStelPaymentOption,
|
|
39
|
+
transformStelProduct,
|
|
40
|
+
transformStelProductWarehouseToInventoryLevel,
|
|
41
|
+
transformStelSalesOrder,
|
|
42
|
+
transformStelWarehouseToStockLocation,
|
|
43
|
+
type StelClient,
|
|
44
|
+
type StelDeliveryOption,
|
|
45
|
+
type StelPaymentOption,
|
|
46
|
+
type StelProduct,
|
|
47
|
+
type StelProductCategory,
|
|
48
|
+
type StelSalesOrder,
|
|
49
|
+
type StelWarehouse,
|
|
50
|
+
} from './transforms';
|
|
51
|
+
|
|
52
|
+
export type { StelOrderProviderConfig } from './client';
|
|
53
|
+
export { StelOrderClient } from './client';
|
|
54
|
+
export * from './transforms';
|
|
55
|
+
|
|
56
|
+
export class StelOrderProvider implements CommerceProvider {
|
|
57
|
+
readonly name = 'stelorder';
|
|
58
|
+
readonly version = '1.0.0';
|
|
59
|
+
readonly capabilities: BackendCapabilities = STELORDER_CAPABILITIES;
|
|
60
|
+
|
|
61
|
+
private readonly client: StelOrderClient;
|
|
62
|
+
private readonly logger: ReturnType<typeof createLogger>;
|
|
63
|
+
private readonly currencyCode: string;
|
|
64
|
+
private readonly storefrontId: string;
|
|
65
|
+
private readonly companyName: string;
|
|
66
|
+
|
|
67
|
+
constructor(config: StelOrderProviderConfig) {
|
|
68
|
+
this.client = new StelOrderClient(config);
|
|
69
|
+
this.logger = createLogger('StelOrderProvider', config.debug);
|
|
70
|
+
this.currencyCode = config.currencyCode ?? 'USD';
|
|
71
|
+
this.storefrontId = config.storefrontId ?? 'stelorder-primary';
|
|
72
|
+
this.companyName = config.companyName ?? 'Stel Order';
|
|
73
|
+
|
|
74
|
+
this.logger.info('Initializing Stel Order provider', {
|
|
75
|
+
storefrontId: this.storefrontId,
|
|
76
|
+
companyName: this.companyName,
|
|
77
|
+
currencyCode: this.currencyCode,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ============================================
|
|
82
|
+
// Product Operations
|
|
83
|
+
// ============================================
|
|
84
|
+
|
|
85
|
+
async getProduct(handle: string, _options?: { regionId?: string }): Promise<Product | undefined> {
|
|
86
|
+
this.logger.debug('getProduct', { handle });
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
// Attempt direct ID lookup if handle looks numeric
|
|
90
|
+
if (/^\d+$/.test(handle)) {
|
|
91
|
+
const raw = await this.client.getOne<StelProduct>('products', handle);
|
|
92
|
+
return raw ? transformStelProduct(raw, this.currencyCode) : undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Otherwise scan products to match by handle (slugified full-reference)
|
|
96
|
+
const rawProducts = await this.client.list<StelProduct>('products', { limit: 100 });
|
|
97
|
+
|
|
98
|
+
for (const raw of rawProducts) {
|
|
99
|
+
const productHandle = buildStelHandle(raw['full-reference'], raw.reference, raw.name, raw.id);
|
|
100
|
+
if (productHandle === handle) {
|
|
101
|
+
return transformStelProduct(raw, this.currencyCode);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return undefined;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
this.logger.error('getProduct failed', { handle, error });
|
|
108
|
+
throw this.wrapError('getProduct', error);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async getProducts(options?: GetProductsOptions): Promise<Product[]> {
|
|
113
|
+
this.logger.debug('getProducts', { options });
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const query: Record<string, string | number | boolean> = {};
|
|
117
|
+
|
|
118
|
+
if (options?.first) {
|
|
119
|
+
query.limit = options.first;
|
|
120
|
+
}
|
|
121
|
+
if (options?.skip) {
|
|
122
|
+
query.start = options.skip;
|
|
123
|
+
}
|
|
124
|
+
if (typeof options?.after === 'number') {
|
|
125
|
+
query.start = options.after;
|
|
126
|
+
}
|
|
127
|
+
if (options?.sortKey) {
|
|
128
|
+
query.sort = options.reverse ? `-${options.sortKey}` : options.sortKey;
|
|
129
|
+
}
|
|
130
|
+
if (options?.categoryIds?.length) {
|
|
131
|
+
query['product-category-id'] = `in:${options.categoryIds.join(',')}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const rawProducts = await this.client.list<StelProduct>('products', query);
|
|
135
|
+
|
|
136
|
+
let products = rawProducts
|
|
137
|
+
.filter((p) => !p.deleted && !p.inactive)
|
|
138
|
+
.map((p) => transformStelProduct(p, this.currencyCode));
|
|
139
|
+
|
|
140
|
+
// Client-side text search (STEL has no fulltext search)
|
|
141
|
+
if (options?.query) {
|
|
142
|
+
const needle = options.query.toLowerCase();
|
|
143
|
+
products = products.filter(
|
|
144
|
+
(p) =>
|
|
145
|
+
p.title.toLowerCase().includes(needle) ||
|
|
146
|
+
p.description.toLowerCase().includes(needle) ||
|
|
147
|
+
p.handle.includes(needle),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return products;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
this.logger.error('getProducts failed', { options, error });
|
|
154
|
+
throw this.wrapError('getProducts', error);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async getProductRecommendations(_productId: string): Promise<Product[]> {
|
|
159
|
+
// STEL has no recommendation engine — return empty array
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ============================================
|
|
164
|
+
// Category Operations
|
|
165
|
+
// ============================================
|
|
166
|
+
|
|
167
|
+
async getCategory(idOrHandle: string): Promise<ProductCategory | undefined> {
|
|
168
|
+
this.logger.debug('getCategory', { idOrHandle });
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
// Direct ID lookup
|
|
172
|
+
if (/^\d+$/.test(idOrHandle)) {
|
|
173
|
+
const raw = await this.client.getOne<StelProductCategory>('productCategories', idOrHandle);
|
|
174
|
+
return raw ? transformStelCategory(raw) : undefined;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Scan by handle
|
|
178
|
+
const rawCategories = await this.client.list<StelProductCategory>('productCategories');
|
|
179
|
+
|
|
180
|
+
for (const raw of rawCategories) {
|
|
181
|
+
const catHandle = buildStelHandle(raw.slug, raw.name, raw.id);
|
|
182
|
+
if (catHandle === idOrHandle) {
|
|
183
|
+
return transformStelCategory(raw);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return undefined;
|
|
188
|
+
} catch (error) {
|
|
189
|
+
this.logger.error('getCategory failed', { idOrHandle, error });
|
|
190
|
+
throw this.wrapError('getCategory', error);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async getCategories(options?: GetCategoriesOptions): Promise<ProductCategory[]> {
|
|
195
|
+
this.logger.debug('getCategories', { options });
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
if (options?.customFetcher) {
|
|
199
|
+
return options.customFetcher(options);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const query: Record<string, string | number> = {};
|
|
203
|
+
|
|
204
|
+
if (options?.limit) {
|
|
205
|
+
query.limit = options.limit;
|
|
206
|
+
}
|
|
207
|
+
if (options?.parentCategoryId) {
|
|
208
|
+
query['parent-category-id'] = options.parentCategoryId;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const rawCategories = await this.client.list<StelProductCategory>('productCategories', query);
|
|
212
|
+
let categories = rawCategories
|
|
213
|
+
.filter((c) => !c.deleted)
|
|
214
|
+
.map(transformStelCategory);
|
|
215
|
+
|
|
216
|
+
if (options?.query) {
|
|
217
|
+
const needle = options.query.toLowerCase();
|
|
218
|
+
categories = categories.filter(
|
|
219
|
+
(c) =>
|
|
220
|
+
c.name.toLowerCase().includes(needle) ||
|
|
221
|
+
c.handle.includes(needle),
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return categories;
|
|
226
|
+
} catch (error) {
|
|
227
|
+
this.logger.error('getCategories failed', { options, error });
|
|
228
|
+
throw this.wrapError('getCategories', error);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async getCategoryProducts(handle: string, options?: CollectionProductsOptions): Promise<Product[]> {
|
|
233
|
+
this.logger.debug('getCategoryProducts', { handle, options });
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
const category = await this.getCategory(handle);
|
|
237
|
+
|
|
238
|
+
if (!category) {
|
|
239
|
+
return [];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return this.getProducts({
|
|
243
|
+
categoryIds: [category.id],
|
|
244
|
+
first: options?.first,
|
|
245
|
+
sortKey: options?.sortKey,
|
|
246
|
+
reverse: options?.reverse,
|
|
247
|
+
after: options?.after,
|
|
248
|
+
});
|
|
249
|
+
} catch (error) {
|
|
250
|
+
this.logger.error('getCategoryProducts failed', { handle, error });
|
|
251
|
+
throw this.wrapError('getCategoryProducts', error);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ============================================
|
|
256
|
+
// Collection Operations (alias categories)
|
|
257
|
+
// ============================================
|
|
258
|
+
|
|
259
|
+
async getCollection(handle: string): Promise<Collection | undefined> {
|
|
260
|
+
this.logger.debug('getCollection', { handle });
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
if (/^\d+$/.test(handle)) {
|
|
264
|
+
const raw = await this.client.getOne<StelProductCategory>('productCategories', handle);
|
|
265
|
+
return raw ? transformStelCategoryToCollection(raw) : undefined;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const rawCategories = await this.client.list<StelProductCategory>('productCategories');
|
|
269
|
+
|
|
270
|
+
for (const raw of rawCategories) {
|
|
271
|
+
const catHandle = buildStelHandle(raw.slug, raw.name, raw.id);
|
|
272
|
+
if (catHandle === handle) {
|
|
273
|
+
return transformStelCategoryToCollection(raw);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return undefined;
|
|
278
|
+
} catch (error) {
|
|
279
|
+
this.logger.error('getCollection failed', { handle, error });
|
|
280
|
+
throw this.wrapError('getCollection', error);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async getCollections(options?: GetCollectionsOptions): Promise<Collection[]> {
|
|
285
|
+
this.logger.debug('getCollections', { options });
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
const query: Record<string, string | number> = {};
|
|
289
|
+
|
|
290
|
+
if (options?.limit) {
|
|
291
|
+
query.limit = options.limit;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const rawCategories = await this.client.list<StelProductCategory>('productCategories', query);
|
|
295
|
+
let collections = rawCategories
|
|
296
|
+
.filter((c) => !c.deleted)
|
|
297
|
+
.map(transformStelCategoryToCollection);
|
|
298
|
+
|
|
299
|
+
if (options?.query) {
|
|
300
|
+
const needle = options.query.toLowerCase();
|
|
301
|
+
collections = collections.filter(
|
|
302
|
+
(c) =>
|
|
303
|
+
c.title.toLowerCase().includes(needle) ||
|
|
304
|
+
c.handle.includes(needle),
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return collections;
|
|
309
|
+
} catch (error) {
|
|
310
|
+
this.logger.error('getCollections failed', { options, error });
|
|
311
|
+
throw this.wrapError('getCollections', error);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async getCollectionProducts(handle: string, options?: CollectionProductsOptions): Promise<Product[]> {
|
|
316
|
+
return this.getCategoryProducts(handle, options);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ============================================
|
|
320
|
+
// Inventory Operations
|
|
321
|
+
// ============================================
|
|
322
|
+
|
|
323
|
+
async getStockLocations(_params?: { salesChannelId?: string }): Promise<StockLocation[]> {
|
|
324
|
+
this.logger.debug('getStockLocations');
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
const rawWarehouses = await this.client.list<StelWarehouse>('warehouses');
|
|
328
|
+
return rawWarehouses
|
|
329
|
+
.filter((w) => !w.deleted)
|
|
330
|
+
.map(transformStelWarehouseToStockLocation);
|
|
331
|
+
} catch (error) {
|
|
332
|
+
this.logger.error('getStockLocations failed', { error });
|
|
333
|
+
throw this.wrapError('getStockLocations', error);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async checkInventoryLevels(variantId: string, _salesChannelId?: string): Promise<InventoryLevel[]> {
|
|
338
|
+
this.logger.debug('checkInventoryLevels', { variantId });
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
// variantId is the product ID (single synthetic variant per product)
|
|
342
|
+
const raw = await this.client.getOne<StelProduct>('products', variantId);
|
|
343
|
+
|
|
344
|
+
if (!raw?.['product-warehouses']?.length) {
|
|
345
|
+
return [];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return raw['product-warehouses'].map(transformStelProductWarehouseToInventoryLevel);
|
|
349
|
+
} catch (error) {
|
|
350
|
+
this.logger.error('checkInventoryLevels failed', { variantId, error });
|
|
351
|
+
throw this.wrapError('checkInventoryLevels', error);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
validateCartStock(_cartId: string): Promise<StockValidation> {
|
|
356
|
+
return this.notSupported('validateCartStock');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
getFulfillmentOptions(_cartId: string, _salesChannelId: string): Promise<FulfillmentOption[]> {
|
|
360
|
+
return this.notSupported('getFulfillmentOptions');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ============================================
|
|
364
|
+
// Cart Operations (unsupported — STEL is ERP, not storefront)
|
|
365
|
+
// ============================================
|
|
366
|
+
|
|
367
|
+
createCart(_options?: { regionId?: string }): Promise<Cart> {
|
|
368
|
+
return this.notSupported('createCart');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
getCart(_cartId: string): Promise<Cart | undefined> {
|
|
372
|
+
return this.notSupported('getCart');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
addToCart(_cartId: string, _lines: CartLineInput[]): Promise<Cart> {
|
|
376
|
+
return this.notSupported('addToCart');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
removeFromCart(_cartId: string, _lineIds: string[]): Promise<Cart> {
|
|
380
|
+
return this.notSupported('removeFromCart');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
updateCart(_cartId: string, _lines: CartLineUpdate[]): Promise<Cart> {
|
|
384
|
+
return this.notSupported('updateCart');
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ============================================
|
|
388
|
+
// Checkout Operations (unsupported)
|
|
389
|
+
// ============================================
|
|
390
|
+
|
|
391
|
+
completeCheckout(_cartId: string): Promise<Order> {
|
|
392
|
+
return this.notSupported('completeCheckout');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
getRegionCountries(_regionId: string): Promise<Country[]> {
|
|
396
|
+
return this.notSupported('getRegionCountries');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
addShippingMethod(_cartId: string, _methodId: string): Promise<Cart> {
|
|
400
|
+
return this.notSupported('addShippingMethod');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
initiatePaymentSession(_cartId: string, _providerId: string): Promise<void> {
|
|
404
|
+
return this.notSupported('initiatePaymentSession');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
applyDiscountCode(_cartId: string, _code: string): Promise<Cart> {
|
|
408
|
+
return this.notSupported('applyDiscountCode');
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
removeDiscountCode(_cartId: string, _code: string): Promise<Cart> {
|
|
412
|
+
return this.notSupported('removeDiscountCode');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ============================================
|
|
416
|
+
// Payment Methods
|
|
417
|
+
// ============================================
|
|
418
|
+
|
|
419
|
+
async getPaymentMethods(_options: PaymentMethodsOptions): Promise<PaymentMethod[]> {
|
|
420
|
+
this.logger.debug('getPaymentMethods');
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
const rawOptions = await this.client.list<StelPaymentOption>('paymentOptions');
|
|
424
|
+
return rawOptions
|
|
425
|
+
.filter((o) => !o.deleted)
|
|
426
|
+
.map(transformStelPaymentOption);
|
|
427
|
+
} catch (error) {
|
|
428
|
+
this.logger.error('getPaymentMethods failed', { error });
|
|
429
|
+
throw this.wrapError('getPaymentMethods', error);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ============================================
|
|
434
|
+
// Shipping / Delivery Methods
|
|
435
|
+
// ============================================
|
|
436
|
+
|
|
437
|
+
async getShippingMethods(_cartId: string): Promise<ShippingMethod[]> {
|
|
438
|
+
this.logger.debug('getShippingMethods (delivery options listing)');
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
const rawOptions = await this.client.list<StelDeliveryOption>('deliveryOptions');
|
|
442
|
+
return rawOptions
|
|
443
|
+
.filter((o) => !o.deleted)
|
|
444
|
+
.map((o) => transformStelDeliveryOption(o, this.currencyCode));
|
|
445
|
+
} catch (error) {
|
|
446
|
+
this.logger.error('getShippingMethods failed', { error });
|
|
447
|
+
throw this.wrapError('getShippingMethods', error);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ============================================
|
|
452
|
+
// Customer Operations (optional)
|
|
453
|
+
// ============================================
|
|
454
|
+
|
|
455
|
+
async getCustomer(customerId: string): Promise<Customer | undefined> {
|
|
456
|
+
this.logger.debug('getCustomer', { customerId });
|
|
457
|
+
|
|
458
|
+
try {
|
|
459
|
+
const raw = await this.client.getOne<StelClient>('clients', customerId);
|
|
460
|
+
return raw ? transformStelClient(raw) : undefined;
|
|
461
|
+
} catch (error) {
|
|
462
|
+
this.logger.error('getCustomer failed', { customerId, error });
|
|
463
|
+
throw this.wrapError('getCustomer', error);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async getCustomers(options?: GetCustomersOptions): Promise<Customer[]> {
|
|
468
|
+
this.logger.debug('getCustomers', { options });
|
|
469
|
+
|
|
470
|
+
try {
|
|
471
|
+
if (options?.customFetcher) {
|
|
472
|
+
return options.customFetcher(options);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const query: Record<string, string | number> = {};
|
|
476
|
+
|
|
477
|
+
if (options?.limit) {
|
|
478
|
+
query.limit = options.limit;
|
|
479
|
+
}
|
|
480
|
+
if (options?.offset) {
|
|
481
|
+
query.start = options.offset;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const rawClients = await this.client.list<StelClient>('clients', query);
|
|
485
|
+
let customers = rawClients
|
|
486
|
+
.filter((c) => !c.deleted)
|
|
487
|
+
.map(transformStelClient);
|
|
488
|
+
|
|
489
|
+
// Client-side search (STEL has no fulltext search for clients)
|
|
490
|
+
if (options?.search) {
|
|
491
|
+
const needle = options.search.toLowerCase();
|
|
492
|
+
customers = customers.filter(
|
|
493
|
+
(c) =>
|
|
494
|
+
c.email.toLowerCase().includes(needle) ||
|
|
495
|
+
(c.firstName ?? '').toLowerCase().includes(needle) ||
|
|
496
|
+
(c.lastName ?? '').toLowerCase().includes(needle),
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return customers;
|
|
501
|
+
} catch (error) {
|
|
502
|
+
this.logger.error('getCustomers failed', { options, error });
|
|
503
|
+
throw this.wrapError('getCustomers', error);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async updateCustomer(
|
|
508
|
+
customerId: string,
|
|
509
|
+
data: Partial<Omit<Customer, 'id' | 'createdAt'>>,
|
|
510
|
+
): Promise<Customer> {
|
|
511
|
+
this.logger.debug('updateCustomer', { customerId, data });
|
|
512
|
+
|
|
513
|
+
try {
|
|
514
|
+
const stelPayload: Record<string, unknown> = {};
|
|
515
|
+
|
|
516
|
+
if (data.firstName || data.lastName) {
|
|
517
|
+
stelPayload.name = [data.firstName, data.lastName].filter(Boolean).join(' ');
|
|
518
|
+
}
|
|
519
|
+
if (data.email) {
|
|
520
|
+
stelPayload.email = data.email;
|
|
521
|
+
}
|
|
522
|
+
if (data.phone) {
|
|
523
|
+
stelPayload.phone = data.phone;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const raw = await this.client.update<StelClient>('clients', customerId, stelPayload);
|
|
527
|
+
return transformStelClient(raw);
|
|
528
|
+
} catch (error) {
|
|
529
|
+
this.logger.error('updateCustomer failed', { customerId, error });
|
|
530
|
+
throw this.wrapError('updateCustomer', error);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// ============================================
|
|
535
|
+
// Order Operations (optional)
|
|
536
|
+
// ============================================
|
|
537
|
+
|
|
538
|
+
async getOrder(orderId: string): Promise<Order | undefined> {
|
|
539
|
+
this.logger.debug('getOrder', { orderId });
|
|
540
|
+
|
|
541
|
+
try {
|
|
542
|
+
const raw = await this.client.getOne<StelSalesOrder>('salesOrders', orderId);
|
|
543
|
+
return raw ? transformStelSalesOrder(raw, this.currencyCode) : undefined;
|
|
544
|
+
} catch (error) {
|
|
545
|
+
this.logger.error('getOrder failed', { orderId, error });
|
|
546
|
+
throw this.wrapError('getOrder', error);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async getOrders(options: GetOrdersOptions): Promise<Order[]> {
|
|
551
|
+
this.logger.debug('getOrders', { options });
|
|
552
|
+
|
|
553
|
+
try {
|
|
554
|
+
if (options?.customFetcher) {
|
|
555
|
+
return options.customFetcher(options);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const query: Record<string, string | number> = {};
|
|
559
|
+
|
|
560
|
+
if (options?.limit) {
|
|
561
|
+
query.limit = options.limit;
|
|
562
|
+
}
|
|
563
|
+
if (options?.offset) {
|
|
564
|
+
query.start = options.offset;
|
|
565
|
+
}
|
|
566
|
+
if (options?.customerId) {
|
|
567
|
+
query['client-id'] = options.customerId;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const rawOrders = await this.client.list<StelSalesOrder>('salesOrders', query);
|
|
571
|
+
return rawOrders
|
|
572
|
+
.filter((o) => !o.deleted)
|
|
573
|
+
.map((o) => transformStelSalesOrder(o, this.currencyCode));
|
|
574
|
+
} catch (error) {
|
|
575
|
+
this.logger.error('getOrders failed', { options, error });
|
|
576
|
+
throw this.wrapError('getOrders', error);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async getAdminOrders(options?: {
|
|
581
|
+
search?: string;
|
|
582
|
+
status?: string | string[];
|
|
583
|
+
limit?: number;
|
|
584
|
+
offset?: number;
|
|
585
|
+
}): Promise<{ orders: Order[]; count: number }> {
|
|
586
|
+
this.logger.debug('getAdminOrders', { options });
|
|
587
|
+
|
|
588
|
+
try {
|
|
589
|
+
const query: Record<string, string | number> = {};
|
|
590
|
+
|
|
591
|
+
if (options?.limit) {
|
|
592
|
+
query.limit = options.limit;
|
|
593
|
+
}
|
|
594
|
+
if (options?.offset) {
|
|
595
|
+
query.start = options.offset;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const rawOrders = await this.client.list<StelSalesOrder>('salesOrders', query);
|
|
599
|
+
let orders = rawOrders
|
|
600
|
+
.filter((o) => !o.deleted)
|
|
601
|
+
.map((o) => transformStelSalesOrder(o, this.currencyCode));
|
|
602
|
+
|
|
603
|
+
if (options?.search) {
|
|
604
|
+
const needle = options.search.toLowerCase();
|
|
605
|
+
orders = orders.filter(
|
|
606
|
+
(o) =>
|
|
607
|
+
o.orderNumber.toLowerCase().includes(needle) ||
|
|
608
|
+
o.email.toLowerCase().includes(needle),
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (options?.status) {
|
|
613
|
+
const statuses = Array.isArray(options.status) ? options.status : [options.status];
|
|
614
|
+
orders = orders.filter((o) => statuses.includes(o.status));
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return { orders, count: orders.length };
|
|
618
|
+
} catch (error) {
|
|
619
|
+
this.logger.error('getAdminOrders failed', { options, error });
|
|
620
|
+
throw this.wrapError('getAdminOrders', error);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
cancelOrder(_orderId: string): Promise<Order> {
|
|
625
|
+
return this.notSupported('cancelOrder');
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
createFulfillment(_orderId: string, _items?: Array<{ id: string; quantity: number }>): Promise<void> {
|
|
629
|
+
return this.notSupported('createFulfillment');
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
createRefund(_orderId: string, _amount?: number, _reason?: string): Promise<void> {
|
|
633
|
+
return this.notSupported('createRefund');
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// ============================================
|
|
637
|
+
// Storefront Context
|
|
638
|
+
// ============================================
|
|
639
|
+
|
|
640
|
+
async getStorefrontContext(): Promise<StorefrontContext> {
|
|
641
|
+
return {
|
|
642
|
+
id: this.storefrontId,
|
|
643
|
+
name: this.companyName,
|
|
644
|
+
platformType: 'stelorder',
|
|
645
|
+
requiresProductScoping: false,
|
|
646
|
+
currencyCode: this.currencyCode,
|
|
647
|
+
metadata: {
|
|
648
|
+
apiBaseUrl: this.client.apiBaseUrl,
|
|
649
|
+
singleCompany: true,
|
|
650
|
+
providerVersion: this.version,
|
|
651
|
+
},
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
isStorefrontRequired(): boolean {
|
|
656
|
+
return false;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// ============================================
|
|
660
|
+
// Internal helpers
|
|
661
|
+
// ============================================
|
|
662
|
+
|
|
663
|
+
private notSupported<T>(operation: string): Promise<T> {
|
|
664
|
+
return Promise.reject(
|
|
665
|
+
new ProviderAPIError(
|
|
666
|
+
`Stel Order does not support \`${operation}\`. STEL is an ERP adapter without cart/checkout/auth capabilities.`,
|
|
667
|
+
'stelorder',
|
|
668
|
+
501,
|
|
669
|
+
),
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
private wrapError(operation: string, error: unknown): Error {
|
|
674
|
+
if (error instanceof ProviderAPIError) {
|
|
675
|
+
return error;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
679
|
+
return new ProviderAPIError(
|
|
680
|
+
`Stel Order \`${operation}\` failed: ${message}`,
|
|
681
|
+
'stelorder',
|
|
682
|
+
500,
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
}
|