@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.
- package/.claude/settings.local.json +28 -0
- package/.env-template +8 -5
- package/.vscode/settings.json +5 -0
- package/README.md +41 -0
- package/core/package.json +3 -1
- package/core/src/cache/cache.interface.ts +14 -18
- package/core/src/cache/memory-cache.ts +56 -0
- package/core/src/cache/noop-cache.ts +5 -23
- package/core/src/cache/redis-cache.ts +28 -38
- package/core/src/client/client-builder.ts +3 -3
- package/core/src/client/client.ts +11 -9
- package/core/src/decorators/reactionary.decorator.ts +80 -8
- package/core/src/index.ts +5 -29
- package/core/src/initialization.ts +43 -0
- package/core/src/providers/analytics.provider.ts +1 -1
- package/core/src/providers/base.provider.ts +61 -25
- package/core/src/providers/cart-payment.provider.ts +57 -0
- package/core/src/providers/cart.provider.ts +131 -8
- package/core/src/providers/category.provider.ts +9 -9
- package/core/src/providers/identity.provider.ts +8 -7
- package/core/src/providers/index.ts +12 -0
- package/core/src/providers/inventory.provider.ts +4 -4
- package/core/src/providers/price.provider.ts +7 -7
- package/core/src/providers/product.provider.ts +17 -5
- package/core/src/providers/profile.provider.ts +22 -0
- package/core/src/providers/search.provider.ts +4 -4
- package/core/src/providers/store.provider.ts +14 -0
- package/core/src/schemas/capabilities.schema.ts +3 -1
- package/core/src/schemas/models/analytics.model.ts +1 -1
- package/core/src/schemas/models/cart.model.ts +16 -3
- package/core/src/schemas/models/identifiers.model.ts +90 -22
- package/core/src/schemas/models/identity.model.ts +23 -7
- package/core/src/schemas/models/index.ts +15 -0
- package/core/src/schemas/models/payment.model.ts +41 -0
- package/core/src/schemas/models/profile.model.ts +35 -0
- package/core/src/schemas/models/shipping-method.model.ts +14 -0
- package/core/src/schemas/models/store.model.ts +11 -0
- package/core/src/schemas/mutations/cart-payment.mutation.ts +21 -0
- package/core/src/schemas/mutations/cart.mutation.ts +62 -3
- package/core/src/schemas/mutations/identity.mutation.ts +8 -1
- package/core/src/schemas/mutations/index.ts +10 -0
- package/core/src/schemas/mutations/profile.mutation.ts +9 -0
- package/core/src/schemas/queries/cart-payment.query.ts +12 -0
- package/core/src/schemas/queries/cart.query.ts +1 -1
- package/core/src/schemas/queries/identity.query.ts +1 -1
- package/core/src/schemas/queries/index.ts +3 -0
- package/core/src/schemas/queries/inventory.query.ts +4 -12
- package/core/src/schemas/queries/price.query.ts +1 -1
- package/core/src/schemas/queries/profile.query.ts +7 -0
- package/core/src/schemas/queries/search.query.ts +1 -1
- package/core/src/schemas/queries/store.query.ts +11 -0
- package/core/src/schemas/session.schema.ts +31 -6
- package/eslint.config.mjs +7 -0
- package/examples/next/src/app/page.tsx +4 -12
- package/examples/node/package.json +1 -3
- package/examples/node/src/basic/basic-node-provider-model-extension.spec.ts +9 -8
- package/examples/node/src/basic/basic-node-provider-query-extension.spec.ts +4 -3
- package/examples/node/src/basic/basic-node-setup.spec.ts +4 -5
- package/nx.json +1 -0
- package/otel/src/metrics.ts +2 -1
- package/otel/src/provider-instrumentation.ts +2 -1
- package/otel/src/tracer.ts +7 -6
- package/otel/src/trpc-middleware.ts +3 -2
- package/package.json +2 -1
- package/providers/algolia/src/core/initialize.ts +4 -3
- package/providers/algolia/src/providers/product.provider.ts +15 -13
- package/providers/algolia/src/providers/search.provider.ts +9 -9
- package/providers/algolia/src/schema/capabilities.schema.ts +1 -1
- package/providers/algolia/src/test/search.provider.spec.ts +10 -10
- package/providers/algolia/src/test/test-utils.ts +9 -4
- package/providers/commercetools/README.md +27 -0
- package/providers/commercetools/src/core/client.ts +164 -117
- package/providers/commercetools/src/core/initialize.ts +24 -14
- package/providers/commercetools/src/providers/cart-payment.provider.ts +193 -0
- package/providers/commercetools/src/providers/cart.provider.ts +402 -125
- package/providers/commercetools/src/providers/category.provider.ts +35 -35
- package/providers/commercetools/src/providers/identity.provider.ts +23 -75
- package/providers/commercetools/src/providers/index.ts +2 -0
- package/providers/commercetools/src/providers/inventory.provider.ts +69 -40
- package/providers/commercetools/src/providers/price.provider.ts +79 -47
- package/providers/commercetools/src/providers/product.provider.ts +36 -30
- package/providers/commercetools/src/providers/profile.provider.ts +61 -0
- package/providers/commercetools/src/providers/search.provider.ts +16 -12
- package/providers/commercetools/src/providers/store.provider.ts +78 -0
- package/providers/commercetools/src/schema/capabilities.schema.ts +3 -1
- package/providers/commercetools/src/schema/commercetools.schema.ts +18 -0
- package/providers/commercetools/src/schema/configuration.schema.ts +2 -1
- package/providers/commercetools/src/test/cart-payment.provider.spec.ts +145 -0
- package/providers/commercetools/src/test/cart.provider.spec.ts +82 -22
- package/providers/commercetools/src/test/category.provider.spec.ts +18 -17
- package/providers/commercetools/src/test/identity.provider.spec.ts +88 -0
- package/providers/commercetools/src/test/inventory.provider.spec.ts +41 -0
- package/providers/commercetools/src/test/price.provider.spec.ts +9 -8
- package/providers/commercetools/src/test/product.provider.spec.ts +33 -5
- package/providers/commercetools/src/test/profile.provider.spec.ts +49 -0
- package/providers/commercetools/src/test/search.provider.spec.ts +8 -7
- package/providers/commercetools/src/test/store.provider.spec.ts +37 -0
- package/providers/commercetools/src/test/test-utils.ts +7 -31
- package/providers/fake/src/core/initialize.ts +96 -38
- package/providers/fake/src/providers/analytics.provider.ts +6 -5
- package/providers/fake/src/providers/cart.provider.ts +66 -19
- package/providers/fake/src/providers/category.provider.ts +12 -12
- package/providers/fake/src/providers/identity.provider.ts +22 -14
- package/providers/fake/src/providers/index.ts +1 -0
- package/providers/fake/src/providers/inventory.provider.ts +13 -13
- package/providers/fake/src/providers/price.provider.ts +13 -13
- package/providers/fake/src/providers/product.provider.ts +13 -10
- package/providers/fake/src/providers/search.provider.ts +7 -5
- package/providers/fake/src/providers/store.provider.ts +47 -0
- package/providers/fake/src/schema/capabilities.schema.ts +4 -1
- package/providers/fake/src/test/cart.provider.spec.ts +18 -18
- package/providers/fake/src/test/category.provider.spec.ts +55 -37
- package/providers/fake/src/test/price.provider.spec.ts +9 -14
- package/providers/fake/src/test/product.provider.spec.ts +27 -0
- package/providers/fake/src/test/test-utils.ts +2 -28
- package/providers/posthog/src/core/initialize.ts +3 -3
- package/providers/posthog/src/schema/capabilities.schema.ts +1 -1
- package/trpc/src/client.ts +42 -41
- package/trpc/src/index.ts +4 -3
- package/trpc/src/integration.spec.ts +11 -11
- package/trpc/src/server.ts +26 -24
- package/trpc/src/test-utils.ts +9 -4
- package/trpc/src/types.ts +24 -22
- package/core/src/cache/cache-evaluation.interface.ts +0 -19
- package/examples/node/src/test-utils.ts +0 -26
|
@@ -1,147 +1,176 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
29
|
+
return undefined;
|
|
41
30
|
}
|
|
42
31
|
|
|
43
|
-
public async
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
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
|
-
|
|
38
|
+
identity.refresh_token = cache.refreshToken;
|
|
39
|
+
identity.token = cache.token;
|
|
40
|
+
identity.expiry = new Date(cache.expirationTime);
|
|
65
41
|
}
|
|
42
|
+
}
|
|
66
43
|
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
47
|
+
constructor(config: CommercetoolsConfiguration) {
|
|
48
|
+
this.config = config;
|
|
87
49
|
}
|
|
88
50
|
|
|
89
|
-
public async
|
|
90
|
-
|
|
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
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
118
|
-
|
|
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
|
-
|
|
89
|
+
const login = await this.login(username, password, reqCtx);
|
|
121
90
|
}
|
|
122
91
|
|
|
123
|
-
public
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
108
|
+
tokenCache: cache,
|
|
109
|
+
scopes: this.config.scopes,
|
|
135
110
|
});
|
|
136
111
|
|
|
137
|
-
|
|
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
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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
|
+
}
|