@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 +22 -0
- package/capabilities/checkout.capability.js +41 -7
- package/core/payment-protocol-data.js +26 -0
- package/factories/checkout/checkout.factory.js +4 -4
- package/package.json +2 -2
- package/src/capabilities/checkout.capability.d.ts +14 -1
- package/src/core/payment-protocol-data.d.ts +18 -0
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
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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).
|
|
184
|
-
key,
|
|
185
|
-
|
|
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.
|
|
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.
|
|
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
|
+
}>;
|