@labdigital/commercetools-mock 2.39.0 → 2.41.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.39.0",
3
+ "version": "2.41.0",
4
4
  "license": "MIT",
5
5
  "author": "Michael van Tellingen",
6
6
  "type": "module",
@@ -0,0 +1,127 @@
1
+ import type { Store } from "@commercetools/platform-sdk";
2
+ import { describe, expect, test } from "vitest";
3
+ import { InMemoryStorage } from "~src/storage";
4
+ import { CustomerRepository } from "./index";
5
+
6
+ describe("Customer repository", () => {
7
+ const storage = new InMemoryStorage();
8
+ const repository = new CustomerRepository(storage);
9
+
10
+ test("query by lowercaseEmail", async () => {
11
+ const customer = repository.create(
12
+ { projectKey: "dummy" },
13
+ { email: "my-customer-UPPERCASE@email.com" },
14
+ );
15
+
16
+ const result = repository.query(
17
+ { projectKey: "dummy" },
18
+ { where: [`lowercaseEmail = "my-customer-uppercase@email.com"`] },
19
+ );
20
+
21
+ expect(result.results).toHaveLength(1);
22
+ expect(result.results[0].id).toEqual(customer.id);
23
+ });
24
+
25
+ test("updating lowercaseEmail", async () => {
26
+ const customer = repository.create(
27
+ { projectKey: "dummy" },
28
+ { email: "my-customer-UPPERCASE-v1@email.com" },
29
+ );
30
+
31
+ repository.saveUpdate({ projectKey: "dummy" }, customer.version, {
32
+ ...customer,
33
+ email: "my-customer-UPPERCASE-v2@email.com",
34
+ version: customer.version + 1,
35
+ });
36
+
37
+ const result = repository.query(
38
+ { projectKey: "dummy" },
39
+ { where: [`lowercaseEmail = "my-customer-uppercase-v2@email.com"`] },
40
+ );
41
+
42
+ expect(result.results).toHaveLength(1);
43
+ expect(result.results[0].id).toEqual(customer.id);
44
+ expect(result.results[0].email).toEqual(
45
+ "my-customer-UPPERCASE-v2@email.com",
46
+ );
47
+ });
48
+
49
+ test("adding stores to customer", async () => {
50
+ const store1: Store = {
51
+ id: "d0016081-e9af-48a7-8133-1f04f340a335",
52
+ key: "store-1",
53
+ name: {
54
+ en: "Store 1",
55
+ },
56
+ version: 1,
57
+ createdAt: "2021-09-02T12:23:30.036Z",
58
+ lastModifiedAt: "2021-09-02T12:23:30.546Z",
59
+ languages: [],
60
+ distributionChannels: [],
61
+ countries: [],
62
+ supplyChannels: [],
63
+ productSelections: [],
64
+ };
65
+
66
+ const store2: Store = {
67
+ id: "6dac7d6d-2a48-4705-aa8b-17b0124a499a",
68
+ key: "store-2",
69
+ name: {
70
+ en: "Store 2",
71
+ },
72
+ version: 1,
73
+ createdAt: "2021-09-02T12:23:30.036Z",
74
+ lastModifiedAt: "2021-09-02T12:23:30.546Z",
75
+ languages: [],
76
+ distributionChannels: [],
77
+ countries: [],
78
+ supplyChannels: [],
79
+ productSelections: [],
80
+ };
81
+
82
+ storage.add("dummy", "store", store1);
83
+ storage.add("dummy", "store", store2);
84
+
85
+ const result = repository.create(
86
+ { projectKey: "dummy" },
87
+ {
88
+ email: "my-customer@email.com",
89
+ stores: [
90
+ {
91
+ typeId: "store",
92
+ id: store1.id,
93
+ },
94
+ {
95
+ typeId: "store",
96
+ key: store2.key,
97
+ },
98
+ ],
99
+ },
100
+ );
101
+
102
+ expect(result?.stores).toHaveLength(2);
103
+ expect(result?.stores).toEqual([
104
+ {
105
+ typeId: "store",
106
+ key: store1.key,
107
+ },
108
+ {
109
+ typeId: "store",
110
+ key: store2.key,
111
+ },
112
+ ]);
113
+ });
114
+
115
+ test("adding customer without linked stores", async () => {
116
+ const result = repository.create(
117
+ { projectKey: "dummy" },
118
+ {
119
+ email: "my-customer-without-stores@email.com",
120
+ stores: [],
121
+ },
122
+ );
123
+
124
+ expect(result.email).toEqual("my-customer-without-stores@email.com");
125
+ expect(result?.stores).toHaveLength(0);
126
+ });
127
+ });
@@ -9,6 +9,7 @@ import type {
9
9
  InvalidInputError,
10
10
  MyCustomerResetPassword,
11
11
  ResourceNotFoundError,
12
+ StoreKeyReference,
12
13
  } from "@commercetools/platform-sdk";
13
14
  import { CommercetoolsError } from "~src/exceptions";
14
15
  import { generateRandomString, getBaseResourceProperties } from "~src/helpers";
@@ -19,7 +20,7 @@ import {
19
20
  validatePasswordResetToken,
20
21
  } from "~src/lib/password";
21
22
  import type { AbstractStorage } from "~src/storage/abstract";
22
- import type { Writable } from "~src/types";
23
+ import type { ResourceMap, ShallowWritable, Writable } from "~src/types";
23
24
  import {
24
25
  AbstractResourceRepository,
25
26
  type RepositoryContext,
@@ -36,7 +37,7 @@ export class CustomerRepository extends AbstractResourceRepository<"customer"> {
36
37
  create(context: RepositoryContext, draft: CustomerDraft): Customer {
37
38
  // Check uniqueness
38
39
  const results = this._storage.query(context.projectKey, this.getTypeId(), {
39
- where: [`email="${draft.email.toLocaleLowerCase()}"`],
40
+ where: [`lowercaseEmail="${draft.email.toLowerCase()}"`],
40
41
  });
41
42
  if (results.count > 0) {
42
43
  throw new CommercetoolsError<any>({
@@ -102,6 +103,33 @@ export class CustomerRepository extends AbstractResourceRepository<"customer"> {
102
103
  lookupAdressId(addresses, addressId),
103
104
  ) ?? [];
104
105
 
106
+ let storesForCustomer: StoreKeyReference[] = [];
107
+
108
+ if (draft.stores && draft.stores.length > 0) {
109
+ const storeIds = draft.stores
110
+ .map((storeReference) => storeReference.id)
111
+ .filter(Boolean);
112
+
113
+ const stores = this._storage.query(context.projectKey, "store", {
114
+ where: storeIds.map((id) => `id="${id}"`),
115
+ }).results;
116
+
117
+ if (storeIds.length !== stores.length) {
118
+ throw new CommercetoolsError<ResourceNotFoundError>({
119
+ code: "ResourceNotFound",
120
+ message: `Store with ID '${storeIds.find((id) => !stores.some((store) => store.id === id))}' was not found.`,
121
+ });
122
+ }
123
+
124
+ storesForCustomer = draft.stores.map((storeReference) => ({
125
+ typeId: "store",
126
+ key:
127
+ storeReference.key ??
128
+ (stores.find((store) => store.id === storeReference.id)
129
+ ?.key as string),
130
+ }));
131
+ }
132
+
105
133
  const resource: Customer = {
106
134
  ...getBaseResourceProperties(),
107
135
  key: draft.key,
@@ -113,6 +141,7 @@ export class CustomerRepository extends AbstractResourceRepository<"customer"> {
113
141
  dateOfBirth: draft.dateOfBirth,
114
142
  companyName: draft.companyName,
115
143
  email: draft.email.toLowerCase(),
144
+ lowercaseEmail: draft.email.toLowerCase(),
116
145
  password: draft.password ? hashPassword(draft.password) : undefined,
117
146
  isEmailVerified: draft.isEmailVerified || false,
118
147
  addresses: addresses,
@@ -127,11 +156,26 @@ export class CustomerRepository extends AbstractResourceRepository<"customer"> {
127
156
  context.projectKey,
128
157
  this._storage,
129
158
  ),
130
- stores: [],
131
- };
159
+ stores: storesForCustomer,
160
+ } satisfies unknown as Customer;
161
+
132
162
  return this.saveNew(context, resource);
133
163
  }
134
164
 
165
+ saveUpdate(
166
+ context: RepositoryContext,
167
+ version: number,
168
+ resource: ShallowWritable<ResourceMap["customer"]>,
169
+ ): ShallowWritable<ResourceMap["customer"]> {
170
+ // Also update lowercaseEmail attribute
171
+ const updatedResource: Customer = {
172
+ ...resource,
173
+ lowercaseEmail: resource.email.toLowerCase(),
174
+ } satisfies unknown as Customer;
175
+
176
+ return super.saveUpdate(context, version, updatedResource);
177
+ }
178
+
135
179
  passwordResetToken(
136
180
  context: RepositoryContext,
137
181
  request: CustomerCreatePasswordResetToken,
@@ -15,6 +15,7 @@ import type {
15
15
  OrderSetDeliveryCustomFieldAction,
16
16
  OrderSetLocaleAction,
17
17
  OrderSetOrderNumberAction,
18
+ OrderSetParcelCustomFieldAction,
18
19
  OrderSetPurchaseOrderNumberAction,
19
20
  OrderSetShippingAddressAction,
20
21
  OrderSetStoreAction,
@@ -26,7 +27,6 @@ import type {
26
27
  Store,
27
28
  SyncInfo,
28
29
  } from "@commercetools/platform-sdk";
29
- import assert from "assert";
30
30
  import { getBaseResourceProperties } from "~src/helpers";
31
31
  import type { Writable } from "~src/types";
32
32
  import type { RepositoryContext, UpdateHandlerInterface } from "../abstract";
@@ -194,18 +194,14 @@ export class OrderUpdateHandler
194
194
  resource: Writable<Order>,
195
195
  { deliveryId, name, value }: OrderSetDeliveryCustomFieldAction,
196
196
  ) {
197
- assert(resource.shippingInfo, "shippingInfo is not defined");
197
+ if (!resource.shippingInfo) {
198
+ throw new Error("Resource has no shipping info");
199
+ }
198
200
 
199
- if (Array.isArray(resource.shippingInfo.deliveries)) {
200
- resource.shippingInfo.deliveries.map((delivery) => {
201
- if (delivery.id !== deliveryId) throw "No matching delivery id found";
202
- if (delivery.custom) {
203
- const update = delivery.custom.fields;
204
- update[name] = value;
205
- Object.assign(delivery.custom.fields, update);
206
- }
207
- return delivery;
208
- });
201
+ for (const delivery of resource.shippingInfo.deliveries || []) {
202
+ if (delivery.id === deliveryId && delivery.custom?.fields) {
203
+ delivery.custom.fields[name] = value;
204
+ }
209
205
  }
210
206
  }
211
207
 
@@ -225,6 +221,24 @@ export class OrderUpdateHandler
225
221
  resource.orderNumber = orderNumber;
226
222
  }
227
223
 
224
+ setParcelCustomField(
225
+ context: RepositoryContext,
226
+ resource: Writable<Order>,
227
+ { parcelId, name, value }: OrderSetParcelCustomFieldAction,
228
+ ) {
229
+ if (!resource.shippingInfo) {
230
+ throw new Error("Resource has no shipping info");
231
+ }
232
+
233
+ for (const delivery of resource.shippingInfo.deliveries || []) {
234
+ for (const parcel of delivery.parcels || []) {
235
+ if (parcel.id === parcelId && parcel.custom?.fields) {
236
+ parcel.custom.fields[name] = value;
237
+ }
238
+ }
239
+ }
240
+ }
241
+
228
242
  setPurchaseOrderNumber(
229
243
  context: RepositoryContext,
230
244
  resource: Writable<Order>,
@@ -31,6 +31,7 @@ describe("Me", () => {
31
31
  customer: {
32
32
  ...draft,
33
33
  password: "cDRzc3cwcmQ=",
34
+ lowercaseEmail: draft.email.toLowerCase(),
34
35
  authenticationMode: "Password",
35
36
  version: 1,
36
37
  isEmailVerified: false,
@@ -593,6 +593,156 @@ describe("Order Update Actions", () => {
593
593
  ).toBe("dhl");
594
594
  });
595
595
 
596
+ test("setParcelCustomField", async () => {
597
+ const order: Order = {
598
+ ...getBaseResourceProperties(),
599
+ customLineItems: [],
600
+ lastMessageSequenceNumber: 0,
601
+ lineItems: [],
602
+ orderNumber: "1390",
603
+ orderState: "Open",
604
+ origin: "Customer",
605
+ paymentInfo: {
606
+ payments: [
607
+ {
608
+ typeId: "payment",
609
+ id: generateRandomString(10),
610
+ },
611
+ ],
612
+ },
613
+ refusedGifts: [],
614
+ shippingInfo: {
615
+ shippingMethodName: "Home delivery (package)",
616
+ price: {
617
+ type: "centPrecision",
618
+ currencyCode: "EUR",
619
+ centAmount: 999,
620
+ fractionDigits: 2,
621
+ },
622
+ shippingRate: {
623
+ price: {
624
+ type: "centPrecision",
625
+ currencyCode: "EUR",
626
+ centAmount: 999,
627
+ fractionDigits: 2,
628
+ },
629
+ tiers: [
630
+ {
631
+ type: "CartScore",
632
+ score: 24,
633
+ price: {
634
+ type: "centPrecision",
635
+ currencyCode: "EUR",
636
+ centAmount: 1998,
637
+ fractionDigits: 2,
638
+ },
639
+ },
640
+ {
641
+ type: "CartScore",
642
+ score: 47,
643
+ price: {
644
+ type: "centPrecision",
645
+ currencyCode: "EUR",
646
+ centAmount: 2997,
647
+ fractionDigits: 2,
648
+ },
649
+ },
650
+ {
651
+ type: "CartScore",
652
+ score: 70,
653
+ price: {
654
+ type: "centPrecision",
655
+ currencyCode: "EUR",
656
+ centAmount: 3996,
657
+ fractionDigits: 2,
658
+ },
659
+ },
660
+ {
661
+ type: "CartScore",
662
+ score: 93,
663
+ price: {
664
+ type: "centPrecision",
665
+ currencyCode: "EUR",
666
+ centAmount: 4995,
667
+ fractionDigits: 2,
668
+ },
669
+ },
670
+ ],
671
+ },
672
+ deliveries: [
673
+ {
674
+ id: "6a458cad-dd46-4f5f-8b73-debOede6a17d",
675
+ key: "CT-Z243002",
676
+ createdAt: "2024-07-29T13:37:48.047Z",
677
+ items: [
678
+ {
679
+ id: "5d209544-2892-45c9-bef0-dde4e250188e",
680
+ quantity: 1,
681
+ },
682
+ ],
683
+ parcels: [
684
+ {
685
+ id: "7a458cad-dd46-4f5f-8b73-debOede6a17d",
686
+ createdAt: "2024-07-29T13:37:48.047Z",
687
+ items: [
688
+ {
689
+ id: "5d209544-2892-45c9-bef0-dde4e250188e",
690
+ quantity: 1,
691
+ },
692
+ ],
693
+ custom: {
694
+ type: {
695
+ typeId: "type",
696
+ id: "c493b7bb-d415-450c-b421-e128a8b26569",
697
+ },
698
+ fields: {
699
+ status: "created",
700
+ },
701
+ },
702
+ },
703
+ ],
704
+ },
705
+ ],
706
+ shippingMethodState: "MatchesCart",
707
+ },
708
+ shipping: [],
709
+ shippingMode: "Single",
710
+ syncInfo: [],
711
+ totalPrice: {
712
+ type: "centPrecision",
713
+ fractionDigits: 2,
714
+ centAmount: 2000,
715
+ currencyCode: "EUR",
716
+ },
717
+ };
718
+ ctMock.project("dummy").add("order", order);
719
+
720
+ const response = await supertest(ctMock.app).get(
721
+ `/dummy/orders/order-number=${order.orderNumber}`,
722
+ );
723
+
724
+ // check if status is set
725
+ const _updateResponse = await supertest(ctMock.app)
726
+ .post(`/dummy/orders/${response.body.id}`)
727
+ .send({
728
+ version: 0,
729
+ actions: [
730
+ {
731
+ action: "setParcelCustomField",
732
+ parcelId: "7a458cad-dd46-4f5f-8b73-debOede6a17d",
733
+ name: "status",
734
+ value: "delayed",
735
+ },
736
+ ],
737
+ });
738
+ expect(_updateResponse.status).toBe(200);
739
+ expect(_updateResponse.body.version).toBe(1);
740
+ expect(
741
+ _updateResponse.body.shippingInfo.deliveries[0].parcels[0].custom.fields
742
+ .status,
743
+ ).toBe("delayed");
744
+ });
745
+
596
746
  test("updateSyncInfo", async () => {
597
747
  assert(order, "order not created");
598
748