@labdigital/commercetools-mock 2.59.0 → 2.60.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.
@@ -2,21 +2,36 @@ import assert from "node:assert";
2
2
  import type {
3
3
  Cart,
4
4
  CartReference,
5
+ CentPrecisionMoney,
5
6
  CustomLineItem,
6
7
  CustomLineItemImportDraft,
7
8
  GeneralError,
9
+ InvalidOperationError,
8
10
  LineItem,
9
11
  LineItemImportDraft,
12
+ MissingTaxRateForCountryError,
10
13
  Order,
11
14
  OrderFromCartDraft,
12
15
  OrderImportDraft,
13
16
  Product,
14
17
  ProductPagedQueryResponse,
15
18
  ProductVariant,
19
+ ShippingInfo,
20
+ ShippingMethodDoesNotMatchCartError,
21
+ ShippingMethodReference,
22
+ TaxPortion,
23
+ TaxedItemPrice,
16
24
  } from "@commercetools/platform-sdk";
25
+ import { Decimal } from "decimal.js/decimal";
17
26
  import type { Config } from "~src/config";
18
27
  import { CommercetoolsError } from "~src/exceptions";
19
28
  import { generateRandomString, getBaseResourceProperties } from "~src/helpers";
29
+ import { calculateTaxTotals, calculateTaxedPriceFromRate } from "~src/lib/tax";
30
+ import {
31
+ createShippingInfoFromMethod,
32
+ getShippingMethodsMatchingCart,
33
+ } from "~src/shipping";
34
+ import type { Writable } from "~src/types";
20
35
  import type { RepositoryContext } from "../abstract";
21
36
  import { AbstractResourceRepository, type QueryParams } from "../abstract";
22
37
  import {
@@ -26,6 +41,7 @@ import {
26
41
  createPrice,
27
42
  createTypedMoney,
28
43
  resolveStoreReference,
44
+ roundDecimal,
29
45
  } from "../helpers";
30
46
  import { OrderUpdateHandler } from "./actions";
31
47
 
@@ -60,7 +76,7 @@ export class OrderRepository extends AbstractResourceRepository<"order"> {
60
76
  throw new Error("Cannot find cart");
61
77
  }
62
78
 
63
- const resource: Order = {
79
+ const resource: Writable<Order> = {
64
80
  ...getBaseResourceProperties(),
65
81
  anonymousId: cart.anonymousId,
66
82
  billingAddress: cart.billingAddress,
@@ -84,6 +100,7 @@ export class OrderRepository extends AbstractResourceRepository<"order"> {
84
100
  refusedGifts: [],
85
101
  shipping: cart.shipping,
86
102
  shippingAddress: cart.shippingAddress,
103
+ shippingInfo: cart.shippingInfo,
87
104
  shippingMode: cart.shippingMode,
88
105
  syncInfo: [],
89
106
  taxCalculationMode: cart.taxCalculationMode,
@@ -94,13 +111,23 @@ export class OrderRepository extends AbstractResourceRepository<"order"> {
94
111
  totalPrice: cart.totalPrice,
95
112
  store: cart.store,
96
113
  };
114
+
115
+ const { taxedPrice, taxedShippingPrice } = calculateTaxTotals({
116
+ lineItems: cart.lineItems,
117
+ customLineItems: cart.customLineItems,
118
+ shippingInfo: cart.shippingInfo,
119
+ totalPrice: cart.totalPrice,
120
+ });
121
+ resource.taxedPrice = resource.taxedPrice ?? taxedPrice;
122
+ resource.taxedShippingPrice =
123
+ resource.taxedShippingPrice ?? taxedShippingPrice;
97
124
  return this.saveNew(context, resource);
98
125
  }
99
126
 
100
127
  import(context: RepositoryContext, draft: OrderImportDraft): Order {
101
128
  // TODO: Check if order with given orderNumber already exists
102
129
  assert(this, "OrderRepository not valid");
103
- const resource: Order = {
130
+ const resource: Writable<Order> = {
104
131
  ...getBaseResourceProperties(),
105
132
 
106
133
  billingAddress: createAddress(
@@ -132,7 +159,7 @@ export class OrderRepository extends AbstractResourceRepository<"order"> {
132
159
  refusedGifts: [],
133
160
  shippingMode: "Single",
134
161
  shipping: [],
135
-
162
+ shippingInfo: undefined,
136
163
  store: resolveStoreReference(
137
164
  draft.store,
138
165
  context.projectKey,
@@ -151,6 +178,43 @@ export class OrderRepository extends AbstractResourceRepository<"order"> {
151
178
 
152
179
  totalPrice: createCentPrecisionMoney(draft.totalPrice),
153
180
  };
181
+
182
+ // Set shipping info after resource is created
183
+ if (draft.shippingInfo?.shippingMethod) {
184
+ const { ...shippingMethodRef } = draft.shippingInfo.shippingMethod;
185
+
186
+ // get id when reference is by key only
187
+ if (shippingMethodRef.key && !shippingMethodRef.id) {
188
+ const shippingMethod =
189
+ this._storage.getByResourceIdentifier<"shipping-method">(
190
+ context.projectKey,
191
+ shippingMethodRef,
192
+ );
193
+ if (!shippingMethod) {
194
+ throw new CommercetoolsError<GeneralError>({
195
+ code: "General",
196
+ message: `A shipping method with key '${shippingMethodRef.key}' does not exist.`,
197
+ });
198
+ }
199
+ shippingMethodRef.id = shippingMethod.id;
200
+ }
201
+
202
+ resource.shippingInfo = this.createShippingInfo(context, resource, {
203
+ typeId: "shipping-method",
204
+ id: shippingMethodRef.id as string,
205
+ });
206
+ }
207
+
208
+ const { taxedPrice, taxedShippingPrice } = calculateTaxTotals({
209
+ lineItems: resource.lineItems,
210
+ customLineItems: resource.customLineItems,
211
+ shippingInfo: resource.shippingInfo,
212
+ totalPrice: resource.totalPrice,
213
+ });
214
+ resource.taxedPrice = resource.taxedPrice ?? taxedPrice;
215
+ resource.taxedShippingPrice =
216
+ resource.taxedShippingPrice ?? taxedShippingPrice;
217
+
154
218
  return this.saveNew(context, resource);
155
219
  }
156
220
 
@@ -195,6 +259,12 @@ export class OrderRepository extends AbstractResourceRepository<"order"> {
195
259
  throw new Error("No product found");
196
260
  }
197
261
 
262
+ const quantity = draft.quantity ?? 1;
263
+ const totalPrice = createCentPrecisionMoney({
264
+ ...draft.price.value,
265
+ centAmount: (draft.price.value.centAmount ?? 0) * quantity,
266
+ });
267
+
198
268
  const lineItem: LineItem = {
199
269
  ...getBaseResourceProperties(),
200
270
  custom: createCustomFields(
@@ -209,12 +279,17 @@ export class OrderRepository extends AbstractResourceRepository<"order"> {
209
279
  priceMode: "Platform",
210
280
  productId: product.id,
211
281
  productType: product.productType,
212
- quantity: draft.quantity,
282
+ quantity,
213
283
  state: draft.state || [],
214
284
  taxRate: draft.taxRate,
285
+ taxedPrice: calculateTaxedPriceFromRate(
286
+ totalPrice.centAmount,
287
+ totalPrice.currencyCode,
288
+ draft.taxRate,
289
+ ),
215
290
  taxedPricePortions: [],
216
291
  perMethodTaxRate: [],
217
- totalPrice: createCentPrecisionMoney(draft.price.value),
292
+ totalPrice,
218
293
  variant: {
219
294
  id: variant.id,
220
295
  sku: variant.sku,
@@ -230,6 +305,12 @@ export class OrderRepository extends AbstractResourceRepository<"order"> {
230
305
  context: RepositoryContext,
231
306
  draft: CustomLineItemImportDraft,
232
307
  ): CustomLineItem {
308
+ const quantity = draft.quantity ?? 1;
309
+ const totalPrice = createCentPrecisionMoney({
310
+ ...draft.money,
311
+ centAmount: (draft.money.centAmount ?? 0) * quantity,
312
+ });
313
+
233
314
  const lineItem: CustomLineItem = {
234
315
  ...getBaseResourceProperties(),
235
316
  custom: createCustomFields(
@@ -240,12 +321,17 @@ export class OrderRepository extends AbstractResourceRepository<"order"> {
240
321
  discountedPricePerQuantity: [],
241
322
  money: createTypedMoney(draft.money),
242
323
  name: draft.name,
243
- quantity: draft.quantity ?? 0,
324
+ quantity,
244
325
  perMethodTaxRate: [],
245
326
  priceMode: draft.priceMode ?? "Standard",
246
327
  slug: draft.slug,
247
328
  state: [],
248
- totalPrice: createCentPrecisionMoney(draft.money),
329
+ totalPrice,
330
+ taxedPrice: calculateTaxedPriceFromRate(
331
+ totalPrice.centAmount,
332
+ totalPrice.currencyCode,
333
+ draft.taxRate,
334
+ ),
249
335
  taxedPricePortions: [],
250
336
  };
251
337
 
@@ -272,4 +358,56 @@ export class OrderRepository extends AbstractResourceRepository<"order"> {
272
358
 
273
359
  return;
274
360
  }
361
+
362
+ createShippingInfo(
363
+ context: RepositoryContext,
364
+ resource: Writable<Order>,
365
+ shippingMethodRef: ShippingMethodReference,
366
+ ): ShippingInfo {
367
+ const cartLikeForMatching: Writable<Cart> = {
368
+ ...resource,
369
+ cartState: "Active" as const,
370
+ inventoryMode: "None" as const,
371
+ itemShippingAddresses: [],
372
+ priceRoundingMode: resource.taxRoundingMode || "HalfEven",
373
+ taxMode: resource.taxMode || "Platform",
374
+ taxCalculationMode: resource.taxCalculationMode || "LineItemLevel",
375
+ taxRoundingMode: resource.taxRoundingMode || "HalfEven",
376
+ discountCodes: resource.discountCodes || [],
377
+ directDiscounts: resource.directDiscounts || [],
378
+ shippingInfo: undefined,
379
+ };
380
+
381
+ const shippingMethods = getShippingMethodsMatchingCart(
382
+ context,
383
+ this._storage,
384
+ cartLikeForMatching,
385
+ {
386
+ expand: ["zoneRates[*].zone"],
387
+ },
388
+ );
389
+
390
+ const method = shippingMethods.results.find(
391
+ (candidate) => candidate.id === shippingMethodRef.id,
392
+ );
393
+
394
+ if (!method) {
395
+ throw new CommercetoolsError<ShippingMethodDoesNotMatchCartError>({
396
+ code: "ShippingMethodDoesNotMatchCart",
397
+ message: `The shipping method with ID '${shippingMethodRef.id}' is not allowed for the order with ID '${resource.id}'.`,
398
+ });
399
+ }
400
+
401
+ const baseShippingInfo = createShippingInfoFromMethod(
402
+ context,
403
+ this._storage,
404
+ resource,
405
+ method,
406
+ );
407
+
408
+ return {
409
+ ...baseShippingInfo,
410
+ deliveries: [],
411
+ };
412
+ }
275
413
  }
@@ -711,6 +711,217 @@ describe("Cart Update Actions", () => {
711
711
  ]);
712
712
  });
713
713
 
714
+ test("setLineItemPrice sets an external price for a line item", async () => {
715
+ const product = await supertest(ctMock.app)
716
+ .post("/dummy/products")
717
+ .send(productDraft)
718
+ .then((x) => x.body);
719
+
720
+ assert(product, "product not created");
721
+
722
+ const baseCartResponse = await supertest(ctMock.app)
723
+ .post("/dummy/carts")
724
+ .send({ currency: "EUR" });
725
+ expect(baseCartResponse.status).toBe(201);
726
+ const baseCart = baseCartResponse.body as Cart;
727
+
728
+ const addLineItemResponse = await supertest(ctMock.app)
729
+ .post(`/dummy/carts/${baseCart.id}`)
730
+ .send({
731
+ version: baseCart.version,
732
+ actions: [
733
+ {
734
+ action: "addLineItem",
735
+ sku: product.masterData.current.masterVariant.sku,
736
+ quantity: 2,
737
+ key: "line-item-key",
738
+ },
739
+ ],
740
+ });
741
+ expect(addLineItemResponse.status).toBe(200);
742
+ const cartWithLineItem = addLineItemResponse.body as Cart;
743
+ const lineItem = cartWithLineItem.lineItems[0];
744
+ assert(lineItem, "lineItem not created");
745
+
746
+ const externalPrice: CentPrecisionMoney = {
747
+ type: "centPrecision",
748
+ currencyCode: "EUR",
749
+ centAmount: 2500,
750
+ fractionDigits: 2,
751
+ };
752
+
753
+ const response = await supertest(ctMock.app)
754
+ .post(`/dummy/carts/${cartWithLineItem.id}`)
755
+ .send({
756
+ version: cartWithLineItem.version,
757
+ actions: [
758
+ {
759
+ action: "setLineItemPrice",
760
+ lineItemKey: lineItem.key,
761
+ externalPrice,
762
+ },
763
+ ],
764
+ });
765
+
766
+ expect(response.status).toBe(200);
767
+ expect(response.body.version).toBe(cartWithLineItem.version + 1);
768
+ expect(response.body.lineItems).toHaveLength(1);
769
+
770
+ const updatedLineItem = response.body.lineItems[0];
771
+ expect(updatedLineItem.priceMode).toBe("ExternalPrice");
772
+ expect(updatedLineItem.price.value.centAmount).toBe(
773
+ externalPrice.centAmount,
774
+ );
775
+ expect(updatedLineItem.price.value.currencyCode).toBe(
776
+ externalPrice.currencyCode,
777
+ );
778
+ expect(updatedLineItem.totalPrice.centAmount).toBe(
779
+ externalPrice.centAmount * updatedLineItem.quantity,
780
+ );
781
+ expect(response.body.totalPrice.centAmount).toBe(
782
+ externalPrice.centAmount * updatedLineItem.quantity,
783
+ );
784
+ });
785
+
786
+ test("setLineItemPrice fails when the money uses another currency", async () => {
787
+ const product = await supertest(ctMock.app)
788
+ .post("/dummy/products")
789
+ .send(productDraft)
790
+ .then((x) => x.body);
791
+
792
+ assert(product, "product not created");
793
+
794
+ const baseCartResponse = await supertest(ctMock.app)
795
+ .post("/dummy/carts")
796
+ .send({ currency: "EUR" });
797
+ expect(baseCartResponse.status).toBe(201);
798
+ const baseCart = baseCartResponse.body as Cart;
799
+
800
+ const addLineItemResponse = await supertest(ctMock.app)
801
+ .post(`/dummy/carts/${baseCart.id}`)
802
+ .send({
803
+ version: baseCart.version,
804
+ actions: [
805
+ {
806
+ action: "addLineItem",
807
+ sku: product.masterData.current.masterVariant.sku,
808
+ quantity: 1,
809
+ },
810
+ ],
811
+ });
812
+ expect(addLineItemResponse.status).toBe(200);
813
+ const cartWithLineItem = addLineItemResponse.body as Cart;
814
+ const lineItem = cartWithLineItem.lineItems[0];
815
+ assert(lineItem, "lineItem not created");
816
+
817
+ const response = await supertest(ctMock.app)
818
+ .post(`/dummy/carts/${cartWithLineItem.id}`)
819
+ .send({
820
+ version: cartWithLineItem.version,
821
+ actions: [
822
+ {
823
+ action: "setLineItemPrice",
824
+ lineItemId: lineItem.id,
825
+ externalPrice: {
826
+ type: "centPrecision",
827
+ currencyCode: "USD",
828
+ centAmount: 5000,
829
+ fractionDigits: 2,
830
+ },
831
+ },
832
+ ],
833
+ });
834
+
835
+ expect(response.status).toBe(400);
836
+ expect(response.body.message).toContain("Currency mismatch");
837
+ });
838
+
839
+ test("setLineItemPrice removes external price when no value is provided", async () => {
840
+ const product = await supertest(ctMock.app)
841
+ .post("/dummy/products")
842
+ .send(productDraft)
843
+ .then((x) => x.body);
844
+
845
+ assert(product, "product not created");
846
+
847
+ const baseCartResponse = await supertest(ctMock.app)
848
+ .post("/dummy/carts")
849
+ .send({ currency: "EUR" });
850
+ expect(baseCartResponse.status).toBe(201);
851
+ const baseCart = baseCartResponse.body as Cart;
852
+
853
+ const addLineItemResponse = await supertest(ctMock.app)
854
+ .post(`/dummy/carts/${baseCart.id}`)
855
+ .send({
856
+ version: baseCart.version,
857
+ actions: [
858
+ {
859
+ action: "addLineItem",
860
+ sku: product.masterData.current.masterVariant.sku,
861
+ quantity: 1,
862
+ },
863
+ ],
864
+ });
865
+ expect(addLineItemResponse.status).toBe(200);
866
+ const cartWithLineItem = addLineItemResponse.body as Cart;
867
+ const lineItem = cartWithLineItem.lineItems[0];
868
+ assert(lineItem, "lineItem not created");
869
+
870
+ const externalPrice: CentPrecisionMoney = {
871
+ type: "centPrecision",
872
+ currencyCode: "EUR",
873
+ centAmount: 1000,
874
+ fractionDigits: 2,
875
+ };
876
+
877
+ const setExternalPriceResponse = await supertest(ctMock.app)
878
+ .post(`/dummy/carts/${cartWithLineItem.id}`)
879
+ .send({
880
+ version: cartWithLineItem.version,
881
+ actions: [
882
+ {
883
+ action: "setLineItemPrice",
884
+ lineItemId: lineItem.id,
885
+ externalPrice,
886
+ },
887
+ ],
888
+ });
889
+ expect(setExternalPriceResponse.status).toBe(200);
890
+ const cartWithExternalPrice = setExternalPriceResponse.body as Cart;
891
+ expect(cartWithExternalPrice.lineItems[0].priceMode).toBe("ExternalPrice");
892
+
893
+ const resetResponse = await supertest(ctMock.app)
894
+ .post(`/dummy/carts/${cartWithExternalPrice.id}`)
895
+ .send({
896
+ version: cartWithExternalPrice.version,
897
+ actions: [
898
+ {
899
+ action: "setLineItemPrice",
900
+ lineItemId: lineItem.id,
901
+ },
902
+ ],
903
+ });
904
+
905
+ expect(resetResponse.status).toBe(200);
906
+ expect(resetResponse.body.version).toBe(cartWithExternalPrice.version + 1);
907
+ expect(resetResponse.body.lineItems).toHaveLength(1);
908
+
909
+ const revertedLineItem = resetResponse.body.lineItems[0];
910
+ const expectedCentAmount =
911
+ product.masterData.current.masterVariant.prices?.[0].value.centAmount;
912
+ if (typeof expectedCentAmount !== "number") {
913
+ throw new Error("product price not found");
914
+ }
915
+ expect(revertedLineItem.priceMode).toBe("Platform");
916
+ expect(revertedLineItem.price.value.centAmount).toBe(expectedCentAmount);
917
+ expect(revertedLineItem.totalPrice.centAmount).toBe(
918
+ expectedCentAmount * revertedLineItem.quantity,
919
+ );
920
+ expect(resetResponse.body.totalPrice.centAmount).toBe(
921
+ expectedCentAmount * revertedLineItem.quantity,
922
+ );
923
+ });
924
+
714
925
  test("setLineItemCustomField", async () => {
715
926
  const product = await supertest(ctMock.app)
716
927
  .post("/dummy/products")
package/src/shipping.ts CHANGED
@@ -1,12 +1,23 @@
1
1
  import type {
2
2
  Cart,
3
3
  CartValueTier,
4
+ CentPrecisionMoney,
4
5
  InvalidOperationError,
6
+ MissingTaxRateForCountryError,
7
+ Order,
8
+ ShippingInfo,
9
+ ShippingMethod,
10
+ ShippingMethodDoesNotMatchCartError,
5
11
  ShippingRate,
6
12
  ShippingRatePriceTier,
13
+ TaxPortion,
14
+ TaxRate,
15
+ TaxedItemPrice,
7
16
  } from "@commercetools/platform-sdk";
17
+ import { Decimal } from "decimal.js";
8
18
  import { CommercetoolsError } from "./exceptions";
9
19
  import type { GetParams, RepositoryContext } from "./repositories/abstract";
20
+ import { createCentPrecisionMoney, roundDecimal } from "./repositories/helpers";
10
21
  import type { AbstractStorage } from "./storage/abstract";
11
22
 
12
23
  export const markMatchingShippingRate = (
@@ -145,3 +156,149 @@ export const getShippingMethodsMatchingCart = (
145
156
  results: results,
146
157
  };
147
158
  };
159
+
160
+ /**
161
+ * Interface for cart-like objects that can be used for shipping calculations
162
+ */
163
+ interface ShippingCalculationSource {
164
+ id: string;
165
+ totalPrice: CentPrecisionMoney;
166
+ shippingAddress?: {
167
+ country: string;
168
+ [key: string]: any;
169
+ };
170
+ taxRoundingMode?: string;
171
+ }
172
+
173
+ /**
174
+ * Creates shipping info from a shipping method, handling all tax calculations and pricing logic.
175
+ */
176
+ export const createShippingInfoFromMethod = (
177
+ context: RepositoryContext,
178
+ storage: AbstractStorage,
179
+ resource: ShippingCalculationSource,
180
+ method: ShippingMethod,
181
+ ): Omit<ShippingInfo, "deliveries"> => {
182
+ const country = resource.shippingAddress!.country;
183
+
184
+ // There should only be one zone rate matching the address, since
185
+ // Locations cannot be assigned to more than one zone.
186
+ // See https://docs.commercetools.com/api/projects/zones#location
187
+ const zoneRate = method.zoneRates.find((rate) =>
188
+ rate.zone.obj?.locations.some((loc) => loc.country === country),
189
+ );
190
+
191
+ if (!zoneRate) {
192
+ // This shouldn't happen because getShippingMethodsMatchingCart already
193
+ // filtered out shipping methods without any zones matching the address
194
+ throw new Error("Zone rate not found");
195
+ }
196
+
197
+ // Shipping rates are defined by currency, and getShippingMethodsMatchingCart
198
+ // also matches on currency, so there should only be one in the array.
199
+ // See https://docs.commercetools.com/api/projects/shippingMethods#zonerate
200
+ const shippingRate = zoneRate.shippingRates[0];
201
+ if (!shippingRate) {
202
+ // This shouldn't happen because getShippingMethodsMatchingCart already
203
+ // filtered out shipping methods without any matching rates
204
+ throw new Error("Shipping rate not found");
205
+ }
206
+
207
+ const taxCategory = storage.getByResourceIdentifier<"tax-category">(
208
+ context.projectKey,
209
+ method.taxCategory,
210
+ );
211
+
212
+ // TODO: match state in addition to country
213
+ const taxRate = taxCategory.rates.find((rate) => rate.country === country);
214
+
215
+ if (!taxRate) {
216
+ throw new CommercetoolsError<MissingTaxRateForCountryError>({
217
+ code: "MissingTaxRateForCountry",
218
+ message: `Tax category '${taxCategory.id}' is missing a tax rate for country '${country}'.`,
219
+ taxCategoryId: taxCategory.id,
220
+ });
221
+ }
222
+
223
+ const shippingRateTier = shippingRate.tiers.find((tier) => tier.isMatching);
224
+ if (shippingRateTier && shippingRateTier.type !== "CartValue") {
225
+ throw new Error("Non-CartValue shipping rate tier is not supported");
226
+ }
227
+
228
+ let shippingPrice = shippingRateTier
229
+ ? createCentPrecisionMoney(shippingRateTier.price)
230
+ : shippingRate.price;
231
+
232
+ // Handle freeAbove: if total is above the freeAbove threshold, shipping is free
233
+ if (
234
+ shippingRate.freeAbove &&
235
+ shippingRate.freeAbove.currencyCode === resource.totalPrice.currencyCode &&
236
+ resource.totalPrice.centAmount >= shippingRate.freeAbove.centAmount
237
+ ) {
238
+ shippingPrice = {
239
+ ...shippingPrice,
240
+ centAmount: 0,
241
+ };
242
+ }
243
+
244
+ // Calculate tax amounts
245
+ const totalGross: CentPrecisionMoney = taxRate.includedInPrice
246
+ ? shippingPrice
247
+ : {
248
+ ...shippingPrice,
249
+ centAmount: roundDecimal(
250
+ new Decimal(shippingPrice.centAmount).mul(1 + taxRate.amount),
251
+ resource.taxRoundingMode || "HalfEven",
252
+ ).toNumber(),
253
+ };
254
+
255
+ const totalNet: CentPrecisionMoney = taxRate.includedInPrice
256
+ ? {
257
+ ...shippingPrice,
258
+ centAmount: roundDecimal(
259
+ new Decimal(shippingPrice.centAmount).div(1 + taxRate.amount),
260
+ resource.taxRoundingMode || "HalfEven",
261
+ ).toNumber(),
262
+ }
263
+ : shippingPrice;
264
+
265
+ const taxPortions: TaxPortion[] = [
266
+ {
267
+ name: taxRate.name,
268
+ rate: taxRate.amount,
269
+ amount: {
270
+ ...shippingPrice,
271
+ centAmount: totalGross.centAmount - totalNet.centAmount,
272
+ },
273
+ },
274
+ ];
275
+
276
+ const totalTax: CentPrecisionMoney = {
277
+ ...shippingPrice,
278
+ centAmount: taxPortions.reduce(
279
+ (acc, portion) => acc + portion.amount.centAmount,
280
+ 0,
281
+ ),
282
+ };
283
+
284
+ const taxedPrice: TaxedItemPrice = {
285
+ totalNet,
286
+ totalGross,
287
+ taxPortions,
288
+ totalTax,
289
+ };
290
+
291
+ return {
292
+ shippingMethod: {
293
+ typeId: "shipping-method" as const,
294
+ id: method.id,
295
+ },
296
+ shippingMethodName: method.name,
297
+ price: shippingPrice,
298
+ shippingRate,
299
+ taxedPrice,
300
+ taxRate,
301
+ taxCategory: method.taxCategory,
302
+ shippingMethodState: "MatchesCart",
303
+ };
304
+ };