@reactionary/magento 0.10.25 → 0.10.27
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/capabilities/checkout.capability.js +132 -20
- package/capabilities/product-search.capability.js +59 -28
- package/factories/product/product.factory.js +6 -1
- package/factories/product-search/product-search.factory.js +6 -1
- package/package.json +2 -2
- package/schema/configuration.schema.js +1 -0
- package/src/capabilities/checkout.capability.d.ts +34 -1
- package/src/capabilities/product-search.capability.d.ts +23 -0
- package/src/factories/product/product.factory.d.ts +2 -1
- package/src/factories/product-search/product-search.factory.d.ts +2 -1
- package/src/index.d.ts +1 -0
- package/src/schema/configuration.schema.d.ts +1 -0
- package/src/schema/magento.types.d.ts +4 -0
- package/utils/magento-product.js +6 -1
|
@@ -32,6 +32,36 @@ import {
|
|
|
32
32
|
encodeShippingMethodKey
|
|
33
33
|
} from "../factories/checkout/checkout.factory.js";
|
|
34
34
|
const debug = createDebug("reactionary:magento:checkout");
|
|
35
|
+
function nonEmptyString(value) {
|
|
36
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
37
|
+
}
|
|
38
|
+
function numberValue(value) {
|
|
39
|
+
return typeof value === "number" ? value : void 0;
|
|
40
|
+
}
|
|
41
|
+
function parseQuoteAddress(raw) {
|
|
42
|
+
const rawStreet = raw["street"];
|
|
43
|
+
const street = Array.isArray(rawStreet) ? rawStreet.filter((line) => nonEmptyString(line) !== void 0) : [];
|
|
44
|
+
const firstname = nonEmptyString(raw["firstname"]);
|
|
45
|
+
const city = nonEmptyString(raw["city"]);
|
|
46
|
+
if (!firstname || !city || street.length === 0) {
|
|
47
|
+
return void 0;
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
firstname,
|
|
51
|
+
lastname: nonEmptyString(raw["lastname"]),
|
|
52
|
+
street,
|
|
53
|
+
city,
|
|
54
|
+
company: nonEmptyString(raw["company"]),
|
|
55
|
+
region: nonEmptyString(raw["region"]),
|
|
56
|
+
region_id: numberValue(raw["region_id"]),
|
|
57
|
+
region_code: nonEmptyString(raw["region_code"]),
|
|
58
|
+
customer_address_id: numberValue(raw["customer_address_id"]),
|
|
59
|
+
postcode: nonEmptyString(raw["postcode"]),
|
|
60
|
+
country_id: nonEmptyString(raw["country_id"]),
|
|
61
|
+
telephone: nonEmptyString(raw["telephone"]),
|
|
62
|
+
email: nonEmptyString(raw["email"])
|
|
63
|
+
};
|
|
64
|
+
}
|
|
35
65
|
class CheckoutNotReadyForFinalizationError extends Error {
|
|
36
66
|
constructor(checkoutIdentifier) {
|
|
37
67
|
super(
|
|
@@ -77,6 +107,68 @@ class MagentoCheckoutCapability extends CheckoutCapability {
|
|
|
77
107
|
}
|
|
78
108
|
return { cart, totals };
|
|
79
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* The quote's `billing_address` is the only checkout data Magento keeps
|
|
112
|
+
* durably before an order is placed, so it is where email and address
|
|
113
|
+
* survive between requests. Never throws: a missing quote simply yields
|
|
114
|
+
* nothing to fall back to.
|
|
115
|
+
*/
|
|
116
|
+
async readQuoteBillingAddress(cartKey) {
|
|
117
|
+
try {
|
|
118
|
+
const cart = await this.magentoApi.getCart(cartKey);
|
|
119
|
+
const raw = cart.billing_address;
|
|
120
|
+
if (!raw) {
|
|
121
|
+
return {};
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
email: nonEmptyString(raw["email"]),
|
|
125
|
+
countryId: nonEmptyString(raw["country_id"]),
|
|
126
|
+
address: parseQuoteAddress(raw)
|
|
127
|
+
};
|
|
128
|
+
} catch (err) {
|
|
129
|
+
debug("Failed to read billing address from quote: %O", err);
|
|
130
|
+
return {};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Loads the session checkout state and, when this request's session does
|
|
135
|
+
* not know the email or billing address (e.g. every request in a web
|
|
136
|
+
* storefront starts with an empty session), rebuilds them from the quote.
|
|
137
|
+
*/
|
|
138
|
+
async loadCheckoutState(cartKey) {
|
|
139
|
+
const state = await this.magentoApi.getCheckoutState(cartKey);
|
|
140
|
+
if (!state.email || !state.billingAddress) {
|
|
141
|
+
const quote = await this.readQuoteBillingAddress(cartKey);
|
|
142
|
+
state.email = state.email || quote.email;
|
|
143
|
+
state.billingAddress = state.billingAddress ?? quote.address;
|
|
144
|
+
}
|
|
145
|
+
return state;
|
|
146
|
+
}
|
|
147
|
+
async persistBillingAddressOnQuote(cartKey, address) {
|
|
148
|
+
try {
|
|
149
|
+
await this.magentoApi.setCheckoutBillingAddress(cartKey, address);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
debug("Failed to persist billing address on quote: %O", err);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Stores the email on the quote before any address is known. Magento only
|
|
156
|
+
* accepts a billing address with a country, so this reuses the country the
|
|
157
|
+
* quote already carries (Magento pre-fills the store's default country)
|
|
158
|
+
* rather than inventing one; without it the email stays session-only.
|
|
159
|
+
*/
|
|
160
|
+
async persistEmailOnQuote(cartKey, state) {
|
|
161
|
+
let address = state.billingAddress;
|
|
162
|
+
if (!address) {
|
|
163
|
+
const { countryId } = await this.readQuoteBillingAddress(cartKey);
|
|
164
|
+
address = countryId ? { country_id: countryId } : void 0;
|
|
165
|
+
}
|
|
166
|
+
if (!address) {
|
|
167
|
+
debug("Quote has no country; email for %s is kept in the session only", cartKey);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
await this.persistBillingAddressOnQuote(cartKey, { ...address, email: state.email });
|
|
171
|
+
}
|
|
80
172
|
async buildCheckout(cartKey, state) {
|
|
81
173
|
const { cart, totals } = await this.loadCartAndTotals(cartKey);
|
|
82
174
|
const data = {
|
|
@@ -89,20 +181,17 @@ class MagentoCheckoutCapability extends CheckoutCapability {
|
|
|
89
181
|
}
|
|
90
182
|
async initiateCheckoutForCart(payload) {
|
|
91
183
|
const cartKey = payload.cart.identifier.key;
|
|
92
|
-
const state = await this.
|
|
184
|
+
const state = await this.loadCheckoutState(cartKey);
|
|
93
185
|
state.email = payload.notificationEmail ?? state.email;
|
|
94
186
|
state.phone = payload.notificationPhone ?? state.phone;
|
|
95
187
|
if (payload.billingAddress) {
|
|
96
|
-
|
|
188
|
+
state.billingAddress = this.toMagentoAddress(
|
|
97
189
|
payload.billingAddress,
|
|
98
190
|
state.email
|
|
99
191
|
);
|
|
100
|
-
state.billingAddress
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
} catch (err) {
|
|
104
|
-
debug("Failed to persist billing address on quote: %O", err);
|
|
105
|
-
}
|
|
192
|
+
await this.persistBillingAddressOnQuote(cartKey, state.billingAddress);
|
|
193
|
+
} else if (payload.notificationEmail) {
|
|
194
|
+
await this.persistEmailOnQuote(cartKey, state);
|
|
106
195
|
}
|
|
107
196
|
await this.magentoApi.setCheckoutState(cartKey, state);
|
|
108
197
|
return success(await this.buildCheckout(cartKey, state));
|
|
@@ -110,7 +199,7 @@ class MagentoCheckoutCapability extends CheckoutCapability {
|
|
|
110
199
|
async getById(payload) {
|
|
111
200
|
const cartKey = payload.identifier.key;
|
|
112
201
|
try {
|
|
113
|
-
const state = await this.
|
|
202
|
+
const state = await this.loadCheckoutState(cartKey);
|
|
114
203
|
return success(await this.buildCheckout(cartKey, state));
|
|
115
204
|
} catch (err) {
|
|
116
205
|
debug("Failed to load checkout: %O", err);
|
|
@@ -122,17 +211,18 @@ class MagentoCheckoutCapability extends CheckoutCapability {
|
|
|
122
211
|
}
|
|
123
212
|
async setShippingAddress(payload) {
|
|
124
213
|
const cartKey = payload.checkout.key;
|
|
125
|
-
const state = await this.
|
|
214
|
+
const state = await this.loadCheckoutState(cartKey);
|
|
126
215
|
state.shippingAddress = this.toMagentoAddress(
|
|
127
216
|
payload.shippingAddress,
|
|
128
217
|
state.email
|
|
129
218
|
);
|
|
219
|
+
await this.persistBillingAddressOnQuote(cartKey, state.shippingAddress);
|
|
130
220
|
await this.magentoApi.setCheckoutState(cartKey, state);
|
|
131
221
|
return success(await this.buildCheckout(cartKey, state));
|
|
132
222
|
}
|
|
133
223
|
async getAvailableShippingMethods(payload) {
|
|
134
224
|
const cartKey = payload.checkout.key;
|
|
135
|
-
const state = await this.
|
|
225
|
+
const state = await this.loadCheckoutState(cartKey);
|
|
136
226
|
const address = state.shippingAddress || state.billingAddress;
|
|
137
227
|
if (!address) {
|
|
138
228
|
return success([]);
|
|
@@ -151,7 +241,7 @@ class MagentoCheckoutCapability extends CheckoutCapability {
|
|
|
151
241
|
}
|
|
152
242
|
async addPaymentInstruction(payload) {
|
|
153
243
|
const cartKey = payload.checkout.key;
|
|
154
|
-
const state = await this.
|
|
244
|
+
const state = await this.loadCheckoutState(cartKey);
|
|
155
245
|
const instruction = {
|
|
156
246
|
key: `pi_${Date.now()}`,
|
|
157
247
|
method: payload.paymentInstruction.paymentMethod.method,
|
|
@@ -171,7 +261,7 @@ class MagentoCheckoutCapability extends CheckoutCapability {
|
|
|
171
261
|
}
|
|
172
262
|
async removePaymentInstruction(payload) {
|
|
173
263
|
const cartKey = payload.checkout.key;
|
|
174
|
-
const state = await this.
|
|
264
|
+
const state = await this.loadCheckoutState(cartKey);
|
|
175
265
|
state.paymentInstructions = (state.paymentInstructions ?? []).filter(
|
|
176
266
|
(pi) => pi.key !== payload.paymentInstruction.key
|
|
177
267
|
);
|
|
@@ -180,7 +270,7 @@ class MagentoCheckoutCapability extends CheckoutCapability {
|
|
|
180
270
|
}
|
|
181
271
|
async setShippingInstruction(payload) {
|
|
182
272
|
const cartKey = payload.checkout.key;
|
|
183
|
-
const state = await this.
|
|
273
|
+
const state = await this.loadCheckoutState(cartKey);
|
|
184
274
|
const address = state.shippingAddress || state.billingAddress;
|
|
185
275
|
if (!address) {
|
|
186
276
|
return error({
|
|
@@ -220,23 +310,45 @@ class MagentoCheckoutCapability extends CheckoutCapability {
|
|
|
220
310
|
}
|
|
221
311
|
async finalizeCheckout(payload) {
|
|
222
312
|
const cartKey = payload.checkout.key;
|
|
223
|
-
const state = await this.
|
|
313
|
+
const state = await this.loadCheckoutState(cartKey);
|
|
314
|
+
if (state.orderId) {
|
|
315
|
+
return success(await this.buildOrderedCheckout(cartKey, state));
|
|
316
|
+
}
|
|
224
317
|
const paymentInstruction = state.paymentInstructions?.[0];
|
|
225
318
|
if (!paymentInstruction || !state.shippingInstruction) {
|
|
226
319
|
throw new CheckoutNotReadyForFinalizationError(payload.checkout);
|
|
227
320
|
}
|
|
228
321
|
const orderId = await this.magentoApi.placeOrder(cartKey, {
|
|
229
322
|
email: state.email,
|
|
230
|
-
paymentMethod:
|
|
231
|
-
method: paymentInstruction.method
|
|
232
|
-
},
|
|
323
|
+
paymentMethod: this.toMagentoPaymentMethod(paymentInstruction),
|
|
233
324
|
billingAddress: state.billingAddress
|
|
234
325
|
});
|
|
235
326
|
state.orderId = String(orderId);
|
|
236
327
|
await this.magentoApi.setCheckoutState(cartKey, state);
|
|
237
328
|
await this.magentoApi.clearActiveCartId();
|
|
329
|
+
return success(await this.buildOrderedCheckout(cartKey, state));
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Forwards the payment instruction's protocol data (e.g. a PSP transaction
|
|
333
|
+
* id) as Magento's `additional_data`, which payment methods read on placement,
|
|
334
|
+
* and the configured terms and conditions ids as `agreement_ids`, without
|
|
335
|
+
* which stores with terms and conditions enabled reject the order.
|
|
336
|
+
*/
|
|
337
|
+
toMagentoPaymentMethod(instruction) {
|
|
338
|
+
const payload = { method: instruction.method };
|
|
339
|
+
if (instruction.protocolData.length > 0) {
|
|
340
|
+
payload.additional_data = Object.fromEntries(
|
|
341
|
+
instruction.protocolData.map(({ key, value }) => [key, value])
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
if (this.config.checkoutAgreementIds) {
|
|
345
|
+
payload.extension_attributes = { agreement_ids: this.config.checkoutAgreementIds };
|
|
346
|
+
}
|
|
347
|
+
return payload;
|
|
348
|
+
}
|
|
349
|
+
async buildOrderedCheckout(cartKey, state) {
|
|
238
350
|
try {
|
|
239
|
-
return
|
|
351
|
+
return await this.buildCheckout(cartKey, state);
|
|
240
352
|
} catch (err) {
|
|
241
353
|
debug("Cart no longer retrievable after order placement: %O", err);
|
|
242
354
|
const data = {
|
|
@@ -244,7 +356,7 @@ class MagentoCheckoutCapability extends CheckoutCapability {
|
|
|
244
356
|
state,
|
|
245
357
|
requestedKey: cartKey
|
|
246
358
|
};
|
|
247
|
-
return
|
|
359
|
+
return this.factory.parseCheckout(this.context, data);
|
|
248
360
|
}
|
|
249
361
|
}
|
|
250
362
|
}
|
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
} from "@reactionary/core";
|
|
20
20
|
import createDebug from "debug";
|
|
21
21
|
const debug = createDebug("reactionary:magento:search");
|
|
22
|
+
const CATEGORY_FACET_KEY = "categories";
|
|
23
|
+
const CATEGORY_FIELD = "category_id";
|
|
22
24
|
class MagentoProductSearchCapability extends ProductSearchCapability {
|
|
23
25
|
constructor(config, cache, context, magentoApi, factory) {
|
|
24
26
|
super(cache, context);
|
|
@@ -34,38 +36,26 @@ class MagentoProductSearchCapability extends ProductSearchCapability {
|
|
|
34
36
|
const pageSize = payload.search.paginationOptions.pageSize;
|
|
35
37
|
const currentPage = payload.search.paginationOptions.pageNumber;
|
|
36
38
|
const params = new URLSearchParams();
|
|
37
|
-
|
|
39
|
+
const filterGroups = [];
|
|
38
40
|
if (finalSearch) {
|
|
39
|
-
|
|
40
|
-
`searchCriteria[filterGroups][${filterGroupIndex}][filters][0][field]`,
|
|
41
|
-
"name"
|
|
42
|
-
);
|
|
43
|
-
params.set(
|
|
44
|
-
`searchCriteria[filterGroups][${filterGroupIndex}][filters][0][value]`,
|
|
45
|
-
`%${finalSearch}%`
|
|
46
|
-
);
|
|
47
|
-
params.set(
|
|
48
|
-
`searchCriteria[filterGroups][${filterGroupIndex}][filters][0][condition_type]`,
|
|
49
|
-
"like"
|
|
50
|
-
);
|
|
51
|
-
filterGroupIndex++;
|
|
41
|
+
filterGroups.push([{ field: "name", value: `%${finalSearch}%`, conditionType: "like" }]);
|
|
52
42
|
}
|
|
53
43
|
if (payload.search.categoryFilter?.key) {
|
|
54
44
|
debug(`Applying category filter: ${payload.search.categoryFilter.key}`);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
);
|
|
59
|
-
params.set(
|
|
60
|
-
`searchCriteria[filterGroups][${filterGroupIndex}][filters][0][value]`,
|
|
61
|
-
payload.search.categoryFilter.key
|
|
62
|
-
);
|
|
63
|
-
params.set(
|
|
64
|
-
`searchCriteria[filterGroups][${filterGroupIndex}][filters][0][condition_type]`,
|
|
65
|
-
"eq"
|
|
66
|
-
);
|
|
67
|
-
filterGroupIndex++;
|
|
45
|
+
filterGroups.push([
|
|
46
|
+
{ field: CATEGORY_FIELD, value: payload.search.categoryFilter.key, conditionType: "eq" }
|
|
47
|
+
]);
|
|
68
48
|
}
|
|
49
|
+
filterGroups.push(...this.getFacetFilterGroups(payload.search.facets));
|
|
50
|
+
filterGroups.push(...this.getStorefrontScopeFilterGroups());
|
|
51
|
+
filterGroups.forEach((filters, groupIndex) => {
|
|
52
|
+
filters.forEach((filter, filterIndex) => {
|
|
53
|
+
const prefix = `searchCriteria[filterGroups][${groupIndex}][filters][${filterIndex}]`;
|
|
54
|
+
params.set(`${prefix}[field]`, filter.field);
|
|
55
|
+
params.set(`${prefix}[value]`, filter.value);
|
|
56
|
+
params.set(`${prefix}[condition_type]`, filter.conditionType);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
69
59
|
params.set("searchCriteria[pageSize]", String(pageSize));
|
|
70
60
|
params.set("searchCriteria[currentPage]", String(currentPage));
|
|
71
61
|
const client = await this.magentoApi.getClient();
|
|
@@ -78,9 +68,50 @@ class MagentoProductSearchCapability extends ProductSearchCapability {
|
|
|
78
68
|
}
|
|
79
69
|
return success(result);
|
|
80
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* One filter group per facet code: Magento ORs the filters within a group and ANDs
|
|
73
|
+
* the groups, so values of one facet widen the result and distinct facets narrow it.
|
|
74
|
+
* The facet code is the Magento attribute code and the facet value key its (option) value.
|
|
75
|
+
*
|
|
76
|
+
* The category navigation facet created by {@link createCategoryNavigationFilter} is the
|
|
77
|
+
* exception: Magento's `category_id` filter (ProductCategoryFilter) ANDs the filters of a
|
|
78
|
+
* group, so its values are OR-ed through a single `category_id in (...)` filter instead.
|
|
79
|
+
*/
|
|
80
|
+
getFacetFilterGroups(facets) {
|
|
81
|
+
const groups = /* @__PURE__ */ new Map();
|
|
82
|
+
const categoryIds = [];
|
|
83
|
+
for (const facetValue of facets) {
|
|
84
|
+
if (facetValue.facet.key === CATEGORY_FACET_KEY) {
|
|
85
|
+
categoryIds.push(facetValue.key);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const field = facetValue.facet.key;
|
|
89
|
+
const group = groups.get(field) ?? [];
|
|
90
|
+
group.push({ field, value: facetValue.key, conditionType: "eq" });
|
|
91
|
+
groups.set(field, group);
|
|
92
|
+
}
|
|
93
|
+
if (categoryIds.length > 0) {
|
|
94
|
+
groups.set(CATEGORY_FIELD, [
|
|
95
|
+
{ field: CATEGORY_FIELD, value: categoryIds.join(","), conditionType: "in" }
|
|
96
|
+
]);
|
|
97
|
+
}
|
|
98
|
+
return [...groups.values()];
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Restricts results to what a storefront may show: enabled products that are visible in
|
|
102
|
+
* the catalog (visibility 2 = Catalog, 4 = Catalog, Search). The admin REST product search
|
|
103
|
+
* does not apply this itself, so without it disabled products and "Not Visible
|
|
104
|
+
* Individually" (1) configurable children would be listed.
|
|
105
|
+
*/
|
|
106
|
+
getStorefrontScopeFilterGroups() {
|
|
107
|
+
return [
|
|
108
|
+
[{ field: "status", value: "1", conditionType: "eq" }],
|
|
109
|
+
[{ field: "visibility", value: "2,4", conditionType: "in" }]
|
|
110
|
+
];
|
|
111
|
+
}
|
|
81
112
|
async createCategoryNavigationFilter(payload) {
|
|
82
113
|
const facetIdentifier = FacetIdentifierSchema.parse({
|
|
83
|
-
key:
|
|
114
|
+
key: CATEGORY_FACET_KEY
|
|
84
115
|
});
|
|
85
116
|
const facetValueIdentifier = FacetValueIdentifierSchema.parse({
|
|
86
117
|
facet: facetIdentifier,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CategoryIdentifierSchema,
|
|
3
|
+
ProductComplianceDataSchema,
|
|
3
4
|
ProductIdentifierSchema
|
|
4
5
|
} from "@reactionary/core";
|
|
5
6
|
import { getProductKey, parseDamImages } from "../../utils/magento-product.js";
|
|
@@ -27,6 +28,9 @@ class MagentoProductFactory {
|
|
|
27
28
|
}
|
|
28
29
|
config;
|
|
29
30
|
productSchema;
|
|
31
|
+
parseComplianceData(context, data) {
|
|
32
|
+
return ProductComplianceDataSchema.parse({});
|
|
33
|
+
}
|
|
30
34
|
parseProduct(_context, data) {
|
|
31
35
|
const sku = data.sku;
|
|
32
36
|
const identifier = ProductIdentifierSchema.parse({ key: getProductKey(data) });
|
|
@@ -50,7 +54,8 @@ class MagentoProductFactory {
|
|
|
50
54
|
published: true,
|
|
51
55
|
sharedAttributes,
|
|
52
56
|
slug,
|
|
53
|
-
variants: []
|
|
57
|
+
variants: [],
|
|
58
|
+
complianceData: this.parseComplianceData(_context, data)
|
|
54
59
|
};
|
|
55
60
|
return this.productSchema.parse(result);
|
|
56
61
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ImageSchema,
|
|
3
|
+
ProductSearchResultItemComplianceDataSchema,
|
|
3
4
|
ProductSearchResultItemVariantSchema,
|
|
4
5
|
ProductVariantIdentifierSchema
|
|
5
6
|
} from "@reactionary/core";
|
|
@@ -48,6 +49,9 @@ class MagentoProductSearchFactory {
|
|
|
48
49
|
};
|
|
49
50
|
return this.productSearchResultSchema.parse(result);
|
|
50
51
|
}
|
|
52
|
+
parseComplianceData(product) {
|
|
53
|
+
return ProductSearchResultItemComplianceDataSchema.parse({});
|
|
54
|
+
}
|
|
51
55
|
parseProductSearchResultItem(_context, product) {
|
|
52
56
|
const idKey = product.id !== void 0 ? String(product.id) : product.sku;
|
|
53
57
|
const identifier = { key: idKey };
|
|
@@ -61,7 +65,8 @@ class MagentoProductSearchFactory {
|
|
|
61
65
|
identifier,
|
|
62
66
|
name,
|
|
63
67
|
slug,
|
|
64
|
-
variants
|
|
68
|
+
variants,
|
|
69
|
+
complianceData: this.parseComplianceData(product)
|
|
65
70
|
};
|
|
66
71
|
}
|
|
67
72
|
parseVariant(variant, product) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reactionary/magento",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.27",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"types": "./src/index.d.ts",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"url": "git+https://github.com/Solteq/reactionary.git"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@reactionary/core": "0.10.
|
|
12
|
+
"@reactionary/core": "0.10.27",
|
|
13
13
|
"zod": "4.1.9",
|
|
14
14
|
"debug": "^4.4.3"
|
|
15
15
|
},
|
|
@@ -10,6 +10,7 @@ const MagentoConfigurationSchema = z.looseObject({
|
|
|
10
10
|
graphqlUrl: z.string().optional().meta({ description: "The GraphQL endpoint. Defaults to `{baseUrl}/graphql`. Reviews and ratings have no REST equivalent in Magento, so the product reviews capability goes through GraphQL." }),
|
|
11
11
|
reviewRatingCode: z.string().optional().meta({ description: 'The name of the Magento review rating (as listed by `productReviewRatingsMetadata`) that carries the overall star rating. Defaults to "Rating"; the first available rating is used as a fallback.' }),
|
|
12
12
|
storeCode: z.string().default(""),
|
|
13
|
+
checkoutAgreementIds: z.array(z.string()).optional().meta({ description: "Ids of the Magento terms and conditions (Magento_CheckoutAgreements) the shopper accepts when placing an order. Required by stores with \"Enable Terms and Conditions\" on, which otherwise reject order placement. Sent as the payment method's `extension_attributes.agreement_ids`. The storefront must collect the shopper's consent before finalizing checkout." }),
|
|
13
14
|
authStoreCode: z.string().default("default").meta({ description: "The store/website scope used for customer authentication and account operations (token, customers/me). Customers typically live on the default website, which may differ from the catalog storeCode." })
|
|
14
15
|
});
|
|
15
16
|
export {
|
|
@@ -2,7 +2,7 @@ import { CheckoutCapability, type Address, type Cache, type CheckoutFactory, typ
|
|
|
2
2
|
import type { MagentoClient } from '../core/client.js';
|
|
3
3
|
import { type MagentoCheckoutFactory } from '../factories/checkout/checkout.factory.js';
|
|
4
4
|
import type { MagentoConfiguration } from '../schema/configuration.schema.js';
|
|
5
|
-
import type { MagentoCart, MagentoCartTotals, MagentoCheckoutAddress, MagentoCheckoutState } from '../schema/magento.types.js';
|
|
5
|
+
import type { MagentoCart, MagentoCartTotals, MagentoCheckoutAddress, MagentoCheckoutState, MagentoPaymentMethodPayload, MagentoStoredPaymentInstruction } from '../schema/magento.types.js';
|
|
6
6
|
export declare class CheckoutNotReadyForFinalizationError extends Error {
|
|
7
7
|
checkoutIdentifier: CheckoutIdentifier;
|
|
8
8
|
constructor(checkoutIdentifier: CheckoutIdentifier);
|
|
@@ -17,6 +17,31 @@ export declare class MagentoCheckoutCapability<TFactory extends CheckoutFactory
|
|
|
17
17
|
cart: MagentoCart;
|
|
18
18
|
totals?: MagentoCartTotals;
|
|
19
19
|
}>;
|
|
20
|
+
/**
|
|
21
|
+
* The quote's `billing_address` is the only checkout data Magento keeps
|
|
22
|
+
* durably before an order is placed, so it is where email and address
|
|
23
|
+
* survive between requests. Never throws: a missing quote simply yields
|
|
24
|
+
* nothing to fall back to.
|
|
25
|
+
*/
|
|
26
|
+
protected readQuoteBillingAddress(cartKey: string): Promise<{
|
|
27
|
+
email?: string;
|
|
28
|
+
countryId?: string;
|
|
29
|
+
address?: MagentoCheckoutAddress;
|
|
30
|
+
}>;
|
|
31
|
+
/**
|
|
32
|
+
* Loads the session checkout state and, when this request's session does
|
|
33
|
+
* not know the email or billing address (e.g. every request in a web
|
|
34
|
+
* storefront starts with an empty session), rebuilds them from the quote.
|
|
35
|
+
*/
|
|
36
|
+
protected loadCheckoutState(cartKey: string): Promise<MagentoCheckoutState>;
|
|
37
|
+
protected persistBillingAddressOnQuote(cartKey: string, address: MagentoCheckoutAddress): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Stores the email on the quote before any address is known. Magento only
|
|
40
|
+
* accepts a billing address with a country, so this reuses the country the
|
|
41
|
+
* quote already carries (Magento pre-fills the store's default country)
|
|
42
|
+
* rather than inventing one; without it the email stays session-only.
|
|
43
|
+
*/
|
|
44
|
+
protected persistEmailOnQuote(cartKey: string, state: MagentoCheckoutState): Promise<void>;
|
|
20
45
|
protected buildCheckout(cartKey: string, state: MagentoCheckoutState): Promise<CheckoutFactoryCheckoutOutput<TFactory>>;
|
|
21
46
|
initiateCheckoutForCart(payload: CheckoutMutationInitiateCheckout): Promise<Result<CheckoutFactoryCheckoutOutput<TFactory>>>;
|
|
22
47
|
getById(payload: CheckoutQueryById): Promise<Result<CheckoutFactoryCheckoutOutput<TFactory>, NotFoundError>>;
|
|
@@ -27,4 +52,12 @@ export declare class MagentoCheckoutCapability<TFactory extends CheckoutFactory
|
|
|
27
52
|
removePaymentInstruction(payload: CheckoutMutationRemovePaymentInstruction): Promise<Result<CheckoutFactoryCheckoutOutput<TFactory>>>;
|
|
28
53
|
setShippingInstruction(payload: CheckoutMutationSetShippingInstruction): Promise<Result<CheckoutFactoryCheckoutOutput<TFactory>>>;
|
|
29
54
|
finalizeCheckout(payload: CheckoutMutationFinalizeCheckout): Promise<Result<CheckoutFactoryCheckoutOutput<TFactory>>>;
|
|
55
|
+
/**
|
|
56
|
+
* Forwards the payment instruction's protocol data (e.g. a PSP transaction
|
|
57
|
+
* id) as Magento's `additional_data`, which payment methods read on placement,
|
|
58
|
+
* and the configured terms and conditions ids as `agreement_ids`, without
|
|
59
|
+
* which stores with terms and conditions enabled reject the order.
|
|
60
|
+
*/
|
|
61
|
+
protected toMagentoPaymentMethod(instruction: MagentoStoredPaymentInstruction): MagentoPaymentMethodPayload;
|
|
62
|
+
protected buildOrderedCheckout(cartKey: string, state: MagentoCheckoutState): Promise<CheckoutFactoryCheckoutOutput<TFactory>>;
|
|
30
63
|
}
|
|
@@ -2,14 +2,37 @@ import { ProductSearchCapability, type Cache, type FacetIdentifier, type FacetVa
|
|
|
2
2
|
import type { MagentoConfiguration } from '../schema/configuration.schema.js';
|
|
3
3
|
import type { MagentoClient } from '../core/client.js';
|
|
4
4
|
import type { MagentoProductSearchFactory } from '../factories/product-search/product-search.factory.js';
|
|
5
|
+
interface MagentoSearchFilter {
|
|
6
|
+
field: string;
|
|
7
|
+
value: string;
|
|
8
|
+
conditionType: 'eq' | 'in' | 'like';
|
|
9
|
+
}
|
|
5
10
|
export declare class MagentoProductSearchCapability<TFactory extends ProductSearchFactory = MagentoProductSearchFactory> extends ProductSearchCapability<ProductSearchFactoryOutput<TFactory>> {
|
|
6
11
|
magentoApi: MagentoClient;
|
|
7
12
|
protected config: MagentoConfiguration;
|
|
8
13
|
protected factory: ProductSearchFactoryWithOutput<TFactory>;
|
|
9
14
|
constructor(config: MagentoConfiguration, cache: Cache, context: RequestContext, magentoApi: MagentoClient, factory: ProductSearchFactoryWithOutput<TFactory>);
|
|
10
15
|
queryByTerm(payload: ProductSearchQueryByTerm): Promise<Result<ProductSearchFactoryOutput<TFactory>>>;
|
|
16
|
+
/**
|
|
17
|
+
* One filter group per facet code: Magento ORs the filters within a group and ANDs
|
|
18
|
+
* the groups, so values of one facet widen the result and distinct facets narrow it.
|
|
19
|
+
* The facet code is the Magento attribute code and the facet value key its (option) value.
|
|
20
|
+
*
|
|
21
|
+
* The category navigation facet created by {@link createCategoryNavigationFilter} is the
|
|
22
|
+
* exception: Magento's `category_id` filter (ProductCategoryFilter) ANDs the filters of a
|
|
23
|
+
* group, so its values are OR-ed through a single `category_id in (...)` filter instead.
|
|
24
|
+
*/
|
|
25
|
+
protected getFacetFilterGroups(facets: FacetValueIdentifier[]): MagentoSearchFilter[][];
|
|
26
|
+
/**
|
|
27
|
+
* Restricts results to what a storefront may show: enabled products that are visible in
|
|
28
|
+
* the catalog (visibility 2 = Catalog, 4 = Catalog, Search). The admin REST product search
|
|
29
|
+
* does not apply this itself, so without it disabled products and "Not Visible
|
|
30
|
+
* Individually" (1) configurable children would be listed.
|
|
31
|
+
*/
|
|
32
|
+
protected getStorefrontScopeFilterGroups(): MagentoSearchFilter[][];
|
|
11
33
|
createCategoryNavigationFilter(payload: ProductSearchQueryCreateNavigationFilter): Promise<Result<FacetValueIdentifier>>;
|
|
12
34
|
protected parseFacetValue(_facetValueIdentifier: FacetValueIdentifier, _label: string, _count: number): ProductSearchResultFacetValue;
|
|
13
35
|
protected parseFacet(_facetIdentifier: FacetIdentifier, _facetValue: unknown): ProductSearchResultFacet;
|
|
14
36
|
protected parseVariant(): never;
|
|
15
37
|
}
|
|
38
|
+
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AnyProductSchema, type Image, type ProductAttribute, type ProductFactory, type ProductSchema, type ProductVariant, type RequestContext } from '@reactionary/core';
|
|
1
|
+
import { type AnyProductSchema, type Image, type ProductAttribute, type ProductComplianceData, type ProductFactory, type ProductSchema, type ProductVariant, type RequestContext } from '@reactionary/core';
|
|
2
2
|
import type * as z from 'zod';
|
|
3
3
|
import type { MagentoConfiguration } from '../../schema/configuration.schema.js';
|
|
4
4
|
import type { MagentoProduct } from '../../schema/magento.types.js';
|
|
@@ -6,6 +6,7 @@ export declare class MagentoProductFactory<TProductSchema extends AnyProductSche
|
|
|
6
6
|
protected config: MagentoConfiguration;
|
|
7
7
|
readonly productSchema: TProductSchema;
|
|
8
8
|
constructor(productSchema: TProductSchema, config: MagentoConfiguration);
|
|
9
|
+
protected parseComplianceData(context: RequestContext, data: MagentoProduct): ProductComplianceData;
|
|
9
10
|
parseProduct(_context: RequestContext, data: MagentoProduct): z.output<TProductSchema>;
|
|
10
11
|
protected parseVariant(product: MagentoProduct): ProductVariant;
|
|
11
12
|
protected parseImages(product: MagentoProduct): Image[];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AnyProductSearchResultSchema, type ProductSearchFactory, type ProductSearchQueryByTerm, type ProductSearchResultItem, type ProductSearchResultItemVariant, type ProductSearchResultSchema, type RequestContext } from '@reactionary/core';
|
|
1
|
+
import { type AnyProductSearchResultSchema, type ProductSearchFactory, type ProductSearchQueryByTerm, type ProductSearchResultItem, type ProductSearchResultItemComplianceData, type ProductSearchResultItemVariant, type ProductSearchResultSchema, type RequestContext } from '@reactionary/core';
|
|
2
2
|
import type * as z from 'zod';
|
|
3
3
|
import type { MagentoConfiguration } from '../../schema/configuration.schema.js';
|
|
4
4
|
import type { MagentoProduct, MagentoProductSearchResult } from '../../schema/magento.types.js';
|
|
@@ -7,6 +7,7 @@ export declare class MagentoProductSearchFactory<TProductSearchResultSchema exte
|
|
|
7
7
|
readonly productSearchResultSchema: TProductSearchResultSchema;
|
|
8
8
|
constructor(productSearchResultSchema: TProductSearchResultSchema, config: MagentoConfiguration);
|
|
9
9
|
parseSearchResult(context: RequestContext, data: MagentoProductSearchResult, query: ProductSearchQueryByTerm): z.output<TProductSearchResultSchema>;
|
|
10
|
+
protected parseComplianceData(product: MagentoProduct): ProductSearchResultItemComplianceData;
|
|
10
11
|
protected parseProductSearchResultItem(_context: RequestContext, product: MagentoProduct): ProductSearchResultItem;
|
|
11
12
|
protected parseVariant(variant: MagentoProduct, product: MagentoProduct): ProductSearchResultItemVariant;
|
|
12
13
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './schema/configuration.schema.js';
|
|
2
2
|
export * from './schema/capabilities.schema.js';
|
|
3
3
|
export * from './schema/magento.schema.js';
|
|
4
|
+
export type { MagentoCategory, MagentoCheckoutAddress, MagentoCheckoutState, MagentoCustomAttribute, MagentoProduct, MagentoProductSearchResult, } from './schema/magento.types.js';
|
|
4
5
|
export * from './core/initialize.js';
|
|
5
6
|
export * from './core/initialize.types.js';
|
|
6
7
|
export * from './core/client.js';
|
|
@@ -13,6 +13,7 @@ export declare const MagentoConfigurationSchema: z.ZodObject<{
|
|
|
13
13
|
graphqlUrl: z.ZodOptional<z.ZodString>;
|
|
14
14
|
reviewRatingCode: z.ZodOptional<z.ZodString>;
|
|
15
15
|
storeCode: z.ZodDefault<z.ZodString>;
|
|
16
|
+
checkoutAgreementIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
16
17
|
authStoreCode: z.ZodDefault<z.ZodString>;
|
|
17
18
|
}, z.core.$loose>;
|
|
18
19
|
export type MagentoConfiguration = z.infer<typeof MagentoConfigurationSchema>;
|
|
@@ -228,6 +228,7 @@ export interface MagentoCheckoutAddress {
|
|
|
228
228
|
email?: string;
|
|
229
229
|
same_as_billing?: number;
|
|
230
230
|
save_in_address_book?: number;
|
|
231
|
+
customer_address_id?: number;
|
|
231
232
|
}
|
|
232
233
|
export interface MagentoShippingMethod {
|
|
233
234
|
carrier_code: string;
|
|
@@ -277,6 +278,9 @@ export interface MagentoPaymentMethodPayload {
|
|
|
277
278
|
method: string;
|
|
278
279
|
po_number?: string;
|
|
279
280
|
additional_data?: Record<string, string> | string[];
|
|
281
|
+
extension_attributes?: {
|
|
282
|
+
agreement_ids?: string[];
|
|
283
|
+
};
|
|
280
284
|
}
|
|
281
285
|
export interface MagentoPlaceOrderPayload {
|
|
282
286
|
email?: string;
|
package/utils/magento-product.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ImageSchema,
|
|
3
|
+
ProductSearchResultItemComplianceDataSchema,
|
|
3
4
|
ProductSearchResultItemVariantSchema,
|
|
4
5
|
ProductVariantIdentifierSchema
|
|
5
6
|
} from "@reactionary/core";
|
|
@@ -96,9 +97,13 @@ function buildProductSearchResultItem(config, product) {
|
|
|
96
97
|
identifier: { key: getProductKey(product) },
|
|
97
98
|
name: product.name || product.sku,
|
|
98
99
|
slug: getCustomAttribute(product, "url_key") ?? getCustomAttribute(product, "url_path") ?? "",
|
|
99
|
-
variants
|
|
100
|
+
variants,
|
|
101
|
+
complianceData: buildProductComplianceData(product)
|
|
100
102
|
};
|
|
101
103
|
}
|
|
104
|
+
function buildProductComplianceData(product) {
|
|
105
|
+
return ProductSearchResultItemComplianceDataSchema.parse({});
|
|
106
|
+
}
|
|
102
107
|
export {
|
|
103
108
|
buildMagentoImageUrl,
|
|
104
109
|
buildProductSearchResultItem,
|