@commercetools/connect-payments-sdk 0.27.0 → 0.27.2
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/CHANGELOG.md +12 -0
- package/dist/commercetools/services/ct-payment.service.d.ts +11 -0
- package/dist/commercetools/services/ct-payment.service.js +37 -16
- package/dist/commercetools/services/ct-recurring-payment-job.service.d.ts +8 -0
- package/dist/commercetools/services/ct-recurring-payment-job.service.js +33 -4
- package/dist/index.js +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @commercetools/connect-payments-sdk
|
|
2
2
|
|
|
3
|
+
## 0.27.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- d66f007: fix(payments): correct retry mechanism and add exponential backoff to payment updates
|
|
8
|
+
|
|
9
|
+
## 0.27.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 7bfd391: Fix issue calling the recurring payment jobs API
|
|
14
|
+
|
|
3
15
|
## 0.27.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
|
@@ -19,6 +19,17 @@ export declare class DefaultPaymentService implements PaymentService {
|
|
|
19
19
|
findPaymentsByInterfaceId(opts: FindPaymentsByInterfaceId): Promise<Payment[]>;
|
|
20
20
|
createPayment(draft: PaymentDraft): Promise<Payment>;
|
|
21
21
|
updatePayment(opts: UpdatePayment): Promise<Payment>;
|
|
22
|
+
/**
|
|
23
|
+
* Calculates exponential backoff delay with jitter
|
|
24
|
+
* @param attempt - Current attempt number (0-based)
|
|
25
|
+
* @returns Delay in milliseconds
|
|
26
|
+
*/
|
|
27
|
+
private calculateExponentialBackoff;
|
|
28
|
+
/**
|
|
29
|
+
* Sleep for a specified duration
|
|
30
|
+
* @param ms - Duration in milliseconds
|
|
31
|
+
*/
|
|
32
|
+
private sleep;
|
|
22
33
|
private consolidateUpdateActions;
|
|
23
34
|
private populateSetInterfaceIdAction;
|
|
24
35
|
private populateChangeTransactionInteractionId;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DefaultPaymentService = void 0;
|
|
4
4
|
const ct_api_error_1 = require("../errors/ct-api.error");
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
6
|
/**
|
|
6
7
|
* This is the default implementation of the PaymentService interface.
|
|
7
8
|
*/
|
|
@@ -24,40 +25,60 @@ class DefaultPaymentService {
|
|
|
24
25
|
}
|
|
25
26
|
async updatePayment(opts) {
|
|
26
27
|
const maxRetries = 3;
|
|
27
|
-
let
|
|
28
|
-
for (let
|
|
28
|
+
let lastError;
|
|
29
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
29
30
|
const payment = await this.getPayment({ id: opts.id });
|
|
30
31
|
const actions = this.consolidateUpdateActions(payment, opts);
|
|
31
|
-
this.logger.info({ paymentId: payment.id, paymentVersion: payment.version, actions,
|
|
32
|
+
this.logger.info({ paymentId: payment.id, paymentVersion: payment.version, actions, attempt }, 'Updating payment with actions');
|
|
32
33
|
if (actions.length === 0) {
|
|
33
|
-
this.logger.info({ paymentId: payment.id, paymentVersion: payment.version,
|
|
34
|
+
this.logger.info({ paymentId: payment.id, paymentVersion: payment.version, attempt }, 'Update payment skipped, no actions to perform');
|
|
34
35
|
return payment;
|
|
35
36
|
}
|
|
36
37
|
try {
|
|
37
|
-
|
|
38
|
+
return await this.ctAPI.payment.updatePayment({
|
|
38
39
|
resource: {
|
|
39
40
|
id: payment.id,
|
|
40
41
|
version: payment.version,
|
|
41
42
|
},
|
|
42
43
|
actions,
|
|
43
44
|
});
|
|
44
|
-
return updatedPayment;
|
|
45
45
|
}
|
|
46
46
|
catch (e) {
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
lastError = e;
|
|
48
|
+
const isRetryableError = e instanceof ct_api_error_1.CommercetoolsAPIError &&
|
|
49
49
|
(e.httpErrorStatus === 409 ||
|
|
50
|
-
(e.httpErrorStatus === 400 && e.message.includes('already used without setting a payment interface')))
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
throw e;
|
|
50
|
+
(e.httpErrorStatus === 400 && e.message.includes('already used without setting a payment interface')));
|
|
51
|
+
if (isRetryableError && attempt < maxRetries - 1) {
|
|
52
|
+
const delayMs = this.calculateExponentialBackoff(attempt);
|
|
53
|
+
this.logger.warn({ paymentId: payment.id, paymentVersion: payment.version, attempt, delayMs, err: e }, 'Unable to update the payment, retrying');
|
|
54
|
+
await this.sleep(delayMs);
|
|
55
|
+
continue;
|
|
57
56
|
}
|
|
57
|
+
this.logger.error({ paymentId: payment.id, paymentVersion: payment.version, attempt, err: e }, 'Unable to update the payment, abort');
|
|
58
|
+
throw e;
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
|
-
throw
|
|
61
|
+
throw lastError;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Calculates exponential backoff delay with jitter
|
|
65
|
+
* @param attempt - Current attempt number (0-based)
|
|
66
|
+
* @returns Delay in milliseconds
|
|
67
|
+
*/
|
|
68
|
+
calculateExponentialBackoff(attempt) {
|
|
69
|
+
const baseDelayMs = 100;
|
|
70
|
+
const maxDelayMs = 2000;
|
|
71
|
+
const jitterMs = 100;
|
|
72
|
+
const exponentialDelay = baseDelayMs * Math.pow(2, attempt);
|
|
73
|
+
const jitter = (0, node_crypto_1.randomInt)(0, jitterMs);
|
|
74
|
+
return Math.min(exponentialDelay + jitter, maxDelayMs);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Sleep for a specified duration
|
|
78
|
+
* @param ms - Duration in milliseconds
|
|
79
|
+
*/
|
|
80
|
+
sleep(ms) {
|
|
81
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
61
82
|
}
|
|
62
83
|
consolidateUpdateActions(payment, updateInfo) {
|
|
63
84
|
const actions = [];
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { Logger } from '../..';
|
|
2
|
+
import { AuthorizationService } from '../types/authorization.type';
|
|
2
3
|
import { RecurringPaymentJob, RecurringPaymentJobDraft, RecurringPaymentJobService } from '../types/recurring-payment-job.type';
|
|
3
4
|
import { DefaultCartService } from './ct-cart.service';
|
|
4
5
|
/**
|
|
5
6
|
* Default implementation of the RecurringPaymentJobService interface.
|
|
6
7
|
*/
|
|
7
8
|
export declare class DefaultRecurringPaymentJobService implements RecurringPaymentJobService {
|
|
9
|
+
private authorizationService;
|
|
8
10
|
private ctCartService;
|
|
9
11
|
private checkoutUrl;
|
|
10
12
|
private projectKey;
|
|
11
13
|
private logger;
|
|
14
|
+
private token;
|
|
12
15
|
constructor(opts: {
|
|
16
|
+
authorizationService: AuthorizationService;
|
|
13
17
|
ctCartService: DefaultCartService;
|
|
14
18
|
checkoutUrl: string;
|
|
15
19
|
projectKey: string;
|
|
@@ -51,5 +55,9 @@ export declare class DefaultRecurringPaymentJobService implements RecurringPayme
|
|
|
51
55
|
* Internal method that performs the HTTP request to create a recurring payment job.
|
|
52
56
|
*/
|
|
53
57
|
private internalCreateRecurringPaymentJob;
|
|
58
|
+
/**
|
|
59
|
+
* Performs a fetch request with automatic token refresh on 401/403 errors.
|
|
60
|
+
*/
|
|
61
|
+
private fetchWithTokenRetry;
|
|
54
62
|
private safeParseErrorResponse;
|
|
55
63
|
}
|
|
@@ -6,11 +6,14 @@ const errorx_1 = require("../../errorx/errorx");
|
|
|
6
6
|
* Default implementation of the RecurringPaymentJobService interface.
|
|
7
7
|
*/
|
|
8
8
|
class DefaultRecurringPaymentJobService {
|
|
9
|
+
authorizationService;
|
|
9
10
|
ctCartService;
|
|
10
11
|
checkoutUrl;
|
|
11
12
|
projectKey;
|
|
12
13
|
logger;
|
|
14
|
+
token;
|
|
13
15
|
constructor(opts) {
|
|
16
|
+
this.authorizationService = opts.authorizationService;
|
|
14
17
|
this.ctCartService = opts.ctCartService;
|
|
15
18
|
this.checkoutUrl = opts.checkoutUrl;
|
|
16
19
|
this.projectKey = opts.projectKey;
|
|
@@ -75,11 +78,8 @@ class DefaultRecurringPaymentJobService {
|
|
|
75
78
|
state: 'Initial',
|
|
76
79
|
},
|
|
77
80
|
};
|
|
78
|
-
const response = await
|
|
81
|
+
const response = await this.fetchWithTokenRetry(url, {
|
|
79
82
|
method: 'POST',
|
|
80
|
-
headers: {
|
|
81
|
-
'Content-Type': 'application/json',
|
|
82
|
-
},
|
|
83
83
|
body: JSON.stringify(payload),
|
|
84
84
|
});
|
|
85
85
|
if (!response.ok) {
|
|
@@ -94,6 +94,35 @@ class DefaultRecurringPaymentJobService {
|
|
|
94
94
|
}
|
|
95
95
|
return (await response.json());
|
|
96
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Performs a fetch request with automatic token refresh on 401/403 errors.
|
|
99
|
+
*/
|
|
100
|
+
async fetchWithTokenRetry(url, options) {
|
|
101
|
+
if (!this.token) {
|
|
102
|
+
this.token = await this.authorizationService.getAccessToken();
|
|
103
|
+
}
|
|
104
|
+
let response = await fetch(url, {
|
|
105
|
+
...options,
|
|
106
|
+
headers: {
|
|
107
|
+
'Content-Type': 'application/json',
|
|
108
|
+
Authorization: `Bearer ${this.token.access_token}`,
|
|
109
|
+
...options.headers,
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
// If commercetools oauth token is expired, get a new one and retry once
|
|
113
|
+
if (response.status === 401 || response.status === 403) {
|
|
114
|
+
this.token = await this.authorizationService.getAccessToken();
|
|
115
|
+
response = await fetch(url, {
|
|
116
|
+
...options,
|
|
117
|
+
headers: {
|
|
118
|
+
'Content-Type': 'application/json',
|
|
119
|
+
Authorization: `Bearer ${this.token.access_token}`,
|
|
120
|
+
...options.headers,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return response;
|
|
125
|
+
}
|
|
97
126
|
async safeParseErrorResponse(response) {
|
|
98
127
|
try {
|
|
99
128
|
return await response.json();
|
package/dist/index.js
CHANGED
|
@@ -71,6 +71,7 @@ const setupPaymentSDK = (opts) => {
|
|
|
71
71
|
const ctPaymentMethodService = new ct_payment_method_service_1.DefaultPaymentMethodService({ ctAPI, logger });
|
|
72
72
|
const ctCustomTypeService = new ct_custom_type_service_1.DefaultCustomTypeService({ ctAPI, logger });
|
|
73
73
|
const ctRecurringPaymentJobService = new ct_recurring_payment_job_service_1.DefaultRecurringPaymentJobService({
|
|
74
|
+
authorizationService: ctAuthorizationService,
|
|
74
75
|
ctCartService,
|
|
75
76
|
checkoutUrl: opts.checkoutUrl,
|
|
76
77
|
projectKey: opts.projectKey,
|