@reactionary/magento 0.10.26 → 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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reactionary/magento",
3
- "version": "0.10.26",
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.26",
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
  }
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;