@reactionary/source 0.0.41 → 0.0.42
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/.env-template +8 -5
- package/README.md +41 -0
- package/core/src/client/client.ts +2 -0
- package/core/src/index.ts +3 -28
- package/core/src/providers/cart-payment.provider.ts +56 -0
- package/core/src/providers/cart.provider.ts +125 -1
- package/core/src/providers/index.ts +10 -0
- package/core/src/providers/price.provider.ts +1 -1
- package/core/src/providers/product.provider.ts +13 -1
- package/core/src/schemas/capabilities.schema.ts +1 -0
- package/core/src/schemas/models/cart.model.ts +16 -3
- package/core/src/schemas/models/identifiers.model.ts +43 -3
- package/core/src/schemas/models/identity.model.ts +22 -2
- package/core/src/schemas/models/index.ts +14 -0
- package/core/src/schemas/models/payment.model.ts +41 -0
- package/core/src/schemas/models/profile.model.ts +34 -0
- package/core/src/schemas/models/shipping-method.model.ts +14 -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 +2 -1
- package/core/src/schemas/mutations/index.ts +9 -0
- package/core/src/schemas/queries/cart-payment.query.ts +12 -0
- package/core/src/schemas/queries/index.ts +1 -0
- package/examples/node/src/test-utils.ts +10 -3
- package/package.json +1 -1
- package/providers/algolia/src/test/test-utils.ts +8 -3
- package/providers/commercetools/README.md +16 -0
- package/providers/commercetools/src/core/client.ts +25 -34
- package/providers/commercetools/src/core/initialize.ts +17 -10
- package/providers/commercetools/src/providers/cart-payment.provider.ts +192 -0
- package/providers/commercetools/src/providers/cart.provider.ts +409 -104
- package/providers/commercetools/src/providers/category.provider.ts +5 -2
- package/providers/commercetools/src/providers/identity.provider.ts +12 -4
- package/providers/commercetools/src/providers/inventory.provider.ts +10 -0
- package/providers/commercetools/src/providers/price.provider.ts +10 -4
- package/providers/commercetools/src/providers/product.provider.ts +23 -14
- package/providers/commercetools/src/providers/search.provider.ts +10 -3
- package/providers/commercetools/src/schema/capabilities.schema.ts +1 -0
- 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 +149 -0
- package/providers/commercetools/src/test/cart.provider.spec.ts +69 -10
- package/providers/commercetools/src/test/product.provider.spec.ts +27 -0
- package/providers/commercetools/src/test/test-utils.ts +22 -7
- package/providers/fake/src/providers/cart.provider.ts +47 -3
- package/providers/fake/src/providers/price.provider.ts +1 -1
- package/providers/fake/src/providers/product.provider.ts +3 -0
- package/providers/fake/src/test/test-utils.ts +7 -2
- package/trpc/src/test-utils.ts +8 -3
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { BaseMutationSchema } from './base.mutation';
|
|
3
|
-
import { CartIdentifierSchema, CartItemIdentifierSchema,
|
|
3
|
+
import { CartIdentifierSchema, CartItemIdentifierSchema, PaymentMethodIdentifierSchema, ShippingMethodIdentifier, SKUIdentifierSchema } from '../models/identifiers.model';
|
|
4
|
+
import { AddressSchema } from '../models/profile.model';
|
|
5
|
+
import { CurrencySchema } from '../models/currency.model';
|
|
6
|
+
import { MonetaryAmountSchema } from '../models/price.model';
|
|
4
7
|
|
|
5
8
|
export const CartMutationItemAddSchema = BaseMutationSchema.extend({
|
|
6
9
|
cart: CartIdentifierSchema.nonoptional(),
|
|
7
|
-
|
|
10
|
+
sku: SKUIdentifierSchema.nonoptional(),
|
|
8
11
|
quantity: z.number()
|
|
9
12
|
});
|
|
10
13
|
|
|
@@ -19,6 +22,62 @@ export const CartMutationItemQuantityChangeSchema = BaseMutationSchema.extend({
|
|
|
19
22
|
quantity: z.number()
|
|
20
23
|
});
|
|
21
24
|
|
|
25
|
+
export const CartMutationDeleteCartSchema = BaseMutationSchema.extend({
|
|
26
|
+
cart: CartIdentifierSchema.required()
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const CartMutationSetShippingInfoSchema = BaseMutationSchema.extend({
|
|
30
|
+
cart: CartIdentifierSchema.required(),
|
|
31
|
+
shippingMethod: ShippingMethodIdentifier.optional(),
|
|
32
|
+
shippingAddress: AddressSchema.optional(),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export const CartMutationSetBillingAddressSchema = BaseMutationSchema.extend({
|
|
36
|
+
cart: CartIdentifierSchema.required(),
|
|
37
|
+
billingAddress: AddressSchema.required(),
|
|
38
|
+
notificationEmailAddress: z.string().optional(),
|
|
39
|
+
notificationPhoneNumber: z.string().optional(),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export const CartMutationApplyCouponSchema = BaseMutationSchema.extend({
|
|
43
|
+
cart: CartIdentifierSchema.required(),
|
|
44
|
+
couponCode: z.string().default('').nonoptional()
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export const CartMutationRemoveCouponSchema = BaseMutationSchema.extend({
|
|
48
|
+
cart: CartIdentifierSchema.required(),
|
|
49
|
+
couponCode: z.string().default('').nonoptional()
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export const CartMutationCheckoutSchema = BaseMutationSchema.extend({
|
|
53
|
+
cart: CartIdentifierSchema.required()
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export const CartMutationAddPaymentMethodSchema = BaseMutationSchema.extend({
|
|
57
|
+
cart: CartIdentifierSchema.required(),
|
|
58
|
+
paymentMethodId: PaymentMethodIdentifierSchema.required(),
|
|
59
|
+
amount: MonetaryAmountSchema.optional().describe('The amount to authorize for the payment method. If not provided, the full remaining balance of the cart will be authorized.')
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export const CartMutationRemovePaymentMethodSchema = BaseMutationSchema.extend({
|
|
63
|
+
cart: CartIdentifierSchema.required(),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export const CartMutationChangeCurrencySchema = BaseMutationSchema.extend({
|
|
67
|
+
cart: CartIdentifierSchema.required(),
|
|
68
|
+
newCurrency: CurrencySchema.default(() => CurrencySchema.parse({})).describe('The new currency to set for the cart.')
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
export type CartMutationChangeCurrency = z.infer<typeof CartMutationChangeCurrencySchema>;
|
|
73
|
+
export type CartMutationAddPaymentMethod = z.infer<typeof CartMutationAddPaymentMethodSchema>;
|
|
74
|
+
export type CartMutationRemovePaymentMethod = z.infer<typeof CartMutationRemovePaymentMethodSchema>;
|
|
75
|
+
export type CartMutationCheckout = z.infer<typeof CartMutationCheckoutSchema>;
|
|
22
76
|
export type CartMutationItemAdd = z.infer<typeof CartMutationItemAddSchema>;
|
|
23
77
|
export type CartMutationItemRemove = z.infer<typeof CartMutationItemRemoveSchema>;
|
|
24
|
-
export type CartMutationItemQuantityChange = z.infer<typeof CartMutationItemQuantityChangeSchema>;
|
|
78
|
+
export type CartMutationItemQuantityChange = z.infer<typeof CartMutationItemQuantityChangeSchema>;
|
|
79
|
+
export type CartMutationDeleteCart = z.infer<typeof CartMutationDeleteCartSchema>;
|
|
80
|
+
export type CartMutationSetShippingInfo = z.infer<typeof CartMutationSetShippingInfoSchema>;
|
|
81
|
+
export type CartMutationSetBillingAddress = z.infer<typeof CartMutationSetBillingAddressSchema>;
|
|
82
|
+
export type CartMutationApplyCoupon = z.infer<typeof CartMutationApplyCouponSchema>;
|
|
83
|
+
export type CartMutationRemoveCoupon = z.infer<typeof CartMutationRemoveCouponSchema>;
|
|
@@ -9,5 +9,6 @@ export const IdentityMutationLoginSchema = BaseMutationSchema.extend({
|
|
|
9
9
|
export const IdentityMutationLogoutSchema = BaseMutationSchema.extend({
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
+
|
|
12
13
|
export type IdentityMutationLogin = z.infer<typeof IdentityMutationLoginSchema>;
|
|
13
|
-
export type IdentityMutationLogout = z.infer<typeof IdentityMutationLogoutSchema>;
|
|
14
|
+
export type IdentityMutationLogout = z.infer<typeof IdentityMutationLogoutSchema>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './analytics.mutation';
|
|
2
|
+
export * from './base.mutation';
|
|
3
|
+
export * from './cart-payment.mutation';
|
|
4
|
+
export * from './cart.mutation';
|
|
5
|
+
export * from './identity.mutation';
|
|
6
|
+
export * from './inventory.mutation';
|
|
7
|
+
export * from './price.mutation';
|
|
8
|
+
export * from './product.mutation';
|
|
9
|
+
export * from './search.mutation';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
import { CartIdentifierSchema } from "../models/identifiers.model";
|
|
3
|
+
import { PaymentStatusSchema } from "../models/payment.model";
|
|
4
|
+
import { BaseQuerySchema } from "./base.query";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export const CartPaymentQueryByCartSchema = BaseQuerySchema.extend({
|
|
8
|
+
cart: CartIdentifierSchema.required(),
|
|
9
|
+
status: z.array(PaymentStatusSchema).optional().describe('Optional status to filter payment instructions by'),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export type CartPaymentQueryByCart = z.infer<typeof CartPaymentQueryByCartSchema>;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Session } from '@reactionary/core';
|
|
2
2
|
|
|
3
|
+
|
|
4
|
+
|
|
3
5
|
export function createAnonymousTestSession(): Session {
|
|
4
6
|
return {
|
|
5
7
|
id: 'test-session-id',
|
|
@@ -9,10 +11,15 @@ export function createAnonymousTestSession(): Session {
|
|
|
9
11
|
cache: { hit: false, key: '' },
|
|
10
12
|
placeholder: false,
|
|
11
13
|
},
|
|
12
|
-
id: '',
|
|
14
|
+
id: { userId: 'anonymous' },
|
|
13
15
|
token: undefined,
|
|
14
16
|
issued: new Date(),
|
|
15
|
-
expiry: new Date(new Date().getTime() + 3600 * 1000),
|
|
17
|
+
expiry: new Date(new Date().getTime() + 3600 * 1000),
|
|
18
|
+
logonId: "",
|
|
19
|
+
createdAt: "",
|
|
20
|
+
updatedAt: "",
|
|
21
|
+
keyring: [],
|
|
22
|
+
currentService: undefined
|
|
16
23
|
},
|
|
17
24
|
languageContext: {
|
|
18
25
|
locale: 'en-US',
|
|
@@ -23,4 +30,4 @@ export function createAnonymousTestSession(): Session {
|
|
|
23
30
|
key: 'the-good-store',
|
|
24
31
|
},
|
|
25
32
|
};
|
|
26
|
-
}
|
|
33
|
+
}
|
package/package.json
CHANGED
|
@@ -9,10 +9,15 @@ export function createAnonymousTestSession(): Session {
|
|
|
9
9
|
cache: { hit: false, key: '' },
|
|
10
10
|
placeholder: false,
|
|
11
11
|
},
|
|
12
|
-
id: '',
|
|
12
|
+
id: { userId: 'anonymous' },
|
|
13
13
|
token: undefined,
|
|
14
14
|
issued: new Date(),
|
|
15
|
-
expiry: new Date(new Date().getTime() + 3600 * 1000),
|
|
15
|
+
expiry: new Date(new Date().getTime() + 3600 * 1000),
|
|
16
|
+
logonId: "",
|
|
17
|
+
createdAt: "",
|
|
18
|
+
updatedAt: "",
|
|
19
|
+
keyring: [],
|
|
20
|
+
currentService: undefined
|
|
16
21
|
},
|
|
17
22
|
languageContext: {
|
|
18
23
|
locale: 'en-US',
|
|
@@ -23,4 +28,4 @@ export function createAnonymousTestSession(): Session {
|
|
|
23
28
|
key: 'the-good-store',
|
|
24
29
|
},
|
|
25
30
|
};
|
|
26
|
-
}
|
|
31
|
+
}
|
|
@@ -9,3 +9,19 @@ 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
|
+
## TODO List
|
|
15
|
+
|
|
16
|
+
### Core
|
|
17
|
+
- [ ] Figure out if we are actually running as anonymous user towards CT. It feels weird right now.
|
|
18
|
+
|
|
19
|
+
### Price
|
|
20
|
+
- [ ] PriceProvider should be able to use both embedded and standalone prices? Possible by querying through product-projection maybe?
|
|
21
|
+
- [ ] If not, using product-projection, the logic in https://docs.commercetools.com/api/pricing-and-discounts-overview#price-selection should be replicated
|
|
22
|
+
- [ ] add list price by convention. Like key: LP-<sku> or something.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Inventory
|
|
26
|
+
- [ ] Be traced and cached
|
|
27
|
+
|
|
@@ -1,29 +1,8 @@
|
|
|
1
1
|
import { ClientBuilder } from '@commercetools/ts-client';
|
|
2
2
|
import { createApiBuilderFromCtpClient } from '@commercetools/platform-sdk';
|
|
3
3
|
import { CommercetoolsConfiguration } from '../schema/configuration.schema';
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
4
5
|
|
|
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
6
|
|
|
28
7
|
export class CommercetoolsClient {
|
|
29
8
|
protected config: CommercetoolsConfiguration;
|
|
@@ -41,14 +20,10 @@ export class CommercetoolsClient {
|
|
|
41
20
|
}
|
|
42
21
|
|
|
43
22
|
public async login(username: string, password: string) {
|
|
44
|
-
const scopes = REGISTERED_SCOPES.map(
|
|
45
|
-
(scope) => `${scope}:${this.config.projectKey}`
|
|
46
|
-
).join(' ');
|
|
47
23
|
const queryParams = new URLSearchParams({
|
|
48
24
|
grant_type: 'password',
|
|
49
25
|
username: username,
|
|
50
26
|
password: password,
|
|
51
|
-
scope: scopes,
|
|
52
27
|
});
|
|
53
28
|
const url = `${this.config.authUrl}/oauth/${
|
|
54
29
|
this.config.projectKey
|
|
@@ -65,12 +40,8 @@ export class CommercetoolsClient {
|
|
|
65
40
|
}
|
|
66
41
|
|
|
67
42
|
public async guest() {
|
|
68
|
-
const scopes = GUEST_SCOPES.map(
|
|
69
|
-
(scope) => `${scope}:${this.config.projectKey}`
|
|
70
|
-
).join(' ');
|
|
71
43
|
const queryParams = new URLSearchParams({
|
|
72
44
|
grant_type: 'client_credentials',
|
|
73
|
-
scope: scopes,
|
|
74
45
|
});
|
|
75
46
|
const url = `${this.config.authUrl}/oauth/${
|
|
76
47
|
this.config.projectKey
|
|
@@ -121,9 +92,7 @@ export class CommercetoolsClient {
|
|
|
121
92
|
}
|
|
122
93
|
|
|
123
94
|
public createAnonymousClient() {
|
|
124
|
-
const scopes =
|
|
125
|
-
(scope) => `${scope}:${this.config.projectKey}`
|
|
126
|
-
).join(' ');
|
|
95
|
+
const scopes = this.config.scopes;
|
|
127
96
|
const builder = this.createBaseClientBuilder().withClientCredentialsFlow({
|
|
128
97
|
host: this.config.authUrl,
|
|
129
98
|
projectKey: this.config.projectKey,
|
|
@@ -131,7 +100,7 @@ export class CommercetoolsClient {
|
|
|
131
100
|
clientId: this.config.clientId,
|
|
132
101
|
clientSecret: this.config.clientSecret,
|
|
133
102
|
},
|
|
134
|
-
scopes: [scopes],
|
|
103
|
+
scopes: [...scopes],
|
|
135
104
|
});
|
|
136
105
|
|
|
137
106
|
return createApiBuilderFromCtpClient(builder.build());
|
|
@@ -146,12 +115,34 @@ export class CommercetoolsClient {
|
|
|
146
115
|
return createApiBuilderFromCtpClient(builder.build());
|
|
147
116
|
}
|
|
148
117
|
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
|
|
149
124
|
protected createBaseClientBuilder() {
|
|
150
125
|
const builder = new ClientBuilder()
|
|
151
126
|
.withProjectKey(this.config.projectKey)
|
|
152
127
|
.withQueueMiddleware({
|
|
153
128
|
concurrency: 20,
|
|
154
129
|
})
|
|
130
|
+
.withConcurrentModificationMiddleware({
|
|
131
|
+
concurrentModificationHandlerFn: (version: number, request: any) => {
|
|
132
|
+
// We basically ignore concurrency issues for now.
|
|
133
|
+
// And yes, ideally the frontend would handle this, but as the customer is not really in a position to DO anything about it,
|
|
134
|
+
// we might as well just deal with it here.....
|
|
135
|
+
|
|
136
|
+
console.log(`Concurrent modification error, retry with version ${version}`);
|
|
137
|
+
const body = request.body as Record<string, any>;
|
|
138
|
+
body['version'] = version;
|
|
139
|
+
return Promise.resolve(body);
|
|
140
|
+
},
|
|
141
|
+
})
|
|
142
|
+
.withCorrelationIdMiddleware({
|
|
143
|
+
// ideally this would be pushed in as part of the session context, so we can trace it end-to-end
|
|
144
|
+
generate: () => `REACTIONARY-${randomUUID()}`,
|
|
145
|
+
})
|
|
155
146
|
.withHttpMiddleware({
|
|
156
147
|
retryConfig: {
|
|
157
148
|
backoff: true,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
CartSchema,
|
|
3
|
-
IdentitySchema,
|
|
4
|
-
InventorySchema,
|
|
5
|
-
PriceSchema,
|
|
6
|
-
ProductSchema,
|
|
7
|
-
SearchResultSchema,
|
|
8
|
-
Cache,
|
|
1
|
+
import {
|
|
2
|
+
CartSchema,
|
|
3
|
+
IdentitySchema,
|
|
4
|
+
InventorySchema,
|
|
5
|
+
PriceSchema,
|
|
6
|
+
ProductSchema,
|
|
7
|
+
SearchResultSchema,
|
|
8
|
+
Cache,
|
|
9
9
|
CategorySchema,
|
|
10
10
|
ProductProvider,
|
|
11
11
|
SearchProvider,
|
|
@@ -13,7 +13,8 @@ import {
|
|
|
13
13
|
CartProvider,
|
|
14
14
|
InventoryProvider,
|
|
15
15
|
PriceProvider,
|
|
16
|
-
CategoryProvider
|
|
16
|
+
CategoryProvider,
|
|
17
|
+
CartPaymentInstructionSchema
|
|
17
18
|
} from "@reactionary/core";
|
|
18
19
|
import { CommercetoolsCapabilities } from "../schema/capabilities.schema";
|
|
19
20
|
import { CommercetoolsSearchProvider } from "../providers/search.provider";
|
|
@@ -24,8 +25,9 @@ import { CommercetoolsCartProvider } from "../providers/cart.provider";
|
|
|
24
25
|
import { CommercetoolsInventoryProvider } from "../providers/inventory.provider";
|
|
25
26
|
import { CommercetoolsPriceProvider } from "../providers/price.provider";
|
|
26
27
|
import { CommercetoolsCategoryProvider } from "../providers/category.provider";
|
|
28
|
+
import { CommercetoolsCartPaymentProvider } from "../providers/cart-payment.provider";
|
|
27
29
|
|
|
28
|
-
type CommercetoolsClient<T extends CommercetoolsCapabilities> =
|
|
30
|
+
type CommercetoolsClient<T extends CommercetoolsCapabilities> =
|
|
29
31
|
(T['cart'] extends true ? { cart: CartProvider } : object) &
|
|
30
32
|
(T['product'] extends true ? { product: ProductProvider } : object) &
|
|
31
33
|
(T['search'] extends true ? { search: SearchProvider } : object) &
|
|
@@ -69,6 +71,11 @@ export function withCommercetoolsCapabilities<T extends CommercetoolsCapabilitie
|
|
|
69
71
|
client.category = new CommercetoolsCategoryProvider(configuration, CategorySchema, cache);
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
if (capabilities.cartPayment) {
|
|
75
|
+
client.cartPayment = new CommercetoolsCartPaymentProvider(configuration, CartPaymentInstructionSchema, cache);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
72
79
|
return client;
|
|
73
80
|
};
|
|
74
81
|
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
|
|
2
|
+
import { CommercetoolsConfiguration } from "../schema/configuration.schema";
|
|
3
|
+
import { CommercetoolsClient } from "../core/client";
|
|
4
|
+
import { Payment as CTPayment, PaymentStatus } from "@commercetools/platform-sdk";
|
|
5
|
+
import { traced } from "@reactionary/otel";
|
|
6
|
+
import { CommercetoolsCartIdentifier, CommercetoolsCartIdentifierSchema, CommercetoolsCartPaymentInstructionIdentifierSchema } from "../schema/commercetools.schema";
|
|
7
|
+
import { Cache, CartPaymentInstruction, CartPaymentProvider, Currency, PaymentMethodIdentifierSchema, } from "@reactionary/core";
|
|
8
|
+
import type { CartPaymentQueryByCart, CartPaymentMutationAddPayment, CartPaymentMutationCancelPayment, Session } from "@reactionary/core";
|
|
9
|
+
import z from "zod";
|
|
10
|
+
|
|
11
|
+
export class CommercetoolsCartPaymentProvider<
|
|
12
|
+
T extends CartPaymentInstruction = CartPaymentInstruction
|
|
13
|
+
> extends CartPaymentProvider<T> {
|
|
14
|
+
protected config: CommercetoolsConfiguration;
|
|
15
|
+
|
|
16
|
+
constructor(config: CommercetoolsConfiguration, schema: z.ZodType<T>, cache: Cache) {
|
|
17
|
+
super(schema, cache);
|
|
18
|
+
|
|
19
|
+
this.config = config;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
protected getClient(session: Session) {
|
|
23
|
+
const token = session.identity.keyring.find(x => x.service === 'commercetools')?.token;
|
|
24
|
+
const client = new CommercetoolsClient(this.config).getClient(
|
|
25
|
+
token
|
|
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, session: Session): Promise<T[]> {
|
|
37
|
+
const client = this.getClient(session);
|
|
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, session));
|
|
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, session: Session): Promise<T> {
|
|
69
|
+
const client = this.getClient(session);
|
|
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
|
+
[session.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
|
+
}).execute();
|
|
97
|
+
|
|
98
|
+
// Now add the payment to the cart
|
|
99
|
+
const ctId = payload.cart as CommercetoolsCartIdentifier
|
|
100
|
+
const updatedCart = await client.carts.withId({ ID: ctId.key }).post({
|
|
101
|
+
body: {
|
|
102
|
+
version: ctId.version,
|
|
103
|
+
actions: [
|
|
104
|
+
{
|
|
105
|
+
'action': 'addPayment',
|
|
106
|
+
'payment': {
|
|
107
|
+
'typeId': 'payment',
|
|
108
|
+
'id': response.body.id
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
}).execute();
|
|
114
|
+
|
|
115
|
+
const payment = this.parseSingle(response.body, session);
|
|
116
|
+
|
|
117
|
+
// we return the newest cart version so caller can update their cart reference, if they want to.
|
|
118
|
+
// hopefully this wont cause excessive confusion
|
|
119
|
+
payment.cart = CommercetoolsCartIdentifierSchema.parse({
|
|
120
|
+
key: updatedCart.body.id,
|
|
121
|
+
version: updatedCart.body.version || 0
|
|
122
|
+
});
|
|
123
|
+
return payment;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@traced()
|
|
128
|
+
public override async cancelPaymentInstruction(payload: CartPaymentMutationCancelPayment, session: Session): Promise<T> {
|
|
129
|
+
const client = this.getClient(session);
|
|
130
|
+
|
|
131
|
+
// get newest version
|
|
132
|
+
const newestVersion = await client.payments.withId({ ID: payload.paymentInstruction.key }).get().execute();
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
// we set the planned amount to 0, which effectively cancels the payment, and also allows the backend to clean it up.
|
|
136
|
+
// 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.
|
|
137
|
+
// Instead, the payment will remain on the cart, but with status 'canceled', and can be removed later if needed.
|
|
138
|
+
// This also allows us to keep a record of the payment instruction for auditing purposes.
|
|
139
|
+
// The cart can be re-used, and a new payment instruction can be added to it later.
|
|
140
|
+
// The frontend should ignore any payment instructions with status 'canceled' when displaying payment options to the user.
|
|
141
|
+
const response = await client.payments.withId({ ID: payload.paymentInstruction.key }).post({
|
|
142
|
+
body: {
|
|
143
|
+
version: newestVersion.body.version,
|
|
144
|
+
actions: [
|
|
145
|
+
{
|
|
146
|
+
action: 'changeAmountPlanned',
|
|
147
|
+
amount: {
|
|
148
|
+
centAmount: 0,
|
|
149
|
+
currencyCode: newestVersion.body.amountPlanned.currencyCode
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
}).execute();
|
|
155
|
+
|
|
156
|
+
const payment = this.parseSingle(response.body, session);
|
|
157
|
+
payment.cart = payload.cart;
|
|
158
|
+
return payment;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@traced()
|
|
164
|
+
protected override parseSingle(_body: unknown, session: Session): T {
|
|
165
|
+
const body = _body as CTPayment;
|
|
166
|
+
|
|
167
|
+
const base = this.newModel();
|
|
168
|
+
base.identifier = CommercetoolsCartPaymentInstructionIdentifierSchema.parse({
|
|
169
|
+
key: body.id,
|
|
170
|
+
version: body.version || 0
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
base.amount = {
|
|
174
|
+
value: body.amountPlanned.centAmount / 100,
|
|
175
|
+
currency: body.amountPlanned.currencyCode as Currency,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
base.paymentMethod = PaymentMethodIdentifierSchema.parse({
|
|
180
|
+
key: body.paymentMethodInfo?.method
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// FIXME: seems wrong
|
|
184
|
+
base.status = body.paymentStatus?.interfaceCode as unknown as any;
|
|
185
|
+
|
|
186
|
+
base.cart = { key: '', version: 0 };
|
|
187
|
+
|
|
188
|
+
return this.assert(base);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
}
|