@labdigital/commercetools-mock 2.45.0 → 2.46.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/dist/index.cjs +56 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +56 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/lib/predicateParser.test.ts +33 -2
- package/src/lib/predicateParser.ts +6 -0
- package/src/product-search.ts +48 -8
- package/src/repositories/cart/index.ts +17 -0
- package/src/repositories/product/helpers.ts +18 -2
- package/src/services/cart.test.ts +25 -0
- package/src/services/customer.test.ts +12 -51
- package/src/services/product.test.ts +154 -70
- package/src/testing/customer.ts +40 -0
package/package.json
CHANGED
|
@@ -13,8 +13,12 @@ describe("Predicate filter", () => {
|
|
|
13
13
|
nested: {
|
|
14
14
|
numberProperty: 1234,
|
|
15
15
|
objectProperty: {
|
|
16
|
-
stringProperty: "foobar",
|
|
17
|
-
booleanProperty: true,
|
|
16
|
+
"stringProperty": "foobar",
|
|
17
|
+
"booleanProperty": true,
|
|
18
|
+
"45c652f2-76e8-48fd-ab64-d11ad99d6631": {
|
|
19
|
+
stringProperty: "foobar",
|
|
20
|
+
uuidProperty: "3a57cc78-db08-4cd3-b778-d59b3326c435",
|
|
21
|
+
},
|
|
18
22
|
},
|
|
19
23
|
array: [
|
|
20
24
|
{
|
|
@@ -331,6 +335,33 @@ describe("Predicate filter", () => {
|
|
|
331
335
|
);
|
|
332
336
|
expect(() => match(`stringProperty`)).toThrow(PredicateError);
|
|
333
337
|
});
|
|
338
|
+
|
|
339
|
+
test("uuid as field name", async () => {
|
|
340
|
+
expect(
|
|
341
|
+
match(
|
|
342
|
+
`nested(objectProperty(45c652f2-76e8-48fd-ab64-d11ad99d6631(stringProperty = "foobar")))`,
|
|
343
|
+
),
|
|
344
|
+
).toBeTruthy();
|
|
345
|
+
|
|
346
|
+
expect(
|
|
347
|
+
match(
|
|
348
|
+
`nested(objectProperty(3a57cc78-db08-4cd3-b778-d59b3326c435(stringProperty = "foobar")))`,
|
|
349
|
+
),
|
|
350
|
+
).toBeFalsy();
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test("uuid as value", async () => {
|
|
354
|
+
expect(
|
|
355
|
+
match(
|
|
356
|
+
`nested(objectProperty(45c652f2-76e8-48fd-ab64-d11ad99d6631(uuidProperty = "3a57cc78-db08-4cd3-b778-d59b3326c435")))`,
|
|
357
|
+
),
|
|
358
|
+
).toBeTruthy();
|
|
359
|
+
expect(
|
|
360
|
+
match(
|
|
361
|
+
`nested(objectProperty(45c652f2-76e8-48fd-ab64-d11ad99d6631(uuidProperty = "45c652f2-76e8-48fd-ab64-d11ad99d6631")))`,
|
|
362
|
+
),
|
|
363
|
+
).toBeFalsy();
|
|
364
|
+
});
|
|
334
365
|
});
|
|
335
366
|
|
|
336
367
|
describe("Report parse errors", () => {
|
|
@@ -124,6 +124,12 @@ const getLexer = (value: string) =>
|
|
|
124
124
|
.token("IS", /is(?![-_a-z0-9]+)/i)
|
|
125
125
|
.token("DEFINED", /defined(?![-_a-z0-9]+)/i)
|
|
126
126
|
|
|
127
|
+
// Special case for UUID identifiers,
|
|
128
|
+
// since they otherwise would get matched as INT, when starting with a digit
|
|
129
|
+
.token(
|
|
130
|
+
"IDENTIFIER",
|
|
131
|
+
/[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}/,
|
|
132
|
+
)
|
|
127
133
|
.token("FLOAT", /\d+\.\d+/)
|
|
128
134
|
.token("INT", /\d+/)
|
|
129
135
|
.token("VARIABLE", /:([-_A-Za-z0-9]+)/)
|
package/src/product-search.ts
CHANGED
|
@@ -13,6 +13,12 @@ import { validateSearchQuery } from "./lib/searchQueryTypeChecker";
|
|
|
13
13
|
import { applyPriceSelector } from "./priceSelector";
|
|
14
14
|
import type { AbstractStorage } from "./storage";
|
|
15
15
|
|
|
16
|
+
interface ProductSearchVariantAvailability {
|
|
17
|
+
isOnStock: boolean;
|
|
18
|
+
availableQuantity: number;
|
|
19
|
+
isOnStockForChannel: string | undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
16
22
|
export class ProductSearch {
|
|
17
23
|
protected _storage: AbstractStorage;
|
|
18
24
|
|
|
@@ -24,10 +30,32 @@ export class ProductSearch {
|
|
|
24
30
|
projectKey: string,
|
|
25
31
|
params: ProductSearchRequest,
|
|
26
32
|
): ProductPagedSearchResponse {
|
|
27
|
-
|
|
33
|
+
const availabilityBySku = this._storage
|
|
34
|
+
.all(projectKey, "inventory-entry")
|
|
35
|
+
.reduce((acc, entry) => {
|
|
36
|
+
const existingEntry = acc.get(entry.sku);
|
|
37
|
+
|
|
38
|
+
acc.set(entry.sku, {
|
|
39
|
+
isOnStock: existingEntry?.isOnStock || entry.quantityOnStock > 0,
|
|
40
|
+
availableQuantity:
|
|
41
|
+
existingEntry?.availableQuantity ?? 0 + entry.quantityOnStock,
|
|
42
|
+
// NOTE: This doesn't handle inventory entries for multiple channels,
|
|
43
|
+
// so it doesn't exactly replicate the behavior of the commercetools api.
|
|
44
|
+
isOnStockForChannel:
|
|
45
|
+
existingEntry?.isOnStockForChannel ?? entry.supplyChannel?.id,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return acc;
|
|
49
|
+
}, new Map<string, ProductSearchVariantAvailability>());
|
|
50
|
+
|
|
51
|
+
let productResources = this._storage
|
|
28
52
|
.all(projectKey, "product")
|
|
29
53
|
.map((r) =>
|
|
30
|
-
this.
|
|
54
|
+
this.transformProduct(
|
|
55
|
+
r,
|
|
56
|
+
params.productProjectionParameters?.staged ?? false,
|
|
57
|
+
availabilityBySku,
|
|
58
|
+
),
|
|
31
59
|
)
|
|
32
60
|
.filter((p) => {
|
|
33
61
|
if (!(params.productProjectionParameters?.staged ?? false)) {
|
|
@@ -46,7 +74,7 @@ export class ProductSearch {
|
|
|
46
74
|
const matchFunc = parseSearchQuery(params.query);
|
|
47
75
|
|
|
48
76
|
// Filters can modify the output. So clone the resources first.
|
|
49
|
-
|
|
77
|
+
productResources = productResources.filter((resource) =>
|
|
50
78
|
matchFunc(resource, markMatchingVariant),
|
|
51
79
|
);
|
|
52
80
|
} catch (err) {
|
|
@@ -63,7 +91,7 @@ export class ProductSearch {
|
|
|
63
91
|
|
|
64
92
|
// Apply the priceSelector
|
|
65
93
|
if (params.productProjectionParameters) {
|
|
66
|
-
applyPriceSelector(
|
|
94
|
+
applyPriceSelector(productResources, {
|
|
67
95
|
country: params.productProjectionParameters.priceCountry,
|
|
68
96
|
channel: params.productProjectionParameters.priceChannel,
|
|
69
97
|
customerGroup: params.productProjectionParameters.priceCustomerGroup,
|
|
@@ -76,7 +104,10 @@ export class ProductSearch {
|
|
|
76
104
|
|
|
77
105
|
const offset = params.offset || 0;
|
|
78
106
|
const limit = params.limit || 20;
|
|
79
|
-
const productProjectionsResult =
|
|
107
|
+
const productProjectionsResult = productResources.slice(
|
|
108
|
+
offset,
|
|
109
|
+
offset + limit,
|
|
110
|
+
);
|
|
80
111
|
|
|
81
112
|
/**
|
|
82
113
|
* Do not supply productProjection if productProjectionParameters are not given
|
|
@@ -100,7 +131,7 @@ export class ProductSearch {
|
|
|
100
131
|
);
|
|
101
132
|
|
|
102
133
|
return {
|
|
103
|
-
total:
|
|
134
|
+
total: productResources.length,
|
|
104
135
|
offset: offset,
|
|
105
136
|
limit: limit,
|
|
106
137
|
results: results,
|
|
@@ -108,7 +139,11 @@ export class ProductSearch {
|
|
|
108
139
|
};
|
|
109
140
|
}
|
|
110
141
|
|
|
111
|
-
|
|
142
|
+
transformProduct(
|
|
143
|
+
product: Product,
|
|
144
|
+
staged: boolean,
|
|
145
|
+
availabilityBySku: Map<string, ProductSearchVariantAvailability>,
|
|
146
|
+
): ProductProjection {
|
|
112
147
|
const obj = !staged
|
|
113
148
|
? product.masterData.current
|
|
114
149
|
: product.masterData.staged;
|
|
@@ -125,7 +160,12 @@ export class ProductSearch {
|
|
|
125
160
|
slug: obj.slug,
|
|
126
161
|
categories: obj.categories,
|
|
127
162
|
masterVariant: obj.masterVariant,
|
|
128
|
-
variants: obj.variants
|
|
163
|
+
variants: obj.variants.map((variant) => ({
|
|
164
|
+
...variant,
|
|
165
|
+
availability: variant.sku
|
|
166
|
+
? availabilityBySku.get(variant.sku)
|
|
167
|
+
: { isOnStock: false, availableQuantity: 0, isOnStockForChannel: [] },
|
|
168
|
+
})),
|
|
129
169
|
productType: product.productType,
|
|
130
170
|
hasStagedChanges: product.masterData.hasStagedChanges,
|
|
131
171
|
published: product.masterData.published,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { InvalidOperationError } from "@commercetools/platform-sdk";
|
|
1
2
|
import {
|
|
2
3
|
type Cart,
|
|
3
4
|
type CartDraft,
|
|
@@ -27,6 +28,21 @@ export class CartRepository extends AbstractResourceRepository<"cart"> {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
create(context: RepositoryContext, draft: CartDraft): Cart {
|
|
31
|
+
if (draft.anonymousId && draft.customerId) {
|
|
32
|
+
throw new CommercetoolsError<InvalidOperationError>({
|
|
33
|
+
code: "InvalidOperation",
|
|
34
|
+
message: "Can set only one of customer OR anonymousId",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Validate that the customer exists
|
|
39
|
+
if (draft.customerId) {
|
|
40
|
+
this._storage.getByResourceIdentifier(context.projectKey, {
|
|
41
|
+
typeId: "customer",
|
|
42
|
+
id: draft.customerId,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
30
46
|
const lineItems =
|
|
31
47
|
draft.lineItems?.map((draftLineItem) =>
|
|
32
48
|
this.draftLineItemtoLineItem(
|
|
@@ -45,6 +61,7 @@ export class CartRepository extends AbstractResourceRepository<"cart"> {
|
|
|
45
61
|
: undefined,
|
|
46
62
|
cartState: "Active",
|
|
47
63
|
country: draft.country,
|
|
64
|
+
customerId: draft.customerId,
|
|
48
65
|
customerEmail: draft.customerEmail,
|
|
49
66
|
customLineItems: [],
|
|
50
67
|
directDiscounts: [],
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
Asset,
|
|
3
|
+
AssetDraft,
|
|
2
4
|
ChannelReference,
|
|
3
5
|
Price,
|
|
4
6
|
PriceDraft,
|
|
@@ -13,6 +15,7 @@ import type { AbstractStorage } from "~src/storage";
|
|
|
13
15
|
import type { Writable } from "~src/types";
|
|
14
16
|
import type { RepositoryContext } from "../abstract";
|
|
15
17
|
import {
|
|
18
|
+
createCustomFields,
|
|
16
19
|
createTypedMoney,
|
|
17
20
|
getReferenceFromResourceIdentifier,
|
|
18
21
|
} from "../helpers";
|
|
@@ -75,10 +78,23 @@ export const variantFromDraft = (
|
|
|
75
78
|
key: variant?.key,
|
|
76
79
|
attributes: variant?.attributes ?? [],
|
|
77
80
|
prices: variant?.prices?.map((p) => priceFromDraft(context, storage, p)),
|
|
78
|
-
assets: [],
|
|
79
|
-
images: [],
|
|
81
|
+
assets: variant.assets?.map((a) => assetFromDraft(context, storage, a)) ?? [],
|
|
82
|
+
images: variant.images ?? [],
|
|
80
83
|
});
|
|
81
84
|
|
|
85
|
+
export const assetFromDraft = (
|
|
86
|
+
context: RepositoryContext,
|
|
87
|
+
storage: AbstractStorage,
|
|
88
|
+
draft: AssetDraft,
|
|
89
|
+
): Asset => {
|
|
90
|
+
const asset: Asset = {
|
|
91
|
+
...draft,
|
|
92
|
+
id: uuidv4(),
|
|
93
|
+
custom: createCustomFields(draft.custom, context.projectKey, storage),
|
|
94
|
+
};
|
|
95
|
+
return asset;
|
|
96
|
+
};
|
|
97
|
+
|
|
82
98
|
export const priceFromDraft = (
|
|
83
99
|
context: RepositoryContext,
|
|
84
100
|
storage: AbstractStorage,
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
import assert from "assert";
|
|
14
14
|
import supertest from "supertest";
|
|
15
15
|
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
16
|
+
import { customerDraftFactory } from "~src/testing/customer";
|
|
16
17
|
import { CommercetoolsMock } from "../index";
|
|
17
18
|
|
|
18
19
|
describe("Carts Query", () => {
|
|
@@ -81,6 +82,30 @@ describe("Carts Query", () => {
|
|
|
81
82
|
expect(myCart.custom?.type.id).toBe(myCart.custom?.type.obj?.id);
|
|
82
83
|
expect(myCart.custom?.type.obj?.description?.en).toBe("Test Type");
|
|
83
84
|
});
|
|
85
|
+
|
|
86
|
+
test("throw error when anonymousId and customerId are given", async () => {
|
|
87
|
+
const customerId = "400be09e-bfe8-4925-a307-4ef6280b063e";
|
|
88
|
+
const anonymousId = "a99f27d1-7e7e-4592-8d5a-aa5da1adfe24";
|
|
89
|
+
const response = await supertest(ctMock.app).post("/dummy/carts").send({
|
|
90
|
+
currency: "EUR",
|
|
91
|
+
anonymousId,
|
|
92
|
+
customerId,
|
|
93
|
+
});
|
|
94
|
+
expect(response.status).toBe(400);
|
|
95
|
+
expect(response.body.message).toBe(
|
|
96
|
+
"Can set only one of customer OR anonymousId",
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("create cart with existing customer", async () => {
|
|
101
|
+
const customer = await customerDraftFactory(ctMock).create();
|
|
102
|
+
const response = await supertest(ctMock.app).post("/dummy/carts").send({
|
|
103
|
+
currency: "EUR",
|
|
104
|
+
customerId: customer.id,
|
|
105
|
+
});
|
|
106
|
+
expect(response.status).toBe(201);
|
|
107
|
+
expect(response.body.customerId).toBe(customer.id);
|
|
108
|
+
});
|
|
84
109
|
});
|
|
85
110
|
|
|
86
111
|
describe("Cart Update Actions", () => {
|
|
@@ -4,60 +4,21 @@ import type {
|
|
|
4
4
|
CustomerToken,
|
|
5
5
|
} from "@commercetools/platform-sdk";
|
|
6
6
|
import assert from "assert";
|
|
7
|
-
import { Factory } from "fishery";
|
|
8
7
|
import supertest from "supertest";
|
|
9
8
|
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
10
9
|
import { hashPassword } from "~src/lib/password";
|
|
10
|
+
import { customerDraftFactory } from "~src/testing/customer";
|
|
11
11
|
import { CommercetoolsMock, getBaseResourceProperties } from "../index";
|
|
12
12
|
|
|
13
13
|
const ctMock = new CommercetoolsMock();
|
|
14
14
|
|
|
15
|
-
const customerDraftFactory = Factory.define<
|
|
16
|
-
CustomerDraft,
|
|
17
|
-
CustomerDraft,
|
|
18
|
-
Customer
|
|
19
|
-
>(({ onCreate }) => {
|
|
20
|
-
onCreate(async (draft) => {
|
|
21
|
-
const response = await supertest(ctMock.app)
|
|
22
|
-
.post(`/dummy/customers`)
|
|
23
|
-
.send(draft);
|
|
24
|
-
|
|
25
|
-
return response.body.customer;
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
return {
|
|
29
|
-
email: "customer@example.com",
|
|
30
|
-
firstName: "John",
|
|
31
|
-
lastName: "Doe",
|
|
32
|
-
locale: "nl-NL",
|
|
33
|
-
password: "my-secret-pw",
|
|
34
|
-
addresses: [
|
|
35
|
-
{
|
|
36
|
-
firstName: "John",
|
|
37
|
-
lastName: "Doe",
|
|
38
|
-
streetName: "Street name",
|
|
39
|
-
streetNumber: "42",
|
|
40
|
-
postalCode: "1234 AB",
|
|
41
|
-
city: "Utrecht",
|
|
42
|
-
country: "NL",
|
|
43
|
-
company: "Lab Digital",
|
|
44
|
-
phone: "+31612345678",
|
|
45
|
-
email: "customer@example.com",
|
|
46
|
-
},
|
|
47
|
-
],
|
|
48
|
-
isEmailVerified: false,
|
|
49
|
-
stores: [],
|
|
50
|
-
authenticationMode: "Password",
|
|
51
|
-
};
|
|
52
|
-
});
|
|
53
|
-
|
|
54
15
|
afterEach(() => {
|
|
55
16
|
ctMock.clear();
|
|
56
17
|
});
|
|
57
18
|
|
|
58
19
|
describe("Customer create", () => {
|
|
59
20
|
test("create new customer", async () => {
|
|
60
|
-
const draft = customerDraftFactory.build();
|
|
21
|
+
const draft = customerDraftFactory(ctMock).build();
|
|
61
22
|
|
|
62
23
|
const response = await supertest(ctMock.app)
|
|
63
24
|
.post(`/dummy/customers`)
|
|
@@ -109,7 +70,7 @@ describe("Customer create", () => {
|
|
|
109
70
|
|
|
110
71
|
describe("Customer Update Actions", () => {
|
|
111
72
|
test("addAddress", async () => {
|
|
112
|
-
const customer = await customerDraftFactory.create();
|
|
73
|
+
const customer = await customerDraftFactory(ctMock).create();
|
|
113
74
|
const response = await supertest(ctMock.app)
|
|
114
75
|
.post(`/dummy/customers/${customer.id}`)
|
|
115
76
|
.send({
|
|
@@ -134,7 +95,7 @@ describe("Customer Update Actions", () => {
|
|
|
134
95
|
});
|
|
135
96
|
|
|
136
97
|
test("removeAddress by ID", async () => {
|
|
137
|
-
const customer = await customerDraftFactory.create({
|
|
98
|
+
const customer = await customerDraftFactory(ctMock).create({
|
|
138
99
|
addresses: [
|
|
139
100
|
{
|
|
140
101
|
key: "address-key",
|
|
@@ -165,7 +126,7 @@ describe("Customer Update Actions", () => {
|
|
|
165
126
|
});
|
|
166
127
|
|
|
167
128
|
test("removeAddress by Key", async () => {
|
|
168
|
-
const customer = await customerDraftFactory.create({
|
|
129
|
+
const customer = await customerDraftFactory(ctMock).create({
|
|
169
130
|
addresses: [
|
|
170
131
|
{
|
|
171
132
|
key: "address-key",
|
|
@@ -196,7 +157,7 @@ describe("Customer Update Actions", () => {
|
|
|
196
157
|
});
|
|
197
158
|
|
|
198
159
|
test("changeAddress by ID", async () => {
|
|
199
|
-
const customer = await customerDraftFactory.create({
|
|
160
|
+
const customer = await customerDraftFactory(ctMock).create({
|
|
200
161
|
addresses: [
|
|
201
162
|
{
|
|
202
163
|
key: "address-key",
|
|
@@ -248,7 +209,7 @@ describe("Customer Update Actions", () => {
|
|
|
248
209
|
});
|
|
249
210
|
|
|
250
211
|
test("addBillingAddressId", async () => {
|
|
251
|
-
const customer = await customerDraftFactory.create({
|
|
212
|
+
const customer = await customerDraftFactory(ctMock).create({
|
|
252
213
|
addresses: [
|
|
253
214
|
{
|
|
254
215
|
key: "address-key",
|
|
@@ -280,7 +241,7 @@ describe("Customer Update Actions", () => {
|
|
|
280
241
|
});
|
|
281
242
|
|
|
282
243
|
test("removeBillingAddressId", async () => {
|
|
283
|
-
const customer = await customerDraftFactory.create({
|
|
244
|
+
const customer = await customerDraftFactory(ctMock).create({
|
|
284
245
|
addresses: [
|
|
285
246
|
{
|
|
286
247
|
key: "address-key",
|
|
@@ -318,7 +279,7 @@ describe("Customer Update Actions", () => {
|
|
|
318
279
|
});
|
|
319
280
|
|
|
320
281
|
test("setDefaultBillingAddress by ID", async () => {
|
|
321
|
-
const customer = await customerDraftFactory.create({
|
|
282
|
+
const customer = await customerDraftFactory(ctMock).create({
|
|
322
283
|
defaultBillingAddress: undefined,
|
|
323
284
|
defaultShippingAddress: undefined,
|
|
324
285
|
addresses: [
|
|
@@ -356,7 +317,7 @@ describe("Customer Update Actions", () => {
|
|
|
356
317
|
});
|
|
357
318
|
|
|
358
319
|
test("addShippingAddressId", async () => {
|
|
359
|
-
const customer = await customerDraftFactory.create({
|
|
320
|
+
const customer = await customerDraftFactory(ctMock).create({
|
|
360
321
|
addresses: [
|
|
361
322
|
{
|
|
362
323
|
key: "address-key",
|
|
@@ -387,7 +348,7 @@ describe("Customer Update Actions", () => {
|
|
|
387
348
|
});
|
|
388
349
|
|
|
389
350
|
test("removeShippingAddressId", async () => {
|
|
390
|
-
const customer = await customerDraftFactory.create({
|
|
351
|
+
const customer = await customerDraftFactory(ctMock).create({
|
|
391
352
|
addresses: [
|
|
392
353
|
{
|
|
393
354
|
key: "address-key",
|
|
@@ -425,7 +386,7 @@ describe("Customer Update Actions", () => {
|
|
|
425
386
|
});
|
|
426
387
|
|
|
427
388
|
test("setDefaultShippingAddress by ID", async () => {
|
|
428
|
-
const customer = await customerDraftFactory.create({
|
|
389
|
+
const customer = await customerDraftFactory(ctMock).create({
|
|
429
390
|
defaultBillingAddress: undefined,
|
|
430
391
|
defaultShippingAddress: undefined,
|
|
431
392
|
addresses: [
|