@reactionary/source 0.3.16 → 0.3.18
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 +2 -2
- package/core/src/initialization.ts +2 -2
- package/core/src/providers/price.provider.ts +2 -1
- package/core/src/schemas/models/cart.model.ts +7 -2
- package/core/src/schemas/models/identifiers.model.ts +12 -8
- package/core/src/schemas/models/price.model.ts +12 -0
- package/examples/node/package.json +7 -7
- package/examples/node/src/capabilities/cart.spec.ts +97 -15
- package/examples/node/src/capabilities/category.spec.ts +27 -32
- package/examples/node/src/capabilities/checkout.spec.ts +5 -5
- package/examples/node/src/capabilities/identity.spec.ts +6 -2
- package/examples/node/src/capabilities/inventory.spec.ts +1 -1
- package/examples/node/src/capabilities/price.spec.ts +7 -7
- package/examples/node/src/utils.ts +4 -1
- package/package.json +3 -3
- package/providers/algolia/src/providers/product-search.provider.ts +19 -14
- package/providers/commercetools/src/core/client.ts +112 -9
- package/providers/commercetools/src/core/token-cache.ts +4 -5
- package/providers/commercetools/src/providers/cart.provider.ts +76 -11
- package/providers/commercetools/src/providers/inventory.provider.ts +5 -7
- package/providers/commercetools/src/providers/price.provider.ts +17 -30
- package/providers/commercetools/src/schema/configuration.schema.ts +4 -0
- package/providers/commercetools/src/schema/session.schema.ts +3 -1
- package/providers/fake/src/providers/cart.provider.ts +1 -0
- package/providers/fake/src/providers/price.provider.ts +54 -95
- package/providers/medusa/src/providers/cart.provider.ts +159 -70
- package/providers/medusa/src/providers/category.provider.ts +35 -23
- package/providers/medusa/src/providers/checkout.provider.ts +78 -41
- package/providers/medusa/src/providers/order-search.provider.ts +21 -10
- package/providers/medusa/src/providers/price.provider.ts +18 -9
- package/providers/medusa/src/providers/product-recommendations.provider.ts +10 -6
- package/providers/medusa/src/providers/product-search.provider.ts +19 -10
- package/providers/medusa/src/providers/product.provider.ts +20 -12
- package/providers/medusa/src/providers/profile.provider.ts +38 -13
- package/providers/meilisearch/src/providers/order-search.provider.ts +17 -12
- package/providers/meilisearch/src/providers/product-recommendations.provider.ts +10 -11
- package/providers/meilisearch/src/providers/product-search.provider.ts +23 -18
|
@@ -112,7 +112,7 @@ export function createClient(provider: PrimaryProvider) {
|
|
|
112
112
|
productAssociations: true,
|
|
113
113
|
orderSearch: true,
|
|
114
114
|
store: true,
|
|
115
|
-
profile: true
|
|
115
|
+
profile: true,
|
|
116
116
|
})
|
|
117
117
|
);
|
|
118
118
|
}
|
|
@@ -120,6 +120,9 @@ export function createClient(provider: PrimaryProvider) {
|
|
|
120
120
|
if (provider === PrimaryProvider.FAKE) {
|
|
121
121
|
builder = builder.withCapability(
|
|
122
122
|
withFakeCapabilities( getFakeConfiguration() , {
|
|
123
|
+
price: true,
|
|
124
|
+
inventory: true,
|
|
125
|
+
product: true,
|
|
123
126
|
productReviews: true,
|
|
124
127
|
productAssociations: true,
|
|
125
128
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reactionary/source",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.18",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"private": false,
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"@commercetools/platform-sdk": "^8.
|
|
8
|
-
"@commercetools/ts-client": "^4.
|
|
7
|
+
"@commercetools/platform-sdk": "^8.25.0",
|
|
8
|
+
"@commercetools/ts-client": "^4.9.1",
|
|
9
9
|
"@commercetools/ts-sdk-apm": "^4.0.0",
|
|
10
10
|
"@docusaurus/core": "^3.9.2",
|
|
11
11
|
"@docusaurus/preset-classic": "^3.9.2",
|
|
@@ -37,14 +37,7 @@ export class AlgoliaProductSearchProvider extends ProductSearchProvider {
|
|
|
37
37
|
this.config = config;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
inputSchema: ProductSearchQueryByTermSchema,
|
|
42
|
-
outputSchema: ProductSearchResultSchema
|
|
43
|
-
})
|
|
44
|
-
public override async queryByTerm(
|
|
45
|
-
payload: ProductSearchQueryByTerm
|
|
46
|
-
): Promise<Result<ProductSearchResult>> {
|
|
47
|
-
const client = algoliasearch(this.config.appId, this.config.apiKey);
|
|
40
|
+
protected queryByTermPayload(payload: ProductSearchQueryByTerm) {
|
|
48
41
|
|
|
49
42
|
const facetsThatAreNotCategory = payload.search.facets.filter(x => x.facet.key !== 'categories');
|
|
50
43
|
const categoryFacet = payload.search.facets.find(x => x.facet.key === 'categories') || payload.search.categoryFilter;
|
|
@@ -60,11 +53,7 @@ export class AlgoliaProductSearchProvider extends ProductSearchProvider {
|
|
|
60
53
|
if (categoryFacet) {
|
|
61
54
|
finalFilters.push(`categories:"${categoryFacet.key}"`);
|
|
62
55
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const remote = await client.search<AlgoliaNativeRecord>({
|
|
66
|
-
requests: [
|
|
67
|
-
{
|
|
56
|
+
return {
|
|
68
57
|
indexName: this.config.indexName,
|
|
69
58
|
query: payload.search.term,
|
|
70
59
|
page: payload.search.paginationOptions.pageNumber - 1,
|
|
@@ -75,7 +64,23 @@ export class AlgoliaProductSearchProvider extends ProductSearchProvider {
|
|
|
75
64
|
facetFilters: finalFacetFilters,
|
|
76
65
|
filters: (finalFilters || [])
|
|
77
66
|
.join(' AND '),
|
|
78
|
-
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@Reactionary({
|
|
71
|
+
inputSchema: ProductSearchQueryByTermSchema,
|
|
72
|
+
outputSchema: ProductSearchResultSchema
|
|
73
|
+
})
|
|
74
|
+
public override async queryByTerm(
|
|
75
|
+
payload: ProductSearchQueryByTerm
|
|
76
|
+
): Promise<Result<ProductSearchResult>> {
|
|
77
|
+
const client = algoliasearch(this.config.appId, this.config.apiKey);
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
const remote = await client.search<AlgoliaNativeRecord>({
|
|
82
|
+
requests: [
|
|
83
|
+
this.queryByTermPayload(payload)
|
|
79
84
|
],
|
|
80
85
|
});
|
|
81
86
|
|
|
@@ -16,19 +16,23 @@ import {
|
|
|
16
16
|
import * as crypto from 'crypto';
|
|
17
17
|
import createDebug from 'debug';
|
|
18
18
|
import { RequestContextTokenCache } from './token-cache.js';
|
|
19
|
+
import { CommercetoolsSessionSchema, type CommercetoolsSession } from '../schema/session.schema.js';
|
|
19
20
|
const debug = createDebug('reactionary:commercetools');
|
|
20
21
|
|
|
22
|
+
export const PROVIDER_SESSION_KEY = 'COMMERCETOOLS_PROVIDER';
|
|
23
|
+
|
|
24
|
+
|
|
21
25
|
export class CommercetoolsAPI {
|
|
22
26
|
protected config: CommercetoolsConfiguration;
|
|
23
27
|
protected context: RequestContext;
|
|
24
|
-
protected
|
|
28
|
+
protected tokenCache: RequestContextTokenCache;
|
|
25
29
|
protected client: Promise<ApiRoot> | undefined;
|
|
26
30
|
protected adminClient: Promise<ApiRoot> | undefined;
|
|
27
31
|
|
|
28
32
|
constructor(config: CommercetoolsConfiguration, context: RequestContext) {
|
|
29
33
|
this.config = config;
|
|
30
34
|
this.context = context;
|
|
31
|
-
this.
|
|
35
|
+
this.tokenCache = new RequestContextTokenCache(this.context, PROVIDER_SESSION_KEY);
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
public async getClient() {
|
|
@@ -47,6 +51,105 @@ export class CommercetoolsAPI {
|
|
|
47
51
|
return this.adminClient;
|
|
48
52
|
}
|
|
49
53
|
|
|
54
|
+
public getSessionData(): CommercetoolsSession {
|
|
55
|
+
return this.context.session[PROVIDER_SESSION_KEY]
|
|
56
|
+
? this.context.session[PROVIDER_SESSION_KEY] as CommercetoolsSession
|
|
57
|
+
: CommercetoolsSessionSchema.parse({});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public setSessionData(sessionData: Partial<CommercetoolsSession>): void {
|
|
61
|
+
const existingData = this.context.session[PROVIDER_SESSION_KEY] as Partial<CommercetoolsSession>;
|
|
62
|
+
|
|
63
|
+
this.context.session[PROVIDER_SESSION_KEY] = {
|
|
64
|
+
...existingData,
|
|
65
|
+
...sessionData,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Only caches it pr session for now...... but still better than every call
|
|
71
|
+
* @param key
|
|
72
|
+
* @returns
|
|
73
|
+
*/
|
|
74
|
+
public async resolveChannelIdByKey(key: string): Promise<string> {
|
|
75
|
+
const sessionData = this.getSessionData();
|
|
76
|
+
|
|
77
|
+
const cacheKey = `___channel_${key}`;
|
|
78
|
+
const cachedValue = sessionData[cacheKey];
|
|
79
|
+
if (cachedValue) {
|
|
80
|
+
if (debug.enabled) {
|
|
81
|
+
debug(`Resolved channel ${key} from cache`);
|
|
82
|
+
}
|
|
83
|
+
return cachedValue + '';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const client = await this.getAdminClient();
|
|
87
|
+
const response = await client
|
|
88
|
+
.withProjectKey({ projectKey: this.config.projectKey })
|
|
89
|
+
.channels()
|
|
90
|
+
.withKey({ key: key })
|
|
91
|
+
.get()
|
|
92
|
+
.execute();
|
|
93
|
+
|
|
94
|
+
const channel = response.body;
|
|
95
|
+
this.setSessionData({
|
|
96
|
+
cacheKey: channel.id,
|
|
97
|
+
})
|
|
98
|
+
if (debug.enabled) {
|
|
99
|
+
debug(`Resolved channel ${key} from API and cached it`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return channel.id;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Only caches it pr session for now...... but still better than every call
|
|
107
|
+
* @param key
|
|
108
|
+
* @returns
|
|
109
|
+
*/
|
|
110
|
+
public async resolveChannelIdByRole(role: string): Promise<string> {
|
|
111
|
+
const sessionData = this.getSessionData();
|
|
112
|
+
|
|
113
|
+
const cacheKey = `___channel_role_${role}`;
|
|
114
|
+
const cachedValue = sessionData[cacheKey];
|
|
115
|
+
if (cachedValue) {
|
|
116
|
+
if (debug.enabled) {
|
|
117
|
+
debug(`Resolved channel ${role} from cache`);
|
|
118
|
+
}
|
|
119
|
+
return cachedValue + '';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const client = await this.getAdminClient();
|
|
123
|
+
const response = await client
|
|
124
|
+
.withProjectKey({ projectKey: this.config.projectKey })
|
|
125
|
+
.channels()
|
|
126
|
+
.get({
|
|
127
|
+
queryArgs: {
|
|
128
|
+
where: `roles contains any (:role)`,
|
|
129
|
+
'var.role': role,
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
.execute();
|
|
133
|
+
|
|
134
|
+
const channels = response.body;
|
|
135
|
+
if (channels.results.length === 0) {
|
|
136
|
+
throw new Error(`No channel found with role ${role}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const channel = channels.results[0];
|
|
140
|
+
this.setSessionData({
|
|
141
|
+
[cacheKey]: channel.id,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (debug.enabled) {
|
|
145
|
+
debug(`Resolved channel ${role} from API and cached it`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return channel.id;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
|
50
153
|
protected async createAdminClient() {
|
|
51
154
|
let builder = this.createBaseClientBuilder();
|
|
52
155
|
builder = builder.withAnonymousSessionFlow({
|
|
@@ -62,13 +165,13 @@ export class CommercetoolsAPI {
|
|
|
62
165
|
}
|
|
63
166
|
|
|
64
167
|
protected async createClient() {
|
|
65
|
-
let session = await this.
|
|
168
|
+
let session = await this.tokenCache.get();
|
|
66
169
|
const isNewSession = !session || !session.refreshToken;
|
|
67
170
|
|
|
68
171
|
if (isNewSession) {
|
|
69
172
|
await this.becomeGuest();
|
|
70
173
|
|
|
71
|
-
session = await this.
|
|
174
|
+
session = await this.tokenCache.get();
|
|
72
175
|
}
|
|
73
176
|
|
|
74
177
|
let builder = this.createBaseClientBuilder();
|
|
@@ -80,7 +183,7 @@ export class CommercetoolsAPI {
|
|
|
80
183
|
host: this.config.authUrl,
|
|
81
184
|
projectKey: this.config.projectKey,
|
|
82
185
|
refreshToken: session?.refreshToken || '',
|
|
83
|
-
tokenCache: this.
|
|
186
|
+
tokenCache: this.tokenCache,
|
|
84
187
|
});
|
|
85
188
|
|
|
86
189
|
return createApiBuilderFromCtpClient(builder.build());
|
|
@@ -128,7 +231,7 @@ export class CommercetoolsAPI {
|
|
|
128
231
|
clientSecret: this.config.clientSecret,
|
|
129
232
|
user: { username, password },
|
|
130
233
|
},
|
|
131
|
-
tokenCache: this.
|
|
234
|
+
tokenCache: this.tokenCache,
|
|
132
235
|
scopes: this.config.scopes,
|
|
133
236
|
});
|
|
134
237
|
|
|
@@ -161,7 +264,7 @@ export class CommercetoolsAPI {
|
|
|
161
264
|
}
|
|
162
265
|
|
|
163
266
|
public async logout() {
|
|
164
|
-
await this.
|
|
267
|
+
await this.tokenCache.set({ token: '', refreshToken: '', expirationTime: 0 });
|
|
165
268
|
|
|
166
269
|
// TODO: We could do token revocation here, if we wanted to. The above simply whacks the session.
|
|
167
270
|
const identity = {
|
|
@@ -174,7 +277,7 @@ export class CommercetoolsAPI {
|
|
|
174
277
|
public async introspect(): Promise<
|
|
175
278
|
AnonymousIdentity | GuestIdentity | RegisteredIdentity
|
|
176
279
|
> {
|
|
177
|
-
const session = await this.
|
|
280
|
+
const session = await this.tokenCache.get();
|
|
178
281
|
|
|
179
282
|
if (!session || !session.token) {
|
|
180
283
|
const identity = {
|
|
@@ -267,7 +370,7 @@ export class CommercetoolsAPI {
|
|
|
267
370
|
|
|
268
371
|
const result = await response.json();
|
|
269
372
|
|
|
270
|
-
this.
|
|
373
|
+
this.tokenCache.set({
|
|
271
374
|
expirationTime:
|
|
272
375
|
Date.now() + Number(result.expires_in) * 1000 - 5 * 60 * 1000,
|
|
273
376
|
token: result.access_token,
|
|
@@ -2,15 +2,14 @@ import type { TokenCache, TokenCacheOptions, TokenStore } from "@commercetools/t
|
|
|
2
2
|
import type { RequestContext } from "@reactionary/core";
|
|
3
3
|
import { CommercetoolsSessionSchema } from "../schema/session.schema.js";
|
|
4
4
|
|
|
5
|
-
export const PROVIDER_COMMERCETOOLS_SESSION_KEY = 'PROVIDER_COMMERCETOOLS';
|
|
6
5
|
export class RequestContextTokenCache implements TokenCache {
|
|
7
|
-
constructor(protected context: RequestContext) {}
|
|
6
|
+
constructor(protected context: RequestContext, protected sessionProviderKey: string) {}
|
|
8
7
|
|
|
9
8
|
public async get(
|
|
10
9
|
tokenCacheOptions?: TokenCacheOptions
|
|
11
10
|
): Promise<TokenStore | undefined> {
|
|
12
11
|
const session = CommercetoolsSessionSchema.parse(
|
|
13
|
-
this.context.session[
|
|
12
|
+
this.context.session[this.sessionProviderKey] || {}
|
|
14
13
|
);
|
|
15
14
|
|
|
16
15
|
if (!session) {
|
|
@@ -33,10 +32,10 @@ export class RequestContextTokenCache implements TokenCache {
|
|
|
33
32
|
tokenCacheOptions?: TokenCacheOptions
|
|
34
33
|
): Promise<void> {
|
|
35
34
|
const session = CommercetoolsSessionSchema.parse(
|
|
36
|
-
this.context.session[
|
|
35
|
+
this.context.session[this.sessionProviderKey] || {}
|
|
37
36
|
);
|
|
38
37
|
|
|
39
|
-
this.context.session[
|
|
38
|
+
this.context.session[this.sessionProviderKey] = session;
|
|
40
39
|
|
|
41
40
|
session.refreshToken = cache.refreshToken;
|
|
42
41
|
session.token = cache.token;
|
|
@@ -35,6 +35,7 @@ import type {
|
|
|
35
35
|
CostBreakDown,
|
|
36
36
|
Result,
|
|
37
37
|
NotFoundError,
|
|
38
|
+
Promotion,
|
|
38
39
|
} from '@reactionary/core';
|
|
39
40
|
import type { CommercetoolsConfiguration } from '../schema/configuration.schema.js';
|
|
40
41
|
import type {
|
|
@@ -49,7 +50,7 @@ import type { CommercetoolsAPI } from '../core/client.js';
|
|
|
49
50
|
export class CommercetoolsCartProvider extends CartProvider {
|
|
50
51
|
protected config: CommercetoolsConfiguration;
|
|
51
52
|
protected commercetools: CommercetoolsAPI;
|
|
52
|
-
|
|
53
|
+
protected expandedCartFields = ['discountCodes[*].discountCode'];
|
|
53
54
|
constructor(
|
|
54
55
|
config: CommercetoolsConfiguration,
|
|
55
56
|
cache: Cache,
|
|
@@ -71,7 +72,12 @@ export class CommercetoolsCartProvider extends CartProvider {
|
|
|
71
72
|
const ctId = payload.cart as CommercetoolsCartIdentifier;
|
|
72
73
|
|
|
73
74
|
try {
|
|
74
|
-
const remote = await client.carts.withId({ ID: ctId.key }).get(
|
|
75
|
+
const remote = await client.carts.withId({ ID: ctId.key }).get(
|
|
76
|
+
{
|
|
77
|
+
queryArgs: {
|
|
78
|
+
expand: this.expandedCartFields
|
|
79
|
+
}
|
|
80
|
+
}).execute();
|
|
75
81
|
|
|
76
82
|
return success(this.parseSingle(remote.body));
|
|
77
83
|
} catch(err) {
|
|
@@ -92,6 +98,8 @@ export class CommercetoolsCartProvider extends CartProvider {
|
|
|
92
98
|
cartIdentifier = await this.createCart();
|
|
93
99
|
}
|
|
94
100
|
|
|
101
|
+
const channelId = await this.commercetools.resolveChannelIdByRole('Primary');
|
|
102
|
+
|
|
95
103
|
const result = await this.applyActions(cartIdentifier, [
|
|
96
104
|
{
|
|
97
105
|
action: 'addLineItem',
|
|
@@ -100,7 +108,7 @@ export class CommercetoolsCartProvider extends CartProvider {
|
|
|
100
108
|
// FIXME: This should be dynamic, probably as part of the context...
|
|
101
109
|
distributionChannel: {
|
|
102
110
|
typeId: 'channel',
|
|
103
|
-
|
|
111
|
+
id: channelId,
|
|
104
112
|
},
|
|
105
113
|
},
|
|
106
114
|
{
|
|
@@ -184,7 +192,6 @@ export class CommercetoolsCartProvider extends CartProvider {
|
|
|
184
192
|
|
|
185
193
|
@Reactionary({
|
|
186
194
|
inputSchema: CartMutationDeleteCartSchema,
|
|
187
|
-
outputSchema: CartSchema,
|
|
188
195
|
})
|
|
189
196
|
public override async deleteCart(
|
|
190
197
|
payload: CartMutationDeleteCart
|
|
@@ -234,11 +241,28 @@ export class CommercetoolsCartProvider extends CartProvider {
|
|
|
234
241
|
public override async removeCouponCode(
|
|
235
242
|
payload: CartMutationRemoveCoupon
|
|
236
243
|
): Promise<Result<Cart>> {
|
|
244
|
+
|
|
245
|
+
const client = await this.getClient();
|
|
246
|
+
const currentCart = await client.carts
|
|
247
|
+
.withId({ ID: payload.cart.key })
|
|
248
|
+
.get({
|
|
249
|
+
queryArgs: {
|
|
250
|
+
expand: this.expandedCartFields
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
.execute();
|
|
254
|
+
|
|
255
|
+
const discountCodeReference = currentCart.body.discountCodes?.find(dc => dc.discountCode.obj?.code === payload.couponCode)?.discountCode;
|
|
256
|
+
|
|
257
|
+
if (!discountCodeReference) {
|
|
258
|
+
// Coupon code is not applied to the cart, so we can just return the cart as is.
|
|
259
|
+
return success(this.parseSingle(currentCart.body));
|
|
260
|
+
}
|
|
237
261
|
const result = await this.applyActions(payload.cart, [
|
|
238
262
|
{
|
|
239
263
|
action: 'removeDiscountCode',
|
|
240
264
|
discountCode: {
|
|
241
|
-
id:
|
|
265
|
+
id: discountCodeReference.id,
|
|
242
266
|
typeId: 'discount-code',
|
|
243
267
|
},
|
|
244
268
|
},
|
|
@@ -319,6 +343,9 @@ export class CommercetoolsCartProvider extends CartProvider {
|
|
|
319
343
|
country: this.context.taxJurisdiction.countryCode || 'US',
|
|
320
344
|
locale: this.context.languageContext.locale,
|
|
321
345
|
},
|
|
346
|
+
queryArgs: {
|
|
347
|
+
expand: this.expandedCartFields
|
|
348
|
+
}
|
|
322
349
|
})
|
|
323
350
|
.execute();
|
|
324
351
|
|
|
@@ -343,6 +370,9 @@ export class CommercetoolsCartProvider extends CartProvider {
|
|
|
343
370
|
version: ctId.version,
|
|
344
371
|
actions,
|
|
345
372
|
},
|
|
373
|
+
queryArgs: {
|
|
374
|
+
expand: this.expandedCartFields
|
|
375
|
+
}
|
|
346
376
|
})
|
|
347
377
|
.execute();
|
|
348
378
|
|
|
@@ -378,7 +408,19 @@ export class CommercetoolsCartProvider extends CartProvider {
|
|
|
378
408
|
protected parseCartItem(remoteItem: LineItem): CartItem {
|
|
379
409
|
const unitPrice = remoteItem.price.value.centAmount;
|
|
380
410
|
const totalPrice = remoteItem.totalPrice.centAmount || 0;
|
|
381
|
-
|
|
411
|
+
let itemDiscount = 0;
|
|
412
|
+
|
|
413
|
+
// look, discounts are weird in commercetools.... i think the .price.discount only applies for embedded prices maybe?
|
|
414
|
+
|
|
415
|
+
if (remoteItem.discountedPricePerQuantity && remoteItem.discountedPricePerQuantity.length > 0) {
|
|
416
|
+
itemDiscount = remoteItem.discountedPricePerQuantity.reduce((sum, discPrQty) => {
|
|
417
|
+
return sum + discPrQty.quantity * discPrQty.discountedPrice?.includedDiscounts?.
|
|
418
|
+
reduce((sum, discount) => sum + discount.discountedAmount.centAmount, 0) || 0
|
|
419
|
+
}, 0);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const totalDiscount = (remoteItem.price.discounted?.value.centAmount || 0) + itemDiscount;
|
|
423
|
+
|
|
382
424
|
const unitDiscount = totalDiscount / remoteItem.quantity;
|
|
383
425
|
const currency =
|
|
384
426
|
remoteItem.price.value.currencyCode.toUpperCase() as Currency;
|
|
@@ -423,12 +465,23 @@ export class CommercetoolsCartProvider extends CartProvider {
|
|
|
423
465
|
version: remote.version || 0,
|
|
424
466
|
} satisfies CommercetoolsCartIdentifier;
|
|
425
467
|
|
|
468
|
+
|
|
469
|
+
const items = new Array<CartItem>();
|
|
470
|
+
for (const remoteItem of remote.lineItems) {
|
|
471
|
+
const item = this.parseCartItem(remoteItem);
|
|
472
|
+
|
|
473
|
+
items.push(item);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
|
|
426
477
|
const grandTotal = remote.totalPrice.centAmount || 0;
|
|
427
478
|
const shippingTotal = remote.shippingInfo?.price.centAmount || 0;
|
|
428
479
|
const productTotal = grandTotal - shippingTotal;
|
|
429
480
|
const taxTotal = remote.taxedPrice?.totalTax?.centAmount || 0;
|
|
481
|
+
|
|
482
|
+
// i think this is missing some elements still?
|
|
430
483
|
const discountTotal =
|
|
431
|
-
remote.discountOnTotalPrice?.discountedAmount.centAmount || 0;
|
|
484
|
+
(remote.discountOnTotalPrice?.discountedAmount.centAmount || 0) + items.reduce((sum, item) => sum + (item.price.totalDiscount.value * 100 || 0), 0);
|
|
432
485
|
const surchargeTotal = 0;
|
|
433
486
|
const currency = remote.totalPrice.currencyCode as Currency;
|
|
434
487
|
|
|
@@ -459,13 +512,24 @@ export class CommercetoolsCartProvider extends CartProvider {
|
|
|
459
512
|
},
|
|
460
513
|
} satisfies CostBreakDown;
|
|
461
514
|
|
|
462
|
-
const items = new Array<CartItem>();
|
|
463
|
-
for (const remoteItem of remote.lineItems) {
|
|
464
|
-
const item = this.parseCartItem(remoteItem);
|
|
465
515
|
|
|
466
|
-
|
|
516
|
+
const localeString = this.context.languageContext.locale || 'en'
|
|
517
|
+
const appliedPromotions = [];
|
|
518
|
+
if (remote.discountCodes) {
|
|
519
|
+
for (const promo of remote.discountCodes) {
|
|
520
|
+
appliedPromotions.push({
|
|
521
|
+
code: promo.discountCode.obj?.code || '',
|
|
522
|
+
isCouponCode: true,
|
|
523
|
+
name: promo.discountCode.obj?.name?.[localeString] || '',
|
|
524
|
+
description: promo.discountCode.obj?.description?.[localeString] || '',
|
|
525
|
+
} satisfies Promotion);
|
|
526
|
+
}
|
|
467
527
|
}
|
|
468
528
|
|
|
529
|
+
// if we want to include the nice name and description of the non-coupon promotions, we have to do some extra work to fetch the referenced promotions and include them here,
|
|
530
|
+
// as the max expand level is 3, and the information is at level 5.
|
|
531
|
+
// For now, we will just include the coupon codes, as that is the most common use case.
|
|
532
|
+
|
|
469
533
|
const cart = {
|
|
470
534
|
identifier,
|
|
471
535
|
userId: {
|
|
@@ -474,6 +538,7 @@ export class CommercetoolsCartProvider extends CartProvider {
|
|
|
474
538
|
name: remote.custom?.fields['name'] || '',
|
|
475
539
|
description: remote.custom?.fields['description'] || '',
|
|
476
540
|
price,
|
|
541
|
+
appliedPromotions,
|
|
477
542
|
items,
|
|
478
543
|
} satisfies Cart;
|
|
479
544
|
|
|
@@ -35,6 +35,10 @@ export class CommercetoolsInventoryProvider extends InventoryProvider {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
@Reactionary({
|
|
38
|
+
cache: true,
|
|
39
|
+
cacheTimeToLiveInSeconds: 300,
|
|
40
|
+
currencyDependentCaching: false,
|
|
41
|
+
localeDependentCaching: false,
|
|
38
42
|
inputSchema: InventoryQueryBySKUSchema,
|
|
39
43
|
outputSchema: InventorySchema,
|
|
40
44
|
})
|
|
@@ -45,13 +49,7 @@ export class CommercetoolsInventoryProvider extends InventoryProvider {
|
|
|
45
49
|
|
|
46
50
|
// TODO: We can't query by supplyChannel.key, so we have to resolve it first.
|
|
47
51
|
// This is probably a good candidate for internal data caching at some point.
|
|
48
|
-
const
|
|
49
|
-
.channels()
|
|
50
|
-
.withKey({ key: payload.fulfilmentCenter.key })
|
|
51
|
-
.get()
|
|
52
|
-
.execute();
|
|
53
|
-
|
|
54
|
-
const channelId = channel.body.id;
|
|
52
|
+
const channelId = await this.commercetools.resolveChannelIdByKey(payload.fulfilmentCenter.key);
|
|
55
53
|
|
|
56
54
|
const remote = await client
|
|
57
55
|
.inventory()
|
|
@@ -48,7 +48,12 @@ export class CommercetoolsPriceProvider extends PriceProvider {
|
|
|
48
48
|
payload: CustomerPriceQuery
|
|
49
49
|
): Promise<Result<Price>> {
|
|
50
50
|
const client = await this.getClient();
|
|
51
|
-
|
|
51
|
+
let priceChannelId;
|
|
52
|
+
if (this.config.customerPriceChannelKey) {
|
|
53
|
+
priceChannelId = await this.commercetools.resolveChannelIdByKey(this.config.customerPriceChannelKey);
|
|
54
|
+
} else {
|
|
55
|
+
priceChannelId = await this.commercetools.resolveChannelIdByRole('Primary');
|
|
56
|
+
}
|
|
52
57
|
|
|
53
58
|
const response = await client
|
|
54
59
|
.productProjections()
|
|
@@ -80,8 +85,12 @@ export class CommercetoolsPriceProvider extends PriceProvider {
|
|
|
80
85
|
})
|
|
81
86
|
public override async getListPrice(payload: ListPriceQuery): Promise<Result<Price>> {
|
|
82
87
|
const client = await this.getClient();
|
|
83
|
-
|
|
84
|
-
|
|
88
|
+
let priceChannelId;
|
|
89
|
+
if (this.config.listPriceChannelKey) {
|
|
90
|
+
priceChannelId = await this.commercetools.resolveChannelIdByKey(this.config.listPriceChannelKey);
|
|
91
|
+
} else {
|
|
92
|
+
priceChannelId = await this.commercetools.resolveChannelIdByRole('Primary');
|
|
93
|
+
}
|
|
85
94
|
const response = await client
|
|
86
95
|
.productProjections()
|
|
87
96
|
.get({
|
|
@@ -127,9 +136,12 @@ export class CommercetoolsPriceProvider extends PriceProvider {
|
|
|
127
136
|
currency: price.value.currencyCode as Currency,
|
|
128
137
|
} satisfies MonetaryAmount;
|
|
129
138
|
|
|
139
|
+
let isOnSale = false;
|
|
130
140
|
if (options.includeDiscounts) {
|
|
131
141
|
const discountedPrice = price.discounted?.value || price.value;
|
|
132
|
-
|
|
142
|
+
if (price.discounted) {
|
|
143
|
+
isOnSale = true;
|
|
144
|
+
}
|
|
133
145
|
unitPrice = {
|
|
134
146
|
value: discountedPrice.centAmount / 100,
|
|
135
147
|
currency: price.value.currencyCode as Currency,
|
|
@@ -146,35 +158,10 @@ export class CommercetoolsPriceProvider extends PriceProvider {
|
|
|
146
158
|
identifier,
|
|
147
159
|
tieredPrices: [],
|
|
148
160
|
unitPrice,
|
|
161
|
+
onSale: isOnSale,
|
|
149
162
|
} satisfies Price;
|
|
150
163
|
|
|
151
164
|
return result;
|
|
152
165
|
}
|
|
153
166
|
|
|
154
|
-
protected async getChannels() {
|
|
155
|
-
const adminClient = await this.commercetools.getAdminClient();
|
|
156
|
-
|
|
157
|
-
const offerPriceChannelPromise = adminClient
|
|
158
|
-
.withProjectKey({ projectKey: this.config.projectKey })
|
|
159
|
-
.channels()
|
|
160
|
-
.withKey({ key: 'Offer Price' })
|
|
161
|
-
.get()
|
|
162
|
-
.execute();
|
|
163
|
-
const listPriceChannelPromise = adminClient
|
|
164
|
-
.withProjectKey({ projectKey: this.config.projectKey })
|
|
165
|
-
.channels()
|
|
166
|
-
.withKey({ key: 'List Price' })
|
|
167
|
-
.get()
|
|
168
|
-
.execute();
|
|
169
|
-
|
|
170
|
-
const [offerChannel, listChannel] = await Promise.all([
|
|
171
|
-
offerPriceChannelPromise,
|
|
172
|
-
listPriceChannelPromise,
|
|
173
|
-
]);
|
|
174
|
-
|
|
175
|
-
return {
|
|
176
|
-
offer: offerChannel.body.id,
|
|
177
|
-
list: listChannel.body.id,
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
167
|
}
|
|
@@ -8,8 +8,12 @@ export const CommercetoolsConfigurationSchema = z.looseObject({
|
|
|
8
8
|
clientId: z.string(),
|
|
9
9
|
clientSecret: z.string(),
|
|
10
10
|
scopes: z.array(z.string()).default(() => []),
|
|
11
|
+
adminClientId: z.string().optional(),
|
|
12
|
+
adminClientSecret: z.string().optional(),
|
|
11
13
|
paymentMethods: PaymentMethodSchema.array().optional().default(() => []),
|
|
12
14
|
facetFieldsForSearch: z.array(z.string()).default(() => []),
|
|
15
|
+
listPriceChannelKey: z.string().optional(),
|
|
16
|
+
customerPriceChannelKey: z.string().optional(),
|
|
13
17
|
});
|
|
14
18
|
|
|
15
19
|
export type CommercetoolsConfiguration = z.infer<typeof CommercetoolsConfigurationSchema>;
|