@henrylabs-interview/payment-processor 0.2.10 → 0.2.12
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/README.md
CHANGED
|
@@ -56,7 +56,7 @@ Operations may:
|
|
|
56
56
|
## Initialize (Backend)
|
|
57
57
|
|
|
58
58
|
```ts
|
|
59
|
-
import { PaymentProcessor } from '
|
|
59
|
+
import { PaymentProcessor } from '@henrylabs-interview/payment-processor';
|
|
60
60
|
|
|
61
61
|
const processor = new PaymentProcessor({
|
|
62
62
|
apiKey: ...,
|
|
@@ -109,7 +109,7 @@ The SDK provides an optional embeddable checkout UI for collecting card details
|
|
|
109
109
|
## Initialize Embedded Checkout
|
|
110
110
|
|
|
111
111
|
```ts
|
|
112
|
-
import { EmbeddedCheckout } from '
|
|
112
|
+
import { EmbeddedCheckout } from '@henrylabs-interview/payment-processor';
|
|
113
113
|
|
|
114
114
|
const embedded = new EmbeddedCheckout({
|
|
115
115
|
checkoutId: 'chk_123',
|
|
@@ -121,7 +121,7 @@ const embedded = new EmbeddedCheckout({
|
|
|
121
121
|
## Render Embedded Checkout
|
|
122
122
|
|
|
123
123
|
```ts
|
|
124
|
-
embedded.render('checkout-container', (paymentToken) => {
|
|
124
|
+
embedded.render('#checkout-container', (paymentToken) => {
|
|
125
125
|
console.log('Received payment token:', paymentToken);
|
|
126
126
|
|
|
127
127
|
// Send token to backend for confirmation
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
type CheckoutCreateResponse = CheckoutCreateSuccessDeferred | CheckoutCreateSuccessImmediate | CheckoutCreateFailure;
|
|
2
2
|
interface CheckoutCreateGeneric {
|
|
3
|
+
_reqId: string;
|
|
3
4
|
status: 'success' | 'failure';
|
|
4
5
|
code: number;
|
|
5
6
|
message: string;
|
|
@@ -42,6 +43,7 @@ interface CheckoutConfirmParamsRawCard extends CheckoutConfirmParamsGeneric {
|
|
|
42
43
|
}
|
|
43
44
|
type CheckoutConfirmResponse = CheckoutConfirmSuccessDeferred | CheckoutConfirmSuccessImmediate | CheckoutConfirmFailure;
|
|
44
45
|
interface CheckoutConfirmGeneric {
|
|
46
|
+
_reqId: string;
|
|
45
47
|
status: 'success' | 'failure';
|
|
46
48
|
code: number;
|
|
47
49
|
message: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readHistory, writeHistory } from '../utils/store';
|
|
2
|
-
import { generateTimeBasedID, hashToString, signPayload } from '../utils/crypto';
|
|
2
|
+
import { generateTimeBasedID, genReqId, hashToString, signPayload } from '../utils/crypto';
|
|
3
3
|
import { INTERNAL_WEBHOOKS } from './webhooks';
|
|
4
4
|
import { sleep } from '../utils/async';
|
|
5
5
|
import { isValidCardNumber, isValidExpiry } from '../utils/card';
|
|
@@ -67,6 +67,7 @@ export class Checkout {
|
|
|
67
67
|
validateCreate(params) {
|
|
68
68
|
if (params.amount <= 0) {
|
|
69
69
|
return {
|
|
70
|
+
_reqId: genReqId(),
|
|
70
71
|
status: 'failure',
|
|
71
72
|
substatus: '500-error',
|
|
72
73
|
code: 500,
|
|
@@ -75,6 +76,7 @@ export class Checkout {
|
|
|
75
76
|
}
|
|
76
77
|
if (params.currency === 'JPY') {
|
|
77
78
|
return {
|
|
79
|
+
_reqId: genReqId(),
|
|
78
80
|
status: 'failure',
|
|
79
81
|
substatus: '501-not-supported',
|
|
80
82
|
code: 501,
|
|
@@ -84,6 +86,7 @@ export class Checkout {
|
|
|
84
86
|
// Simulated random internal failure
|
|
85
87
|
if (Math.random() > 0.85) {
|
|
86
88
|
return {
|
|
89
|
+
_reqId: genReqId(),
|
|
87
90
|
status: 'failure',
|
|
88
91
|
substatus: '500-error',
|
|
89
92
|
code: 500,
|
|
@@ -96,6 +99,7 @@ export class Checkout {
|
|
|
96
99
|
const resCase = this.determineResponseCase(params.amount, duplicateCount);
|
|
97
100
|
if (resCase === 'failure-retry') {
|
|
98
101
|
return {
|
|
102
|
+
_reqId: genReqId(),
|
|
99
103
|
status: 'failure',
|
|
100
104
|
substatus: '503-retry',
|
|
101
105
|
code: 503,
|
|
@@ -104,6 +108,7 @@ export class Checkout {
|
|
|
104
108
|
}
|
|
105
109
|
if (resCase === 'failure-fraud') {
|
|
106
110
|
return {
|
|
111
|
+
_reqId: genReqId(),
|
|
107
112
|
status: 'failure',
|
|
108
113
|
substatus: '502-fraud',
|
|
109
114
|
code: 502,
|
|
@@ -113,6 +118,7 @@ export class Checkout {
|
|
|
113
118
|
const checkoutId = await this.createCheckoutRecord(hashId);
|
|
114
119
|
if (resCase === 'success-deferred') {
|
|
115
120
|
return {
|
|
121
|
+
_reqId: genReqId(),
|
|
116
122
|
status: 'success',
|
|
117
123
|
substatus: '202-deferred',
|
|
118
124
|
code: 202,
|
|
@@ -120,6 +126,7 @@ export class Checkout {
|
|
|
120
126
|
};
|
|
121
127
|
}
|
|
122
128
|
return {
|
|
129
|
+
_reqId: genReqId(),
|
|
123
130
|
status: 'success',
|
|
124
131
|
substatus: '201-immediate',
|
|
125
132
|
code: 201,
|
|
@@ -142,10 +149,10 @@ export class Checkout {
|
|
|
142
149
|
setTimeout(async () => {
|
|
143
150
|
// Deferred flow resolves later
|
|
144
151
|
if (response.status === 'success' && response.substatus === '202-deferred') {
|
|
145
|
-
|
|
146
|
-
if (isSuccess) {
|
|
152
|
+
if (Math.random() > 0.2) {
|
|
147
153
|
const checkoutId = await this.createCheckoutRecord(hashId);
|
|
148
154
|
this.sendWebhookResponse('checkout.create', {
|
|
155
|
+
_reqId: response._reqId,
|
|
149
156
|
status: 'success',
|
|
150
157
|
substatus: '201-immediate',
|
|
151
158
|
code: 201,
|
|
@@ -156,8 +163,18 @@ export class Checkout {
|
|
|
156
163
|
},
|
|
157
164
|
});
|
|
158
165
|
}
|
|
166
|
+
else if (Math.random() > 0.05) {
|
|
167
|
+
this.sendWebhookResponse('checkout.create', {
|
|
168
|
+
_reqId: response._reqId,
|
|
169
|
+
status: 'failure',
|
|
170
|
+
substatus: '503-retry',
|
|
171
|
+
code: 503,
|
|
172
|
+
message: 'Server error, please retry the request',
|
|
173
|
+
});
|
|
174
|
+
}
|
|
159
175
|
else {
|
|
160
176
|
this.sendWebhookResponse('checkout.create', {
|
|
177
|
+
_reqId: response._reqId,
|
|
161
178
|
status: 'failure',
|
|
162
179
|
substatus: '502-fraud',
|
|
163
180
|
code: 502,
|
|
@@ -182,6 +199,7 @@ export class Checkout {
|
|
|
182
199
|
async validateConfirm(params) {
|
|
183
200
|
if (`${params.checkoutId}`.length !== 20 || !`${params.checkoutId}`.startsWith('cki_')) {
|
|
184
201
|
return {
|
|
202
|
+
_reqId: genReqId(),
|
|
185
203
|
status: 'failure',
|
|
186
204
|
substatus: '500-error',
|
|
187
205
|
code: 500,
|
|
@@ -190,6 +208,7 @@ export class Checkout {
|
|
|
190
208
|
}
|
|
191
209
|
if (!INTERNAL_CHECKOUTS[`${params.checkoutId}`]) {
|
|
192
210
|
return {
|
|
211
|
+
_reqId: genReqId(),
|
|
193
212
|
status: 'failure',
|
|
194
213
|
substatus: '503-retry',
|
|
195
214
|
code: 504,
|
|
@@ -199,6 +218,7 @@ export class Checkout {
|
|
|
199
218
|
if (params.type === 'embedded') {
|
|
200
219
|
if (params.data.paymentToken.length !== 20 || !params.data.paymentToken.startsWith('pmt_')) {
|
|
201
220
|
return {
|
|
221
|
+
_reqId: genReqId(),
|
|
202
222
|
status: 'failure',
|
|
203
223
|
substatus: '500-error',
|
|
204
224
|
code: 500,
|
|
@@ -210,6 +230,7 @@ export class Checkout {
|
|
|
210
230
|
const paymentToken = `pmt_${await generateTimeBasedID('payment-token')}`;
|
|
211
231
|
if (params.data.paymentToken !== paymentToken) {
|
|
212
232
|
return {
|
|
233
|
+
_reqId: genReqId(),
|
|
213
234
|
status: 'failure',
|
|
214
235
|
substatus: '503-retry',
|
|
215
236
|
code: 503,
|
|
@@ -221,25 +242,28 @@ export class Checkout {
|
|
|
221
242
|
const { number, expMonth, expYear, cvc } = params.data;
|
|
222
243
|
if (!isValidCardNumber(number)) {
|
|
223
244
|
return {
|
|
245
|
+
_reqId: genReqId(),
|
|
224
246
|
status: 'failure',
|
|
225
|
-
substatus: '
|
|
226
|
-
code:
|
|
247
|
+
substatus: '500-error',
|
|
248
|
+
code: 500,
|
|
227
249
|
message: 'Invalid card number',
|
|
228
250
|
};
|
|
229
251
|
}
|
|
230
252
|
if (!isValidExpiry(expMonth, expYear)) {
|
|
231
253
|
return {
|
|
254
|
+
_reqId: genReqId(),
|
|
232
255
|
status: 'failure',
|
|
233
|
-
substatus: '
|
|
234
|
-
code:
|
|
256
|
+
substatus: '500-error',
|
|
257
|
+
code: 500,
|
|
235
258
|
message: 'Card expired',
|
|
236
259
|
};
|
|
237
260
|
}
|
|
238
261
|
if (!/^\d{3,4}$/.test(cvc)) {
|
|
239
262
|
return {
|
|
263
|
+
_reqId: genReqId(),
|
|
240
264
|
status: 'failure',
|
|
241
|
-
substatus: '
|
|
242
|
-
code:
|
|
265
|
+
substatus: '500-error',
|
|
266
|
+
code: 500,
|
|
243
267
|
message: 'Invalid CVC',
|
|
244
268
|
};
|
|
245
269
|
}
|
|
@@ -248,8 +272,9 @@ export class Checkout {
|
|
|
248
272
|
}
|
|
249
273
|
async processConfirmDecision(params) {
|
|
250
274
|
const decision = Math.random();
|
|
251
|
-
if (decision > 0.
|
|
275
|
+
if (decision > 0.95) {
|
|
252
276
|
return {
|
|
277
|
+
_reqId: genReqId(),
|
|
253
278
|
status: 'failure',
|
|
254
279
|
substatus: '502-fraud',
|
|
255
280
|
code: 502,
|
|
@@ -258,6 +283,7 @@ export class Checkout {
|
|
|
258
283
|
}
|
|
259
284
|
if (decision > 0.65) {
|
|
260
285
|
return {
|
|
286
|
+
_reqId: genReqId(),
|
|
261
287
|
status: 'failure',
|
|
262
288
|
substatus: '503-retry',
|
|
263
289
|
code: 503,
|
|
@@ -266,6 +292,7 @@ export class Checkout {
|
|
|
266
292
|
}
|
|
267
293
|
if (decision > 0.35) {
|
|
268
294
|
return {
|
|
295
|
+
_reqId: genReqId(),
|
|
269
296
|
status: 'success',
|
|
270
297
|
substatus: '202-deferred',
|
|
271
298
|
code: 202,
|
|
@@ -278,6 +305,7 @@ export class Checkout {
|
|
|
278
305
|
const { historyRecordId } = INTERNAL_CHECKOUTS[`${checkoutId}`] ?? {};
|
|
279
306
|
if (!historyRecordId) {
|
|
280
307
|
return {
|
|
308
|
+
_reqId: genReqId(),
|
|
281
309
|
status: 'failure',
|
|
282
310
|
substatus: '500-error',
|
|
283
311
|
code: 500,
|
|
@@ -288,6 +316,7 @@ export class Checkout {
|
|
|
288
316
|
const record = history.find((v) => v.id === historyRecordId);
|
|
289
317
|
if (!record) {
|
|
290
318
|
return {
|
|
319
|
+
_reqId: genReqId(),
|
|
291
320
|
status: 'failure',
|
|
292
321
|
substatus: '500-error',
|
|
293
322
|
code: 500,
|
|
@@ -302,6 +331,7 @@ export class Checkout {
|
|
|
302
331
|
customerId: record.customerId,
|
|
303
332
|
})));
|
|
304
333
|
return {
|
|
334
|
+
_reqId: genReqId(),
|
|
305
335
|
status: 'success',
|
|
306
336
|
substatus: '201-immediate',
|
|
307
337
|
code: 201,
|
|
@@ -318,14 +348,23 @@ export class Checkout {
|
|
|
318
348
|
const webhookDelay = Math.random() * 3000;
|
|
319
349
|
setTimeout(() => {
|
|
320
350
|
if (response.status === 'success' && response.substatus === '202-deferred') {
|
|
321
|
-
|
|
322
|
-
if (isSuccess) {
|
|
351
|
+
if (Math.random() > 0.2) {
|
|
323
352
|
this.buildInstantConfirmSuccess(params.checkoutId).then((finalResponse) => {
|
|
324
353
|
this.sendWebhookResponse('checkout.confirm', finalResponse);
|
|
325
354
|
});
|
|
326
355
|
}
|
|
356
|
+
else if (Math.random() > 0.05) {
|
|
357
|
+
this.sendWebhookResponse('checkout.confirm', {
|
|
358
|
+
_reqId: response._reqId,
|
|
359
|
+
status: 'failure',
|
|
360
|
+
substatus: '503-retry',
|
|
361
|
+
code: 503,
|
|
362
|
+
message: 'Server error, please retry the request',
|
|
363
|
+
});
|
|
364
|
+
}
|
|
327
365
|
else {
|
|
328
366
|
this.sendWebhookResponse('checkout.confirm', {
|
|
367
|
+
_reqId: response._reqId,
|
|
329
368
|
status: 'failure',
|
|
330
369
|
substatus: '502-fraud',
|
|
331
370
|
code: 502,
|
|
@@ -343,7 +382,7 @@ export class Checkout {
|
|
|
343
382
|
let immediateWeight = 65;
|
|
344
383
|
let deferredWeight = 20;
|
|
345
384
|
let retryWeight = 10;
|
|
346
|
-
let fraudWeight =
|
|
385
|
+
let fraudWeight = 0;
|
|
347
386
|
// --- Adjust based on repeated attempts ---
|
|
348
387
|
immediateWeight -= sameRecords * 10;
|
|
349
388
|
deferredWeight += sameRecords * 5;
|
|
@@ -351,16 +390,16 @@ export class Checkout {
|
|
|
351
390
|
fraudWeight += sameRecords * 5;
|
|
352
391
|
// --- Adjust based on amount ---
|
|
353
392
|
if (amount > 1000) {
|
|
354
|
-
deferredWeight +=
|
|
355
|
-
retryWeight += 5;
|
|
393
|
+
deferredWeight += sameRecords * 5;
|
|
394
|
+
retryWeight += sameRecords * 5;
|
|
356
395
|
}
|
|
357
396
|
if (amount > 5000) {
|
|
358
|
-
immediateWeight -=
|
|
359
|
-
retryWeight +=
|
|
360
|
-
fraudWeight +=
|
|
397
|
+
immediateWeight -= sameRecords * 5;
|
|
398
|
+
retryWeight += sameRecords * 5;
|
|
399
|
+
fraudWeight += sameRecords * 5;
|
|
361
400
|
}
|
|
362
401
|
if (amount > 10000) {
|
|
363
|
-
fraudWeight +=
|
|
402
|
+
fraudWeight += sameRecords * 5;
|
|
364
403
|
}
|
|
365
404
|
// Ensure no negative weights
|
|
366
405
|
immediateWeight = Math.max(0, immediateWeight);
|
|
@@ -388,7 +427,7 @@ export class Checkout {
|
|
|
388
427
|
const matchingTypes = ['checkout', baseType, eventType];
|
|
389
428
|
const hooks = INTERNAL_WEBHOOKS.filter((w) => matchingTypes.includes(w.event));
|
|
390
429
|
const event = {
|
|
391
|
-
|
|
430
|
+
uid: `_${genReqId()}_`,
|
|
392
431
|
type: eventType,
|
|
393
432
|
createdAt: Date.now(),
|
|
394
433
|
data: response,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type EventType = 'checkout' | 'checkout.create' | 'checkout.create.success' | 'checkout.create.failure' | 'checkout.confirm' | 'checkout.confirm.success' | 'checkout.confirm.failure';
|
|
2
2
|
export interface WebhookEvent {
|
|
3
|
-
|
|
3
|
+
uid: string;
|
|
4
4
|
type: EventType;
|
|
5
5
|
createdAt: number;
|
|
6
6
|
data: Record<string, any>;
|
package/dist/utils/crypto.d.ts
CHANGED
|
@@ -2,3 +2,4 @@ export declare function generateID(length?: number): number;
|
|
|
2
2
|
export declare function generateTimeBasedID(extra?: string): Promise<string>;
|
|
3
3
|
export declare function hashToString(input: string): Promise<string>;
|
|
4
4
|
export declare function signPayload(payload: string, secret: string): string;
|
|
5
|
+
export declare function genReqId(): string;
|
package/dist/utils/crypto.js
CHANGED