@reactionary/source 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 (125) hide show
  1. package/.claude/settings.local.json +28 -0
  2. package/.env-template +8 -5
  3. package/.vscode/settings.json +5 -0
  4. package/README.md +41 -0
  5. package/core/package.json +3 -1
  6. package/core/src/cache/cache.interface.ts +14 -18
  7. package/core/src/cache/memory-cache.ts +56 -0
  8. package/core/src/cache/noop-cache.ts +5 -23
  9. package/core/src/cache/redis-cache.ts +28 -38
  10. package/core/src/client/client-builder.ts +3 -3
  11. package/core/src/client/client.ts +11 -9
  12. package/core/src/decorators/reactionary.decorator.ts +80 -8
  13. package/core/src/index.ts +5 -29
  14. package/core/src/initialization.ts +43 -0
  15. package/core/src/providers/analytics.provider.ts +1 -1
  16. package/core/src/providers/base.provider.ts +61 -25
  17. package/core/src/providers/cart-payment.provider.ts +57 -0
  18. package/core/src/providers/cart.provider.ts +131 -8
  19. package/core/src/providers/category.provider.ts +9 -9
  20. package/core/src/providers/identity.provider.ts +8 -7
  21. package/core/src/providers/index.ts +12 -0
  22. package/core/src/providers/inventory.provider.ts +4 -4
  23. package/core/src/providers/price.provider.ts +7 -7
  24. package/core/src/providers/product.provider.ts +17 -5
  25. package/core/src/providers/profile.provider.ts +22 -0
  26. package/core/src/providers/search.provider.ts +4 -4
  27. package/core/src/providers/store.provider.ts +14 -0
  28. package/core/src/schemas/capabilities.schema.ts +3 -1
  29. package/core/src/schemas/models/analytics.model.ts +1 -1
  30. package/core/src/schemas/models/cart.model.ts +16 -3
  31. package/core/src/schemas/models/identifiers.model.ts +90 -22
  32. package/core/src/schemas/models/identity.model.ts +23 -7
  33. package/core/src/schemas/models/index.ts +15 -0
  34. package/core/src/schemas/models/payment.model.ts +41 -0
  35. package/core/src/schemas/models/profile.model.ts +35 -0
  36. package/core/src/schemas/models/shipping-method.model.ts +14 -0
  37. package/core/src/schemas/models/store.model.ts +11 -0
  38. package/core/src/schemas/mutations/cart-payment.mutation.ts +21 -0
  39. package/core/src/schemas/mutations/cart.mutation.ts +62 -3
  40. package/core/src/schemas/mutations/identity.mutation.ts +8 -1
  41. package/core/src/schemas/mutations/index.ts +10 -0
  42. package/core/src/schemas/mutations/profile.mutation.ts +9 -0
  43. package/core/src/schemas/queries/cart-payment.query.ts +12 -0
  44. package/core/src/schemas/queries/cart.query.ts +1 -1
  45. package/core/src/schemas/queries/identity.query.ts +1 -1
  46. package/core/src/schemas/queries/index.ts +3 -0
  47. package/core/src/schemas/queries/inventory.query.ts +4 -12
  48. package/core/src/schemas/queries/price.query.ts +1 -1
  49. package/core/src/schemas/queries/profile.query.ts +7 -0
  50. package/core/src/schemas/queries/search.query.ts +1 -1
  51. package/core/src/schemas/queries/store.query.ts +11 -0
  52. package/core/src/schemas/session.schema.ts +31 -6
  53. package/eslint.config.mjs +7 -0
  54. package/examples/next/src/app/page.tsx +4 -12
  55. package/examples/node/package.json +1 -3
  56. package/examples/node/src/basic/basic-node-provider-model-extension.spec.ts +9 -8
  57. package/examples/node/src/basic/basic-node-provider-query-extension.spec.ts +4 -3
  58. package/examples/node/src/basic/basic-node-setup.spec.ts +4 -5
  59. package/nx.json +1 -0
  60. package/otel/src/metrics.ts +2 -1
  61. package/otel/src/provider-instrumentation.ts +2 -1
  62. package/otel/src/tracer.ts +7 -6
  63. package/otel/src/trpc-middleware.ts +3 -2
  64. package/package.json +2 -1
  65. package/providers/algolia/src/core/initialize.ts +4 -3
  66. package/providers/algolia/src/providers/product.provider.ts +15 -13
  67. package/providers/algolia/src/providers/search.provider.ts +9 -9
  68. package/providers/algolia/src/schema/capabilities.schema.ts +1 -1
  69. package/providers/algolia/src/test/search.provider.spec.ts +10 -10
  70. package/providers/algolia/src/test/test-utils.ts +9 -4
  71. package/providers/commercetools/README.md +27 -0
  72. package/providers/commercetools/src/core/client.ts +164 -117
  73. package/providers/commercetools/src/core/initialize.ts +24 -14
  74. package/providers/commercetools/src/providers/cart-payment.provider.ts +193 -0
  75. package/providers/commercetools/src/providers/cart.provider.ts +402 -125
  76. package/providers/commercetools/src/providers/category.provider.ts +35 -35
  77. package/providers/commercetools/src/providers/identity.provider.ts +23 -75
  78. package/providers/commercetools/src/providers/index.ts +2 -0
  79. package/providers/commercetools/src/providers/inventory.provider.ts +69 -40
  80. package/providers/commercetools/src/providers/price.provider.ts +79 -47
  81. package/providers/commercetools/src/providers/product.provider.ts +36 -30
  82. package/providers/commercetools/src/providers/profile.provider.ts +61 -0
  83. package/providers/commercetools/src/providers/search.provider.ts +16 -12
  84. package/providers/commercetools/src/providers/store.provider.ts +78 -0
  85. package/providers/commercetools/src/schema/capabilities.schema.ts +3 -1
  86. package/providers/commercetools/src/schema/commercetools.schema.ts +18 -0
  87. package/providers/commercetools/src/schema/configuration.schema.ts +2 -1
  88. package/providers/commercetools/src/test/cart-payment.provider.spec.ts +145 -0
  89. package/providers/commercetools/src/test/cart.provider.spec.ts +82 -22
  90. package/providers/commercetools/src/test/category.provider.spec.ts +18 -17
  91. package/providers/commercetools/src/test/identity.provider.spec.ts +88 -0
  92. package/providers/commercetools/src/test/inventory.provider.spec.ts +41 -0
  93. package/providers/commercetools/src/test/price.provider.spec.ts +9 -8
  94. package/providers/commercetools/src/test/product.provider.spec.ts +33 -5
  95. package/providers/commercetools/src/test/profile.provider.spec.ts +49 -0
  96. package/providers/commercetools/src/test/search.provider.spec.ts +8 -7
  97. package/providers/commercetools/src/test/store.provider.spec.ts +37 -0
  98. package/providers/commercetools/src/test/test-utils.ts +7 -31
  99. package/providers/fake/src/core/initialize.ts +96 -38
  100. package/providers/fake/src/providers/analytics.provider.ts +6 -5
  101. package/providers/fake/src/providers/cart.provider.ts +66 -19
  102. package/providers/fake/src/providers/category.provider.ts +12 -12
  103. package/providers/fake/src/providers/identity.provider.ts +22 -14
  104. package/providers/fake/src/providers/index.ts +1 -0
  105. package/providers/fake/src/providers/inventory.provider.ts +13 -13
  106. package/providers/fake/src/providers/price.provider.ts +13 -13
  107. package/providers/fake/src/providers/product.provider.ts +13 -10
  108. package/providers/fake/src/providers/search.provider.ts +7 -5
  109. package/providers/fake/src/providers/store.provider.ts +47 -0
  110. package/providers/fake/src/schema/capabilities.schema.ts +4 -1
  111. package/providers/fake/src/test/cart.provider.spec.ts +18 -18
  112. package/providers/fake/src/test/category.provider.spec.ts +55 -37
  113. package/providers/fake/src/test/price.provider.spec.ts +9 -14
  114. package/providers/fake/src/test/product.provider.spec.ts +27 -0
  115. package/providers/fake/src/test/test-utils.ts +2 -28
  116. package/providers/posthog/src/core/initialize.ts +3 -3
  117. package/providers/posthog/src/schema/capabilities.schema.ts +1 -1
  118. package/trpc/src/client.ts +42 -41
  119. package/trpc/src/index.ts +4 -3
  120. package/trpc/src/integration.spec.ts +11 -11
  121. package/trpc/src/server.ts +26 -24
  122. package/trpc/src/test-utils.ts +9 -4
  123. package/trpc/src/types.ts +24 -22
  124. package/core/src/cache/cache-evaluation.interface.ts +0 -19
  125. package/examples/node/src/test-utils.ts +0 -26
@@ -1,147 +1,176 @@
1
- import { ClientBuilder } from '@commercetools/ts-client';
1
+ import {
2
+ ClientBuilder,
3
+ type TokenCache,
4
+ type TokenCacheOptions,
5
+ type TokenStore,
6
+ } from '@commercetools/ts-client';
2
7
  import { createApiBuilderFromCtpClient } from '@commercetools/platform-sdk';
3
- import { CommercetoolsConfiguration } from '../schema/configuration.schema';
4
-
5
- const ANONYMOUS_SCOPES = [
6
- 'view_published_products',
7
- 'manage_shopping_lists',
8
- 'view_shipping_methods',
9
- 'manage_customers',
10
- 'view_product_selections',
11
- 'view_categories',
12
- 'view_project_settings',
13
- 'manage_order_edits',
14
- 'view_sessions',
15
- 'view_standalone_prices',
16
- 'manage_orders',
17
- 'view_tax_categories',
18
- 'view_cart_discounts',
19
- 'view_discount_codes',
20
- 'create_anonymous_token',
21
- 'manage_sessions',
22
- 'view_products',
23
- 'view_types',
24
- ];
25
- const GUEST_SCOPES = [...ANONYMOUS_SCOPES];
26
- const REGISTERED_SCOPES = [...GUEST_SCOPES];
27
-
28
- export class CommercetoolsClient {
29
- protected config: CommercetoolsConfiguration;
30
-
31
- constructor(config: CommercetoolsConfiguration) {
32
- this.config = config;
33
- }
34
-
35
- public getClient(token?: string) {
36
- if (token) {
37
- return this.createClientWithToken(token);
8
+ import type { CommercetoolsConfiguration } from '../schema/configuration.schema';
9
+ import { randomUUID } from 'crypto';
10
+ import { GuestIdentitySchema, IdentitySchema, type RequestContext } from '@reactionary/core';
11
+ import * as crypto from 'crypto';
12
+
13
+ export class RequestContextTokenCache implements TokenCache {
14
+ constructor(protected context: RequestContext) {}
15
+
16
+ public async get(
17
+ tokenCacheOptions?: TokenCacheOptions
18
+ ): Promise<TokenStore | undefined> {
19
+ const identity = this.context.identity;
20
+
21
+ if (identity.type !== 'Anonymous') {
22
+ return {
23
+ refreshToken: identity.refresh_token,
24
+ token: identity.token || '',
25
+ expirationTime: identity.expiry.getTime(),
26
+ };
38
27
  }
39
28
 
40
- return this.createAnonymousClient();
29
+ return undefined;
41
30
  }
42
31
 
43
- public async login(username: string, password: string) {
44
- const scopes = REGISTERED_SCOPES.map(
45
- (scope) => `${scope}:${this.config.projectKey}`
46
- ).join(' ');
47
- const queryParams = new URLSearchParams({
48
- grant_type: 'password',
49
- username: username,
50
- password: password,
51
- scope: scopes,
52
- });
53
- const url = `${this.config.authUrl}/oauth/${
54
- this.config.projectKey
55
- }/customers/token?${queryParams.toString()}`;
56
- const headers = {
57
- Authorization:
58
- 'Basic ' + btoa(this.config.clientId + ':' + this.config.clientSecret),
59
- };
60
-
61
- const remote = await fetch(url, { method: 'POST', headers });
62
- const json = await remote.json();
32
+ public async set(
33
+ cache: TokenStore,
34
+ tokenCacheOptions?: TokenCacheOptions
35
+ ): Promise<void> {
36
+ const identity = this.context.identity;
63
37
 
64
- return json;
38
+ identity.refresh_token = cache.refreshToken;
39
+ identity.token = cache.token;
40
+ identity.expiry = new Date(cache.expirationTime);
65
41
  }
42
+ }
66
43
 
67
- public async guest() {
68
- const scopes = GUEST_SCOPES.map(
69
- (scope) => `${scope}:${this.config.projectKey}`
70
- ).join(' ');
71
- const queryParams = new URLSearchParams({
72
- grant_type: 'client_credentials',
73
- scope: scopes,
74
- });
75
- const url = `${this.config.authUrl}/oauth/${
76
- this.config.projectKey
77
- }/anonymous/token?${queryParams.toString()}`;
78
- const headers = {
79
- Authorization:
80
- 'Basic ' + btoa(this.config.clientId + ':' + this.config.clientSecret),
81
- };
82
-
83
- const remote = await fetch(url, { method: 'POST', headers });
84
- const json = await remote.json();
44
+ export class CommercetoolsClient {
45
+ protected config: CommercetoolsConfiguration;
85
46
 
86
- return json;
47
+ constructor(config: CommercetoolsConfiguration) {
48
+ this.config = config;
87
49
  }
88
50
 
89
- public async logout(token: string) {
90
- const queryParams = new URLSearchParams({
91
- token: token,
92
- token_type_hint: 'access_token',
93
- });
94
- const url = `${
95
- this.config.authUrl
96
- }/oauth/token/revoke?${queryParams.toString()}`;
97
- const headers = {
98
- Authorization:
99
- 'Basic ' + btoa(this.config.clientId + ':' + this.config.clientSecret),
100
- };
101
-
102
- const remote = await fetch(url, { method: 'POST', headers });
103
-
104
- return remote;
51
+ public async getClient(reqCtx: RequestContext) {
52
+ return this.createClient(reqCtx);
105
53
  }
106
54
 
107
- public async introspect(token: string) {
108
- const queryParams = new URLSearchParams({
109
- token,
110
- });
111
- const url = `${this.config.authUrl}/oauth/introspect?` + queryParams;
112
- const headers = {
113
- Authorization:
114
- 'Basic ' + btoa(this.config.clientId + ':' + this.config.clientSecret),
115
- };
55
+ public async register(
56
+ username: string,
57
+ password: string,
58
+ reqCtx: RequestContext
59
+ ) {
60
+ const cache = new RequestContextTokenCache(reqCtx);
61
+
62
+ const registrationBuilder =
63
+ this.createBaseClientBuilder().withAnonymousSessionFlow({
64
+ host: this.config.authUrl,
65
+ projectKey: this.config.projectKey,
66
+ credentials: {
67
+ clientId: this.config.clientId,
68
+ clientSecret: this.config.clientSecret,
69
+ },
70
+ scopes: this.config.scopes,
71
+ tokenCache: cache,
72
+ });
116
73
 
117
- const remote = await fetch(url, { method: 'POST', headers });
118
- const json = await remote.json();
74
+ const registrationClient = createApiBuilderFromCtpClient(
75
+ registrationBuilder.build()
76
+ );
77
+ const registration = await registrationClient
78
+ .withProjectKey({ projectKey: this.config.projectKey })
79
+ .me()
80
+ .signup()
81
+ .post({
82
+ body: {
83
+ email: username,
84
+ password: password,
85
+ },
86
+ })
87
+ .execute();
119
88
 
120
- return json;
89
+ const login = await this.login(username, password, reqCtx);
121
90
  }
122
91
 
123
- public createAnonymousClient() {
124
- const scopes = ANONYMOUS_SCOPES.map(
125
- (scope) => `${scope}:${this.config.projectKey}`
126
- ).join(' ');
127
- const builder = this.createBaseClientBuilder().withClientCredentialsFlow({
92
+ public async login(
93
+ username: string,
94
+ password: string,
95
+ reqCtx: RequestContext
96
+ ) {
97
+ const cache = new RequestContextTokenCache(reqCtx);
98
+ const identity = reqCtx.identity;
99
+
100
+ const loginBuilder = this.createBaseClientBuilder().withPasswordFlow({
128
101
  host: this.config.authUrl,
129
102
  projectKey: this.config.projectKey,
130
103
  credentials: {
131
104
  clientId: this.config.clientId,
132
105
  clientSecret: this.config.clientSecret,
106
+ user: { username, password },
133
107
  },
134
- scopes: [scopes],
108
+ tokenCache: cache,
109
+ scopes: this.config.scopes,
135
110
  });
136
111
 
137
- return createApiBuilderFromCtpClient(builder.build());
112
+ const loginClient = createApiBuilderFromCtpClient(loginBuilder.build());
113
+
114
+ const login = await loginClient
115
+ .withProjectKey({ projectKey: this.config.projectKey })
116
+ .me()
117
+ .get()
118
+ .execute();
119
+
120
+ identity.type = 'Registered';
121
+ identity.logonId = username;
122
+ identity.id = {
123
+ userId: login.body.id
124
+ };
138
125
  }
139
126
 
140
- protected createClientWithToken(token: string) {
141
- const builder = this.createBaseClientBuilder().withExistingTokenFlow(
142
- `Bearer ${token}`,
143
- { force: true }
144
- );
127
+ public async logout(reqCtx: RequestContext) {
128
+ const cache = new RequestContextTokenCache(reqCtx);
129
+ await cache.set({ token: '', refreshToken: '', expirationTime: 0 });
130
+
131
+ reqCtx.identity = IdentitySchema.parse({});
132
+
133
+ // TODO: We could do token revocation here, if we wanted to. The above simply whacks the session.
134
+ }
135
+
136
+ protected createClient(reqCtx: RequestContext) {
137
+ const cache = new RequestContextTokenCache(reqCtx);
138
+
139
+ if (reqCtx.identity.type === 'Anonymous') {
140
+ reqCtx.identity = GuestIdentitySchema.parse({
141
+ id: {
142
+ userId: crypto.randomUUID().toString(),
143
+ },
144
+ type: 'Guest'
145
+ });
146
+ }
147
+
148
+ const identity = reqCtx.identity;
149
+ let builder = this.createBaseClientBuilder();
150
+
151
+ if (!identity.token || !identity.refresh_token) {
152
+ builder = builder.withAnonymousSessionFlow({
153
+ host: this.config.authUrl,
154
+ projectKey: this.config.projectKey,
155
+ credentials: {
156
+ clientId: this.config.clientId,
157
+ clientSecret: this.config.clientSecret,
158
+ anonymousId: identity.id.userId,
159
+ },
160
+ tokenCache: cache,
161
+ });
162
+ } else {
163
+ builder = builder.withRefreshTokenFlow({
164
+ credentials: {
165
+ clientId: this.config.clientId,
166
+ clientSecret: this.config.clientSecret,
167
+ },
168
+ host: this.config.authUrl,
169
+ projectKey: this.config.projectKey,
170
+ refreshToken: identity.refresh_token || '',
171
+ tokenCache: cache,
172
+ });
173
+ }
145
174
 
146
175
  return createApiBuilderFromCtpClient(builder.build());
147
176
  }
@@ -152,6 +181,24 @@ export class CommercetoolsClient {
152
181
  .withQueueMiddleware({
153
182
  concurrency: 20,
154
183
  })
184
+ .withConcurrentModificationMiddleware({
185
+ concurrentModificationHandlerFn: (version: number, request: any) => {
186
+ // We basically ignore concurrency issues for now.
187
+ // And yes, ideally the frontend would handle this, but as the customer is not really in a position to DO anything about it,
188
+ // we might as well just deal with it here.....
189
+
190
+ console.log(
191
+ `Concurrent modification error, retry with version ${version}`
192
+ );
193
+ const body = request.body as Record<string, any>;
194
+ body['version'] = version;
195
+ return Promise.resolve(body);
196
+ },
197
+ })
198
+ .withCorrelationIdMiddleware({
199
+ // ideally this would be pushed in as part of the session context, so we can trace it end-to-end
200
+ generate: () => `REACTIONARY-${randomUUID()}`,
201
+ })
155
202
  .withHttpMiddleware({
156
203
  retryConfig: {
157
204
  backoff: true,
@@ -1,38 +1,43 @@
1
- import {
2
- CartSchema,
3
- IdentitySchema,
4
- InventorySchema,
5
- PriceSchema,
6
- ProductSchema,
7
- SearchResultSchema,
8
- Cache,
9
- CategorySchema,
1
+ import type {
2
+ Cache,
10
3
  ProductProvider,
11
4
  SearchProvider,
12
5
  IdentityProvider,
13
6
  CartProvider,
14
7
  InventoryProvider,
15
8
  PriceProvider,
16
- CategoryProvider
9
+ CategoryProvider,
10
+ StoreProvider} from "@reactionary/core";
11
+ import {
12
+ CartSchema,
13
+ IdentitySchema,
14
+ InventorySchema,
15
+ PriceSchema,
16
+ ProductSchema,
17
+ SearchResultSchema,
18
+ CategorySchema,
19
+ CartPaymentInstructionSchema
17
20
  } from "@reactionary/core";
18
- import { CommercetoolsCapabilities } from "../schema/capabilities.schema";
21
+ import type { CommercetoolsCapabilities } from "../schema/capabilities.schema";
19
22
  import { CommercetoolsSearchProvider } from "../providers/search.provider";
20
23
  import { CommercetoolsProductProvider } from '../providers/product.provider';
21
- import { CommercetoolsConfiguration } from "../schema/configuration.schema";
24
+ import type { CommercetoolsConfiguration } from "../schema/configuration.schema";
22
25
  import { CommercetoolsIdentityProvider } from "../providers/identity.provider";
23
26
  import { CommercetoolsCartProvider } from "../providers/cart.provider";
24
27
  import { CommercetoolsInventoryProvider } from "../providers/inventory.provider";
25
28
  import { CommercetoolsPriceProvider } from "../providers/price.provider";
26
29
  import { CommercetoolsCategoryProvider } from "../providers/category.provider";
30
+ import { CommercetoolsCartPaymentProvider } from "../providers/cart-payment.provider";
27
31
 
28
- type CommercetoolsClient<T extends CommercetoolsCapabilities> =
32
+ type CommercetoolsClient<T extends CommercetoolsCapabilities> =
29
33
  (T['cart'] extends true ? { cart: CartProvider } : object) &
30
34
  (T['product'] extends true ? { product: ProductProvider } : object) &
31
35
  (T['search'] extends true ? { search: SearchProvider } : object) &
32
36
  (T['identity'] extends true ? { identity: IdentityProvider } : object) &
33
37
  (T['category'] extends true ? { category: CategoryProvider } : object) &
34
38
  (T['inventory'] extends true ? { inventory: InventoryProvider } : object) &
35
- (T['price'] extends true ? { price: PriceProvider } : object);
39
+ (T['price'] extends true ? { price: PriceProvider } : object) &
40
+ (T['store'] extends true ? { store: StoreProvider } : object);
36
41
 
37
42
  export function withCommercetoolsCapabilities<T extends CommercetoolsCapabilities>(
38
43
  configuration: CommercetoolsConfiguration,
@@ -69,6 +74,11 @@ export function withCommercetoolsCapabilities<T extends CommercetoolsCapabilitie
69
74
  client.category = new CommercetoolsCategoryProvider(configuration, CategorySchema, cache);
70
75
  }
71
76
 
77
+ if (capabilities.cartPayment) {
78
+ client.cartPayment = new CommercetoolsCartPaymentProvider(configuration, CartPaymentInstructionSchema, cache);
79
+ }
80
+
81
+
72
82
  return client;
73
83
  };
74
84
  }
@@ -0,0 +1,193 @@
1
+
2
+ import type { CommercetoolsConfiguration } from "../schema/configuration.schema";
3
+ import { CommercetoolsClient } from "../core/client";
4
+ import type { Payment as CTPayment} from "@commercetools/platform-sdk";
5
+ import { PaymentStatus } from "@commercetools/platform-sdk";
6
+ import { traced } from "@reactionary/otel";
7
+ import type { CommercetoolsCartIdentifier} from "../schema/commercetools.schema";
8
+ import { CommercetoolsCartIdentifierSchema, CommercetoolsCartPaymentInstructionIdentifierSchema } from "../schema/commercetools.schema";
9
+ import { CartPaymentProvider, PaymentMethodIdentifierSchema, } from "@reactionary/core";
10
+ import type { CartPaymentQueryByCart, CartPaymentMutationAddPayment, CartPaymentMutationCancelPayment, Session, RequestContext , Cache, CartPaymentInstruction, Currency} from "@reactionary/core";
11
+ import type z from "zod";
12
+
13
+ export class CommercetoolsCartPaymentProvider<
14
+ T extends CartPaymentInstruction = CartPaymentInstruction
15
+ > extends CartPaymentProvider<T> {
16
+ protected config: CommercetoolsConfiguration;
17
+
18
+ constructor(config: CommercetoolsConfiguration, schema: z.ZodType<T>, cache: Cache) {
19
+ super(schema, cache);
20
+
21
+ this.config = config;
22
+ }
23
+
24
+ protected async getClient(reqCtx: RequestContext) {
25
+ const client = await new CommercetoolsClient(this.config).getClient(reqCtx);
26
+
27
+ return {
28
+ payments: client.withProjectKey({ projectKey: this.config.projectKey }).me().payments(),
29
+ carts: client.withProjectKey({ projectKey: this.config.projectKey }).me().carts()
30
+ };
31
+ }
32
+
33
+
34
+
35
+ @traced()
36
+ public override async getByCartIdentifier(payload: CartPaymentQueryByCart, reqCtx: RequestContext): Promise<T[]> {
37
+ const client = await this.getClient(reqCtx);
38
+
39
+ const ctId = payload.cart as CommercetoolsCartIdentifier;
40
+ const ctVersion = ctId.version || 0;
41
+
42
+ const cart = await client.carts.withId({ ID: ctId.key })
43
+ .get({
44
+ queryArgs: {
45
+ expand: 'paymentInfo.payments[*]',
46
+ },
47
+ })
48
+ .execute();
49
+
50
+ let payments = (cart.body.paymentInfo?.payments || []).map(x => x.obj!).filter(x => x);
51
+ if (payload.status) {
52
+ payments = payments.filter(payment => payload.status!.some(status => payment.paymentStatus?.interfaceCode === status));
53
+ }
54
+
55
+ // Map over the payments and parse each one
56
+ const parsedPayments = payments.map(payment => this.parseSingle(payment, reqCtx));
57
+
58
+ // Commercetools does not link carts to payments, but the other way around, so for this we have to synthesize the link.
59
+ const returnPayments = parsedPayments.map(x => {
60
+ x.cart = { key: cart.body.id, version: cart.body.version || 0 };
61
+ return x;
62
+ });
63
+ return returnPayments;
64
+ }
65
+
66
+
67
+
68
+ public override async initiatePaymentForCart(payload: CartPaymentMutationAddPayment, reqCtx: RequestContext): Promise<T> {
69
+ const client = await this.getClient(reqCtx);
70
+ const cartId = payload.cart as CommercetoolsCartIdentifier;
71
+ const response = await client.payments.post({
72
+ body: {
73
+
74
+ amountPlanned: {
75
+ centAmount: Math.round(payload.paymentInstruction.amount.value * 100),
76
+ currencyCode: payload.paymentInstruction.amount.currency
77
+ },
78
+ paymentMethodInfo: {
79
+ method: payload.paymentInstruction.paymentMethod.method,
80
+ name: {
81
+ [reqCtx.languageContext.locale]: payload.paymentInstruction.paymentMethod.name
82
+ },
83
+ paymentInterface: payload.paymentInstruction.paymentMethod.paymentProcessor
84
+ },
85
+ custom:{
86
+ type: {
87
+ typeId: 'type',
88
+ key: 'reactionaryPaymentCustomFields',
89
+ },
90
+ fields: {
91
+ cartId: cartId.key,
92
+ cartVersion: cartId.version + '',
93
+ }
94
+ },
95
+
96
+ },
97
+ }).execute();
98
+
99
+ // Now add the payment to the cart
100
+ const ctId = payload.cart as CommercetoolsCartIdentifier
101
+ const updatedCart = await client.carts.withId({ ID: ctId.key }).post({
102
+ body: {
103
+ version: ctId.version,
104
+ actions: [
105
+ {
106
+ 'action': 'addPayment',
107
+ 'payment': {
108
+ 'typeId': 'payment',
109
+ 'id': response.body.id
110
+ }
111
+ }
112
+ ]
113
+ }
114
+ }).execute();
115
+
116
+ const payment = this.parseSingle(response.body, reqCtx);
117
+
118
+ // we return the newest cart version so caller can update their cart reference, if they want to.
119
+ // hopefully this wont cause excessive confusion
120
+ payment.cart = CommercetoolsCartIdentifierSchema.parse({
121
+ key: updatedCart.body.id,
122
+ version: updatedCart.body.version || 0
123
+ });
124
+ return payment;
125
+ }
126
+
127
+
128
+ @traced()
129
+ public override async cancelPaymentInstruction(payload: CartPaymentMutationCancelPayment, reqCtx: RequestContext): Promise<T> {
130
+ const client = await this.getClient(reqCtx);
131
+
132
+ // get newest version
133
+ const newestVersion = await client.payments.withId({ ID: payload.paymentInstruction.key }).get().execute();
134
+
135
+
136
+ // we set the planned amount to 0, which effectively cancels the payment, and also allows the backend to clean it up.
137
+ // Note: This does NOT remove the payment from the cart, as that would be a breaking change to the cart, and we want to avoid that during checkout.
138
+ // Instead, the payment will remain on the cart, but with status 'canceled', and can be removed later if needed.
139
+ // This also allows us to keep a record of the payment instruction for auditing purposes.
140
+ // The cart can be re-used, and a new payment instruction can be added to it later.
141
+ // The frontend should ignore any payment instructions with status 'canceled' when displaying payment options to the user.
142
+ const response = await client.payments.withId({ ID: payload.paymentInstruction.key }).post({
143
+ body: {
144
+ version: newestVersion.body.version,
145
+ actions: [
146
+ {
147
+ action: 'changeAmountPlanned',
148
+ amount: {
149
+ centAmount: 0,
150
+ currencyCode: newestVersion.body.amountPlanned.currencyCode
151
+ }
152
+ },
153
+ ]
154
+ }
155
+ }).execute();
156
+
157
+ const payment = this.parseSingle(response.body, reqCtx);
158
+ payment.cart = payload.cart;
159
+ return payment;
160
+ }
161
+
162
+
163
+
164
+ @traced()
165
+ protected override parseSingle(_body: unknown, reqCtx: RequestContext): T {
166
+ const body = _body as CTPayment;
167
+
168
+ const base = this.newModel();
169
+ base.identifier = CommercetoolsCartPaymentInstructionIdentifierSchema.parse({
170
+ key: body.id,
171
+ version: body.version || 0
172
+ });
173
+
174
+ base.amount = {
175
+ value: body.amountPlanned.centAmount / 100,
176
+ currency: body.amountPlanned.currencyCode as Currency,
177
+ };
178
+
179
+
180
+ base.paymentMethod = PaymentMethodIdentifierSchema.parse({
181
+ key: body.paymentMethodInfo?.method
182
+ });
183
+
184
+ // FIXME: seems wrong
185
+ base.status = body.paymentStatus?.interfaceCode as unknown as any;
186
+
187
+ base.cart = { key: '', version: 0 };
188
+
189
+ return this.assert(base);
190
+ }
191
+
192
+
193
+ }