@labdigital/commercetools-mock 2.59.1 → 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.
@@ -192,6 +192,106 @@ describe("Order repository", () => {
192
192
  expect(result.shippingInfo).toEqual(cart.shippingInfo);
193
193
  });
194
194
 
195
+ test("should calculate taxed price when creating order from cart", () => {
196
+ const cart: Cart = {
197
+ ...getBaseResourceProperties(),
198
+ id: "cart-with-taxed-items",
199
+ version: 1,
200
+ cartState: "Active",
201
+ lineItems: [
202
+ {
203
+ id: "li-1",
204
+ productId: "product-1",
205
+ variantId: 1,
206
+ quantity: 1,
207
+ name: { en: "Test" },
208
+ variant: {
209
+ id: 1,
210
+ sku: "TEST",
211
+ },
212
+ price: {
213
+ value: {
214
+ type: "centPrecision",
215
+ currencyCode: "EUR",
216
+ centAmount: 1000,
217
+ fractionDigits: 2,
218
+ },
219
+ },
220
+ totalPrice: {
221
+ type: "centPrecision",
222
+ currencyCode: "EUR",
223
+ centAmount: 1000,
224
+ fractionDigits: 2,
225
+ },
226
+ taxedPrice: {
227
+ totalNet: {
228
+ type: "centPrecision",
229
+ currencyCode: "EUR",
230
+ centAmount: 1000,
231
+ fractionDigits: 2,
232
+ },
233
+ totalGross: {
234
+ type: "centPrecision",
235
+ currencyCode: "EUR",
236
+ centAmount: 1210,
237
+ fractionDigits: 2,
238
+ },
239
+ taxPortions: [
240
+ {
241
+ rate: 0.21,
242
+ amount: {
243
+ type: "centPrecision",
244
+ currencyCode: "EUR",
245
+ centAmount: 210,
246
+ fractionDigits: 2,
247
+ },
248
+ },
249
+ ],
250
+ totalTax: {
251
+ type: "centPrecision",
252
+ currencyCode: "EUR",
253
+ centAmount: 210,
254
+ fractionDigits: 2,
255
+ },
256
+ },
257
+ } as unknown as LineItem,
258
+ ],
259
+ customLineItems: [],
260
+ totalPrice: {
261
+ type: "centPrecision",
262
+ currencyCode: "EUR",
263
+ centAmount: 1000,
264
+ fractionDigits: 2,
265
+ },
266
+ priceRoundingMode: "HalfEven",
267
+ refusedGifts: [],
268
+ shippingMode: "Single",
269
+ shipping: [],
270
+ shippingAddress: { country: "NL" },
271
+ taxMode: "Platform",
272
+ taxRoundingMode: "HalfEven",
273
+ taxCalculationMode: "LineItemLevel",
274
+ origin: "Customer",
275
+ itemShippingAddresses: [],
276
+ directDiscounts: [],
277
+ discountCodes: [],
278
+ discountOnTotalPrice: undefined,
279
+ inventoryMode: "None",
280
+ };
281
+
282
+ storage.add("dummy", "cart", cart);
283
+ const ctx = { projectKey: "dummy" };
284
+ const result = repository.create(ctx, {
285
+ cart: { id: cart.id, typeId: "cart" },
286
+ version: cart.version,
287
+ });
288
+
289
+ expect(result.taxedPrice).toBeDefined();
290
+ expect(result.taxedPrice?.totalNet.centAmount).toBe(1000);
291
+ expect(result.taxedPrice?.totalGross.centAmount).toBe(1210);
292
+ expect(result.taxedPrice?.totalTax?.centAmount).toBe(210);
293
+ });
294
+
195
295
  test("create order in store", async () => {
196
296
  storage.add("dummy", "store", {
197
297
  ...getBaseResourceProperties(),
@@ -274,6 +374,89 @@ describe("Order repository", () => {
274
374
  expect(result.paymentState).toBe("Paid");
275
375
  });
276
376
 
377
+ test("should calculate taxed price when importing order", () => {
378
+ storage.add("dummy", "product", {
379
+ ...getBaseResourceProperties(),
380
+ id: "product-import",
381
+ productType: {
382
+ typeId: "product-type",
383
+ id: "product-type-id",
384
+ },
385
+ masterData: {
386
+ current: {
387
+ name: { en: "Imported" },
388
+ slug: { en: "imported" },
389
+ categories: [],
390
+ masterVariant: {
391
+ id: 1,
392
+ sku: "IMPORT-SKU",
393
+ prices: [],
394
+ attributes: [],
395
+ },
396
+ variants: [],
397
+ searchKeywords: {},
398
+ attributes: [],
399
+ },
400
+ staged: {
401
+ name: { en: "Imported" },
402
+ slug: { en: "imported" },
403
+ categories: [],
404
+ masterVariant: {
405
+ id: 1,
406
+ sku: "IMPORT-SKU",
407
+ prices: [],
408
+ attributes: [],
409
+ },
410
+ variants: [],
411
+ searchKeywords: {},
412
+ attributes: [],
413
+ },
414
+ published: false,
415
+ hasStagedChanges: false,
416
+ },
417
+ });
418
+
419
+ const draft: OrderImportDraft = {
420
+ orderNumber: "IMPORT-ORDER-1",
421
+ totalPrice: {
422
+ centAmount: 1000,
423
+ currencyCode: "EUR",
424
+ },
425
+ lineItems: [
426
+ {
427
+ name: { en: "Imported" },
428
+ variant: {
429
+ sku: "IMPORT-SKU",
430
+ },
431
+ price: {
432
+ value: {
433
+ type: "centPrecision",
434
+ currencyCode: "EUR",
435
+ centAmount: 1000,
436
+ fractionDigits: 2,
437
+ },
438
+ },
439
+ quantity: 1,
440
+ taxRate: {
441
+ name: "Standard VAT",
442
+ amount: 0.21,
443
+ includedInPrice: false,
444
+ country: "NL",
445
+ id: "import-tax-rate",
446
+ subRates: [],
447
+ },
448
+ },
449
+ ],
450
+ customLineItems: [],
451
+ };
452
+
453
+ const result = repository.import({ projectKey: "dummy" }, draft);
454
+ expect(result.taxedPrice).toBeDefined();
455
+ expect(result.taxedPrice?.totalNet.centAmount).toBe(1000);
456
+ expect(result.taxedPrice?.totalGross.centAmount).toBe(1210);
457
+ expect(result.taxedPrice?.totalTax?.centAmount).toBe(210);
458
+ });
459
+
277
460
  test("import exiting product", async () => {
278
461
  storage.add("dummy", "product", {
279
462
  id: "15fc56ba-a74e-4cf8-b4b0-bada5c101541",
@@ -26,6 +26,7 @@ import { Decimal } from "decimal.js/decimal";
26
26
  import type { Config } from "~src/config";
27
27
  import { CommercetoolsError } from "~src/exceptions";
28
28
  import { generateRandomString, getBaseResourceProperties } from "~src/helpers";
29
+ import { calculateTaxTotals, calculateTaxedPriceFromRate } from "~src/lib/tax";
29
30
  import {
30
31
  createShippingInfoFromMethod,
31
32
  getShippingMethodsMatchingCart,
@@ -75,7 +76,7 @@ export class OrderRepository extends AbstractResourceRepository<"order"> {
75
76
  throw new Error("Cannot find cart");
76
77
  }
77
78
 
78
- const resource: Order = {
79
+ const resource: Writable<Order> = {
79
80
  ...getBaseResourceProperties(),
80
81
  anonymousId: cart.anonymousId,
81
82
  billingAddress: cart.billingAddress,
@@ -110,6 +111,16 @@ export class OrderRepository extends AbstractResourceRepository<"order"> {
110
111
  totalPrice: cart.totalPrice,
111
112
  store: cart.store,
112
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;
113
124
  return this.saveNew(context, resource);
114
125
  }
115
126
 
@@ -194,6 +205,16 @@ export class OrderRepository extends AbstractResourceRepository<"order"> {
194
205
  });
195
206
  }
196
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
+
197
218
  return this.saveNew(context, resource);
198
219
  }
199
220
 
@@ -238,6 +259,12 @@ export class OrderRepository extends AbstractResourceRepository<"order"> {
238
259
  throw new Error("No product found");
239
260
  }
240
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
+
241
268
  const lineItem: LineItem = {
242
269
  ...getBaseResourceProperties(),
243
270
  custom: createCustomFields(
@@ -252,12 +279,17 @@ export class OrderRepository extends AbstractResourceRepository<"order"> {
252
279
  priceMode: "Platform",
253
280
  productId: product.id,
254
281
  productType: product.productType,
255
- quantity: draft.quantity,
282
+ quantity,
256
283
  state: draft.state || [],
257
284
  taxRate: draft.taxRate,
285
+ taxedPrice: calculateTaxedPriceFromRate(
286
+ totalPrice.centAmount,
287
+ totalPrice.currencyCode,
288
+ draft.taxRate,
289
+ ),
258
290
  taxedPricePortions: [],
259
291
  perMethodTaxRate: [],
260
- totalPrice: createCentPrecisionMoney(draft.price.value),
292
+ totalPrice,
261
293
  variant: {
262
294
  id: variant.id,
263
295
  sku: variant.sku,
@@ -273,6 +305,12 @@ export class OrderRepository extends AbstractResourceRepository<"order"> {
273
305
  context: RepositoryContext,
274
306
  draft: CustomLineItemImportDraft,
275
307
  ): CustomLineItem {
308
+ const quantity = draft.quantity ?? 1;
309
+ const totalPrice = createCentPrecisionMoney({
310
+ ...draft.money,
311
+ centAmount: (draft.money.centAmount ?? 0) * quantity,
312
+ });
313
+
276
314
  const lineItem: CustomLineItem = {
277
315
  ...getBaseResourceProperties(),
278
316
  custom: createCustomFields(
@@ -283,12 +321,17 @@ export class OrderRepository extends AbstractResourceRepository<"order"> {
283
321
  discountedPricePerQuantity: [],
284
322
  money: createTypedMoney(draft.money),
285
323
  name: draft.name,
286
- quantity: draft.quantity ?? 0,
324
+ quantity,
287
325
  perMethodTaxRate: [],
288
326
  priceMode: draft.priceMode ?? "Standard",
289
327
  slug: draft.slug,
290
328
  state: [],
291
- totalPrice: createCentPrecisionMoney(draft.money),
329
+ totalPrice,
330
+ taxedPrice: calculateTaxedPriceFromRate(
331
+ totalPrice.centAmount,
332
+ totalPrice.currencyCode,
333
+ draft.taxRate,
334
+ ),
292
335
  taxedPricePortions: [],
293
336
  };
294
337
 
@@ -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")