@reactionary/source 0.3.17 → 0.6.1

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 (25) hide show
  1. package/README.md +22 -11
  2. package/core/src/schemas/models/cart.model.ts +7 -2
  3. package/core/src/schemas/models/identifiers.model.ts +12 -8
  4. package/core/src/schemas/models/price.model.ts +9 -0
  5. package/examples/node/package.json +7 -7
  6. package/examples/node/src/capabilities/cart.spec.ts +97 -15
  7. package/examples/node/src/capabilities/category.spec.ts +27 -32
  8. package/examples/node/src/capabilities/checkout.spec.ts +5 -5
  9. package/examples/node/src/capabilities/identity.spec.ts +5 -1
  10. package/examples/node/src/capabilities/inventory.spec.ts +1 -1
  11. package/package.json +3 -3
  12. package/providers/algolia/src/providers/product-search.provider.ts +19 -14
  13. package/providers/commercetools/src/providers/cart.provider.ts +76 -11
  14. package/providers/fake/src/providers/cart.provider.ts +1 -0
  15. package/providers/medusa/src/providers/cart.provider.ts +159 -70
  16. package/providers/medusa/src/providers/category.provider.ts +35 -23
  17. package/providers/medusa/src/providers/checkout.provider.ts +78 -41
  18. package/providers/medusa/src/providers/order-search.provider.ts +21 -10
  19. package/providers/medusa/src/providers/product-recommendations.provider.ts +10 -6
  20. package/providers/medusa/src/providers/product-search.provider.ts +19 -10
  21. package/providers/medusa/src/providers/product.provider.ts +20 -12
  22. package/providers/medusa/src/providers/profile.provider.ts +38 -13
  23. package/providers/meilisearch/src/providers/order-search.provider.ts +17 -12
  24. package/providers/meilisearch/src/providers/product-recommendations.provider.ts +10 -11
  25. package/providers/meilisearch/src/providers/product-search.provider.ts +23 -18
@@ -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
 
@@ -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
 
@@ -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
  },
@@ -16,6 +16,7 @@ import type {
16
16
  ItemCostBreakdown,
17
17
  NotFoundError,
18
18
  ProductVariantIdentifier,
19
+ Promotion,
19
20
  RequestContext,
20
21
  Result,
21
22
  } from '@reactionary/core';
@@ -31,10 +32,10 @@ import {
31
32
  CartProvider,
32
33
  CartQueryByIdSchema,
33
34
  CartSchema,
35
+ error,
34
36
  ProductVariantIdentifierSchema,
35
37
  Reactionary,
36
38
  success,
37
- error,
38
39
  } from '@reactionary/core';
39
40
 
40
41
  import createDebug from 'debug';
@@ -47,8 +48,7 @@ import {
47
48
  parseMedusaCostBreakdown,
48
49
  parseMedusaItemPrice,
49
50
  } from '../utils/medusa-helpers.js';
50
- import type MedusaTypes = require('@medusajs/types');
51
- import type StoreCartPromotion = require('@medusajs/types');
51
+ import type { StoreAddCartLineItem, StoreCart, StoreCartAddPromotion, StoreCartLineItem, StoreCartRemovePromotion, StoreCartResponse, StoreCreateCart, StoreProduct, StoreUpdateCart, StoreUpdateCartLineItem } from '@medusajs/types';
52
52
 
53
53
  const debug = createDebug('reactionary:medusa:cart');
54
54
 
@@ -87,7 +87,7 @@ export class MedusaCartProvider extends CartProvider {
87
87
  debug('Fetching cart by ID:', medusaId.key);
88
88
  }
89
89
 
90
- const cartResponse = await client.store.cart.retrieve(medusaId.key);
90
+ const cartResponse = await client.store.cart.retrieve(medusaId.key, { fields: this.includedFields });
91
91
 
92
92
  if (debug.enabled) {
93
93
  debug('Received cart response:', cartResponse);
@@ -111,6 +111,21 @@ export class MedusaCartProvider extends CartProvider {
111
111
  }
112
112
  }
113
113
 
114
+
115
+ /**
116
+ * Extension point for the `add` operation to control the payload sent to Medusa when adding an item to the cart. By default, it only includes the variant ID and quantity, but you can override it to include more fields as needed.
117
+ *
118
+ * @param payload
119
+ * @param variantId
120
+ * @returns
121
+ */
122
+ protected addPayload(payload: CartMutationItemAdd, variantId: string): StoreAddCartLineItem {
123
+ return {
124
+ variant_id: variantId,
125
+ quantity: payload.quantity,
126
+ };
127
+ }
128
+
114
129
  @Reactionary({
115
130
  inputSchema: CartMutationItemAddSchema,
116
131
  outputSchema: CartSchema,
@@ -148,13 +163,11 @@ export class MedusaCartProvider extends CartProvider {
148
163
 
149
164
  const response = await client.store.cart.createLineItem(
150
165
  medusaId.key,
151
- {
152
- variant_id: variantId,
153
- quantity: payload.quantity,
154
- },
166
+ this.addPayload(payload, variantId),
155
167
  {
156
168
  fields: this.includedFields,
157
169
  }
170
+
158
171
  );
159
172
 
160
173
  if (debug.enabled) {
@@ -200,6 +213,18 @@ export class MedusaCartProvider extends CartProvider {
200
213
  }
201
214
  }
202
215
 
216
+ /**
217
+ * Extension point for the `changeQuantity` operation to control the payload sent to Medusa when changing the quantity of an item in the cart. By default, it only includes the new quantity, but you can override it to include more fields as needed.
218
+ * @param payload
219
+ * @returns
220
+ */
221
+ protected changeQuantityPayload(payload: CartMutationItemQuantityChange): StoreUpdateCartLineItem {
222
+ return {
223
+ quantity: payload.quantity,
224
+ };
225
+ }
226
+
227
+
203
228
  @Reactionary({
204
229
  inputSchema: CartMutationItemQuantityChangeSchema,
205
230
  outputSchema: CartSchema,
@@ -222,9 +247,7 @@ export class MedusaCartProvider extends CartProvider {
222
247
  const response = await client.store.cart.updateLineItem(
223
248
  medusaId.key,
224
249
  payload.item.key,
225
- {
226
- quantity: payload.quantity,
227
- },
250
+ this.changeQuantityPayload(payload),
228
251
  {
229
252
  fields: this.includedFields,
230
253
  }
@@ -272,7 +295,7 @@ export class MedusaCartProvider extends CartProvider {
272
295
  identifier: undefined,
273
296
  });
274
297
  } catch (err) {
275
- debug('Failed to get active cart ID:', error);
298
+ debug('Failed to get active cart ID:', err);
276
299
 
277
300
  return error<NotFoundError>({
278
301
  type: 'NotFound',
@@ -282,8 +305,8 @@ export class MedusaCartProvider extends CartProvider {
282
305
  }
283
306
 
284
307
  @Reactionary({
308
+ cache: false,
285
309
  inputSchema: CartMutationDeleteCartSchema,
286
- outputSchema: CartSchema,
287
310
  })
288
311
  public override async deleteCart(
289
312
  payload: CartMutationDeleteCart
@@ -319,6 +342,18 @@ export class MedusaCartProvider extends CartProvider {
319
342
  }
320
343
  }
321
344
 
345
+
346
+ /**
347
+ * Extension point to apply a coupon code to the cart. By default, it only includes the coupon code, but you can override it to include more fields as needed.
348
+ * @param payload
349
+ * @returns
350
+ */
351
+ protected applyCouponCodePayload(payload: CartMutationApplyCoupon): StoreCartAddPromotion {
352
+ return {
353
+ promo_codes: [payload.couponCode],
354
+ };
355
+ }
356
+
322
357
  @Reactionary({
323
358
  inputSchema: CartMutationApplyCouponSchema,
324
359
  outputSchema: CartSchema,
@@ -330,16 +365,27 @@ export class MedusaCartProvider extends CartProvider {
330
365
  const client = await this.getClient();
331
366
  const medusaId = payload.cart as MedusaCartIdentifier;
332
367
 
333
- const response = await client.store.cart.update(
334
- medusaId.key,
368
+
369
+ const response = await client.client.fetch<StoreCartResponse>(
370
+ `/store/carts/${medusaId.key}/promotions`,
335
371
  {
336
- promo_codes: [payload.couponCode],
337
- },
372
+ method: "POST",
373
+ body: this.applyCouponCodePayload(payload),
374
+ query: {
375
+ fields: this.includedFields,
376
+ }
377
+ }
378
+ );
379
+
380
+ /** When PR: https://github.com/medusajs/medusa/pull/14850 gets merged, revert to the below
381
+ const response = await client.store.cart.addPromotionCodes(
382
+ medusaId.key,
383
+ this.applyCouponCodePayload(payload),
338
384
  {
339
385
  fields: this.includedFields,
340
386
  }
341
387
  );
342
-
388
+ */
343
389
  if (response.cart) {
344
390
  return success(this.parseSingle(response.cart));
345
391
  }
@@ -350,6 +396,18 @@ export class MedusaCartProvider extends CartProvider {
350
396
  }
351
397
  }
352
398
 
399
+ /**
400
+ * Extension point to remove a coupon code from the cart. By default, it only includes the coupon code to be removed, but you can override it to include more fields as needed.
401
+ * @param payload
402
+ * @returns
403
+ */
404
+ protected removeCouponCodePayload(payload: CartMutationRemoveCoupon): StoreCartRemovePromotion {
405
+ return {
406
+ promo_codes: [payload.couponCode],
407
+
408
+ };
409
+ }
410
+
353
411
  @Reactionary({
354
412
  inputSchema: CartMutationRemoveCouponSchema,
355
413
  outputSchema: CartSchema,
@@ -361,39 +419,49 @@ export class MedusaCartProvider extends CartProvider {
361
419
  const client = await this.getClient();
362
420
  const medusaId = payload.cart as MedusaCartIdentifier;
363
421
 
364
- // Get current cart to find the discount to remove
365
- const cartResponse = await client.store.cart.retrieve(medusaId.key);
366
-
367
- if (cartResponse.cart?.promotions) {
368
- const manualDiscounts = cartResponse.cart.promotions.filter(
369
- (x) => !x.is_automatic && x.code
370
- );
371
-
372
- const remainingCodes = (manualDiscounts
373
- .filter((x) => x.code !== payload.couponCode)
374
- .map((promotion: MedusaTypes.StoreCartPromotion) => promotion.code) ||
375
- []) as string[];
376
- const response = await client.store.cart.update(
377
- medusaId.key,
378
- {
379
- promo_codes: remainingCodes || [],
380
- },
381
- {
422
+ const response = await client.client.fetch<StoreCartResponse>(
423
+ `/store/carts/${medusaId.key}/promotions`,
424
+ {
425
+ method: "DELETE",
426
+ body: this.removeCouponCodePayload(payload),
427
+ query: {
382
428
  fields: this.includedFields,
383
429
  }
384
- );
430
+ }
431
+ );
432
+
433
+ /*
385
434
 
386
- if (response.cart) {
387
- return success(this.parseSingle(response.cart));
435
+ const response = await client.store.cart.removePromotionCodes(
436
+ medusaId.key,
437
+ this.removeCouponCodePayload(payload),
438
+ {
439
+ fields: this.includedFields,
388
440
  }
389
- }
441
+ );
442
+ */
390
443
 
444
+ if (response.cart) {
445
+ return success(this.parseSingle(response.cart));
446
+ }
391
447
  throw new Error('Failed to remove coupon code');
392
448
  } catch (error) {
393
449
  handleProviderError('remove coupon code', error);
394
450
  }
395
451
  }
396
452
 
453
+ /**
454
+ * Extension point to control the payload sent to Medusa when changing the currency of the cart. By default, it only includes the new region ID, but you can override it to include more fields as needed.
455
+ * @param payload
456
+ * @param newRegionId
457
+ * @returns
458
+ */
459
+ protected changeCurrencyPayload(payload: CartMutationChangeCurrency, newRegionId: string): StoreUpdateCart {
460
+ return {
461
+ region_id: newRegionId,
462
+ };
463
+ }
464
+
397
465
  @Reactionary({
398
466
  inputSchema: CartMutationChangeCurrencySchema,
399
467
  outputSchema: CartSchema,
@@ -404,35 +472,22 @@ export class MedusaCartProvider extends CartProvider {
404
472
  try {
405
473
  const client = await this.getClient();
406
474
 
407
- // Get current cart
408
- const currentCartResponse = await client.store.cart.retrieve(
409
- payload.cart.key
410
- );
411
- client.store.cart.update(
475
+ const newRegionId = (await this.medusaApi.getActiveRegion()).id;
476
+ const updatedCartResponse = await client.store.cart.update(
412
477
  payload.cart.key,
413
- {
414
- region_id: (await this.medusaApi.getActiveRegion()).id,
415
- },
478
+ this.changeCurrencyPayload(payload, newRegionId),
416
479
  {
417
480
  fields: this.includedFields,
418
481
  }
419
482
  );
420
- if (!currentCartResponse.cart) {
421
- throw new Error('Cart not found');
422
- }
423
-
424
- // Get the new cart
425
- const newCartResponse = await client.store.cart.retrieve(
426
- payload.cart.key
427
- );
428
483
 
429
- if (newCartResponse.cart) {
484
+ if (updatedCartResponse.cart) {
430
485
  // Update session to use new cart
431
486
  this.medusaApi.setSessionData({
432
- activeCartId: newCartResponse.cart.id,
487
+ activeCartId: updatedCartResponse.cart.id,
433
488
  });
434
489
 
435
- return success(this.parseSingle(newCartResponse.cart));
490
+ return success(this.parseSingle(updatedCartResponse.cart));
436
491
  }
437
492
 
438
493
  throw new Error('Failed to change currency');
@@ -441,18 +496,28 @@ export class MedusaCartProvider extends CartProvider {
441
496
  }
442
497
  }
443
498
 
499
+ /**
500
+ * Extension point to control the payload sent to Medusa when creating a cart. By default, it only includes the currency code, but you can override it to include more fields as needed.
501
+ * @param currency
502
+ * @returns
503
+ */
504
+ protected createCartPayload(currency?: string): StoreCreateCart {
505
+ return {
506
+ currency_code: (
507
+ currency ||
508
+ this.context.languageContext.currencyCode ||
509
+ ''
510
+ ).toLowerCase(),
511
+ };
512
+ }
513
+
514
+
444
515
  protected async createCart(currency?: string): Promise<CartIdentifier> {
445
516
  try {
446
517
  const client = await this.getClient();
447
518
 
448
519
  const response = await client.store.cart.create(
449
- {
450
- currency_code: (
451
- currency ||
452
- this.context.languageContext.currencyCode ||
453
- ''
454
- ).toLowerCase(),
455
- },
520
+ this.createCartPayload(currency),
456
521
  {
457
522
  fields: this.includedFields,
458
523
  }
@@ -489,7 +554,7 @@ export class MedusaCartProvider extends CartProvider {
489
554
  * @returns
490
555
  */
491
556
  protected parseItemPrice(
492
- remoteItem: MedusaTypes.StoreCartLineItem,
557
+ remoteItem: StoreCartLineItem,
493
558
  currency: Currency
494
559
  ): ItemCostBreakdown {
495
560
  return parseMedusaItemPrice(remoteItem, currency);
@@ -500,7 +565,7 @@ export class MedusaCartProvider extends CartProvider {
500
565
  * @param remote
501
566
  * @returns
502
567
  */
503
- protected parseCostBreakdown(remote: MedusaTypes.StoreCart): CostBreakDown {
568
+ protected parseCostBreakdown(remote: StoreCart): CostBreakDown {
504
569
  return parseMedusaCostBreakdown(remote);
505
570
  }
506
571
 
@@ -511,9 +576,10 @@ export class MedusaCartProvider extends CartProvider {
511
576
  * @returns
512
577
  */
513
578
  protected parseCartItem(
514
- remoteItem: MedusaTypes.StoreCartLineItem,
579
+ remoteItem: StoreCartLineItem,
515
580
  currency: Currency
516
581
  ): CartItem {
582
+
517
583
  const item: CartItem = {
518
584
  identifier: {
519
585
  key: remoteItem.id,
@@ -535,7 +601,7 @@ export class MedusaCartProvider extends CartProvider {
535
601
  * @param remote
536
602
  * @returns
537
603
  */
538
- protected parseSingle(remote: MedusaTypes.StoreCart): Cart {
604
+ protected parseSingle(remote: StoreCart): Cart {
539
605
  const identifier = MedusaCartIdentifierSchema.parse({
540
606
  key: remote.id,
541
607
  region_id: remote.region_id,
@@ -559,12 +625,35 @@ export class MedusaCartProvider extends CartProvider {
559
625
  items.push(this.parseCartItem(remoteItem, price.grandTotal.currency));
560
626
  }
561
627
 
628
+
629
+ const appliedPromotions = [];
630
+ if (remote.promotions) {
631
+ for (const promo of remote.promotions) {
632
+
633
+ const promotionName = promo.code;
634
+ let promoDescription = '';
635
+ if (promo.application_method?.type === 'percentage') {
636
+ promoDescription = `-${promo.application_method.value}%`;
637
+ }
638
+ if (promo.application_method?.type === 'fixed') {
639
+ promoDescription = `-${promo.application_method.value} ${price.grandTotal.currency}`;
640
+ }
641
+ appliedPromotions.push({
642
+ code: promo.code || '',
643
+ isCouponCode: promo.is_automatic ? false : true,
644
+ name: promotionName || promoDescription,
645
+ description: promoDescription
646
+ } satisfies Promotion);
647
+ }
648
+ }
649
+
562
650
  const result = {
563
651
  identifier,
564
652
  name,
565
653
  description,
566
654
  price,
567
655
  items,
656
+ appliedPromotions,
568
657
  userId: {
569
658
  userId: '???',
570
659
  },