@labdigital/commercetools-mock 2.57.1 → 2.58.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@labdigital/commercetools-mock",
3
- "version": "2.57.1",
3
+ "version": "2.58.0",
4
4
  "license": "MIT",
5
5
  "author": "Michael van Tellingen",
6
6
  "type": "module",
@@ -2,10 +2,6 @@ import type {
2
2
  CartSetAnonymousIdAction,
3
3
  CartSetCustomerIdAction,
4
4
  CartUpdateAction,
5
- CentPrecisionMoney,
6
- InvalidOperationError,
7
- MissingTaxRateForCountryError,
8
- ShippingMethodDoesNotMatchCartError,
9
5
  } from "@commercetools/platform-sdk";
10
6
  import type {
11
7
  Address,
@@ -48,15 +44,12 @@ import type {
48
44
  import type {
49
45
  CustomLineItem,
50
46
  DirectDiscount,
51
- TaxPortion,
52
- TaxedItemPrice,
53
47
  } from "@commercetools/platform-sdk/dist/declarations/src/generated/models/cart";
54
48
  import type { ShippingMethodResourceIdentifier } from "@commercetools/platform-sdk/dist/declarations/src/generated/models/shipping-method";
55
- import { Decimal } from "decimal.js/decimal";
56
49
  import { v4 as uuidv4 } from "uuid";
57
50
  import { CommercetoolsError } from "~src/exceptions";
58
- import { getShippingMethodsMatchingCart } from "~src/shipping";
59
51
  import type { Writable } from "~src/types";
52
+ import type { CartRepository } from ".";
60
53
  import type { UpdateHandlerInterface } from "../abstract";
61
54
  import { AbstractUpdateHandler, type RepositoryContext } from "../abstract";
62
55
  import {
@@ -64,13 +57,10 @@ import {
64
57
  createCentPrecisionMoney,
65
58
  createCustomFields,
66
59
  createTypedMoney,
67
- getReferenceFromResourceIdentifier,
68
- roundDecimal,
69
60
  } from "../helpers";
70
61
  import {
71
62
  calculateCartTotalPrice,
72
63
  calculateLineItemTotalPrice,
73
- calculateTaxedPrice,
74
64
  createCustomLineItemFromDraft,
75
65
  selectPrice,
76
66
  } from "./helpers";
@@ -79,6 +69,12 @@ export class CartUpdateHandler
79
69
  extends AbstractUpdateHandler
80
70
  implements Partial<UpdateHandlerInterface<Cart, CartUpdateAction>>
81
71
  {
72
+ private repository: CartRepository;
73
+
74
+ constructor(storage: any, repository: CartRepository) {
75
+ super(storage);
76
+ this.repository = repository;
77
+ }
82
78
  addItemShippingAddress(
83
79
  context: RepositoryContext,
84
80
  resource: Writable<Cart>,
@@ -769,166 +765,11 @@ export class CartUpdateHandler
769
765
  { shippingMethod }: CartSetShippingMethodAction,
770
766
  ) {
771
767
  if (shippingMethod) {
772
- if (resource.taxMode === "External") {
773
- throw new Error("External tax rate is not supported");
774
- }
775
-
776
- const country = resource.shippingAddress?.country;
777
-
778
- if (!country) {
779
- throw new CommercetoolsError<InvalidOperationError>({
780
- code: "InvalidOperation",
781
- message: `The cart with ID '${resource.id}' does not have a shipping address set.`,
782
- });
783
- }
784
-
785
- // Bit of a hack: calling this checks that the resource identifier is
786
- // valid (i.e. id xor key) and that the shipping method exists.
787
- this._storage.getByResourceIdentifier<"shipping-method">(
788
- context.projectKey,
789
- shippingMethod,
790
- );
791
-
792
- // getShippingMethodsMatchingCart does the work of determining whether the
793
- // shipping method is allowed for the cart, and which shipping rate to use
794
- const shippingMethods = getShippingMethodsMatchingCart(
768
+ resource.shippingInfo = this.repository.createShippingInfo(
795
769
  context,
796
- this._storage,
797
770
  resource,
798
- {
799
- expand: ["zoneRates[*].zone"],
800
- },
801
- );
802
-
803
- const method = shippingMethods.results.find((candidate) =>
804
- shippingMethod.id
805
- ? candidate.id === shippingMethod.id
806
- : candidate.key === shippingMethod.key,
807
- );
808
-
809
- // Not finding the method in the results means it's not allowed, since
810
- // getShippingMethodsMatchingCart only returns allowed methods and we
811
- // already checked that the method exists.
812
- if (!method) {
813
- throw new CommercetoolsError<ShippingMethodDoesNotMatchCartError>({
814
- code: "ShippingMethodDoesNotMatchCart",
815
- message: `The shipping method with ${shippingMethod.id ? `ID '${shippingMethod.id}'` : `key '${shippingMethod.key}'`} is not allowed for the cart with ID '${resource.id}'.`,
816
- });
817
- }
818
-
819
- const taxCategory = this._storage.getByResourceIdentifier<"tax-category">(
820
- context.projectKey,
821
- method.taxCategory,
822
- );
823
-
824
- // TODO: match state in addition to country
825
- const taxRate = taxCategory.rates.find(
826
- (rate) => rate.country === country,
827
- );
828
-
829
- if (!taxRate) {
830
- throw new CommercetoolsError<MissingTaxRateForCountryError>({
831
- code: "MissingTaxRateForCountry",
832
- message: `Tax category '${taxCategory.id}' is missing a tax rate for country '${country}'.`,
833
- taxCategoryId: taxCategory.id,
834
- });
835
- }
836
-
837
- // There should only be one zone rate matching the address, since
838
- // Locations cannot be assigned to more than one zone.
839
- // See https://docs.commercetools.com/api/projects/zones#location
840
- const zoneRate = method.zoneRates.find((rate) =>
841
- rate.zone.obj?.locations.some((loc) => loc.country === country),
842
- );
843
-
844
- if (!zoneRate) {
845
- // This shouldn't happen because getShippingMethodsMatchingCart already
846
- // filtered out shipping methods without any zones matching the address
847
- throw new Error("Zone rate not found");
848
- }
849
-
850
- // Shipping rates are defined by currency, and getShippingMethodsMatchingCart
851
- // also matches on currency, so there should only be one in the array.
852
- // See https://docs.commercetools.com/api/projects/shippingMethods#zonerate
853
- const shippingRate = zoneRate.shippingRates[0];
854
- if (!shippingRate) {
855
- // This shouldn't happen because getShippingMethodsMatchingCart already
856
- // filtered out shipping methods without any matching rates
857
- throw new Error("Shipping rate not found");
858
- }
859
-
860
- const shippingRateTier = shippingRate.tiers.find(
861
- (tier) => tier.isMatching,
771
+ shippingMethod,
862
772
  );
863
- if (shippingRateTier && shippingRateTier.type !== "CartValue") {
864
- throw new Error("Non-CartValue shipping rate tier is not supported");
865
- }
866
-
867
- const shippingPrice = shippingRateTier
868
- ? createCentPrecisionMoney(shippingRateTier.price)
869
- : shippingRate.price;
870
-
871
- // TODO: handle freeAbove
872
-
873
- const totalGross: CentPrecisionMoney = taxRate.includedInPrice
874
- ? shippingPrice
875
- : {
876
- ...shippingPrice,
877
- centAmount: roundDecimal(
878
- new Decimal(shippingPrice.centAmount).mul(1 + taxRate.amount),
879
- resource.taxRoundingMode,
880
- ).toNumber(),
881
- };
882
-
883
- const totalNet: CentPrecisionMoney = taxRate.includedInPrice
884
- ? {
885
- ...shippingPrice,
886
- centAmount: roundDecimal(
887
- new Decimal(shippingPrice.centAmount).div(1 + taxRate.amount),
888
- resource.taxRoundingMode,
889
- ).toNumber(),
890
- }
891
- : shippingPrice;
892
-
893
- const taxPortions: TaxPortion[] = [
894
- {
895
- name: taxRate.name,
896
- rate: taxRate.amount,
897
- amount: {
898
- ...shippingPrice,
899
- centAmount: totalGross.centAmount - totalNet.centAmount,
900
- },
901
- },
902
- ];
903
-
904
- const totalTax: CentPrecisionMoney = {
905
- ...shippingPrice,
906
- centAmount: taxPortions.reduce(
907
- (acc, portion) => acc + portion.amount.centAmount,
908
- 0,
909
- ),
910
- };
911
-
912
- const taxedPrice: TaxedItemPrice = {
913
- totalNet,
914
- totalGross,
915
- taxPortions,
916
- totalTax,
917
- };
918
-
919
- // @ts-ignore
920
- resource.shippingInfo = {
921
- shippingMethod: {
922
- typeId: "shipping-method",
923
- id: method.id,
924
- },
925
- shippingMethodName: method.name,
926
- price: shippingPrice,
927
- shippingRate,
928
- taxedPrice,
929
- taxRate,
930
- taxCategory: method.taxCategory,
931
- };
932
773
  } else {
933
774
  resource.shippingInfo = undefined;
934
775
  }
@@ -1,9 +1,10 @@
1
1
  import type {
2
+ Cart,
2
3
  CartDraft,
3
4
  CustomLineItemDraft,
4
5
  LineItem,
5
6
  } from "@commercetools/platform-sdk";
6
- import { describe, expect, test } from "vitest";
7
+ import { beforeEach, describe, expect, test } from "vitest";
7
8
  import type { Config } from "~src/config";
8
9
  import { getBaseResourceProperties } from "~src/helpers";
9
10
  import { InMemoryStorage } from "~src/storage";
@@ -92,7 +93,70 @@ describe("Cart repository", () => {
92
93
  id: "tax-category-id",
93
94
  key: "standard-tax",
94
95
  name: "Standard Tax",
95
- rates: [],
96
+ rates: [
97
+ {
98
+ id: "nl-rate",
99
+ name: "Standard VAT",
100
+ amount: 0.21,
101
+ includedInPrice: false,
102
+ country: "NL",
103
+ },
104
+ ],
105
+ });
106
+
107
+ storage.add("dummy", "zone", {
108
+ ...getBaseResourceProperties(),
109
+ id: "nl-zone-id",
110
+ key: "nl-zone",
111
+ name: "Netherlands Zone",
112
+ locations: [
113
+ {
114
+ country: "NL",
115
+ },
116
+ ],
117
+ });
118
+
119
+ storage.add("dummy", "shipping-method", {
120
+ ...getBaseResourceProperties(),
121
+ id: "shipping-method-id",
122
+ key: "standard-shipping",
123
+ name: "Standard Shipping",
124
+ taxCategory: {
125
+ typeId: "tax-category",
126
+ id: "tax-category-id",
127
+ },
128
+ zoneRates: [
129
+ {
130
+ zone: {
131
+ typeId: "zone",
132
+ id: "nl-zone-id",
133
+ obj: {
134
+ ...getBaseResourceProperties(),
135
+ id: "nl-zone-id",
136
+ key: "nl-zone",
137
+ name: "Netherlands Zone",
138
+ locations: [
139
+ {
140
+ country: "NL",
141
+ },
142
+ ],
143
+ },
144
+ },
145
+ shippingRates: [
146
+ {
147
+ price: {
148
+ currencyCode: "EUR",
149
+ centAmount: 500,
150
+ type: "centPrecision",
151
+ fractionDigits: 2,
152
+ },
153
+ tiers: [],
154
+ },
155
+ ],
156
+ },
157
+ ],
158
+ active: true,
159
+ isDefault: false,
96
160
  });
97
161
 
98
162
  const cart: CartDraft = {
@@ -109,6 +173,10 @@ describe("Cart repository", () => {
109
173
  country: "NL",
110
174
  currency: "EUR",
111
175
  customerEmail: "john.doe@example.com",
176
+ shippingMethod: {
177
+ typeId: "shipping-method",
178
+ id: "shipping-method-id",
179
+ },
112
180
  customLineItems: [
113
181
  {
114
182
  name: { "nl-NL": "Douane kosten" },
@@ -204,6 +272,28 @@ describe("Cart repository", () => {
204
272
  cart.customLineItems?.[0].name,
205
273
  );
206
274
  expect(result.totalPrice.centAmount).toBe(3500);
275
+
276
+ expect(result.shippingInfo).toBeDefined();
277
+ expect(result.shippingInfo!.shippingMethod!.id).toBe("shipping-method-id");
278
+ expect(result.shippingInfo!.shippingMethodName).toBe("Standard Shipping");
279
+ expect(result.shippingInfo?.price).toBeDefined();
280
+ expect(result.shippingInfo?.price.centAmount).toBe(500);
281
+ expect(result.shippingInfo?.price.currencyCode).toBe("EUR");
282
+ expect(result.shippingInfo?.taxedPrice).toBeDefined();
283
+ expect(result.shippingInfo?.taxedPrice?.totalGross.centAmount).toBe(605);
284
+ expect(result.shippingInfo?.taxedPrice?.totalNet.centAmount).toBe(500);
285
+ expect(result.shippingInfo?.taxRate?.amount).toBe(0.21);
286
+ expect(result.shippingInfo?.taxRate?.name).toBe("Standard VAT");
287
+ });
288
+
289
+ test("create start with store from draft", () => {
290
+ const ctx = { projectKey: "dummy" };
291
+ const draft: CartDraft = {
292
+ currency: "USD",
293
+ store: { key: "draftStore", typeId: "store" },
294
+ };
295
+ const result = repository.create(ctx, draft);
296
+ expect(result.store).toEqual({ typeId: "store", key: "draftStore" });
207
297
  });
208
298
 
209
299
  test("create cart with business unit", async () => {
@@ -300,3 +390,287 @@ describe("Cart repository", () => {
300
390
  expect(customLineItem.taxRate?.country).toBe("NL");
301
391
  });
302
392
  });
393
+
394
+ describe("createShippingInfo", () => {
395
+ const storage = new InMemoryStorage();
396
+ const config: Config = { storage, strict: false };
397
+ const repository = new CartRepository(config);
398
+
399
+ beforeEach(() => {
400
+ storage.add("dummy", "tax-category", {
401
+ ...getBaseResourceProperties(),
402
+ id: "shipping-tax-category-id",
403
+ key: "shipping-tax",
404
+ name: "Shipping Tax",
405
+ rates: [
406
+ {
407
+ id: "nl-shipping-rate",
408
+ name: "Standard VAT",
409
+ amount: 0.21,
410
+ includedInPrice: false,
411
+ country: "NL",
412
+ },
413
+ ],
414
+ });
415
+
416
+ storage.add("dummy", "zone", {
417
+ ...getBaseResourceProperties(),
418
+ id: "test-zone-id",
419
+ name: "Test Zone",
420
+ locations: [
421
+ {
422
+ country: "NL",
423
+ },
424
+ ],
425
+ });
426
+ });
427
+
428
+ test("should calculate shipping info", () => {
429
+ storage.add("dummy", "shipping-method", {
430
+ ...getBaseResourceProperties(),
431
+ id: "basic-shipping-id",
432
+ name: "Standard Shipping",
433
+ taxCategory: {
434
+ typeId: "tax-category",
435
+ id: "shipping-tax-category-id",
436
+ },
437
+ zoneRates: [
438
+ {
439
+ zone: {
440
+ typeId: "zone",
441
+ id: "test-zone-id",
442
+ obj: {
443
+ ...getBaseResourceProperties(),
444
+ id: "test-zone-id",
445
+ name: "Test Zone",
446
+ locations: [
447
+ {
448
+ country: "NL",
449
+ },
450
+ ],
451
+ },
452
+ },
453
+ shippingRates: [
454
+ {
455
+ price: {
456
+ currencyCode: "EUR",
457
+ centAmount: 595,
458
+ type: "centPrecision",
459
+ fractionDigits: 2,
460
+ },
461
+ tiers: [],
462
+ },
463
+ ],
464
+ },
465
+ ],
466
+ active: true,
467
+ isDefault: false,
468
+ });
469
+
470
+ const cart: any = {
471
+ ...getBaseResourceProperties(),
472
+ id: "basic-cart-id",
473
+ version: 1,
474
+ cartState: "Active",
475
+ totalPrice: {
476
+ currencyCode: "EUR",
477
+ centAmount: 3000,
478
+ type: "centPrecision",
479
+ fractionDigits: 2,
480
+ },
481
+ shippingAddress: {
482
+ country: "NL",
483
+ },
484
+ taxRoundingMode: "HalfEven",
485
+ };
486
+
487
+ const context = { projectKey: "dummy", storeKey: "testStore" };
488
+ const shippingMethodRef = {
489
+ typeId: "shipping-method" as const,
490
+ id: "basic-shipping-id",
491
+ };
492
+
493
+ const result = repository.createShippingInfo(
494
+ context,
495
+ cart,
496
+ shippingMethodRef,
497
+ );
498
+
499
+ expect(result.price.centAmount).toBe(595);
500
+ expect(result.shippingMethodName).toBe("Standard Shipping");
501
+ expect(result.shippingMethod!.id).toBe("basic-shipping-id");
502
+ expect(result.taxRate?.amount).toBe(0.21);
503
+ expect(result.taxedPrice!.totalNet.centAmount).toBe(595);
504
+ expect(result.taxedPrice!.totalGross.centAmount).toBe(720);
505
+ });
506
+
507
+ test("should apply free shipping when cart total is above freeAbove threshold", () => {
508
+ storage.add("dummy", "shipping-method", {
509
+ ...getBaseResourceProperties(),
510
+ id: "free-above-shipping-id",
511
+ key: "free-above-shipping",
512
+ name: "Free Above €50",
513
+ taxCategory: {
514
+ typeId: "tax-category",
515
+ id: "shipping-tax-category-id",
516
+ },
517
+ zoneRates: [
518
+ {
519
+ zone: {
520
+ typeId: "zone",
521
+ id: "test-zone-id",
522
+ obj: {
523
+ ...getBaseResourceProperties(),
524
+ id: "test-zone-id",
525
+ key: "test-zone",
526
+ name: "Test Zone",
527
+ locations: [
528
+ {
529
+ country: "NL",
530
+ },
531
+ ],
532
+ },
533
+ },
534
+ shippingRates: [
535
+ {
536
+ price: {
537
+ currencyCode: "EUR",
538
+ centAmount: 995,
539
+ type: "centPrecision",
540
+ fractionDigits: 2,
541
+ },
542
+ freeAbove: {
543
+ currencyCode: "EUR",
544
+ centAmount: 5000,
545
+ type: "centPrecision",
546
+ fractionDigits: 2,
547
+ },
548
+ tiers: [],
549
+ },
550
+ ],
551
+ },
552
+ ],
553
+ active: true,
554
+ isDefault: false,
555
+ });
556
+
557
+ const cart: any = {
558
+ ...getBaseResourceProperties(),
559
+ id: "test-cart-id",
560
+ version: 1,
561
+ cartState: "Active",
562
+ totalPrice: {
563
+ currencyCode: "EUR",
564
+ centAmount: 6000,
565
+ type: "centPrecision",
566
+ fractionDigits: 2,
567
+ },
568
+ shippingAddress: {
569
+ country: "NL",
570
+ },
571
+ taxRoundingMode: "HalfEven",
572
+ };
573
+
574
+ const context = { projectKey: "dummy", storeKey: "testStore" };
575
+ const shippingMethodRef = {
576
+ typeId: "shipping-method" as const,
577
+ id: "free-above-shipping-id",
578
+ };
579
+
580
+ const result = repository.createShippingInfo(
581
+ context,
582
+ cart,
583
+ shippingMethodRef,
584
+ );
585
+
586
+ expect(result.price.centAmount).toBe(0);
587
+ expect(result.shippingMethodName).toBe("Free Above €50");
588
+ expect(result.taxedPrice!.totalGross.centAmount).toBe(0);
589
+ expect(result.taxedPrice!.totalNet.centAmount).toBe(0);
590
+ });
591
+
592
+ test("should charge normal shipping when cart total is below freeAbove threshold", () => {
593
+ storage.add("dummy", "shipping-method", {
594
+ ...getBaseResourceProperties(),
595
+ id: "free-above-shipping-id-2",
596
+ key: "free-above-shipping-2",
597
+ name: "Free Above €50",
598
+ taxCategory: {
599
+ typeId: "tax-category",
600
+ id: "shipping-tax-category-id",
601
+ },
602
+ zoneRates: [
603
+ {
604
+ zone: {
605
+ typeId: "zone",
606
+ id: "test-zone-id",
607
+ obj: {
608
+ ...getBaseResourceProperties(),
609
+ id: "test-zone-id",
610
+ key: "test-zone",
611
+ name: "Test Zone",
612
+ locations: [
613
+ {
614
+ country: "NL",
615
+ },
616
+ ],
617
+ },
618
+ },
619
+ shippingRates: [
620
+ {
621
+ price: {
622
+ currencyCode: "EUR",
623
+ centAmount: 995,
624
+ type: "centPrecision",
625
+ fractionDigits: 2,
626
+ },
627
+ freeAbove: {
628
+ currencyCode: "EUR",
629
+ centAmount: 5000,
630
+ type: "centPrecision",
631
+ fractionDigits: 2,
632
+ },
633
+ tiers: [],
634
+ },
635
+ ],
636
+ },
637
+ ],
638
+ active: true,
639
+ isDefault: false,
640
+ });
641
+
642
+ const cart: any = {
643
+ ...getBaseResourceProperties(),
644
+ id: "test-cart-id-2",
645
+ version: 1,
646
+ cartState: "Active",
647
+ totalPrice: {
648
+ currencyCode: "EUR",
649
+ centAmount: 2000,
650
+ type: "centPrecision",
651
+ fractionDigits: 2,
652
+ },
653
+ shippingAddress: {
654
+ country: "NL",
655
+ },
656
+ taxRoundingMode: "HalfEven",
657
+ };
658
+
659
+ const context = { projectKey: "dummy", storeKey: "testStore" };
660
+ const shippingMethodRef = {
661
+ typeId: "shipping-method" as const,
662
+ id: "free-above-shipping-id-2",
663
+ };
664
+
665
+ const result = repository.createShippingInfo(
666
+ context,
667
+ cart,
668
+ shippingMethodRef,
669
+ );
670
+
671
+ expect(result.price.centAmount).toBe(995);
672
+ expect(result.shippingMethodName).toBe("Free Above €50");
673
+ expect(result.taxedPrice!.totalGross.centAmount).toBe(1204);
674
+ expect(result.taxedPrice!.totalNet.centAmount).toBe(995);
675
+ });
676
+ });