@reactionary/provider-commercetools 0.3.16 → 0.3.17

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 CHANGED
@@ -10,12 +10,14 @@ import {
10
10
  import * as crypto from "crypto";
11
11
  import createDebug from "debug";
12
12
  import { RequestContextTokenCache } from "./token-cache.js";
13
+ import { CommercetoolsSessionSchema } from "../schema/session.schema.js";
13
14
  const debug = createDebug("reactionary:commercetools");
15
+ const PROVIDER_SESSION_KEY = "COMMERCETOOLS_PROVIDER";
14
16
  class CommercetoolsAPI {
15
17
  constructor(config, context) {
16
18
  this.config = config;
17
19
  this.context = context;
18
- this.cache = new RequestContextTokenCache(this.context);
20
+ this.tokenCache = new RequestContextTokenCache(this.context, PROVIDER_SESSION_KEY);
19
21
  }
20
22
  async getClient() {
21
23
  if (!this.client) {
@@ -29,6 +31,77 @@ class CommercetoolsAPI {
29
31
  }
30
32
  return this.adminClient;
31
33
  }
34
+ getSessionData() {
35
+ return this.context.session[PROVIDER_SESSION_KEY] ? this.context.session[PROVIDER_SESSION_KEY] : CommercetoolsSessionSchema.parse({});
36
+ }
37
+ setSessionData(sessionData) {
38
+ const existingData = this.context.session[PROVIDER_SESSION_KEY];
39
+ this.context.session[PROVIDER_SESSION_KEY] = {
40
+ ...existingData,
41
+ ...sessionData
42
+ };
43
+ }
44
+ /**
45
+ * Only caches it pr session for now...... but still better than every call
46
+ * @param key
47
+ * @returns
48
+ */
49
+ async resolveChannelIdByKey(key) {
50
+ const sessionData = this.getSessionData();
51
+ const cacheKey = `___channel_${key}`;
52
+ const cachedValue = sessionData[cacheKey];
53
+ if (cachedValue) {
54
+ if (debug.enabled) {
55
+ debug(`Resolved channel ${key} from cache`);
56
+ }
57
+ return cachedValue + "";
58
+ }
59
+ const client = await this.getAdminClient();
60
+ const response = await client.withProjectKey({ projectKey: this.config.projectKey }).channels().withKey({ key }).get().execute();
61
+ const channel = response.body;
62
+ this.setSessionData({
63
+ cacheKey: channel.id
64
+ });
65
+ if (debug.enabled) {
66
+ debug(`Resolved channel ${key} from API and cached it`);
67
+ }
68
+ return channel.id;
69
+ }
70
+ /**
71
+ * Only caches it pr session for now...... but still better than every call
72
+ * @param key
73
+ * @returns
74
+ */
75
+ async resolveChannelIdByRole(role) {
76
+ const sessionData = this.getSessionData();
77
+ const cacheKey = `___channel_role_${role}`;
78
+ const cachedValue = sessionData[cacheKey];
79
+ if (cachedValue) {
80
+ if (debug.enabled) {
81
+ debug(`Resolved channel ${role} from cache`);
82
+ }
83
+ return cachedValue + "";
84
+ }
85
+ const client = await this.getAdminClient();
86
+ const response = await client.withProjectKey({ projectKey: this.config.projectKey }).channels().get({
87
+ queryArgs: {
88
+ where: `roles contains any (:role)`,
89
+ "var.role": role
90
+ }
91
+ }).execute();
92
+ const channels = response.body;
93
+ if (channels.results.length === 0) {
94
+ throw new Error(`No channel found with role ${role}`);
95
+ }
96
+ const channel = channels.results[0];
97
+ this.setSessionData({
98
+ [cacheKey]: channel.id
99
+ });
100
+ if (debug.enabled) {
101
+ debug(`Resolved channel ${role} from API and cached it`);
102
+ }
103
+ return channel.id;
104
+ }
32
105
  async createAdminClient() {
33
106
  let builder = this.createBaseClientBuilder();
34
107
  builder = builder.withAnonymousSessionFlow({
@@ -42,11 +115,11 @@ class CommercetoolsAPI {
42
115
  return createApiBuilderFromCtpClient(builder.build());
43
116
  }
44
117
  async createClient() {
45
- let session = await this.cache.get();
118
+ let session = await this.tokenCache.get();
46
119
  const isNewSession = !session || !session.refreshToken;
47
120
  if (isNewSession) {
48
121
  await this.becomeGuest();
49
- session = await this.cache.get();
122
+ session = await this.tokenCache.get();
50
123
  }
51
124
  let builder = this.createBaseClientBuilder();
52
125
  builder = builder.withRefreshTokenFlow({
@@ -57,7 +130,7 @@ class CommercetoolsAPI {
57
130
  host: this.config.authUrl,
58
131
  projectKey: this.config.projectKey,
59
132
  refreshToken: session?.refreshToken || "",
60
- tokenCache: this.cache
133
+ tokenCache: this.tokenCache
61
134
  });
62
135
  return createApiBuilderFromCtpClient(builder.build());
63
136
  }
@@ -92,7 +165,7 @@ class CommercetoolsAPI {
92
165
  clientSecret: this.config.clientSecret,
93
166
  user: { username, password }
94
167
  },
95
- tokenCache: this.cache,
168
+ tokenCache: this.tokenCache,
96
169
  scopes: this.config.scopes
97
170
  });
98
171
  const loginClient = createApiBuilderFromCtpClient(loginBuilder.build());
@@ -111,7 +184,7 @@ class CommercetoolsAPI {
111
184
  });
112
185
  }
113
186
  async logout() {
114
- await this.cache.set({ token: "", refreshToken: "", expirationTime: 0 });
187
+ await this.tokenCache.set({ token: "", refreshToken: "", expirationTime: 0 });
115
188
  const identity = {
116
189
  type: "Anonymous"
117
190
  };
@@ -119,7 +192,7 @@ class CommercetoolsAPI {
119
192
  }
120
193
  // FIXME: This can fail if the short-lived access token has expired. In other words, probably missing a token refresh.
121
194
  async introspect() {
122
- const session = await this.cache.get();
195
+ const session = await this.tokenCache.get();
123
196
  if (!session || !session.token) {
124
197
  const identity = {
125
198
  type: "Anonymous"
@@ -191,7 +264,7 @@ class CommercetoolsAPI {
191
264
  }
192
265
  );
193
266
  const result = await response.json();
194
- this.cache.set({
267
+ this.tokenCache.set({
195
268
  expirationTime: Date.now() + Number(result.expires_in) * 1e3 - 5 * 60 * 1e3,
196
269
  token: result.access_token,
197
270
  refreshToken: result.refresh_token
@@ -235,5 +308,6 @@ class CommercetoolsAPI {
235
308
  }
236
309
  }
237
310
  export {
238
- CommercetoolsAPI
311
+ CommercetoolsAPI,
312
+ PROVIDER_SESSION_KEY
239
313
  };
@@ -1,12 +1,12 @@
1
1
  import { CommercetoolsSessionSchema } from "../schema/session.schema.js";
2
- const PROVIDER_COMMERCETOOLS_SESSION_KEY = "PROVIDER_COMMERCETOOLS";
3
2
  class RequestContextTokenCache {
4
- constructor(context) {
3
+ constructor(context, sessionProviderKey) {
5
4
  this.context = context;
5
+ this.sessionProviderKey = sessionProviderKey;
6
6
  }
7
7
  async get(tokenCacheOptions) {
8
8
  const session = CommercetoolsSessionSchema.parse(
9
- this.context.session[PROVIDER_COMMERCETOOLS_SESSION_KEY] || {}
9
+ this.context.session[this.sessionProviderKey] || {}
10
10
  );
11
11
  if (!session) {
12
12
  return {
@@ -23,15 +23,14 @@ class RequestContextTokenCache {
23
23
  }
24
24
  async set(cache, tokenCacheOptions) {
25
25
  const session = CommercetoolsSessionSchema.parse(
26
- this.context.session[PROVIDER_COMMERCETOOLS_SESSION_KEY] || {}
26
+ this.context.session[this.sessionProviderKey] || {}
27
27
  );
28
- this.context.session[PROVIDER_COMMERCETOOLS_SESSION_KEY] = session;
28
+ this.context.session[this.sessionProviderKey] = session;
29
29
  session.refreshToken = cache.refreshToken;
30
30
  session.token = cache.token;
31
31
  session.expirationTime = cache.expirationTime;
32
32
  }
33
33
  }
34
34
  export {
35
- PROVIDER_COMMERCETOOLS_SESSION_KEY,
36
35
  RequestContextTokenCache
37
36
  };
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@reactionary/provider-commercetools",
3
- "version": "0.3.16",
3
+ "version": "0.3.17",
4
4
  "main": "index.js",
5
5
  "types": "src/index.d.ts",
6
6
  "dependencies": {
7
- "@reactionary/core": "0.3.16",
7
+ "@reactionary/core": "0.3.17",
8
8
  "debug": "^4.4.3",
9
9
  "zod": "4.1.9",
10
10
  "@commercetools/ts-client": "^4.2.1",
@@ -23,8 +23,7 @@ class CommercetoolsInventoryProvider extends InventoryProvider {
23
23
  async getBySKU(payload) {
24
24
  const client = await this.getClient();
25
25
  try {
26
- const channel = await client.channels().withKey({ key: payload.fulfilmentCenter.key }).get().execute();
27
- const channelId = channel.body.id;
26
+ const channelId = await this.commercetools.resolveChannelIdByKey(payload.fulfilmentCenter.key);
28
27
  const remote = await client.inventory().get({
29
28
  queryArgs: {
30
29
  where: "sku=:sku AND supplyChannel(id=:channel)",
@@ -66,6 +65,10 @@ class CommercetoolsInventoryProvider extends InventoryProvider {
66
65
  }
67
66
  __decorateClass([
68
67
  Reactionary({
68
+ cache: true,
69
+ cacheTimeToLiveInSeconds: 300,
70
+ currencyDependentCaching: false,
71
+ localeDependentCaching: false,
69
72
  inputSchema: InventoryQueryBySKUSchema,
70
73
  outputSchema: InventorySchema
71
74
  })
@@ -25,7 +25,12 @@ class CommercetoolsPriceProvider extends PriceProvider {
25
25
  }
26
26
  async getCustomerPrice(payload) {
27
27
  const client = await this.getClient();
28
- const priceChannelId = "ee6e75e9-c9ab-4e2f-85f1-d8c734d0cb86";
28
+ let priceChannelId;
29
+ if (this.config.customerPriceChannelKey) {
30
+ priceChannelId = await this.commercetools.resolveChannelIdByKey(this.config.customerPriceChannelKey);
31
+ } else {
32
+ priceChannelId = await this.commercetools.resolveChannelIdByRole("Primary");
33
+ }
29
34
  const response = await client.productProjections().get({
30
35
  queryArgs: {
31
36
  staged: false,
@@ -46,7 +51,12 @@ class CommercetoolsPriceProvider extends PriceProvider {
46
51
  }
47
52
  async getListPrice(payload) {
48
53
  const client = await this.getClient();
49
- const priceChannelId = "ee6e75e9-c9ab-4e2f-85f1-d8c734d0cb86";
54
+ let priceChannelId;
55
+ if (this.config.listPriceChannelKey) {
56
+ priceChannelId = await this.commercetools.resolveChannelIdByKey(this.config.listPriceChannelKey);
57
+ } else {
58
+ priceChannelId = await this.commercetools.resolveChannelIdByRole("Primary");
59
+ }
50
60
  const response = await client.productProjections().get({
51
61
  queryArgs: {
52
62
  staged: false,
@@ -79,8 +89,12 @@ class CommercetoolsPriceProvider extends PriceProvider {
79
89
  value: price.value.centAmount / 100,
80
90
  currency: price.value.currencyCode
81
91
  };
92
+ let isOnSale = false;
82
93
  if (options.includeDiscounts) {
83
94
  const discountedPrice = price.discounted?.value || price.value;
95
+ if (price.discounted) {
96
+ isOnSale = true;
97
+ }
84
98
  unitPrice = {
85
99
  value: discountedPrice.centAmount / 100,
86
100
  currency: price.value.currencyCode
@@ -94,23 +108,11 @@ class CommercetoolsPriceProvider extends PriceProvider {
94
108
  const result = {
95
109
  identifier,
96
110
  tieredPrices: [],
97
- unitPrice
111
+ unitPrice,
112
+ onSale: isOnSale
98
113
  };
99
114
  return result;
100
115
  }
101
- async getChannels() {
102
- const adminClient = await this.commercetools.getAdminClient();
103
- const offerPriceChannelPromise = adminClient.withProjectKey({ projectKey: this.config.projectKey }).channels().withKey({ key: "Offer Price" }).get().execute();
104
- const listPriceChannelPromise = adminClient.withProjectKey({ projectKey: this.config.projectKey }).channels().withKey({ key: "List Price" }).get().execute();
105
- const [offerChannel, listChannel] = await Promise.all([
106
- offerPriceChannelPromise,
107
- listPriceChannelPromise
108
- ]);
109
- return {
110
- offer: offerChannel.body.id,
111
- list: listChannel.body.id
112
- };
113
- }
114
116
  }
115
117
  __decorateClass([
116
118
  Reactionary({
@@ -7,8 +7,12 @@ const CommercetoolsConfigurationSchema = z.looseObject({
7
7
  clientId: z.string(),
8
8
  clientSecret: z.string(),
9
9
  scopes: z.array(z.string()).default(() => []),
10
+ adminClientId: z.string().optional(),
11
+ adminClientSecret: z.string().optional(),
10
12
  paymentMethods: PaymentMethodSchema.array().optional().default(() => []),
11
- facetFieldsForSearch: z.array(z.string()).default(() => [])
13
+ facetFieldsForSearch: z.array(z.string()).default(() => []),
14
+ listPriceChannelKey: z.string().optional(),
15
+ customerPriceChannelKey: z.string().optional()
12
16
  });
13
17
  export {
14
18
  CommercetoolsConfigurationSchema
@@ -3,15 +3,31 @@ import { type ApiRoot } from '@commercetools/platform-sdk';
3
3
  import type { CommercetoolsConfiguration } from '../schema/configuration.schema.js';
4
4
  import { type AnonymousIdentity, type GuestIdentity, type RegisteredIdentity, type RequestContext } from '@reactionary/core';
5
5
  import { RequestContextTokenCache } from './token-cache.js';
6
+ import { type CommercetoolsSession } from '../schema/session.schema.js';
7
+ export declare const PROVIDER_SESSION_KEY = "COMMERCETOOLS_PROVIDER";
6
8
  export declare class CommercetoolsAPI {
7
9
  protected config: CommercetoolsConfiguration;
8
10
  protected context: RequestContext;
9
- protected cache: RequestContextTokenCache;
11
+ protected tokenCache: RequestContextTokenCache;
10
12
  protected client: Promise<ApiRoot> | undefined;
11
13
  protected adminClient: Promise<ApiRoot> | undefined;
12
14
  constructor(config: CommercetoolsConfiguration, context: RequestContext);
13
15
  getClient(): Promise<ApiRoot>;
14
16
  getAdminClient(): Promise<ApiRoot>;
17
+ getSessionData(): CommercetoolsSession;
18
+ setSessionData(sessionData: Partial<CommercetoolsSession>): void;
19
+ /**
20
+ * Only caches it pr session for now...... but still better than every call
21
+ * @param key
22
+ * @returns
23
+ */
24
+ resolveChannelIdByKey(key: string): Promise<string>;
25
+ /**
26
+ * Only caches it pr session for now...... but still better than every call
27
+ * @param key
28
+ * @returns
29
+ */
30
+ resolveChannelIdByRole(role: string): Promise<string>;
15
31
  protected createAdminClient(): Promise<ApiRoot>;
16
32
  protected createClient(): Promise<ApiRoot>;
17
33
  register(username: string, password: string): Promise<{
@@ -1,9 +1,9 @@
1
1
  import type { TokenCache, TokenCacheOptions, TokenStore } from "@commercetools/ts-client";
2
2
  import type { RequestContext } from "@reactionary/core";
3
- export declare const PROVIDER_COMMERCETOOLS_SESSION_KEY = "PROVIDER_COMMERCETOOLS";
4
3
  export declare class RequestContextTokenCache implements TokenCache {
5
4
  protected context: RequestContext;
6
- constructor(context: RequestContext);
5
+ protected sessionProviderKey: string;
6
+ constructor(context: RequestContext, sessionProviderKey: string);
7
7
  get(tokenCacheOptions?: TokenCacheOptions): Promise<TokenStore | undefined>;
8
8
  set(cache: TokenStore, tokenCacheOptions?: TokenCacheOptions): Promise<void>;
9
9
  }
@@ -12,8 +12,4 @@ export declare class CommercetoolsPriceProvider extends PriceProvider {
12
12
  protected parseSingle(_body: unknown, options?: {
13
13
  includeDiscounts: boolean;
14
14
  }): Price;
15
- protected getChannels(): Promise<{
16
- offer: string;
17
- list: string;
18
- }>;
19
15
  }
@@ -6,6 +6,8 @@ export declare const CommercetoolsConfigurationSchema: z.ZodObject<{
6
6
  clientId: z.ZodString;
7
7
  clientSecret: z.ZodString;
8
8
  scopes: z.ZodDefault<z.ZodArray<z.ZodString>>;
9
+ adminClientId: z.ZodOptional<z.ZodString>;
10
+ adminClientSecret: z.ZodOptional<z.ZodString>;
9
11
  paymentMethods: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
10
12
  identifier: z.ZodObject<{
11
13
  method: z.ZodString;
@@ -22,5 +24,7 @@ export declare const CommercetoolsConfigurationSchema: z.ZodObject<{
22
24
  isPunchOut: z.ZodBoolean;
23
25
  }, z.core.$loose>>>>;
24
26
  facetFieldsForSearch: z.ZodDefault<z.ZodArray<z.ZodString>>;
27
+ listPriceChannelKey: z.ZodOptional<z.ZodString>;
28
+ customerPriceChannelKey: z.ZodOptional<z.ZodString>;
25
29
  }, z.core.$loose>;
26
30
  export type CommercetoolsConfiguration = z.infer<typeof CommercetoolsConfigurationSchema>;
@@ -4,3 +4,4 @@ export declare const CommercetoolsSessionSchema: z.ZodObject<{
4
4
  refreshToken: z.ZodOptional<z.ZodString>;
5
5
  expirationTime: z.ZodDefault<z.ZodNumber>;
6
6
  }, z.core.$loose>;
7
+ export type CommercetoolsSession = z.infer<typeof CommercetoolsSessionSchema>;