@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.
@@ -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.magentoApi.getCheckoutState(cartKey);
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
- const billingAddress = this.toMagentoAddress(
188
+ state.billingAddress = this.toMagentoAddress(
97
189
  payload.billingAddress,
98
190
  state.email
99
191
  );
100
- state.billingAddress = billingAddress;
101
- try {
102
- await this.magentoApi.setCheckoutBillingAddress(cartKey, billingAddress);
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.magentoApi.getCheckoutState(cartKey);
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.magentoApi.getCheckoutState(cartKey);
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.magentoApi.getCheckoutState(cartKey);
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.magentoApi.getCheckoutState(cartKey);
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.magentoApi.getCheckoutState(cartKey);
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.magentoApi.getCheckoutState(cartKey);
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.magentoApi.getCheckoutState(cartKey);
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 success(await this.buildCheckout(cartKey, state));
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 success(this.factory.parseCheckout(this.context, data));
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
- let filterGroupIndex = 0;
39
+ const filterGroups = [];
38
40
  if (finalSearch) {
39
- params.set(
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
- params.set(
56
- `searchCriteria[filterGroups][${filterGroupIndex}][filters][0][field]`,
57
- "category_id"
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: "categories"
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.25",
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.25",
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;
@@ -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,