@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.
Files changed (49) hide show
  1. package/.env-template +8 -5
  2. package/README.md +41 -0
  3. package/core/src/client/client.ts +2 -0
  4. package/core/src/index.ts +3 -28
  5. package/core/src/providers/cart-payment.provider.ts +56 -0
  6. package/core/src/providers/cart.provider.ts +125 -1
  7. package/core/src/providers/index.ts +10 -0
  8. package/core/src/providers/price.provider.ts +1 -1
  9. package/core/src/providers/product.provider.ts +13 -1
  10. package/core/src/schemas/capabilities.schema.ts +1 -0
  11. package/core/src/schemas/models/cart.model.ts +16 -3
  12. package/core/src/schemas/models/identifiers.model.ts +43 -3
  13. package/core/src/schemas/models/identity.model.ts +22 -2
  14. package/core/src/schemas/models/index.ts +14 -0
  15. package/core/src/schemas/models/payment.model.ts +41 -0
  16. package/core/src/schemas/models/profile.model.ts +34 -0
  17. package/core/src/schemas/models/shipping-method.model.ts +14 -0
  18. package/core/src/schemas/mutations/cart-payment.mutation.ts +21 -0
  19. package/core/src/schemas/mutations/cart.mutation.ts +62 -3
  20. package/core/src/schemas/mutations/identity.mutation.ts +2 -1
  21. package/core/src/schemas/mutations/index.ts +9 -0
  22. package/core/src/schemas/queries/cart-payment.query.ts +12 -0
  23. package/core/src/schemas/queries/index.ts +1 -0
  24. package/examples/node/src/test-utils.ts +10 -3
  25. package/package.json +1 -1
  26. package/providers/algolia/src/test/test-utils.ts +8 -3
  27. package/providers/commercetools/README.md +16 -0
  28. package/providers/commercetools/src/core/client.ts +25 -34
  29. package/providers/commercetools/src/core/initialize.ts +17 -10
  30. package/providers/commercetools/src/providers/cart-payment.provider.ts +192 -0
  31. package/providers/commercetools/src/providers/cart.provider.ts +409 -104
  32. package/providers/commercetools/src/providers/category.provider.ts +5 -2
  33. package/providers/commercetools/src/providers/identity.provider.ts +12 -4
  34. package/providers/commercetools/src/providers/inventory.provider.ts +10 -0
  35. package/providers/commercetools/src/providers/price.provider.ts +10 -4
  36. package/providers/commercetools/src/providers/product.provider.ts +23 -14
  37. package/providers/commercetools/src/providers/search.provider.ts +10 -3
  38. package/providers/commercetools/src/schema/capabilities.schema.ts +1 -0
  39. package/providers/commercetools/src/schema/commercetools.schema.ts +18 -0
  40. package/providers/commercetools/src/schema/configuration.schema.ts +2 -1
  41. package/providers/commercetools/src/test/cart-payment.provider.spec.ts +149 -0
  42. package/providers/commercetools/src/test/cart.provider.spec.ts +69 -10
  43. package/providers/commercetools/src/test/product.provider.spec.ts +27 -0
  44. package/providers/commercetools/src/test/test-utils.ts +22 -7
  45. package/providers/fake/src/providers/cart.provider.ts +47 -3
  46. package/providers/fake/src/providers/price.provider.ts +1 -1
  47. package/providers/fake/src/providers/product.provider.ts +3 -0
  48. package/providers/fake/src/test/test-utils.ts +7 -2
  49. 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, ProductIdentifierSchema } from '../models/identifiers.model';
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
- product: ProductIdentifierSchema.nonoptional(),
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,6 @@
1
1
  export * from './analytics.query';
2
2
  export * from './base.query';
3
+ export * from './cart-payment.query';
3
4
  export * from './cart.query';
4
5
  export * from './category.query';
5
6
  export * from './identity.query';
@@ -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), // 1 hour from now
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reactionary/source",
3
- "version": "0.0.41",
3
+ "version": "0.0.42",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "dependencies": {
@@ -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), // 1 hour from now
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 = ANONYMOUS_SCOPES.map(
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
+ }