@reactionary/provider-commercetools 0.0.48 → 0.0.51
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/core/client.js +41 -26
- package/core/initialize.js +4 -4
- package/package.json +4 -3
- package/providers/cart.provider.js +14 -6
- package/providers/checkout.provider.js +457 -0
- package/providers/identity.provider.js +6 -6
- package/providers/index.js +2 -0
- package/providers/order.provider.js +118 -0
- package/providers/price.provider.js +1 -1
- package/providers/product.provider.js +15 -0
- package/schema/capabilities.schema.js +2 -1
- package/schema/commercetools.schema.js +4 -4
- package/schema/configuration.schema.js +3 -1
- package/src/core/client.d.ts +59 -4
- package/src/core/initialize.d.ts +5 -1
- package/src/providers/checkout.provider.d.ts +67 -0
- package/src/providers/index.d.ts +2 -0
- package/src/providers/order.provider.d.ts +11 -0
- package/src/providers/product.provider.d.ts +2 -1
- package/src/schema/capabilities.schema.d.ts +3 -2
- package/src/schema/commercetools.schema.d.ts +3 -3
- package/src/schema/configuration.schema.d.ts +22 -0
- package/providers/cart-payment.provider.js +0 -149
- package/src/providers/cart-payment.provider.d.ts +0 -16
package/core/client.js
CHANGED
|
@@ -3,22 +3,25 @@ import {
|
|
|
3
3
|
} from "@commercetools/ts-client";
|
|
4
4
|
import { createApiBuilderFromCtpClient } from "@commercetools/platform-sdk";
|
|
5
5
|
import { randomUUID } from "crypto";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
AnonymousIdentitySchema,
|
|
8
|
+
GuestIdentitySchema,
|
|
9
|
+
RegisteredIdentitySchema
|
|
10
|
+
} from "@reactionary/core";
|
|
7
11
|
import * as crypto from "crypto";
|
|
12
|
+
import createDebug from "debug";
|
|
13
|
+
const debug = createDebug("commercetools:debug");
|
|
8
14
|
class RequestContextTokenCache {
|
|
9
15
|
constructor(context) {
|
|
10
16
|
this.context = context;
|
|
11
17
|
}
|
|
12
18
|
async get(tokenCacheOptions) {
|
|
13
19
|
const identity = this.context.identity;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
return void 0;
|
|
20
|
+
return {
|
|
21
|
+
refreshToken: identity.refresh_token,
|
|
22
|
+
token: identity.token || "",
|
|
23
|
+
expirationTime: identity.expiry.getTime()
|
|
24
|
+
};
|
|
22
25
|
}
|
|
23
26
|
async set(cache, tokenCacheOptions) {
|
|
24
27
|
const identity = this.context.identity;
|
|
@@ -35,7 +38,6 @@ class CommercetoolsClient {
|
|
|
35
38
|
return this.createClient(reqCtx);
|
|
36
39
|
}
|
|
37
40
|
async register(username, password, reqCtx) {
|
|
38
|
-
const cache = new RequestContextTokenCache(reqCtx);
|
|
39
41
|
const registrationBuilder = this.createBaseClientBuilder().withAnonymousSessionFlow({
|
|
40
42
|
host: this.config.authUrl,
|
|
41
43
|
projectKey: this.config.projectKey,
|
|
@@ -43,8 +45,7 @@ class CommercetoolsClient {
|
|
|
43
45
|
clientId: this.config.clientId,
|
|
44
46
|
clientSecret: this.config.clientSecret
|
|
45
47
|
},
|
|
46
|
-
scopes: this.config.scopes
|
|
47
|
-
tokenCache: cache
|
|
48
|
+
scopes: this.config.scopes
|
|
48
49
|
});
|
|
49
50
|
const registrationClient = createApiBuilderFromCtpClient(
|
|
50
51
|
registrationBuilder.build()
|
|
@@ -56,10 +57,10 @@ class CommercetoolsClient {
|
|
|
56
57
|
}
|
|
57
58
|
}).execute();
|
|
58
59
|
const login = await this.login(username, password, reqCtx);
|
|
60
|
+
return login;
|
|
59
61
|
}
|
|
60
62
|
async login(username, password, reqCtx) {
|
|
61
63
|
const cache = new RequestContextTokenCache(reqCtx);
|
|
62
|
-
const identity = reqCtx.identity;
|
|
63
64
|
const loginBuilder = this.createBaseClientBuilder().withPasswordFlow({
|
|
64
65
|
host: this.config.authUrl,
|
|
65
66
|
projectKey: this.config.projectKey,
|
|
@@ -72,17 +73,27 @@ class CommercetoolsClient {
|
|
|
72
73
|
scopes: this.config.scopes
|
|
73
74
|
});
|
|
74
75
|
const loginClient = createApiBuilderFromCtpClient(loginBuilder.build());
|
|
75
|
-
const login = await loginClient.withProjectKey({ projectKey: this.config.projectKey }).me().
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
};
|
|
76
|
+
const login = await loginClient.withProjectKey({ projectKey: this.config.projectKey }).me().login().post({
|
|
77
|
+
body: {
|
|
78
|
+
email: username,
|
|
79
|
+
password
|
|
80
|
+
}
|
|
81
|
+
}).execute();
|
|
82
|
+
reqCtx.identity = RegisteredIdentitySchema.parse({
|
|
83
|
+
...reqCtx.identity,
|
|
84
|
+
type: "Registered",
|
|
85
|
+
logonId: username,
|
|
86
|
+
id: {
|
|
87
|
+
userId: login.body.customer.id
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
return reqCtx.identity;
|
|
81
91
|
}
|
|
82
92
|
async logout(reqCtx) {
|
|
83
93
|
const cache = new RequestContextTokenCache(reqCtx);
|
|
84
94
|
await cache.set({ token: "", refreshToken: "", expirationTime: 0 });
|
|
85
|
-
reqCtx.identity =
|
|
95
|
+
reqCtx.identity = AnonymousIdentitySchema.parse({});
|
|
96
|
+
return reqCtx.identity;
|
|
86
97
|
}
|
|
87
98
|
createClient(reqCtx) {
|
|
88
99
|
const cache = new RequestContextTokenCache(reqCtx);
|
|
@@ -95,7 +106,7 @@ class CommercetoolsClient {
|
|
|
95
106
|
});
|
|
96
107
|
}
|
|
97
108
|
const identity = reqCtx.identity;
|
|
98
|
-
let builder = this.createBaseClientBuilder();
|
|
109
|
+
let builder = this.createBaseClientBuilder(reqCtx);
|
|
99
110
|
if (!identity.token || !identity.refresh_token) {
|
|
100
111
|
builder = builder.withAnonymousSessionFlow({
|
|
101
112
|
host: this.config.authUrl,
|
|
@@ -121,8 +132,8 @@ class CommercetoolsClient {
|
|
|
121
132
|
}
|
|
122
133
|
return createApiBuilderFromCtpClient(builder.build());
|
|
123
134
|
}
|
|
124
|
-
createBaseClientBuilder() {
|
|
125
|
-
|
|
135
|
+
createBaseClientBuilder(reqCtx) {
|
|
136
|
+
let builder = new ClientBuilder().withProjectKey(this.config.projectKey).withQueueMiddleware({
|
|
126
137
|
concurrency: 20
|
|
127
138
|
}).withConcurrentModificationMiddleware({
|
|
128
139
|
concurrentModificationHandlerFn: (version, request) => {
|
|
@@ -133,9 +144,6 @@ class CommercetoolsClient {
|
|
|
133
144
|
body["version"] = version;
|
|
134
145
|
return Promise.resolve(body);
|
|
135
146
|
}
|
|
136
|
-
}).withCorrelationIdMiddleware({
|
|
137
|
-
// ideally this would be pushed in as part of the session context, so we can trace it end-to-end
|
|
138
|
-
generate: () => `REACTIONARY-${randomUUID()}`
|
|
139
147
|
}).withHttpMiddleware({
|
|
140
148
|
retryConfig: {
|
|
141
149
|
backoff: true,
|
|
@@ -151,6 +159,13 @@ class CommercetoolsClient {
|
|
|
151
159
|
host: this.config.apiUrl,
|
|
152
160
|
httpClient: fetch
|
|
153
161
|
});
|
|
162
|
+
const correlationId = reqCtx?.correlationId || "REACTIONARY-" + (typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : randomUUID());
|
|
163
|
+
builder = builder.withCorrelationIdMiddleware({
|
|
164
|
+
generate: () => correlationId
|
|
165
|
+
});
|
|
166
|
+
if (debug.enabled) {
|
|
167
|
+
builder = builder.withLoggerMiddleware();
|
|
168
|
+
}
|
|
154
169
|
return builder;
|
|
155
170
|
}
|
|
156
171
|
}
|
package/core/initialize.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
ProductSchema,
|
|
7
7
|
SearchResultSchema,
|
|
8
8
|
CategorySchema,
|
|
9
|
-
|
|
9
|
+
CheckoutSchema
|
|
10
10
|
} from "@reactionary/core";
|
|
11
11
|
import { CommercetoolsSearchProvider } from "../providers/search.provider";
|
|
12
12
|
import { CommercetoolsProductProvider } from "../providers/product.provider";
|
|
@@ -15,7 +15,7 @@ import { CommercetoolsCartProvider } from "../providers/cart.provider";
|
|
|
15
15
|
import { CommercetoolsInventoryProvider } from "../providers/inventory.provider";
|
|
16
16
|
import { CommercetoolsPriceProvider } from "../providers/price.provider";
|
|
17
17
|
import { CommercetoolsCategoryProvider } from "../providers/category.provider";
|
|
18
|
-
import {
|
|
18
|
+
import { CommercetoolsCheckoutProvider } from "../providers";
|
|
19
19
|
function withCommercetoolsCapabilities(configuration, capabilities) {
|
|
20
20
|
return (cache) => {
|
|
21
21
|
const client = {};
|
|
@@ -40,8 +40,8 @@ function withCommercetoolsCapabilities(configuration, capabilities) {
|
|
|
40
40
|
if (capabilities.category) {
|
|
41
41
|
client.category = new CommercetoolsCategoryProvider(configuration, CategorySchema, cache);
|
|
42
42
|
}
|
|
43
|
-
if (capabilities.
|
|
44
|
-
client.
|
|
43
|
+
if (capabilities.checkout) {
|
|
44
|
+
client.checkout = new CommercetoolsCheckoutProvider(configuration, CheckoutSchema, cache);
|
|
45
45
|
}
|
|
46
46
|
return client;
|
|
47
47
|
};
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reactionary/provider-commercetools",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.51",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"types": "src/index.d.ts",
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"@reactionary/core": "0.0.
|
|
8
|
-
"@reactionary/otel": "0.0.
|
|
7
|
+
"@reactionary/core": "0.0.51",
|
|
8
|
+
"@reactionary/otel": "0.0.51",
|
|
9
|
+
"debug": "^4.4.3",
|
|
9
10
|
"zod": "4.1.9",
|
|
10
11
|
"@commercetools/ts-client": "^4.2.1",
|
|
11
12
|
"@commercetools/platform-sdk": "^8.8.0"
|
|
@@ -279,13 +279,21 @@ class CommercetoolsCartProvider extends CartProvider {
|
|
|
279
279
|
async applyActions(cart, actions, reqCtx) {
|
|
280
280
|
const client = await this.getClient(reqCtx);
|
|
281
281
|
const ctId = cart;
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
282
|
+
try {
|
|
283
|
+
const response = await client.carts.withId({ ID: ctId.key }).post({
|
|
284
|
+
body: {
|
|
285
|
+
version: ctId.version,
|
|
286
|
+
actions
|
|
287
|
+
}
|
|
288
|
+
}).execute();
|
|
289
|
+
if (response.error) {
|
|
290
|
+
console.error(response.error);
|
|
286
291
|
}
|
|
287
|
-
|
|
288
|
-
|
|
292
|
+
return this.parseSingle(response.body, reqCtx);
|
|
293
|
+
} catch (e) {
|
|
294
|
+
console.error("Error applying actions to cart:", e);
|
|
295
|
+
throw e;
|
|
296
|
+
}
|
|
289
297
|
}
|
|
290
298
|
async getClient(reqCtx) {
|
|
291
299
|
const client = await new CommercetoolsClient(this.config).getClient(reqCtx);
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import { AddressSchema, CheckoutItemSchema, CheckoutProvider, PaymentInstructionIdentifierSchema, PaymentInstructionSchema, PaymentMethodIdentifierSchema, ShippingInstructionSchema, ShippingMethodSchema } from "@reactionary/core";
|
|
2
|
+
import { CommercetoolsClient } from "../core/client";
|
|
3
|
+
import { CommercetoolsCartIdentifierSchema, CommercetoolsCheckoutIdentifierSchema, CommercetoolsOrderIdentifierSchema } from "../schema/commercetools.schema";
|
|
4
|
+
class CheckoutNotReadyForFinalizationError extends Error {
|
|
5
|
+
constructor(checkoutIdentifier) {
|
|
6
|
+
super("Checkout is not ready for finalization. Ensure all required fields are set and valid. " + (checkoutIdentifier ? `Checkout ID: ${JSON.stringify(checkoutIdentifier)}` : ""));
|
|
7
|
+
this.checkoutIdentifier = checkoutIdentifier;
|
|
8
|
+
this.name = "CheckoutNotReadyForFinalizationError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
class CommercetoolsCheckoutProvider extends CheckoutProvider {
|
|
12
|
+
constructor(config, schema, cache) {
|
|
13
|
+
super(schema, cache);
|
|
14
|
+
this.config = config;
|
|
15
|
+
}
|
|
16
|
+
async getClient(reqCtx) {
|
|
17
|
+
const client = await new CommercetoolsClient(this.config).getClient(reqCtx);
|
|
18
|
+
return {
|
|
19
|
+
payments: client.withProjectKey({ projectKey: this.config.projectKey }).me().payments(),
|
|
20
|
+
carts: client.withProjectKey({ projectKey: this.config.projectKey }).me().carts(),
|
|
21
|
+
shippingMethods: client.withProjectKey({ projectKey: this.config.projectKey }).shippingMethods(),
|
|
22
|
+
orders: client.withProjectKey({ projectKey: this.config.projectKey }).me().orders()
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
async initiateCheckoutForCart(payload, reqCtx) {
|
|
26
|
+
const client = await this.getClient(reqCtx);
|
|
27
|
+
const cart = await client.carts.withId({ ID: payload.cart.key }).get().execute();
|
|
28
|
+
const replicationResponse = await client.carts.replicate().post({
|
|
29
|
+
body: {
|
|
30
|
+
reference: {
|
|
31
|
+
typeId: "cart",
|
|
32
|
+
id: cart.body.id
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}).execute();
|
|
36
|
+
const actions = [
|
|
37
|
+
{
|
|
38
|
+
action: "setCustomType",
|
|
39
|
+
type: {
|
|
40
|
+
typeId: "type",
|
|
41
|
+
key: "reactionaryCheckout"
|
|
42
|
+
},
|
|
43
|
+
fields: {
|
|
44
|
+
commerceToolsCartId: payload.cart.key
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
];
|
|
48
|
+
if (payload.billingAddress) {
|
|
49
|
+
actions.push({
|
|
50
|
+
action: "setBillingAddress",
|
|
51
|
+
address: {
|
|
52
|
+
country: payload.billingAddress.countryCode,
|
|
53
|
+
firstName: payload.billingAddress.firstName || "",
|
|
54
|
+
lastName: payload.billingAddress.lastName || "",
|
|
55
|
+
streetName: payload.billingAddress.streetAddress || "",
|
|
56
|
+
streetNumber: payload.billingAddress.streetNumber || "",
|
|
57
|
+
postalCode: payload.billingAddress.postalCode || "",
|
|
58
|
+
city: payload.billingAddress.city || "",
|
|
59
|
+
email: payload.notificationEmail || "",
|
|
60
|
+
phone: payload.notificationPhone || ""
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
actions.push({
|
|
64
|
+
action: "setShippingAddress",
|
|
65
|
+
address: {
|
|
66
|
+
country: payload.billingAddress.countryCode,
|
|
67
|
+
firstName: payload.billingAddress.firstName || "",
|
|
68
|
+
lastName: payload.billingAddress.lastName || "",
|
|
69
|
+
streetName: payload.billingAddress.streetAddress || "",
|
|
70
|
+
streetNumber: payload.billingAddress.streetNumber || "",
|
|
71
|
+
postalCode: payload.billingAddress.postalCode || "",
|
|
72
|
+
city: payload.billingAddress.city || ""
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const checkoutResponse = await client.carts.withId({ ID: replicationResponse.body.id }).post({
|
|
77
|
+
body: {
|
|
78
|
+
version: replicationResponse.body.version || 0,
|
|
79
|
+
actions: [
|
|
80
|
+
...actions
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
}).execute();
|
|
84
|
+
return this.parseSingle(checkoutResponse.body, reqCtx);
|
|
85
|
+
}
|
|
86
|
+
async getById(payload, reqCtx) {
|
|
87
|
+
const client = await this.getClient(reqCtx);
|
|
88
|
+
const checkoutResponse = await client.carts.withId({ ID: payload.identifier.key }).get({
|
|
89
|
+
queryArgs: {
|
|
90
|
+
expand: ["paymentInfo.payments[*]", "shippingInfo.shippingMethod"]
|
|
91
|
+
}
|
|
92
|
+
}).execute();
|
|
93
|
+
return this.parseSingle(checkoutResponse.body, reqCtx);
|
|
94
|
+
}
|
|
95
|
+
async setShippingAddress(payload, reqCtx) {
|
|
96
|
+
const client = await this.getClient(reqCtx);
|
|
97
|
+
const version = payload.checkout.version;
|
|
98
|
+
const checkoutResponse = await client.carts.withId({ ID: payload.checkout.key }).post({
|
|
99
|
+
body: {
|
|
100
|
+
version,
|
|
101
|
+
actions: [
|
|
102
|
+
{
|
|
103
|
+
action: "setShippingAddress",
|
|
104
|
+
address: {
|
|
105
|
+
country: payload.shippingAddress.countryCode,
|
|
106
|
+
firstName: payload.shippingAddress.firstName || "",
|
|
107
|
+
lastName: payload.shippingAddress.lastName || "",
|
|
108
|
+
streetName: payload.shippingAddress.streetAddress || "",
|
|
109
|
+
streetNumber: payload.shippingAddress.streetNumber || "",
|
|
110
|
+
postalCode: payload.shippingAddress.postalCode || "",
|
|
111
|
+
city: payload.shippingAddress.city || ""
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
}).execute();
|
|
117
|
+
return this.parseSingle(checkoutResponse.body, reqCtx);
|
|
118
|
+
}
|
|
119
|
+
async getAvailableShippingMethods(payload, reqCtx) {
|
|
120
|
+
const client = await this.getClient(reqCtx);
|
|
121
|
+
const shippingMethodsResponse = await client.shippingMethods.matchingCart().get({
|
|
122
|
+
queryArgs: {
|
|
123
|
+
cartId: payload.checkout.key
|
|
124
|
+
}
|
|
125
|
+
}).execute();
|
|
126
|
+
const result = [];
|
|
127
|
+
const inputShippingMethods = shippingMethodsResponse.body.results;
|
|
128
|
+
for (const sm of inputShippingMethods) {
|
|
129
|
+
const shippingMethod = ShippingMethodSchema.parse({
|
|
130
|
+
identifier: {
|
|
131
|
+
key: sm.key
|
|
132
|
+
},
|
|
133
|
+
name: sm.name,
|
|
134
|
+
description: sm.localizedDescription?.[reqCtx.languageContext.locale] || "",
|
|
135
|
+
price: sm.zoneRates[0].shippingRates[0].price ? {
|
|
136
|
+
value: (sm.zoneRates[0].shippingRates[0].price.centAmount || 0) / 100,
|
|
137
|
+
currency: sm.zoneRates[0].shippingRates[0].price.currencyCode || reqCtx.languageContext.currencyCode
|
|
138
|
+
} : { value: 0, currency: reqCtx.languageContext.currencyCode }
|
|
139
|
+
});
|
|
140
|
+
result.push(shippingMethod);
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
async getAvailablePaymentMethods(payload, reqCtx) {
|
|
145
|
+
const staticMethods = this.getStaticPaymentMethods(payload.checkout, reqCtx);
|
|
146
|
+
const dynamicMethods = [];
|
|
147
|
+
return [...staticMethods, ...dynamicMethods];
|
|
148
|
+
}
|
|
149
|
+
async addPaymentInstruction(payload, reqCtx) {
|
|
150
|
+
const client = await this.getClient(reqCtx);
|
|
151
|
+
const response = await client.payments.post({
|
|
152
|
+
body: {
|
|
153
|
+
amountPlanned: {
|
|
154
|
+
centAmount: Math.round(payload.paymentInstruction.amount.value * 100),
|
|
155
|
+
currencyCode: payload.paymentInstruction.amount.currency
|
|
156
|
+
},
|
|
157
|
+
paymentMethodInfo: {
|
|
158
|
+
method: payload.paymentInstruction.paymentMethod.method,
|
|
159
|
+
name: {
|
|
160
|
+
[reqCtx.languageContext.locale]: payload.paymentInstruction.paymentMethod.name
|
|
161
|
+
},
|
|
162
|
+
paymentInterface: payload.paymentInstruction.paymentMethod.paymentProcessor
|
|
163
|
+
},
|
|
164
|
+
custom: {
|
|
165
|
+
type: {
|
|
166
|
+
typeId: "type",
|
|
167
|
+
key: "reactionaryPaymentCustomFields"
|
|
168
|
+
},
|
|
169
|
+
fields: {
|
|
170
|
+
"commerceToolsCartId": payload.checkout.key
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}).execute();
|
|
175
|
+
const version = payload.checkout.version;
|
|
176
|
+
const actions = [
|
|
177
|
+
{
|
|
178
|
+
action: "addPayment",
|
|
179
|
+
payment: {
|
|
180
|
+
typeId: "payment",
|
|
181
|
+
id: response.body.id
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
];
|
|
185
|
+
return this.applyActions(payload.checkout, actions, reqCtx);
|
|
186
|
+
}
|
|
187
|
+
async removePaymentInstruction(payload, reqCtx) {
|
|
188
|
+
const client = await this.getClient(reqCtx);
|
|
189
|
+
const checkout = await this.getById({ identifier: payload.checkout }, reqCtx);
|
|
190
|
+
return checkout;
|
|
191
|
+
}
|
|
192
|
+
async setShippingInstruction(payload, reqCtx) {
|
|
193
|
+
const client = await this.getClient(reqCtx);
|
|
194
|
+
const ctId = payload.checkout;
|
|
195
|
+
const actions = [];
|
|
196
|
+
actions.push({
|
|
197
|
+
action: "setShippingMethod",
|
|
198
|
+
shippingMethod: {
|
|
199
|
+
typeId: "shipping-method",
|
|
200
|
+
key: payload.shippingInstruction.shippingMethod.key
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
actions.push({
|
|
204
|
+
action: "setCustomField",
|
|
205
|
+
name: "shippingInstruction",
|
|
206
|
+
value: payload.shippingInstruction.instructions
|
|
207
|
+
});
|
|
208
|
+
actions.push({
|
|
209
|
+
action: "setCustomField",
|
|
210
|
+
name: "consentForUnattendedDelivery",
|
|
211
|
+
value: payload.shippingInstruction.consentForUnattendedDelivery + ""
|
|
212
|
+
});
|
|
213
|
+
actions.push({
|
|
214
|
+
action: "setCustomField",
|
|
215
|
+
name: "pickupPointId",
|
|
216
|
+
value: payload.shippingInstruction.pickupPoint
|
|
217
|
+
});
|
|
218
|
+
return this.applyActions(payload.checkout, actions, reqCtx);
|
|
219
|
+
}
|
|
220
|
+
async finalizeCheckout(payload, reqCtx) {
|
|
221
|
+
const checkout = await this.getById({ identifier: payload.checkout }, reqCtx);
|
|
222
|
+
if (!checkout || !checkout.readyForFinalization) {
|
|
223
|
+
throw new CheckoutNotReadyForFinalizationError(payload.checkout);
|
|
224
|
+
}
|
|
225
|
+
const client = await this.getClient(reqCtx);
|
|
226
|
+
const ctId = payload.checkout;
|
|
227
|
+
const orderResponse = await client.orders.post({
|
|
228
|
+
body: {
|
|
229
|
+
id: ctId.key,
|
|
230
|
+
version: ctId.version
|
|
231
|
+
}
|
|
232
|
+
}).execute();
|
|
233
|
+
const actions = [];
|
|
234
|
+
actions.push({
|
|
235
|
+
action: "setCustomField",
|
|
236
|
+
name: "commerceToolsOrderId",
|
|
237
|
+
value: orderResponse.body.id
|
|
238
|
+
});
|
|
239
|
+
return this.applyActions(payload.checkout, actions, reqCtx);
|
|
240
|
+
}
|
|
241
|
+
async applyActions(checkout, actions, reqCtx) {
|
|
242
|
+
const client = await this.getClient(reqCtx);
|
|
243
|
+
const ctId = checkout;
|
|
244
|
+
try {
|
|
245
|
+
const response = await client.carts.withId({ ID: ctId.key }).post({
|
|
246
|
+
queryArgs: {
|
|
247
|
+
expand: ["paymentInfo.payments[*]", "shippingInfo.shippingMethod"]
|
|
248
|
+
},
|
|
249
|
+
body: {
|
|
250
|
+
version: ctId.version,
|
|
251
|
+
actions
|
|
252
|
+
}
|
|
253
|
+
}).execute();
|
|
254
|
+
if (response.error) {
|
|
255
|
+
console.error(response.error);
|
|
256
|
+
}
|
|
257
|
+
return this.parseSingle(response.body, reqCtx);
|
|
258
|
+
} catch (e) {
|
|
259
|
+
console.error("Error applying actions to cart:", e);
|
|
260
|
+
throw e;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Extension point, to allow filtering the options, or adding new ones, like invoicing for b2b.
|
|
265
|
+
*
|
|
266
|
+
* Usecase: Override this, if you need to change the payment options based on the request context.
|
|
267
|
+
* @param reqCtx
|
|
268
|
+
* @returns
|
|
269
|
+
*/
|
|
270
|
+
getStaticPaymentMethods(_checkout, reqCtx) {
|
|
271
|
+
return this.config.paymentMethods || [];
|
|
272
|
+
}
|
|
273
|
+
getResourceName() {
|
|
274
|
+
return "checkout";
|
|
275
|
+
}
|
|
276
|
+
parseSingle(remote, reqCtx) {
|
|
277
|
+
const result = this.newModel();
|
|
278
|
+
result.identifier = CommercetoolsCheckoutIdentifierSchema.parse({
|
|
279
|
+
key: remote.id,
|
|
280
|
+
version: remote.version || 0
|
|
281
|
+
});
|
|
282
|
+
result.name = remote.custom?.fields["name"] || "";
|
|
283
|
+
result.description = remote.custom?.fields["description"] || "";
|
|
284
|
+
result.originalCartReference = CommercetoolsCartIdentifierSchema.parse({
|
|
285
|
+
key: remote.custom?.fields["commerceToolsCartId"] || "",
|
|
286
|
+
version: 0
|
|
287
|
+
});
|
|
288
|
+
const orderId = remote.custom?.fields["commerceToolsOrderId"];
|
|
289
|
+
if (orderId) {
|
|
290
|
+
result.resultingOrder = CommercetoolsOrderIdentifierSchema.parse({
|
|
291
|
+
key: orderId,
|
|
292
|
+
version: 0
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
if (remote.shippingAddress) {
|
|
296
|
+
result.shippingAddress = this.parseAddress(remote.shippingAddress, reqCtx);
|
|
297
|
+
}
|
|
298
|
+
if (remote.billingAddress) {
|
|
299
|
+
result.billingAddress = this.parseAddress(remote.billingAddress, reqCtx);
|
|
300
|
+
}
|
|
301
|
+
result.shippingInstruction = this.parseShippingInstruction(remote);
|
|
302
|
+
for (const p of remote.paymentInfo?.payments || []) {
|
|
303
|
+
if (p.obj) {
|
|
304
|
+
result.paymentInstructions.push(this.parsePaymentInstruction(p.obj, reqCtx));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const grandTotal = remote.totalPrice.centAmount || 0;
|
|
308
|
+
const shippingTotal = remote.shippingInfo?.price.centAmount || 0;
|
|
309
|
+
const productTotal = grandTotal - shippingTotal;
|
|
310
|
+
const taxTotal = remote.taxedPrice?.totalTax?.centAmount || 0;
|
|
311
|
+
const discountTotal = remote.discountOnTotalPrice?.discountedAmount.centAmount || 0;
|
|
312
|
+
const surchargeTotal = 0;
|
|
313
|
+
const currency = remote.totalPrice.currencyCode;
|
|
314
|
+
result.price = {
|
|
315
|
+
totalTax: {
|
|
316
|
+
value: taxTotal / 100,
|
|
317
|
+
currency
|
|
318
|
+
},
|
|
319
|
+
totalDiscount: {
|
|
320
|
+
value: discountTotal / 100,
|
|
321
|
+
currency
|
|
322
|
+
},
|
|
323
|
+
totalSurcharge: {
|
|
324
|
+
value: surchargeTotal / 100,
|
|
325
|
+
currency
|
|
326
|
+
},
|
|
327
|
+
totalShipping: {
|
|
328
|
+
value: shippingTotal / 100,
|
|
329
|
+
currency: remote.shippingInfo?.price.currencyCode
|
|
330
|
+
},
|
|
331
|
+
totalProductPrice: {
|
|
332
|
+
value: productTotal / 100,
|
|
333
|
+
currency
|
|
334
|
+
},
|
|
335
|
+
grandTotal: {
|
|
336
|
+
value: grandTotal / 100,
|
|
337
|
+
currency
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
for (const remoteItem of remote.lineItems) {
|
|
341
|
+
const item = CheckoutItemSchema.parse({});
|
|
342
|
+
item.identifier.key = remoteItem.id;
|
|
343
|
+
item.sku.key = remoteItem.variant.sku || "";
|
|
344
|
+
item.quantity = remoteItem.quantity;
|
|
345
|
+
const unitPrice = remoteItem.price.value.centAmount;
|
|
346
|
+
const totalPrice = remoteItem.totalPrice.centAmount || 0;
|
|
347
|
+
const totalDiscount = remoteItem.price.discounted?.value.centAmount || 0;
|
|
348
|
+
const unitDiscount = totalDiscount / remoteItem.quantity;
|
|
349
|
+
item.price = {
|
|
350
|
+
unitPrice: {
|
|
351
|
+
value: unitPrice / 100,
|
|
352
|
+
currency
|
|
353
|
+
},
|
|
354
|
+
unitDiscount: {
|
|
355
|
+
value: unitDiscount / 100,
|
|
356
|
+
currency
|
|
357
|
+
},
|
|
358
|
+
totalPrice: {
|
|
359
|
+
value: (totalPrice || 0) / 100,
|
|
360
|
+
currency
|
|
361
|
+
},
|
|
362
|
+
totalDiscount: {
|
|
363
|
+
value: totalDiscount / 100,
|
|
364
|
+
currency
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
result.items.push(item);
|
|
368
|
+
}
|
|
369
|
+
result.readyForFinalization = this.isReadyForFinalization(result);
|
|
370
|
+
result.meta = {
|
|
371
|
+
cache: {
|
|
372
|
+
hit: false,
|
|
373
|
+
key: this.generateCacheKeySingle(result.identifier, reqCtx)
|
|
374
|
+
},
|
|
375
|
+
placeholder: false
|
|
376
|
+
};
|
|
377
|
+
return this.assert(result);
|
|
378
|
+
}
|
|
379
|
+
isReadyForFinalization(checkout) {
|
|
380
|
+
if (!checkout.billingAddress)
|
|
381
|
+
return false;
|
|
382
|
+
if (!checkout.shippingInstruction)
|
|
383
|
+
return false;
|
|
384
|
+
if (!checkout.shippingAddress && !checkout.shippingInstruction.pickupPoint)
|
|
385
|
+
return false;
|
|
386
|
+
if (checkout.paymentInstructions.length === 0)
|
|
387
|
+
return false;
|
|
388
|
+
const authorizedPayments = checkout.paymentInstructions.filter((pi) => pi.status === "authorized").map((x) => x.amount.value).reduce((a, b) => a + b, 0);
|
|
389
|
+
if (checkout.price.grandTotal.value !== authorizedPayments)
|
|
390
|
+
return false;
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
parsePaymentInstruction(remote, reqCtx) {
|
|
394
|
+
const newModel = PaymentInstructionSchema.parse({});
|
|
395
|
+
newModel.identifier = PaymentInstructionIdentifierSchema.parse({ key: remote.id || "" });
|
|
396
|
+
newModel.amount = {
|
|
397
|
+
value: remote.amountPlanned.centAmount / 100,
|
|
398
|
+
currency: remote.amountPlanned.currencyCode
|
|
399
|
+
};
|
|
400
|
+
const method = remote.paymentMethodInfo?.method || "unknown";
|
|
401
|
+
const paymentProcessor = remote.paymentMethodInfo?.paymentInterface || method;
|
|
402
|
+
const paymentName = remote.paymentMethodInfo.name[reqCtx.languageContext.locale];
|
|
403
|
+
newModel.paymentMethod = PaymentMethodIdentifierSchema.parse({
|
|
404
|
+
method,
|
|
405
|
+
paymentProcessor,
|
|
406
|
+
name: paymentName || method || "Unknown"
|
|
407
|
+
});
|
|
408
|
+
const customData = remote.custom?.fields || {};
|
|
409
|
+
newModel.protocolData = Object.keys(customData).map((x) => ({ key: x, value: customData[x] })) || [];
|
|
410
|
+
if (remote.transactions && remote.transactions.length > 0) {
|
|
411
|
+
const lastTransaction = remote.transactions[remote.transactions.length - 1];
|
|
412
|
+
if (lastTransaction.type === "Authorization" && lastTransaction.state === "Pending") {
|
|
413
|
+
newModel.status = "pending";
|
|
414
|
+
} else if (lastTransaction.type === "Authorization" && lastTransaction.state === "Success") {
|
|
415
|
+
newModel.status = "authorized";
|
|
416
|
+
}
|
|
417
|
+
} else {
|
|
418
|
+
newModel.status = "pending";
|
|
419
|
+
}
|
|
420
|
+
return PaymentInstructionSchema.parse(newModel);
|
|
421
|
+
}
|
|
422
|
+
parseAddress(remote, reqCtx) {
|
|
423
|
+
return AddressSchema.parse({
|
|
424
|
+
countryCode: remote.country || "",
|
|
425
|
+
firstName: remote.firstName || "",
|
|
426
|
+
lastName: remote.lastName || "",
|
|
427
|
+
streetAddress: remote.streetName || "",
|
|
428
|
+
streetNumber: remote.streetNumber || "",
|
|
429
|
+
postalCode: remote.postalCode || "",
|
|
430
|
+
city: remote.city || ""
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
parseShippingInstruction(remote) {
|
|
434
|
+
if (!remote.shippingInfo)
|
|
435
|
+
return void 0;
|
|
436
|
+
const instructions = remote.custom?.fields["shippingInstruction"] || "";
|
|
437
|
+
const consentForUnattendedDelivery = remote.custom?.fields["consentForUnattendedDelivery"] === "true" || false;
|
|
438
|
+
const pickupPoint = remote.custom?.fields["pickupPointId"] || "";
|
|
439
|
+
const shippingInstruction = ShippingInstructionSchema.parse({
|
|
440
|
+
amount: {
|
|
441
|
+
value: (remote.shippingInfo.price.centAmount || 0) / 100,
|
|
442
|
+
currency: remote.shippingInfo.price.currencyCode
|
|
443
|
+
},
|
|
444
|
+
shippingMethod: {
|
|
445
|
+
key: remote.shippingInfo.shippingMethod?.obj?.key || ""
|
|
446
|
+
},
|
|
447
|
+
pickupPoint: pickupPoint || "",
|
|
448
|
+
instructions: instructions || "",
|
|
449
|
+
consentForUnattendedDelivery: consentForUnattendedDelivery || false
|
|
450
|
+
});
|
|
451
|
+
return shippingInstruction;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
export {
|
|
455
|
+
CheckoutNotReadyForFinalizationError,
|
|
456
|
+
CommercetoolsCheckoutProvider
|
|
457
|
+
};
|