@reactionary/commercetools 0.10.35 → 0.10.37

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/README.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  This library was generated with [Nx](https://nx.dev).
4
4
 
5
+ ## Payment custom fields
6
+
7
+ Payments created by `checkout.addPaymentInstruction` use the
8
+ `reactionaryPaymentCustomFields` custom type, which must define:
9
+
10
+ | Field | Type | Purpose |
11
+ | --- | --- | --- |
12
+ | `commerceToolsCartId` | String | The checkout (cart) the payment belongs to. |
13
+ | `reactionaryProtocolData` | String | The payment's protocol channel, a JSON object exchanged with the payment extensions in both directions. |
14
+
15
+ `reactionaryProtocolData` is how provider-specific payment data travels
16
+ without reactionary knowing the provider:
17
+
18
+ - **Inbound:** `addPaymentInstruction` writes the instruction's `protocolData`
19
+ into it (only when non-empty) — for example the delegated payment tokens of
20
+ UCP/ACP agents (`delegated_payment_token`, `ucp_payment_credential`).
21
+ - **Outbound:** a payment API extension (e.g. the Stripe one) consumes the keys
22
+ it understands, then replaces the content with its results (e.g.
23
+ `stripe_clientSecret`, `stripe_status`), which also removes any token.
24
+ - **Reading back:** the checkout factory flattens the JSON object into the
25
+ payment instruction's `protocolData` entries.
26
+
5
27
  ## Building
6
28
 
7
29
  Run `nx build commercetools` to build the library.
@@ -8,6 +8,7 @@ var __decorateClass = (decorators, target, key, kind) => {
8
8
  if (kind && result) __defProp(target, key, result);
9
9
  return result;
10
10
  };
11
+ import { PROTOCOL_DATA_FIELD, serializeProtocolData } from "../core/payment-protocol-data.js";
11
12
  import {
12
13
  CheckoutMutationAddPaymentInstructionSchema,
13
14
  CheckoutMutationFinalizeCheckoutSchema,
@@ -164,16 +165,46 @@ class CommercetoolsCheckoutCapability extends CheckoutCapability {
164
165
  }
165
166
  async getAvailableShippingMethods(payload) {
166
167
  const client = await this.getClient();
167
- const shippingMethodsResponse = await client.shippingMethods.matchingCart().get({
168
- queryArgs: {
169
- cartId: payload.checkout.key
170
- }
171
- }).execute();
168
+ const [shippingMethodsResponse, cartResponse] = await Promise.all([
169
+ client.shippingMethods.matchingCart().get({
170
+ queryArgs: {
171
+ cartId: payload.checkout.key
172
+ }
173
+ }).execute(),
174
+ client.carts.withId({ ID: payload.checkout.key }).get().execute()
175
+ ]);
172
176
  const result = shippingMethodsResponse.body.results.map(
173
- (shippingMethod) => this.factory.parseShippingMethod(this.context, shippingMethod)
177
+ (shippingMethod) => this.factory.parseShippingMethod(this.context, this.withFreeShipping(shippingMethod, cartResponse.body))
174
178
  );
175
179
  return success(result);
176
180
  }
181
+ /**
182
+ * Quotes the matching shipping rates as free where the cart reaches the
183
+ * rate's free-above threshold. commercetools' matching-cart query returns
184
+ * the rates at their configured price and only applies the threshold once
185
+ * the method is set on the cart.
186
+ */
187
+ withFreeShipping(shippingMethod, cart) {
188
+ return {
189
+ ...shippingMethod,
190
+ zoneRates: shippingMethod.zoneRates.map((zoneRate) => ({
191
+ ...zoneRate,
192
+ shippingRates: zoneRate.shippingRates.map((rate) => rate.isMatching && this.isFreeShipping(rate, cart) ? { ...rate, price: { ...rate.price, centAmount: 0 } } : rate)
193
+ }))
194
+ };
195
+ }
196
+ /**
197
+ * Whether commercetools charges nothing for the rate once the method is set:
198
+ * shipping is free if the sum of the (custom) line item prices, after line
199
+ * item discounts, reaches the rate's free-above threshold.
200
+ */
201
+ isFreeShipping(rate, cart) {
202
+ if (!rate.freeAbove || rate.freeAbove.currencyCode !== cart.totalPrice.currencyCode) {
203
+ return false;
204
+ }
205
+ const itemsTotal = [...cart.lineItems, ...cart.customLineItems].reduce((sum, item) => sum + item.totalPrice.centAmount, 0);
206
+ return itemsTotal >= rate.freeAbove.centAmount;
207
+ }
177
208
  async getAvailablePaymentMethods(payload) {
178
209
  const staticMethods = this.getStaticPaymentMethods(payload.checkout);
179
210
  const dynamicMethods = [];
@@ -205,7 +236,10 @@ class CommercetoolsCheckoutCapability extends CheckoutCapability {
205
236
  key: "reactionaryPaymentCustomFields"
206
237
  },
207
238
  fields: {
208
- commerceToolsCartId: payload.checkout.key
239
+ commerceToolsCartId: payload.checkout.key,
240
+ // Protocol data (e.g. agentic delegated payment tokens) is passed
241
+ // verbatim for the project's payment extensions to interpret.
242
+ ...payload.paymentInstruction.protocolData.length > 0 ? { [PROTOCOL_DATA_FIELD]: serializeProtocolData(payload.paymentInstruction.protocolData) } : {}
209
243
  }
210
244
  }
211
245
  }
@@ -0,0 +1,26 @@
1
+ const PROTOCOL_DATA_FIELD = "reactionaryProtocolData";
2
+ function serializeProtocolData(entries) {
3
+ return JSON.stringify(Object.fromEntries(entries.map((entry) => [entry.key, entry.value])));
4
+ }
5
+ function parseProtocolDataField(value) {
6
+ if (typeof value !== "string" || value.length === 0) {
7
+ return [];
8
+ }
9
+ try {
10
+ const parsed = JSON.parse(value);
11
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
12
+ return [];
13
+ }
14
+ return Object.entries(parsed).map(([key, entry]) => ({
15
+ key,
16
+ value: typeof entry === "string" ? entry : JSON.stringify(entry)
17
+ }));
18
+ } catch {
19
+ return [];
20
+ }
21
+ }
22
+ export {
23
+ PROTOCOL_DATA_FIELD,
24
+ parseProtocolDataField,
25
+ serializeProtocolData
26
+ };
@@ -1,3 +1,4 @@
1
+ import { PROTOCOL_DATA_FIELD, parseProtocolDataField } from "../../core/payment-protocol-data.js";
1
2
  import {
2
3
  CheckoutItemSchema
3
4
  } from "@reactionary/core";
@@ -180,10 +181,9 @@ class CommercetoolsCheckoutFactory {
180
181
  name: paymentName || method || "Unknown"
181
182
  };
182
183
  const customData = data.custom?.fields || {};
183
- const protocolData = Object.keys(customData).map((key) => ({
184
- key,
185
- value: customData[key]
186
- }));
184
+ const protocolData = Object.keys(customData).flatMap(
185
+ (key) => key === PROTOCOL_DATA_FIELD ? parseProtocolDataField(customData[key]) : [{ key, value: customData[key] }]
186
+ );
187
187
  let status = "pending";
188
188
  if (data.transactions && data.transactions.length > 0) {
189
189
  const lastTransaction = data.transactions[data.transactions.length - 1];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reactionary/commercetools",
3
- "version": "0.10.35",
3
+ "version": "0.10.37",
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.35",
12
+ "@reactionary/core": "0.10.37",
13
13
  "zod": "4.1.9",
14
14
  "@commercetools/ts-client": "^4.9.1",
15
15
  "@commercetools/platform-sdk": "^8.25.0",
@@ -1,4 +1,4 @@
1
- import type { MyCartUpdateAction } from '@commercetools/platform-sdk';
1
+ import type { Cart as CTCart, MyCartUpdateAction, ShippingMethod as CTShippingMethod, ShippingRate as CTShippingRate } from '@commercetools/platform-sdk';
2
2
  import type { Cache, CheckoutFactory, CheckoutFactoryCheckoutOutput, CheckoutFactoryPaymentMethodOutput, CheckoutFactoryShippingMethodOutput, CheckoutFactoryWithOutput, CheckoutIdentifier, CheckoutMutationAddPaymentInstruction, CheckoutMutationFinalizeCheckout, CheckoutMutationInitiateCheckout, CheckoutMutationRemovePaymentInstruction, CheckoutMutationSetShippingAddress, CheckoutMutationSetShippingInstruction, CheckoutQueryById, CheckoutQueryForAvailablePaymentMethods, CheckoutQueryForAvailableShippingMethods, NotFoundError, PaymentMethod, RequestContext, Result } from '@reactionary/core';
3
3
  import { CheckoutCapability } from '@reactionary/core';
4
4
  import type { CommercetoolsAPI } from '../core/client.js';
@@ -23,6 +23,19 @@ export declare class CommercetoolsCheckoutCapability<TFactory extends CheckoutFa
23
23
  getById(payload: CheckoutQueryById): Promise<Result<CheckoutFactoryCheckoutOutput<TFactory>, NotFoundError>>;
24
24
  setShippingAddress(payload: CheckoutMutationSetShippingAddress): Promise<Result<CheckoutFactoryCheckoutOutput<TFactory>>>;
25
25
  getAvailableShippingMethods(payload: CheckoutQueryForAvailableShippingMethods): Promise<Result<CheckoutFactoryShippingMethodOutput<TFactory>[]>>;
26
+ /**
27
+ * Quotes the matching shipping rates as free where the cart reaches the
28
+ * rate's free-above threshold. commercetools' matching-cart query returns
29
+ * the rates at their configured price and only applies the threshold once
30
+ * the method is set on the cart.
31
+ */
32
+ protected withFreeShipping(shippingMethod: CTShippingMethod, cart: CTCart): CTShippingMethod;
33
+ /**
34
+ * Whether commercetools charges nothing for the rate once the method is set:
35
+ * shipping is free if the sum of the (custom) line item prices, after line
36
+ * item discounts, reaches the rate's free-above threshold.
37
+ */
38
+ protected isFreeShipping(rate: CTShippingRate, cart: CTCart): boolean;
26
39
  getAvailablePaymentMethods(payload: CheckoutQueryForAvailablePaymentMethods): Promise<Result<CheckoutFactoryPaymentMethodOutput<TFactory>[]>>;
27
40
  addPaymentInstruction(payload: CheckoutMutationAddPaymentInstruction): Promise<Result<CheckoutFactoryCheckoutOutput<TFactory>>>;
28
41
  removePaymentInstruction(payload: CheckoutMutationRemovePaymentInstruction): Promise<Result<CheckoutFactoryCheckoutOutput<TFactory>>>;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * A payment's protocol channel: one JSON custom field on the
3
+ * `reactionaryPaymentCustomFields` type, exchanged in both directions with the
4
+ * project's payment API extensions. reactionary writes a payment instruction's
5
+ * protocol data into it; extensions consume what they understand (e.g. agent
6
+ * payment tokens) and write their results back (e.g. a Stripe client secret).
7
+ * One declared field carries arbitrary keys, which separate custom fields could
8
+ * not: commercetools rejects undeclared fields.
9
+ */
10
+ export declare const PROTOCOL_DATA_FIELD = "reactionaryProtocolData";
11
+ export declare function serializeProtocolData(entries: Array<{
12
+ key: string;
13
+ value: string;
14
+ }>): string;
15
+ export declare function parseProtocolDataField(value: unknown): Array<{
16
+ key: string;
17
+ value: string;
18
+ }>;