@reactionary/commercetools 0.10.46 → 0.10.48

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.
@@ -180,7 +180,7 @@ class CommercetoolsCartCapability extends CartCapability {
180
180
  ...lineIdentifier.originalPrice && {
181
181
  externalPrice: {
182
182
  currencyCode: lineIdentifier.originalPrice.currency,
183
- centAmount: Math.floor(lineIdentifier.originalPrice.value * 100)
183
+ centAmount: Math.round(lineIdentifier.originalPrice.value * 100)
184
184
  }
185
185
  }
186
186
  },
@@ -394,7 +394,7 @@ class CommercetoolsProductListCapability extends ProductListCapability {
394
394
  }
395
395
  __decorateClass([
396
396
  Reactionary({
397
- cache: true,
397
+ cache: false,
398
398
  cacheTimeToLiveInSeconds: 300,
399
399
  currencyDependentCaching: false,
400
400
  localeDependentCaching: false,
@@ -404,7 +404,7 @@ __decorateClass([
404
404
  ], CommercetoolsProductListCapability.prototype, "getById", 1);
405
405
  __decorateClass([
406
406
  Reactionary({
407
- cache: true,
407
+ cache: false,
408
408
  cacheTimeToLiveInSeconds: 300,
409
409
  currencyDependentCaching: false,
410
410
  localeDependentCaching: false,
@@ -435,7 +435,7 @@ __decorateClass([
435
435
  ], CommercetoolsProductListCapability.prototype, "deleteList", 1);
436
436
  __decorateClass([
437
437
  Reactionary({
438
- cache: true,
438
+ cache: false,
439
439
  cacheTimeToLiveInSeconds: 300,
440
440
  currencyDependentCaching: false,
441
441
  localeDependentCaching: false,
@@ -133,7 +133,7 @@ class CommercetoolsProductReviewsCapability extends ProductReviewsCapability {
133
133
  },
134
134
  target: {
135
135
  typeId: "product",
136
- id: mutation.product.key
136
+ key: mutation.product.key
137
137
  },
138
138
  locale: getLanguageCodeFromLocale(this.context.languageContext.locale)
139
139
  }
@@ -144,7 +144,7 @@ class CommercetoolsProductReviewsCapability extends ProductReviewsCapability {
144
144
  parseSingle(review, productKey) {
145
145
  return {
146
146
  identifier: {
147
- key: review.key || ""
147
+ key: review.key ?? review.id
148
148
  },
149
149
  product: {
150
150
  key: productKey
@@ -0,0 +1,36 @@
1
+ function isRecord(value) {
2
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3
+ }
4
+ function getAttributeValueKeyAndLabel(value, language) {
5
+ if (Array.isArray(value)) {
6
+ return getAttributeValueKeyAndLabel(value[0], language);
7
+ }
8
+ if (isRecord(value)) {
9
+ if ("key" in value && "label" in value) {
10
+ const key = String(value["key"]);
11
+ const label = getAttributeValueKeyAndLabel(value["label"], language).label;
12
+ return { key, label: label || key };
13
+ }
14
+ const localized = value[language] ?? Object.values(value).find((text3) => typeof text3 === "string");
15
+ const text2 = typeof localized === "string" ? localized : "";
16
+ return { key: text2, label: text2 };
17
+ }
18
+ const text = value === void 0 || value === null ? "" : String(value);
19
+ return { key: text, label: text };
20
+ }
21
+ function parseVariantOptionFromAttribute(attribute, language) {
22
+ const option = { key: attribute.name };
23
+ const { key, label } = getAttributeValueKeyAndLabel(attribute.value, language);
24
+ return {
25
+ identifier: option,
26
+ name: attribute.name,
27
+ value: {
28
+ identifier: { option, key },
29
+ label
30
+ }
31
+ };
32
+ }
33
+ export {
34
+ getAttributeValueKeyAndLabel,
35
+ parseVariantOptionFromAttribute
36
+ };
package/core/client.js CHANGED
@@ -13,6 +13,10 @@ import { RequestContextTokenCache } from "./token-cache.js";
13
13
  import {
14
14
  CommercetoolsSessionSchema
15
15
  } from "../schema/session.schema.js";
16
+ import {
17
+ CommercetoolsAccessTokenSchema,
18
+ CommercetoolsTokenIntrospectionSchema
19
+ } from "../schema/oauth.schema.js";
16
20
  const debug = createDebug("reactionary:commercetools");
17
21
  const PROVIDER_SESSION_KEY = "COMMERCETOOLS_PROVIDER";
18
22
  class CommercetoolsAPI {
@@ -81,7 +85,7 @@ class CommercetoolsAPI {
81
85
  const response = await client.withProjectKey({ projectKey: this.config.projectKey }).channels().withKey({ key }).get().execute();
82
86
  const channel = response.body;
83
87
  this.setSessionData({
84
- cacheKey: channel.id
88
+ [cacheKey]: channel.id
85
89
  });
86
90
  if (debug.enabled) {
87
91
  debug(`Resolved channel ${key} from API and cached it`);
@@ -215,7 +219,6 @@ class CommercetoolsAPI {
215
219
  };
216
220
  return identity;
217
221
  }
218
- // FIXME: This can fail if the short-lived access token has expired. In other words, probably missing a token refresh.
219
222
  async introspect() {
220
223
  const session = await this.tokenCache.get();
221
224
  if (!session || !session.token) {
@@ -224,21 +227,13 @@ class CommercetoolsAPI {
224
227
  };
225
228
  return identity;
226
229
  }
227
- const authHeader = "Basic " + Buffer.from(
228
- `${this.config.clientId}:${this.config.clientSecret}`
229
- ).toString("base64");
230
- const introspectionUrl = `${this.config.authUrl}/oauth/introspect`;
231
- const response = await fetch(introspectionUrl, {
232
- method: "POST",
233
- headers: {
234
- Authorization: authHeader,
235
- "Content-Type": "application/x-www-form-urlencoded"
236
- },
237
- body: new URLSearchParams({
238
- token: session.token
239
- })
240
- });
241
- const body = await response.json();
230
+ let body = await this.introspectToken(session.token);
231
+ if (body && !body.active && session.refreshToken) {
232
+ const refreshedToken = await this.refreshAccessToken(session.refreshToken);
233
+ if (refreshedToken) {
234
+ body = await this.introspectToken(refreshedToken);
235
+ }
236
+ }
242
237
  if (!body) {
243
238
  return AnonymousIdentitySchema.parse({});
244
239
  }
@@ -271,6 +266,56 @@ class CommercetoolsAPI {
271
266
  type: "Anonymous"
272
267
  };
273
268
  }
269
+ getBasicAuthHeader() {
270
+ return "Basic " + Buffer.from(
271
+ `${this.config.clientId}:${this.config.clientSecret}`
272
+ ).toString("base64");
273
+ }
274
+ async introspectToken(token) {
275
+ const response = await fetch(`${this.config.authUrl}/oauth/introspect`, {
276
+ method: "POST",
277
+ headers: {
278
+ Authorization: this.getBasicAuthHeader(),
279
+ "Content-Type": "application/x-www-form-urlencoded"
280
+ },
281
+ body: new URLSearchParams({
282
+ token
283
+ })
284
+ });
285
+ const result = CommercetoolsTokenIntrospectionSchema.safeParse(await response.json());
286
+ return result.success ? result.data : void 0;
287
+ }
288
+ /**
289
+ * Exchanges the refresh token for a new access token and stores it in the token cache.
290
+ * Returns the new access token, or undefined if the refresh token is no longer valid.
291
+ */
292
+ async refreshAccessToken(refreshToken) {
293
+ const response = await fetch(`${this.config.authUrl}/oauth/token`, {
294
+ method: "POST",
295
+ headers: {
296
+ Authorization: this.getBasicAuthHeader(),
297
+ "Content-Type": "application/x-www-form-urlencoded"
298
+ },
299
+ body: new URLSearchParams({
300
+ grant_type: "refresh_token",
301
+ refresh_token: refreshToken
302
+ })
303
+ });
304
+ if (!response.ok) {
305
+ return void 0;
306
+ }
307
+ const parsed = CommercetoolsAccessTokenSchema.safeParse(await response.json());
308
+ if (!parsed.success) {
309
+ return void 0;
310
+ }
311
+ const result = parsed.data;
312
+ await this.tokenCache.set({
313
+ expirationTime: Date.now() + result.expires_in * 1e3 - 5 * 60 * 1e3,
314
+ token: result.access_token,
315
+ refreshToken
316
+ });
317
+ return result.access_token;
318
+ }
274
319
  async becomeGuest() {
275
320
  const credentials = Buffer.from(
276
321
  `${this.config.clientId}:${this.config.clientSecret}`
@@ -0,0 +1,15 @@
1
+ function getOrderStatusFromOrderState(orderState) {
2
+ switch (orderState) {
3
+ case "Cancelled":
4
+ return "Cancelled";
5
+ case "Complete":
6
+ return "Shipped";
7
+ case "Confirmed":
8
+ return "ReleasedToFulfillment";
9
+ default:
10
+ return "AwaitingPayment";
11
+ }
12
+ }
13
+ export {
14
+ getOrderStatusFromOrderState
15
+ };
@@ -160,8 +160,9 @@ class CommercetoolsCheckoutFactory {
160
160
  if (!shippingInstruction) return false;
161
161
  if (!shippingAddress && !shippingInstruction.pickupPoint) return false;
162
162
  if (paymentInstructions.length === 0) return false;
163
- const authorizedPayments = paymentInstructions.filter((paymentInstruction) => paymentInstruction.status === "authorized").map((paymentInstruction) => paymentInstruction.amount.value).reduce((sum, value) => sum + value, 0);
164
- if (price.grandTotal.value !== authorizedPayments) return false;
163
+ const toCents = (value) => Math.round(value * 100);
164
+ const authorizedPayments = paymentInstructions.filter((paymentInstruction) => paymentInstruction.status === "authorized").map((paymentInstruction) => toCents(paymentInstruction.amount.value)).reduce((sum, value) => sum + value, 0);
165
+ if (toCents(price.grandTotal.value) !== authorizedPayments) return false;
165
166
  return true;
166
167
  }
167
168
  parsePaymentInstruction(context, data) {
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  } from "@reactionary/core";
3
+ import { getOrderStatusFromOrderState } from "../../core/order-status.js";
3
4
  class CommercetoolsOrderFactory {
4
5
  orderSchema;
5
6
  constructor(orderSchema) {
@@ -34,7 +35,7 @@ class CommercetoolsOrderFactory {
34
35
  },
35
36
  totalShipping: {
36
37
  value: shippingTotal / 100,
37
- currency: data.shippingInfo?.price.currencyCode
38
+ currency
38
39
  },
39
40
  totalProductPrice: {
40
41
  value: productTotal / 100,
@@ -45,16 +46,7 @@ class CommercetoolsOrderFactory {
45
46
  currency
46
47
  }
47
48
  };
48
- let orderStatus = "AwaitingPayment";
49
- if (data.paymentState === "Paid" && data.orderState === "Confirmed") {
50
- orderStatus = "ReleasedToFulfillment";
51
- }
52
- if (data.shipmentState === "Ready" && data.orderState === "Confirmed") {
53
- orderStatus = "ReleasedToFulfillment";
54
- }
55
- if ((data.shipmentState === "Shipped" || data.shipmentState === "Delivered") && data.orderState === "Completed") {
56
- orderStatus = "Shipped";
57
- }
49
+ const orderStatus = getOrderStatusFromOrderState(data.orderState);
58
50
  const items = [];
59
51
  for (const remoteItem of data.lineItems) {
60
52
  const lineIdentifier = {
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  } from "@reactionary/core";
3
+ import { getOrderStatusFromOrderState } from "../../core/order-status.js";
3
4
  class CommercetoolsOrderSearchFactory {
4
5
  orderSearchResultSchema;
5
6
  constructor(orderSearchResultSchema) {
@@ -37,16 +38,7 @@ class CommercetoolsOrderSearchFactory {
37
38
  const userId = {
38
39
  userId: data.customerId || data.anonymousId || ""
39
40
  };
40
- let orderStatus = "AwaitingPayment";
41
- if (data.paymentState === "Paid" && data.orderState === "Confirmed") {
42
- orderStatus = "ReleasedToFulfillment";
43
- }
44
- if (data.shipmentState === "Ready" && data.orderState === "Confirmed") {
45
- orderStatus = "ReleasedToFulfillment";
46
- }
47
- if ((data.shipmentState === "Shipped" || data.shipmentState === "Delivered") && data.orderState === "Completed") {
48
- orderStatus = "Shipped";
49
- }
41
+ const orderStatus = getOrderStatusFromOrderState(data.orderState);
50
42
  const totalAmount = {
51
43
  currency: data.totalPrice ? data.totalPrice.currencyCode : context.languageContext.currencyCode,
52
44
  value: data.totalPrice ? data.totalPrice.centAmount / 100 : 0
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  ImageSchema,
3
- ProductOptionIdentifierSchema,
4
3
  ProductSearchResultItemComplianceDataSchema,
5
4
  ProductSearchResultItemVariantSchema,
6
5
  ProductVariantIdentifierSchema,
7
6
  ProductVariantOptionSchema
8
7
  } from "@reactionary/core";
8
+ import { parseVariantOptionFromAttribute } from "../../core/attribute-utils.js";
9
9
  import { getLanguageCodeFromLocale } from "../../core/locale-utils.js";
10
10
  class CommercetoolsProductAssociationsFactory {
11
11
  productAssociationSchema;
@@ -45,12 +45,7 @@ class CommercetoolsProductAssociationsFactory {
45
45
  altText: sourceImage?.label || product.name[getLanguageCodeFromLocale(context.languageContext.locale)] || void 0
46
46
  });
47
47
  const mappedOptions = variant.attributes?.filter((attribute) => attribute.name === "Color").map(
48
- (option) => ProductVariantOptionSchema.parse({
49
- identifier: ProductOptionIdentifierSchema.parse({
50
- key: option.name
51
- }),
52
- name: option.value || ""
53
- })
48
+ (option) => ProductVariantOptionSchema.parse(parseVariantOptionFromAttribute(option, getLanguageCodeFromLocale(context.languageContext.locale)))
54
49
  ) || [];
55
50
  const mappedOption = mappedOptions?.[0];
56
51
  return ProductSearchResultItemVariantSchema.parse({
@@ -2,7 +2,6 @@ import {
2
2
  FacetIdentifierSchema,
3
3
  FacetValueIdentifierSchema,
4
4
  ImageSchema,
5
- ProductOptionIdentifierSchema,
6
5
  ProductSearchResultFacetSchema,
7
6
  ProductSearchResultFacetValueSchema,
8
7
  ProductSearchResultItemComplianceDataSchema,
@@ -10,6 +9,7 @@ import {
10
9
  ProductVariantIdentifierSchema,
11
10
  ProductVariantOptionSchema
12
11
  } from "@reactionary/core";
12
+ import { parseVariantOptionFromAttribute } from "../../core/attribute-utils.js";
13
13
  import { getLanguageCodeFromLocale } from "../../core/locale-utils.js";
14
14
  class CommercetoolsProductSearchFactory {
15
15
  productSearchResultSchema;
@@ -100,12 +100,7 @@ class CommercetoolsProductSearchFactory {
100
100
  altText: sourceImage?.label || product.name[localeStr] || void 0
101
101
  });
102
102
  const mappedOptions = variant.attributes?.filter((attribute) => attribute.name === "Color").map(
103
- (option) => ProductVariantOptionSchema.parse({
104
- identifier: ProductOptionIdentifierSchema.parse({
105
- key: option.name
106
- }),
107
- name: option.value || ""
108
- })
103
+ (option) => ProductVariantOptionSchema.parse(parseVariantOptionFromAttribute(option, localeStr))
109
104
  ) || [];
110
105
  const mappedOption = mappedOptions?.[0];
111
106
  return ProductSearchResultItemVariantSchema.parse({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reactionary/commercetools",
3
- "version": "0.10.46",
3
+ "version": "0.10.48",
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.46",
12
+ "@reactionary/core": "0.10.48",
13
13
  "zod": "4.1.9",
14
14
  "@commercetools/ts-client": "^4.9.1",
15
15
  "@commercetools/platform-sdk": "^8.25.0",
@@ -0,0 +1,13 @@
1
+ import * as z from "zod";
2
+ const CommercetoolsTokenIntrospectionSchema = z.looseObject({
3
+ active: z.boolean(),
4
+ scope: z.string().optional()
5
+ });
6
+ const CommercetoolsAccessTokenSchema = z.looseObject({
7
+ access_token: z.string(),
8
+ expires_in: z.number()
9
+ });
10
+ export {
11
+ CommercetoolsAccessTokenSchema,
12
+ CommercetoolsTokenIntrospectionSchema
13
+ };
@@ -0,0 +1,14 @@
1
+ import type { Attribute as CTAttribute } from '@commercetools/platform-sdk';
2
+ import type { ProductVariantOption } from '@reactionary/core';
3
+ /**
4
+ * Resolves an attribute value to a key and a display label, covering plain values,
5
+ * localized text and (localized) enums. Sets and other complex values use their first entry.
6
+ */
7
+ export declare function getAttributeValueKeyAndLabel(value: unknown, language: string): {
8
+ key: string;
9
+ label: string;
10
+ };
11
+ /**
12
+ * Maps a variant attribute to a variant option, using the attribute name as the option.
13
+ */
14
+ export declare function parseVariantOptionFromAttribute(attribute: CTAttribute, language: string): ProductVariantOption;
@@ -53,6 +53,17 @@ export declare class CommercetoolsAPI {
53
53
  type: "Anonymous";
54
54
  }>;
55
55
  introspect(): Promise<AnonymousIdentity | GuestIdentity | RegisteredIdentity>;
56
+ protected getBasicAuthHeader(): string;
57
+ protected introspectToken(token: string): Promise<{
58
+ [x: string]: unknown;
59
+ active: boolean;
60
+ scope?: string | undefined;
61
+ } | undefined>;
62
+ /**
63
+ * Exchanges the refresh token for a new access token and stores it in the token cache.
64
+ * Returns the new access token, or undefined if the refresh token is no longer valid.
65
+ */
66
+ protected refreshAccessToken(refreshToken: string): Promise<string | undefined>;
56
67
  protected becomeGuest(): Promise<void>;
57
68
  protected createBaseClientBuilder(): ClientBuilder;
58
69
  }
@@ -0,0 +1,9 @@
1
+ import type { OrderState } from '@commercetools/platform-sdk';
2
+ import type { OrderStatus } from '@reactionary/core';
3
+ /**
4
+ * Maps a Commercetools order state to an order status.
5
+ *
6
+ * This is the inverse of the order state filter applied by order search, so orders
7
+ * found by searching for a status are also reported with that status.
8
+ */
9
+ export declare function getOrderStatusFromOrderState(orderState: OrderState): OrderStatus;
@@ -0,0 +1,9 @@
1
+ import * as z from 'zod';
2
+ export declare const CommercetoolsTokenIntrospectionSchema: z.ZodObject<{
3
+ active: z.ZodBoolean;
4
+ scope: z.ZodOptional<z.ZodString>;
5
+ }, z.core.$loose>;
6
+ export declare const CommercetoolsAccessTokenSchema: z.ZodObject<{
7
+ access_token: z.ZodString;
8
+ expires_in: z.ZodNumber;
9
+ }, z.core.$loose>;