@reactionary/source 0.3.16 → 0.3.18

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.
Files changed (37) hide show
  1. package/README.md +2 -2
  2. package/core/src/initialization.ts +2 -2
  3. package/core/src/providers/price.provider.ts +2 -1
  4. package/core/src/schemas/models/cart.model.ts +7 -2
  5. package/core/src/schemas/models/identifiers.model.ts +12 -8
  6. package/core/src/schemas/models/price.model.ts +12 -0
  7. package/examples/node/package.json +7 -7
  8. package/examples/node/src/capabilities/cart.spec.ts +97 -15
  9. package/examples/node/src/capabilities/category.spec.ts +27 -32
  10. package/examples/node/src/capabilities/checkout.spec.ts +5 -5
  11. package/examples/node/src/capabilities/identity.spec.ts +6 -2
  12. package/examples/node/src/capabilities/inventory.spec.ts +1 -1
  13. package/examples/node/src/capabilities/price.spec.ts +7 -7
  14. package/examples/node/src/utils.ts +4 -1
  15. package/package.json +3 -3
  16. package/providers/algolia/src/providers/product-search.provider.ts +19 -14
  17. package/providers/commercetools/src/core/client.ts +112 -9
  18. package/providers/commercetools/src/core/token-cache.ts +4 -5
  19. package/providers/commercetools/src/providers/cart.provider.ts +76 -11
  20. package/providers/commercetools/src/providers/inventory.provider.ts +5 -7
  21. package/providers/commercetools/src/providers/price.provider.ts +17 -30
  22. package/providers/commercetools/src/schema/configuration.schema.ts +4 -0
  23. package/providers/commercetools/src/schema/session.schema.ts +3 -1
  24. package/providers/fake/src/providers/cart.provider.ts +1 -0
  25. package/providers/fake/src/providers/price.provider.ts +54 -95
  26. package/providers/medusa/src/providers/cart.provider.ts +159 -70
  27. package/providers/medusa/src/providers/category.provider.ts +35 -23
  28. package/providers/medusa/src/providers/checkout.provider.ts +78 -41
  29. package/providers/medusa/src/providers/order-search.provider.ts +21 -10
  30. package/providers/medusa/src/providers/price.provider.ts +18 -9
  31. package/providers/medusa/src/providers/product-recommendations.provider.ts +10 -6
  32. package/providers/medusa/src/providers/product-search.provider.ts +19 -10
  33. package/providers/medusa/src/providers/product.provider.ts +20 -12
  34. package/providers/medusa/src/providers/profile.provider.ts +38 -13
  35. package/providers/meilisearch/src/providers/order-search.provider.ts +17 -12
  36. package/providers/meilisearch/src/providers/product-recommendations.provider.ts +10 -11
  37. package/providers/meilisearch/src/providers/product-search.provider.ts +23 -18
@@ -112,7 +112,7 @@ export function createClient(provider: PrimaryProvider) {
112
112
  productAssociations: true,
113
113
  orderSearch: true,
114
114
  store: true,
115
- profile: true
115
+ profile: true,
116
116
  })
117
117
  );
118
118
  }
@@ -120,6 +120,9 @@ export function createClient(provider: PrimaryProvider) {
120
120
  if (provider === PrimaryProvider.FAKE) {
121
121
  builder = builder.withCapability(
122
122
  withFakeCapabilities( getFakeConfiguration() , {
123
+ price: true,
124
+ inventory: true,
125
+ product: true,
123
126
  productReviews: true,
124
127
  productAssociations: true,
125
128
  }
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@reactionary/source",
3
- "version": "0.3.16",
3
+ "version": "0.3.18",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "dependencies": {
7
- "@commercetools/platform-sdk": "^8.16.0",
8
- "@commercetools/ts-client": "^4.2.1",
7
+ "@commercetools/platform-sdk": "^8.25.0",
8
+ "@commercetools/ts-client": "^4.9.1",
9
9
  "@commercetools/ts-sdk-apm": "^4.0.0",
10
10
  "@docusaurus/core": "^3.9.2",
11
11
  "@docusaurus/preset-classic": "^3.9.2",
@@ -37,14 +37,7 @@ export class AlgoliaProductSearchProvider extends ProductSearchProvider {
37
37
  this.config = config;
38
38
  }
39
39
 
40
- @Reactionary({
41
- inputSchema: ProductSearchQueryByTermSchema,
42
- outputSchema: ProductSearchResultSchema
43
- })
44
- public override async queryByTerm(
45
- payload: ProductSearchQueryByTerm
46
- ): Promise<Result<ProductSearchResult>> {
47
- const client = algoliasearch(this.config.appId, this.config.apiKey);
40
+ protected queryByTermPayload(payload: ProductSearchQueryByTerm) {
48
41
 
49
42
  const facetsThatAreNotCategory = payload.search.facets.filter(x => x.facet.key !== 'categories');
50
43
  const categoryFacet = payload.search.facets.find(x => x.facet.key === 'categories') || payload.search.categoryFilter;
@@ -60,11 +53,7 @@ export class AlgoliaProductSearchProvider extends ProductSearchProvider {
60
53
  if (categoryFacet) {
61
54
  finalFilters.push(`categories:"${categoryFacet.key}"`);
62
55
  }
63
-
64
-
65
- const remote = await client.search<AlgoliaNativeRecord>({
66
- requests: [
67
- {
56
+ return {
68
57
  indexName: this.config.indexName,
69
58
  query: payload.search.term,
70
59
  page: payload.search.paginationOptions.pageNumber - 1,
@@ -75,7 +64,23 @@ export class AlgoliaProductSearchProvider extends ProductSearchProvider {
75
64
  facetFilters: finalFacetFilters,
76
65
  filters: (finalFilters || [])
77
66
  .join(' AND '),
78
- },
67
+ };
68
+ }
69
+
70
+ @Reactionary({
71
+ inputSchema: ProductSearchQueryByTermSchema,
72
+ outputSchema: ProductSearchResultSchema
73
+ })
74
+ public override async queryByTerm(
75
+ payload: ProductSearchQueryByTerm
76
+ ): Promise<Result<ProductSearchResult>> {
77
+ const client = algoliasearch(this.config.appId, this.config.apiKey);
78
+
79
+
80
+
81
+ const remote = await client.search<AlgoliaNativeRecord>({
82
+ requests: [
83
+ this.queryByTermPayload(payload)
79
84
  ],
80
85
  });
81
86
 
@@ -16,19 +16,23 @@ import {
16
16
  import * as crypto from 'crypto';
17
17
  import createDebug from 'debug';
18
18
  import { RequestContextTokenCache } from './token-cache.js';
19
+ import { CommercetoolsSessionSchema, type CommercetoolsSession } from '../schema/session.schema.js';
19
20
  const debug = createDebug('reactionary:commercetools');
20
21
 
22
+ export const PROVIDER_SESSION_KEY = 'COMMERCETOOLS_PROVIDER';
23
+
24
+
21
25
  export class CommercetoolsAPI {
22
26
  protected config: CommercetoolsConfiguration;
23
27
  protected context: RequestContext;
24
- protected cache: RequestContextTokenCache;
28
+ protected tokenCache: RequestContextTokenCache;
25
29
  protected client: Promise<ApiRoot> | undefined;
26
30
  protected adminClient: Promise<ApiRoot> | undefined;
27
31
 
28
32
  constructor(config: CommercetoolsConfiguration, context: RequestContext) {
29
33
  this.config = config;
30
34
  this.context = context;
31
- this.cache = new RequestContextTokenCache(this.context);
35
+ this.tokenCache = new RequestContextTokenCache(this.context, PROVIDER_SESSION_KEY);
32
36
  }
33
37
 
34
38
  public async getClient() {
@@ -47,6 +51,105 @@ export class CommercetoolsAPI {
47
51
  return this.adminClient;
48
52
  }
49
53
 
54
+ public getSessionData(): CommercetoolsSession {
55
+ return this.context.session[PROVIDER_SESSION_KEY]
56
+ ? this.context.session[PROVIDER_SESSION_KEY] as CommercetoolsSession
57
+ : CommercetoolsSessionSchema.parse({});
58
+ }
59
+
60
+ public setSessionData(sessionData: Partial<CommercetoolsSession>): void {
61
+ const existingData = this.context.session[PROVIDER_SESSION_KEY] as Partial<CommercetoolsSession>;
62
+
63
+ this.context.session[PROVIDER_SESSION_KEY] = {
64
+ ...existingData,
65
+ ...sessionData,
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Only caches it pr session for now...... but still better than every call
71
+ * @param key
72
+ * @returns
73
+ */
74
+ public async resolveChannelIdByKey(key: string): Promise<string> {
75
+ const sessionData = this.getSessionData();
76
+
77
+ const cacheKey = `___channel_${key}`;
78
+ const cachedValue = sessionData[cacheKey];
79
+ if (cachedValue) {
80
+ if (debug.enabled) {
81
+ debug(`Resolved channel ${key} from cache`);
82
+ }
83
+ return cachedValue + '';
84
+ }
85
+
86
+ const client = await this.getAdminClient();
87
+ const response = await client
88
+ .withProjectKey({ projectKey: this.config.projectKey })
89
+ .channels()
90
+ .withKey({ key: key })
91
+ .get()
92
+ .execute();
93
+
94
+ const channel = response.body;
95
+ this.setSessionData({
96
+ cacheKey: channel.id,
97
+ })
98
+ if (debug.enabled) {
99
+ debug(`Resolved channel ${key} from API and cached it`);
100
+ }
101
+
102
+ return channel.id;
103
+ }
104
+
105
+ /**
106
+ * Only caches it pr session for now...... but still better than every call
107
+ * @param key
108
+ * @returns
109
+ */
110
+ public async resolveChannelIdByRole(role: string): Promise<string> {
111
+ const sessionData = this.getSessionData();
112
+
113
+ const cacheKey = `___channel_role_${role}`;
114
+ const cachedValue = sessionData[cacheKey];
115
+ if (cachedValue) {
116
+ if (debug.enabled) {
117
+ debug(`Resolved channel ${role} from cache`);
118
+ }
119
+ return cachedValue + '';
120
+ }
121
+
122
+ const client = await this.getAdminClient();
123
+ const response = await client
124
+ .withProjectKey({ projectKey: this.config.projectKey })
125
+ .channels()
126
+ .get({
127
+ queryArgs: {
128
+ where: `roles contains any (:role)`,
129
+ 'var.role': role,
130
+ }
131
+ })
132
+ .execute();
133
+
134
+ const channels = response.body;
135
+ if (channels.results.length === 0) {
136
+ throw new Error(`No channel found with role ${role}`);
137
+ }
138
+
139
+ const channel = channels.results[0];
140
+ this.setSessionData({
141
+ [cacheKey]: channel.id,
142
+ });
143
+
144
+ if (debug.enabled) {
145
+ debug(`Resolved channel ${role} from API and cached it`);
146
+ }
147
+
148
+ return channel.id;
149
+ }
150
+
151
+
152
+
50
153
  protected async createAdminClient() {
51
154
  let builder = this.createBaseClientBuilder();
52
155
  builder = builder.withAnonymousSessionFlow({
@@ -62,13 +165,13 @@ export class CommercetoolsAPI {
62
165
  }
63
166
 
64
167
  protected async createClient() {
65
- let session = await this.cache.get();
168
+ let session = await this.tokenCache.get();
66
169
  const isNewSession = !session || !session.refreshToken;
67
170
 
68
171
  if (isNewSession) {
69
172
  await this.becomeGuest();
70
173
 
71
- session = await this.cache.get();
174
+ session = await this.tokenCache.get();
72
175
  }
73
176
 
74
177
  let builder = this.createBaseClientBuilder();
@@ -80,7 +183,7 @@ export class CommercetoolsAPI {
80
183
  host: this.config.authUrl,
81
184
  projectKey: this.config.projectKey,
82
185
  refreshToken: session?.refreshToken || '',
83
- tokenCache: this.cache,
186
+ tokenCache: this.tokenCache,
84
187
  });
85
188
 
86
189
  return createApiBuilderFromCtpClient(builder.build());
@@ -128,7 +231,7 @@ export class CommercetoolsAPI {
128
231
  clientSecret: this.config.clientSecret,
129
232
  user: { username, password },
130
233
  },
131
- tokenCache: this.cache,
234
+ tokenCache: this.tokenCache,
132
235
  scopes: this.config.scopes,
133
236
  });
134
237
 
@@ -161,7 +264,7 @@ export class CommercetoolsAPI {
161
264
  }
162
265
 
163
266
  public async logout() {
164
- await this.cache.set({ token: '', refreshToken: '', expirationTime: 0 });
267
+ await this.tokenCache.set({ token: '', refreshToken: '', expirationTime: 0 });
165
268
 
166
269
  // TODO: We could do token revocation here, if we wanted to. The above simply whacks the session.
167
270
  const identity = {
@@ -174,7 +277,7 @@ export class CommercetoolsAPI {
174
277
  public async introspect(): Promise<
175
278
  AnonymousIdentity | GuestIdentity | RegisteredIdentity
176
279
  > {
177
- const session = await this.cache.get();
280
+ const session = await this.tokenCache.get();
178
281
 
179
282
  if (!session || !session.token) {
180
283
  const identity = {
@@ -267,7 +370,7 @@ export class CommercetoolsAPI {
267
370
 
268
371
  const result = await response.json();
269
372
 
270
- this.cache.set({
373
+ this.tokenCache.set({
271
374
  expirationTime:
272
375
  Date.now() + Number(result.expires_in) * 1000 - 5 * 60 * 1000,
273
376
  token: result.access_token,
@@ -2,15 +2,14 @@ import type { TokenCache, TokenCacheOptions, TokenStore } from "@commercetools/t
2
2
  import type { RequestContext } from "@reactionary/core";
3
3
  import { CommercetoolsSessionSchema } from "../schema/session.schema.js";
4
4
 
5
- export const PROVIDER_COMMERCETOOLS_SESSION_KEY = 'PROVIDER_COMMERCETOOLS';
6
5
  export class RequestContextTokenCache implements TokenCache {
7
- constructor(protected context: RequestContext) {}
6
+ constructor(protected context: RequestContext, protected sessionProviderKey: string) {}
8
7
 
9
8
  public async get(
10
9
  tokenCacheOptions?: TokenCacheOptions
11
10
  ): Promise<TokenStore | undefined> {
12
11
  const session = CommercetoolsSessionSchema.parse(
13
- this.context.session[PROVIDER_COMMERCETOOLS_SESSION_KEY] || {}
12
+ this.context.session[this.sessionProviderKey] || {}
14
13
  );
15
14
 
16
15
  if (!session) {
@@ -33,10 +32,10 @@ export class RequestContextTokenCache implements TokenCache {
33
32
  tokenCacheOptions?: TokenCacheOptions
34
33
  ): Promise<void> {
35
34
  const session = CommercetoolsSessionSchema.parse(
36
- this.context.session[PROVIDER_COMMERCETOOLS_SESSION_KEY] || {}
35
+ this.context.session[this.sessionProviderKey] || {}
37
36
  );
38
37
 
39
- this.context.session[PROVIDER_COMMERCETOOLS_SESSION_KEY] = session;
38
+ this.context.session[this.sessionProviderKey] = session;
40
39
 
41
40
  session.refreshToken = cache.refreshToken;
42
41
  session.token = cache.token;
@@ -35,6 +35,7 @@ import type {
35
35
  CostBreakDown,
36
36
  Result,
37
37
  NotFoundError,
38
+ Promotion,
38
39
  } from '@reactionary/core';
39
40
  import type { CommercetoolsConfiguration } from '../schema/configuration.schema.js';
40
41
  import type {
@@ -49,7 +50,7 @@ import type { CommercetoolsAPI } from '../core/client.js';
49
50
  export class CommercetoolsCartProvider extends CartProvider {
50
51
  protected config: CommercetoolsConfiguration;
51
52
  protected commercetools: CommercetoolsAPI;
52
-
53
+ protected expandedCartFields = ['discountCodes[*].discountCode'];
53
54
  constructor(
54
55
  config: CommercetoolsConfiguration,
55
56
  cache: Cache,
@@ -71,7 +72,12 @@ export class CommercetoolsCartProvider extends CartProvider {
71
72
  const ctId = payload.cart as CommercetoolsCartIdentifier;
72
73
 
73
74
  try {
74
- const remote = await client.carts.withId({ ID: ctId.key }).get().execute();
75
+ const remote = await client.carts.withId({ ID: ctId.key }).get(
76
+ {
77
+ queryArgs: {
78
+ expand: this.expandedCartFields
79
+ }
80
+ }).execute();
75
81
 
76
82
  return success(this.parseSingle(remote.body));
77
83
  } catch(err) {
@@ -92,6 +98,8 @@ export class CommercetoolsCartProvider extends CartProvider {
92
98
  cartIdentifier = await this.createCart();
93
99
  }
94
100
 
101
+ const channelId = await this.commercetools.resolveChannelIdByRole('Primary');
102
+
95
103
  const result = await this.applyActions(cartIdentifier, [
96
104
  {
97
105
  action: 'addLineItem',
@@ -100,7 +108,7 @@ export class CommercetoolsCartProvider extends CartProvider {
100
108
  // FIXME: This should be dynamic, probably as part of the context...
101
109
  distributionChannel: {
102
110
  typeId: 'channel',
103
- key: 'OnlineFfmChannel',
111
+ id: channelId,
104
112
  },
105
113
  },
106
114
  {
@@ -184,7 +192,6 @@ export class CommercetoolsCartProvider extends CartProvider {
184
192
 
185
193
  @Reactionary({
186
194
  inputSchema: CartMutationDeleteCartSchema,
187
- outputSchema: CartSchema,
188
195
  })
189
196
  public override async deleteCart(
190
197
  payload: CartMutationDeleteCart
@@ -234,11 +241,28 @@ export class CommercetoolsCartProvider extends CartProvider {
234
241
  public override async removeCouponCode(
235
242
  payload: CartMutationRemoveCoupon
236
243
  ): Promise<Result<Cart>> {
244
+
245
+ const client = await this.getClient();
246
+ const currentCart = await client.carts
247
+ .withId({ ID: payload.cart.key })
248
+ .get({
249
+ queryArgs: {
250
+ expand: this.expandedCartFields
251
+ }
252
+ })
253
+ .execute();
254
+
255
+ const discountCodeReference = currentCart.body.discountCodes?.find(dc => dc.discountCode.obj?.code === payload.couponCode)?.discountCode;
256
+
257
+ if (!discountCodeReference) {
258
+ // Coupon code is not applied to the cart, so we can just return the cart as is.
259
+ return success(this.parseSingle(currentCart.body));
260
+ }
237
261
  const result = await this.applyActions(payload.cart, [
238
262
  {
239
263
  action: 'removeDiscountCode',
240
264
  discountCode: {
241
- id: payload.couponCode,
265
+ id: discountCodeReference.id,
242
266
  typeId: 'discount-code',
243
267
  },
244
268
  },
@@ -319,6 +343,9 @@ export class CommercetoolsCartProvider extends CartProvider {
319
343
  country: this.context.taxJurisdiction.countryCode || 'US',
320
344
  locale: this.context.languageContext.locale,
321
345
  },
346
+ queryArgs: {
347
+ expand: this.expandedCartFields
348
+ }
322
349
  })
323
350
  .execute();
324
351
 
@@ -343,6 +370,9 @@ export class CommercetoolsCartProvider extends CartProvider {
343
370
  version: ctId.version,
344
371
  actions,
345
372
  },
373
+ queryArgs: {
374
+ expand: this.expandedCartFields
375
+ }
346
376
  })
347
377
  .execute();
348
378
 
@@ -378,7 +408,19 @@ export class CommercetoolsCartProvider extends CartProvider {
378
408
  protected parseCartItem(remoteItem: LineItem): CartItem {
379
409
  const unitPrice = remoteItem.price.value.centAmount;
380
410
  const totalPrice = remoteItem.totalPrice.centAmount || 0;
381
- const totalDiscount = remoteItem.price.discounted?.value.centAmount || 0;
411
+ let itemDiscount = 0;
412
+
413
+ // look, discounts are weird in commercetools.... i think the .price.discount only applies for embedded prices maybe?
414
+
415
+ if (remoteItem.discountedPricePerQuantity && remoteItem.discountedPricePerQuantity.length > 0) {
416
+ itemDiscount = remoteItem.discountedPricePerQuantity.reduce((sum, discPrQty) => {
417
+ return sum + discPrQty.quantity * discPrQty.discountedPrice?.includedDiscounts?.
418
+ reduce((sum, discount) => sum + discount.discountedAmount.centAmount, 0) || 0
419
+ }, 0);
420
+ }
421
+
422
+ const totalDiscount = (remoteItem.price.discounted?.value.centAmount || 0) + itemDiscount;
423
+
382
424
  const unitDiscount = totalDiscount / remoteItem.quantity;
383
425
  const currency =
384
426
  remoteItem.price.value.currencyCode.toUpperCase() as Currency;
@@ -423,12 +465,23 @@ export class CommercetoolsCartProvider extends CartProvider {
423
465
  version: remote.version || 0,
424
466
  } satisfies CommercetoolsCartIdentifier;
425
467
 
468
+
469
+ const items = new Array<CartItem>();
470
+ for (const remoteItem of remote.lineItems) {
471
+ const item = this.parseCartItem(remoteItem);
472
+
473
+ items.push(item);
474
+ }
475
+
476
+
426
477
  const grandTotal = remote.totalPrice.centAmount || 0;
427
478
  const shippingTotal = remote.shippingInfo?.price.centAmount || 0;
428
479
  const productTotal = grandTotal - shippingTotal;
429
480
  const taxTotal = remote.taxedPrice?.totalTax?.centAmount || 0;
481
+
482
+ // i think this is missing some elements still?
430
483
  const discountTotal =
431
- remote.discountOnTotalPrice?.discountedAmount.centAmount || 0;
484
+ (remote.discountOnTotalPrice?.discountedAmount.centAmount || 0) + items.reduce((sum, item) => sum + (item.price.totalDiscount.value * 100 || 0), 0);
432
485
  const surchargeTotal = 0;
433
486
  const currency = remote.totalPrice.currencyCode as Currency;
434
487
 
@@ -459,13 +512,24 @@ export class CommercetoolsCartProvider extends CartProvider {
459
512
  },
460
513
  } satisfies CostBreakDown;
461
514
 
462
- const items = new Array<CartItem>();
463
- for (const remoteItem of remote.lineItems) {
464
- const item = this.parseCartItem(remoteItem);
465
515
 
466
- items.push(item);
516
+ const localeString = this.context.languageContext.locale || 'en'
517
+ const appliedPromotions = [];
518
+ if (remote.discountCodes) {
519
+ for (const promo of remote.discountCodes) {
520
+ appliedPromotions.push({
521
+ code: promo.discountCode.obj?.code || '',
522
+ isCouponCode: true,
523
+ name: promo.discountCode.obj?.name?.[localeString] || '',
524
+ description: promo.discountCode.obj?.description?.[localeString] || '',
525
+ } satisfies Promotion);
526
+ }
467
527
  }
468
528
 
529
+ // if we want to include the nice name and description of the non-coupon promotions, we have to do some extra work to fetch the referenced promotions and include them here,
530
+ // as the max expand level is 3, and the information is at level 5.
531
+ // For now, we will just include the coupon codes, as that is the most common use case.
532
+
469
533
  const cart = {
470
534
  identifier,
471
535
  userId: {
@@ -474,6 +538,7 @@ export class CommercetoolsCartProvider extends CartProvider {
474
538
  name: remote.custom?.fields['name'] || '',
475
539
  description: remote.custom?.fields['description'] || '',
476
540
  price,
541
+ appliedPromotions,
477
542
  items,
478
543
  } satisfies Cart;
479
544
 
@@ -35,6 +35,10 @@ export class CommercetoolsInventoryProvider extends InventoryProvider {
35
35
  }
36
36
 
37
37
  @Reactionary({
38
+ cache: true,
39
+ cacheTimeToLiveInSeconds: 300,
40
+ currencyDependentCaching: false,
41
+ localeDependentCaching: false,
38
42
  inputSchema: InventoryQueryBySKUSchema,
39
43
  outputSchema: InventorySchema,
40
44
  })
@@ -45,13 +49,7 @@ export class CommercetoolsInventoryProvider extends InventoryProvider {
45
49
 
46
50
  // TODO: We can't query by supplyChannel.key, so we have to resolve it first.
47
51
  // This is probably a good candidate for internal data caching at some point.
48
- const channel = await client
49
- .channels()
50
- .withKey({ key: payload.fulfilmentCenter.key })
51
- .get()
52
- .execute();
53
-
54
- const channelId = channel.body.id;
52
+ const channelId = await this.commercetools.resolveChannelIdByKey(payload.fulfilmentCenter.key);
55
53
 
56
54
  const remote = await client
57
55
  .inventory()
@@ -48,7 +48,12 @@ export class CommercetoolsPriceProvider extends PriceProvider {
48
48
  payload: CustomerPriceQuery
49
49
  ): Promise<Result<Price>> {
50
50
  const client = await this.getClient();
51
- const priceChannelId = 'ee6e75e9-c9ab-4e2f-85f1-d8c734d0cb86';
51
+ let priceChannelId;
52
+ if (this.config.customerPriceChannelKey) {
53
+ priceChannelId = await this.commercetools.resolveChannelIdByKey(this.config.customerPriceChannelKey);
54
+ } else {
55
+ priceChannelId = await this.commercetools.resolveChannelIdByRole('Primary');
56
+ }
52
57
 
53
58
  const response = await client
54
59
  .productProjections()
@@ -80,8 +85,12 @@ export class CommercetoolsPriceProvider extends PriceProvider {
80
85
  })
81
86
  public override async getListPrice(payload: ListPriceQuery): Promise<Result<Price>> {
82
87
  const client = await this.getClient();
83
- const priceChannelId = 'ee6e75e9-c9ab-4e2f-85f1-d8c734d0cb86';
84
-
88
+ let priceChannelId;
89
+ if (this.config.listPriceChannelKey) {
90
+ priceChannelId = await this.commercetools.resolveChannelIdByKey(this.config.listPriceChannelKey);
91
+ } else {
92
+ priceChannelId = await this.commercetools.resolveChannelIdByRole('Primary');
93
+ }
85
94
  const response = await client
86
95
  .productProjections()
87
96
  .get({
@@ -127,9 +136,12 @@ export class CommercetoolsPriceProvider extends PriceProvider {
127
136
  currency: price.value.currencyCode as Currency,
128
137
  } satisfies MonetaryAmount;
129
138
 
139
+ let isOnSale = false;
130
140
  if (options.includeDiscounts) {
131
141
  const discountedPrice = price.discounted?.value || price.value;
132
-
142
+ if (price.discounted) {
143
+ isOnSale = true;
144
+ }
133
145
  unitPrice = {
134
146
  value: discountedPrice.centAmount / 100,
135
147
  currency: price.value.currencyCode as Currency,
@@ -146,35 +158,10 @@ export class CommercetoolsPriceProvider extends PriceProvider {
146
158
  identifier,
147
159
  tieredPrices: [],
148
160
  unitPrice,
161
+ onSale: isOnSale,
149
162
  } satisfies Price;
150
163
 
151
164
  return result;
152
165
  }
153
166
 
154
- protected async getChannels() {
155
- const adminClient = await this.commercetools.getAdminClient();
156
-
157
- const offerPriceChannelPromise = adminClient
158
- .withProjectKey({ projectKey: this.config.projectKey })
159
- .channels()
160
- .withKey({ key: 'Offer Price' })
161
- .get()
162
- .execute();
163
- const listPriceChannelPromise = adminClient
164
- .withProjectKey({ projectKey: this.config.projectKey })
165
- .channels()
166
- .withKey({ key: 'List Price' })
167
- .get()
168
- .execute();
169
-
170
- const [offerChannel, listChannel] = await Promise.all([
171
- offerPriceChannelPromise,
172
- listPriceChannelPromise,
173
- ]);
174
-
175
- return {
176
- offer: offerChannel.body.id,
177
- list: listChannel.body.id,
178
- };
179
- }
180
167
  }
@@ -8,8 +8,12 @@ export const CommercetoolsConfigurationSchema = z.looseObject({
8
8
  clientId: z.string(),
9
9
  clientSecret: z.string(),
10
10
  scopes: z.array(z.string()).default(() => []),
11
+ adminClientId: z.string().optional(),
12
+ adminClientSecret: z.string().optional(),
11
13
  paymentMethods: PaymentMethodSchema.array().optional().default(() => []),
12
14
  facetFieldsForSearch: z.array(z.string()).default(() => []),
15
+ listPriceChannelKey: z.string().optional(),
16
+ customerPriceChannelKey: z.string().optional(),
13
17
  });
14
18
 
15
19
  export type CommercetoolsConfiguration = z.infer<typeof CommercetoolsConfigurationSchema>;
@@ -4,4 +4,6 @@ export const CommercetoolsSessionSchema = z.looseObject({
4
4
  token: z.string().default(''),
5
5
  refreshToken: z.string().optional(),
6
6
  expirationTime: z.number().default(0)
7
- });
7
+ });
8
+
9
+ export type CommercetoolsSession = z.infer<typeof CommercetoolsSessionSchema>;
@@ -274,6 +274,7 @@ export class FakeCartProvider extends CartProvider {
274
274
  currency: 'XXX',
275
275
  },
276
276
  },
277
+ appliedPromotions: [],
277
278
  userId: {
278
279
  userId: '',
279
280
  },