@reactionary/commercetools 0.10.47 → 0.10.49

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.
@@ -33,7 +33,11 @@ import { getLanguageCodeFromLocale } from "../core/locale-utils.js";
33
33
  class CommercetoolsCartCapability extends CartCapability {
34
34
  config;
35
35
  commercetools;
36
- expandedCartFields = ["discountCodes[*].discountCode"];
36
+ expandedCartFields = [
37
+ "discountCodes[*].discountCode",
38
+ "lineItems[*].discountedPricePerQuantity[*].discountedPrice.includedDiscounts[*].discount",
39
+ "discountOnTotalPrice.includedDiscounts[*].discount"
40
+ ];
37
41
  factory;
38
42
  constructor(config, cache, context, commercetools, factory) {
39
43
  super(cache, context);
@@ -180,7 +184,7 @@ class CommercetoolsCartCapability extends CartCapability {
180
184
  ...lineIdentifier.originalPrice && {
181
185
  externalPrice: {
182
186
  currencyCode: lineIdentifier.originalPrice.currency,
183
- centAmount: Math.floor(lineIdentifier.originalPrice.value * 100)
187
+ centAmount: Math.round(lineIdentifier.originalPrice.value * 100)
184
188
  }
185
189
  }
186
190
  },
@@ -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
+ };
@@ -104,17 +104,38 @@ class CommercetoolsCartFactory {
104
104
  }
105
105
  };
106
106
  const localeString = getLanguageCodeFromLocale(context.languageContext.locale) || "en";
107
+ const discountedAmountByCartDiscountId = this.groupDiscountedAmountsByCartDiscountId(data);
107
108
  const appliedPromotions = [];
109
+ const codeClaimedCartDiscountIds = /* @__PURE__ */ new Set();
108
110
  if (data.discountCodes) {
109
111
  for (const promo of data.discountCodes) {
112
+ const cartDiscountIds = promo.discountCode.obj?.cartDiscounts.map((ref) => ref.id) || [];
113
+ cartDiscountIds.forEach((id) => codeClaimedCartDiscountIds.add(id));
114
+ const discountedCentAmount = cartDiscountIds.reduce(
115
+ (sum, id) => sum + (discountedAmountByCartDiscountId.get(id)?.centAmount || 0),
116
+ 0
117
+ );
110
118
  appliedPromotions.push({
111
119
  code: promo.discountCode.obj?.code || "",
112
120
  isCouponCode: true,
113
121
  name: promo.discountCode.obj?.name?.[localeString] || "",
114
- description: promo.discountCode.obj?.description?.[localeString] || ""
122
+ description: promo.discountCode.obj?.description?.[localeString] || "",
123
+ ...discountedCentAmount > 0 ? { amount: { value: discountedCentAmount / 100, currency } } : {}
115
124
  });
116
125
  }
117
126
  }
127
+ for (const [cartDiscountId, applied] of discountedAmountByCartDiscountId) {
128
+ if (codeClaimedCartDiscountIds.has(cartDiscountId)) {
129
+ continue;
130
+ }
131
+ appliedPromotions.push({
132
+ code: "",
133
+ isCouponCode: false,
134
+ name: applied.cartDiscount?.name?.[localeString] || "",
135
+ description: applied.cartDiscount?.description?.[localeString] || "",
136
+ ...applied.centAmount > 0 ? { amount: { value: applied.centAmount / 100, currency } } : {}
137
+ });
138
+ }
118
139
  const result = {
119
140
  identifier,
120
141
  user: {
@@ -129,6 +150,46 @@ class CommercetoolsCartFactory {
129
150
  };
130
151
  return this.cartSchema.parse(result);
131
152
  }
153
+ /**
154
+ * commercetools links a discount code to the amount it discounted only
155
+ * indirectly: `discountCode.obj.cartDiscounts` gives the CartDiscount ids a
156
+ * code triggers, while the actual discounted amounts are reported per line
157
+ * item (`discountedPricePerQuantity[].discountedPrice.includedDiscounts`)
158
+ * and on the cart total (`discountOnTotalPrice.includedDiscounts`), each
159
+ * keyed by that same CartDiscount id. Sum both sources by CartDiscount id
160
+ * so a discount code's total can be looked up by joining on its ids, and
161
+ * keep the expanded CartDiscount (when available) so automatically applied
162
+ * discounts — those not claimed by any code — can be surfaced by name.
163
+ */
164
+ groupDiscountedAmountsByCartDiscountId(data) {
165
+ const totals = /* @__PURE__ */ new Map();
166
+ const add = (cartDiscountId, centAmount, cartDiscount) => {
167
+ const entry = totals.get(cartDiscountId) || { centAmount: 0 };
168
+ entry.centAmount += centAmount;
169
+ entry.cartDiscount = entry.cartDiscount || cartDiscount;
170
+ totals.set(cartDiscountId, entry);
171
+ };
172
+ const expandedCartDiscount = (reference) => reference.typeId === "cart-discount" ? reference.obj : void 0;
173
+ for (const lineItem of data.lineItems) {
174
+ for (const discPrQty of lineItem.discountedPricePerQuantity || []) {
175
+ for (const included of discPrQty.discountedPrice?.includedDiscounts || []) {
176
+ add(
177
+ included.discount.id,
178
+ included.discountedAmount.centAmount * discPrQty.quantity,
179
+ expandedCartDiscount(included.discount)
180
+ );
181
+ }
182
+ }
183
+ }
184
+ for (const included of data.discountOnTotalPrice?.includedDiscounts || []) {
185
+ add(
186
+ included.discount.id,
187
+ included.discountedAmount.centAmount,
188
+ expandedCartDiscount(included.discount)
189
+ );
190
+ }
191
+ return totals;
192
+ }
132
193
  parseCartItem(lineItem) {
133
194
  const unitPrice = lineItem.price.value.centAmount;
134
195
  const totalPrice = lineItem.totalPrice.centAmount || 0;
@@ -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.47",
3
+ "version": "0.10.49",
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.47",
12
+ "@reactionary/core": "0.10.49",
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;
@@ -1,4 +1,4 @@
1
- import type { CartPagedQueryResponse, Cart as CTCart, LineItem } from '@commercetools/platform-sdk';
1
+ import type { CartDiscount, CartPagedQueryResponse, Cart as CTCart, LineItem } from '@commercetools/platform-sdk';
2
2
  import type { AnyCartPaginatedSearchResult, CartPaginatedSearchResultSchema, CartQueryList, CartSchema, CartSearchResultItem } from '@reactionary/core';
3
3
  import { type AnyCartIdentifierSchema, type AnyCartSchema, type CartFactory, type CartItem, type RequestContext } from '@reactionary/core';
4
4
  import type * as z from 'zod';
@@ -12,5 +12,20 @@ export declare class CommercetoolsCartFactory<TCartSchema extends AnyCartSchema
12
12
  parseCartIdentifier(_context: RequestContext, data: CTCart): z.output<TCartIdentifierSchema>;
13
13
  parseCartSearchResultItem(context: RequestContext, data: CTCart): CartSearchResultItem;
14
14
  parseCart(context: RequestContext, data: CTCart): z.output<TCartSchema>;
15
+ /**
16
+ * commercetools links a discount code to the amount it discounted only
17
+ * indirectly: `discountCode.obj.cartDiscounts` gives the CartDiscount ids a
18
+ * code triggers, while the actual discounted amounts are reported per line
19
+ * item (`discountedPricePerQuantity[].discountedPrice.includedDiscounts`)
20
+ * and on the cart total (`discountOnTotalPrice.includedDiscounts`), each
21
+ * keyed by that same CartDiscount id. Sum both sources by CartDiscount id
22
+ * so a discount code's total can be looked up by joining on its ids, and
23
+ * keep the expanded CartDiscount (when available) so automatically applied
24
+ * discounts — those not claimed by any code — can be surfaced by name.
25
+ */
26
+ protected groupDiscountedAmountsByCartDiscountId(data: CTCart): Map<string, {
27
+ centAmount: number;
28
+ cartDiscount?: CartDiscount;
29
+ }>;
15
30
  protected parseCartItem(lineItem: LineItem): CartItem;
16
31
  }
@@ -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>;