@reactionary/provider-commercetools 0.0.41 → 0.0.48

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.
Files changed (34) hide show
  1. package/README.md +27 -0
  2. package/core/client.js +119 -97
  3. package/core/initialize.js +6 -1
  4. package/package.json +3 -3
  5. package/providers/cart-payment.provider.js +149 -0
  6. package/providers/cart.provider.js +292 -73
  7. package/providers/category.provider.js +30 -30
  8. package/providers/identity.provider.js +11 -57
  9. package/providers/index.js +2 -0
  10. package/providers/inventory.provider.js +25 -17
  11. package/providers/price.provider.js +49 -33
  12. package/providers/product.provider.js +39 -16
  13. package/providers/profile.provider.js +30 -0
  14. package/providers/search.provider.js +13 -9
  15. package/providers/store.provider.js +42 -0
  16. package/schema/capabilities.schema.js +3 -1
  17. package/schema/commercetools.schema.js +16 -0
  18. package/schema/configuration.schema.js +2 -1
  19. package/src/core/client.d.ts +14 -9
  20. package/src/core/initialize.d.ts +5 -3
  21. package/src/providers/cart-payment.provider.d.ts +16 -0
  22. package/src/providers/cart.provider.d.ts +34 -11
  23. package/src/providers/category.provider.d.ts +13 -13
  24. package/src/providers/identity.provider.d.ts +7 -7
  25. package/src/providers/index.d.ts +2 -0
  26. package/src/providers/inventory.provider.d.ts +8 -5
  27. package/src/providers/price.provider.d.ts +12 -8
  28. package/src/providers/product.provider.d.ts +9 -7
  29. package/src/providers/profile.provider.d.ts +14 -0
  30. package/src/providers/search.provider.d.ts +6 -5
  31. package/src/providers/store.provider.d.ts +13 -0
  32. package/src/schema/capabilities.schema.d.ts +6 -4
  33. package/src/schema/commercetools.schema.d.ts +16 -0
  34. package/src/schema/configuration.schema.d.ts +1 -0
package/README.md CHANGED
@@ -9,3 +9,30 @@ Run `nx build provider-commercetools` to build the library.
9
9
  ## Running unit tests
10
10
 
11
11
  Run `nx test provider-commercetools` to execute the unit tests via [Jest](https://jestjs.io).
12
+
13
+
14
+ # ASSUMPTIONS for backend config
15
+
16
+ - You will have 2 different channels for prices, one called `Offer Price` and one called `List Price`
17
+ - ProductVariants will all have unique SKU values.
18
+ - Your Supply Channels double as Store Locations
19
+
20
+
21
+
22
+ ## TODO List
23
+
24
+ ### Core
25
+ - [ ] Figure out if we are actually running as anonymous user towards CT. It feels weird right now.
26
+
27
+ ### Price
28
+ - [ ] PriceProvider should be able to use both embedded and standalone prices? Possible by querying through product-projection maybe?
29
+ - [ ] If not, using product-projection, the logic in https://docs.commercetools.com/api/pricing-and-discounts-overview#price-selection should be replicated
30
+ - [ ] add list price by convention. Like key: LP-<sku> or something.
31
+
32
+
33
+ ### Inventory
34
+ - [ ] Be traced and cached
35
+
36
+
37
+
38
+
package/core/client.js CHANGED
@@ -1,120 +1,141 @@
1
- import { ClientBuilder } from "@commercetools/ts-client";
1
+ import {
2
+ ClientBuilder
3
+ } from "@commercetools/ts-client";
2
4
  import { createApiBuilderFromCtpClient } from "@commercetools/platform-sdk";
3
- const ANONYMOUS_SCOPES = [
4
- "view_published_products",
5
- "manage_shopping_lists",
6
- "view_shipping_methods",
7
- "manage_customers",
8
- "view_product_selections",
9
- "view_categories",
10
- "view_project_settings",
11
- "manage_order_edits",
12
- "view_sessions",
13
- "view_standalone_prices",
14
- "manage_orders",
15
- "view_tax_categories",
16
- "view_cart_discounts",
17
- "view_discount_codes",
18
- "create_anonymous_token",
19
- "manage_sessions",
20
- "view_products",
21
- "view_types"
22
- ];
23
- const GUEST_SCOPES = [...ANONYMOUS_SCOPES];
24
- const REGISTERED_SCOPES = [...GUEST_SCOPES];
25
- class CommercetoolsClient {
26
- constructor(config) {
27
- this.config = config;
5
+ import { randomUUID } from "crypto";
6
+ import { GuestIdentitySchema, IdentitySchema } from "@reactionary/core";
7
+ import * as crypto from "crypto";
8
+ class RequestContextTokenCache {
9
+ constructor(context) {
10
+ this.context = context;
28
11
  }
29
- getClient(token) {
30
- if (token) {
31
- return this.createClientWithToken(token);
12
+ async get(tokenCacheOptions) {
13
+ const identity = this.context.identity;
14
+ if (identity.type !== "Anonymous") {
15
+ return {
16
+ refreshToken: identity.refresh_token,
17
+ token: identity.token || "",
18
+ expirationTime: identity.expiry.getTime()
19
+ };
32
20
  }
33
- return this.createAnonymousClient();
21
+ return void 0;
34
22
  }
35
- async login(username, password) {
36
- const scopes = REGISTERED_SCOPES.map(
37
- (scope) => `${scope}:${this.config.projectKey}`
38
- ).join(" ");
39
- const queryParams = new URLSearchParams({
40
- grant_type: "password",
41
- username,
42
- password,
43
- scope: scopes
44
- });
45
- const url = `${this.config.authUrl}/oauth/${this.config.projectKey}/customers/token?${queryParams.toString()}`;
46
- const headers = {
47
- Authorization: "Basic " + btoa(this.config.clientId + ":" + this.config.clientSecret)
48
- };
49
- const remote = await fetch(url, { method: "POST", headers });
50
- const json = await remote.json();
51
- return json;
23
+ async set(cache, tokenCacheOptions) {
24
+ const identity = this.context.identity;
25
+ identity.refresh_token = cache.refreshToken;
26
+ identity.token = cache.token;
27
+ identity.expiry = new Date(cache.expirationTime);
52
28
  }
53
- async guest() {
54
- const scopes = GUEST_SCOPES.map(
55
- (scope) => `${scope}:${this.config.projectKey}`
56
- ).join(" ");
57
- const queryParams = new URLSearchParams({
58
- grant_type: "client_credentials",
59
- scope: scopes
60
- });
61
- const url = `${this.config.authUrl}/oauth/${this.config.projectKey}/anonymous/token?${queryParams.toString()}`;
62
- const headers = {
63
- Authorization: "Basic " + btoa(this.config.clientId + ":" + this.config.clientSecret)
64
- };
65
- const remote = await fetch(url, { method: "POST", headers });
66
- const json = await remote.json();
67
- return json;
29
+ }
30
+ class CommercetoolsClient {
31
+ constructor(config) {
32
+ this.config = config;
68
33
  }
69
- async logout(token) {
70
- const queryParams = new URLSearchParams({
71
- token,
72
- token_type_hint: "access_token"
73
- });
74
- const url = `${this.config.authUrl}/oauth/token/revoke?${queryParams.toString()}`;
75
- const headers = {
76
- Authorization: "Basic " + btoa(this.config.clientId + ":" + this.config.clientSecret)
77
- };
78
- const remote = await fetch(url, { method: "POST", headers });
79
- return remote;
34
+ async getClient(reqCtx) {
35
+ return this.createClient(reqCtx);
80
36
  }
81
- async introspect(token) {
82
- const queryParams = new URLSearchParams({
83
- token
37
+ async register(username, password, reqCtx) {
38
+ const cache = new RequestContextTokenCache(reqCtx);
39
+ const registrationBuilder = this.createBaseClientBuilder().withAnonymousSessionFlow({
40
+ host: this.config.authUrl,
41
+ projectKey: this.config.projectKey,
42
+ credentials: {
43
+ clientId: this.config.clientId,
44
+ clientSecret: this.config.clientSecret
45
+ },
46
+ scopes: this.config.scopes,
47
+ tokenCache: cache
84
48
  });
85
- const url = `${this.config.authUrl}/oauth/introspect?` + queryParams;
86
- const headers = {
87
- Authorization: "Basic " + btoa(this.config.clientId + ":" + this.config.clientSecret)
88
- };
89
- const remote = await fetch(url, { method: "POST", headers });
90
- const json = await remote.json();
91
- return json;
49
+ const registrationClient = createApiBuilderFromCtpClient(
50
+ registrationBuilder.build()
51
+ );
52
+ const registration = await registrationClient.withProjectKey({ projectKey: this.config.projectKey }).me().signup().post({
53
+ body: {
54
+ email: username,
55
+ password
56
+ }
57
+ }).execute();
58
+ const login = await this.login(username, password, reqCtx);
92
59
  }
93
- createAnonymousClient() {
94
- const scopes = ANONYMOUS_SCOPES.map(
95
- (scope) => `${scope}:${this.config.projectKey}`
96
- ).join(" ");
97
- const builder = this.createBaseClientBuilder().withClientCredentialsFlow({
60
+ async login(username, password, reqCtx) {
61
+ const cache = new RequestContextTokenCache(reqCtx);
62
+ const identity = reqCtx.identity;
63
+ const loginBuilder = this.createBaseClientBuilder().withPasswordFlow({
98
64
  host: this.config.authUrl,
99
65
  projectKey: this.config.projectKey,
100
66
  credentials: {
101
67
  clientId: this.config.clientId,
102
- clientSecret: this.config.clientSecret
68
+ clientSecret: this.config.clientSecret,
69
+ user: { username, password }
103
70
  },
104
- scopes: [scopes]
71
+ tokenCache: cache,
72
+ scopes: this.config.scopes
105
73
  });
106
- return createApiBuilderFromCtpClient(builder.build());
74
+ const loginClient = createApiBuilderFromCtpClient(loginBuilder.build());
75
+ const login = await loginClient.withProjectKey({ projectKey: this.config.projectKey }).me().get().execute();
76
+ identity.type = "Registered";
77
+ identity.logonId = username;
78
+ identity.id = {
79
+ userId: login.body.id
80
+ };
107
81
  }
108
- createClientWithToken(token) {
109
- const builder = this.createBaseClientBuilder().withExistingTokenFlow(
110
- `Bearer ${token}`,
111
- { force: true }
112
- );
82
+ async logout(reqCtx) {
83
+ const cache = new RequestContextTokenCache(reqCtx);
84
+ await cache.set({ token: "", refreshToken: "", expirationTime: 0 });
85
+ reqCtx.identity = IdentitySchema.parse({});
86
+ }
87
+ createClient(reqCtx) {
88
+ const cache = new RequestContextTokenCache(reqCtx);
89
+ if (reqCtx.identity.type === "Anonymous") {
90
+ reqCtx.identity = GuestIdentitySchema.parse({
91
+ id: {
92
+ userId: crypto.randomUUID().toString()
93
+ },
94
+ type: "Guest"
95
+ });
96
+ }
97
+ const identity = reqCtx.identity;
98
+ let builder = this.createBaseClientBuilder();
99
+ if (!identity.token || !identity.refresh_token) {
100
+ builder = builder.withAnonymousSessionFlow({
101
+ host: this.config.authUrl,
102
+ projectKey: this.config.projectKey,
103
+ credentials: {
104
+ clientId: this.config.clientId,
105
+ clientSecret: this.config.clientSecret,
106
+ anonymousId: identity.id.userId
107
+ },
108
+ tokenCache: cache
109
+ });
110
+ } else {
111
+ builder = builder.withRefreshTokenFlow({
112
+ credentials: {
113
+ clientId: this.config.clientId,
114
+ clientSecret: this.config.clientSecret
115
+ },
116
+ host: this.config.authUrl,
117
+ projectKey: this.config.projectKey,
118
+ refreshToken: identity.refresh_token || "",
119
+ tokenCache: cache
120
+ });
121
+ }
113
122
  return createApiBuilderFromCtpClient(builder.build());
114
123
  }
115
124
  createBaseClientBuilder() {
116
125
  const builder = new ClientBuilder().withProjectKey(this.config.projectKey).withQueueMiddleware({
117
126
  concurrency: 20
127
+ }).withConcurrentModificationMiddleware({
128
+ concurrentModificationHandlerFn: (version, request) => {
129
+ console.log(
130
+ `Concurrent modification error, retry with version ${version}`
131
+ );
132
+ const body = request.body;
133
+ body["version"] = version;
134
+ return Promise.resolve(body);
135
+ }
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()}`
118
139
  }).withHttpMiddleware({
119
140
  retryConfig: {
120
141
  backoff: true,
@@ -134,5 +155,6 @@ class CommercetoolsClient {
134
155
  }
135
156
  }
136
157
  export {
137
- CommercetoolsClient
158
+ CommercetoolsClient,
159
+ RequestContextTokenCache
138
160
  };
@@ -5,7 +5,8 @@ import {
5
5
  PriceSchema,
6
6
  ProductSchema,
7
7
  SearchResultSchema,
8
- CategorySchema
8
+ CategorySchema,
9
+ CartPaymentInstructionSchema
9
10
  } from "@reactionary/core";
10
11
  import { CommercetoolsSearchProvider } from "../providers/search.provider";
11
12
  import { CommercetoolsProductProvider } from "../providers/product.provider";
@@ -14,6 +15,7 @@ import { CommercetoolsCartProvider } from "../providers/cart.provider";
14
15
  import { CommercetoolsInventoryProvider } from "../providers/inventory.provider";
15
16
  import { CommercetoolsPriceProvider } from "../providers/price.provider";
16
17
  import { CommercetoolsCategoryProvider } from "../providers/category.provider";
18
+ import { CommercetoolsCartPaymentProvider } from "../providers/cart-payment.provider";
17
19
  function withCommercetoolsCapabilities(configuration, capabilities) {
18
20
  return (cache) => {
19
21
  const client = {};
@@ -38,6 +40,9 @@ function withCommercetoolsCapabilities(configuration, capabilities) {
38
40
  if (capabilities.category) {
39
41
  client.category = new CommercetoolsCategoryProvider(configuration, CategorySchema, cache);
40
42
  }
43
+ if (capabilities.cartPayment) {
44
+ client.cartPayment = new CommercetoolsCartPaymentProvider(configuration, CartPaymentInstructionSchema, cache);
45
+ }
41
46
  return client;
42
47
  };
43
48
  }
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@reactionary/provider-commercetools",
3
- "version": "0.0.41",
3
+ "version": "0.0.48",
4
4
  "main": "index.js",
5
5
  "types": "src/index.d.ts",
6
6
  "dependencies": {
7
- "@reactionary/core": "0.0.41",
8
- "@reactionary/otel": "0.0.41",
7
+ "@reactionary/core": "0.0.48",
8
+ "@reactionary/otel": "0.0.48",
9
9
  "zod": "4.1.9",
10
10
  "@commercetools/ts-client": "^4.2.1",
11
11
  "@commercetools/platform-sdk": "^8.8.0"
@@ -0,0 +1,149 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __decorateClass = (decorators, target, key, kind) => {
4
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
5
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
6
+ if (decorator = decorators[i])
7
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
8
+ if (kind && result)
9
+ __defProp(target, key, result);
10
+ return result;
11
+ };
12
+ import { CommercetoolsClient } from "../core/client";
13
+ import { traced } from "@reactionary/otel";
14
+ import { CommercetoolsCartIdentifierSchema, CommercetoolsCartPaymentInstructionIdentifierSchema } from "../schema/commercetools.schema";
15
+ import { CartPaymentProvider, PaymentMethodIdentifierSchema } from "@reactionary/core";
16
+ class CommercetoolsCartPaymentProvider extends CartPaymentProvider {
17
+ constructor(config, schema, cache) {
18
+ super(schema, cache);
19
+ this.config = config;
20
+ }
21
+ async getClient(reqCtx) {
22
+ const client = await new CommercetoolsClient(this.config).getClient(reqCtx);
23
+ return {
24
+ payments: client.withProjectKey({ projectKey: this.config.projectKey }).me().payments(),
25
+ carts: client.withProjectKey({ projectKey: this.config.projectKey }).me().carts()
26
+ };
27
+ }
28
+ async getByCartIdentifier(payload, reqCtx) {
29
+ const client = await this.getClient(reqCtx);
30
+ const ctId = payload.cart;
31
+ const ctVersion = ctId.version || 0;
32
+ const cart = await client.carts.withId({ ID: ctId.key }).get({
33
+ queryArgs: {
34
+ expand: "paymentInfo.payments[*]"
35
+ }
36
+ }).execute();
37
+ let payments = (cart.body.paymentInfo?.payments || []).map((x) => x.obj).filter((x) => x);
38
+ if (payload.status) {
39
+ payments = payments.filter((payment) => payload.status.some((status) => payment.paymentStatus?.interfaceCode === status));
40
+ }
41
+ const parsedPayments = payments.map((payment) => this.parseSingle(payment, reqCtx));
42
+ const returnPayments = parsedPayments.map((x) => {
43
+ x.cart = { key: cart.body.id, version: cart.body.version || 0 };
44
+ return x;
45
+ });
46
+ return returnPayments;
47
+ }
48
+ async initiatePaymentForCart(payload, reqCtx) {
49
+ const client = await this.getClient(reqCtx);
50
+ const cartId = payload.cart;
51
+ const response = await client.payments.post({
52
+ body: {
53
+ amountPlanned: {
54
+ centAmount: Math.round(payload.paymentInstruction.amount.value * 100),
55
+ currencyCode: payload.paymentInstruction.amount.currency
56
+ },
57
+ paymentMethodInfo: {
58
+ method: payload.paymentInstruction.paymentMethod.method,
59
+ name: {
60
+ [reqCtx.languageContext.locale]: payload.paymentInstruction.paymentMethod.name
61
+ },
62
+ paymentInterface: payload.paymentInstruction.paymentMethod.paymentProcessor
63
+ },
64
+ custom: {
65
+ type: {
66
+ typeId: "type",
67
+ key: "reactionaryPaymentCustomFields"
68
+ },
69
+ fields: {
70
+ cartId: cartId.key,
71
+ cartVersion: cartId.version + ""
72
+ }
73
+ }
74
+ }
75
+ }).execute();
76
+ const ctId = payload.cart;
77
+ const updatedCart = await client.carts.withId({ ID: ctId.key }).post({
78
+ body: {
79
+ version: ctId.version,
80
+ actions: [
81
+ {
82
+ "action": "addPayment",
83
+ "payment": {
84
+ "typeId": "payment",
85
+ "id": response.body.id
86
+ }
87
+ }
88
+ ]
89
+ }
90
+ }).execute();
91
+ const payment = this.parseSingle(response.body, reqCtx);
92
+ payment.cart = CommercetoolsCartIdentifierSchema.parse({
93
+ key: updatedCart.body.id,
94
+ version: updatedCart.body.version || 0
95
+ });
96
+ return payment;
97
+ }
98
+ async cancelPaymentInstruction(payload, reqCtx) {
99
+ const client = await this.getClient(reqCtx);
100
+ const newestVersion = await client.payments.withId({ ID: payload.paymentInstruction.key }).get().execute();
101
+ const response = await client.payments.withId({ ID: payload.paymentInstruction.key }).post({
102
+ body: {
103
+ version: newestVersion.body.version,
104
+ actions: [
105
+ {
106
+ action: "changeAmountPlanned",
107
+ amount: {
108
+ centAmount: 0,
109
+ currencyCode: newestVersion.body.amountPlanned.currencyCode
110
+ }
111
+ }
112
+ ]
113
+ }
114
+ }).execute();
115
+ const payment = this.parseSingle(response.body, reqCtx);
116
+ payment.cart = payload.cart;
117
+ return payment;
118
+ }
119
+ parseSingle(_body, reqCtx) {
120
+ const body = _body;
121
+ const base = this.newModel();
122
+ base.identifier = CommercetoolsCartPaymentInstructionIdentifierSchema.parse({
123
+ key: body.id,
124
+ version: body.version || 0
125
+ });
126
+ base.amount = {
127
+ value: body.amountPlanned.centAmount / 100,
128
+ currency: body.amountPlanned.currencyCode
129
+ };
130
+ base.paymentMethod = PaymentMethodIdentifierSchema.parse({
131
+ key: body.paymentMethodInfo?.method
132
+ });
133
+ base.status = body.paymentStatus?.interfaceCode;
134
+ base.cart = { key: "", version: 0 };
135
+ return this.assert(base);
136
+ }
137
+ }
138
+ __decorateClass([
139
+ traced()
140
+ ], CommercetoolsCartPaymentProvider.prototype, "getByCartIdentifier", 1);
141
+ __decorateClass([
142
+ traced()
143
+ ], CommercetoolsCartPaymentProvider.prototype, "cancelPaymentInstruction", 1);
144
+ __decorateClass([
145
+ traced()
146
+ ], CommercetoolsCartPaymentProvider.prototype, "parseSingle", 1);
147
+ export {
148
+ CommercetoolsCartPaymentProvider
149
+ };