@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 'henry-labs/take-home';
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 'henry-labs/take-home';
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
- const isSuccess = Math.random() > 0.35;
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: '502-fraud',
226
- code: 502,
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: '503-retry',
234
- code: 503,
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: '502-fraud',
242
- code: 502,
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.85) {
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
- const isSuccess = Math.random() > 0.35;
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 = 5;
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 += 10;
355
- retryWeight += 5;
393
+ deferredWeight += sameRecords * 5;
394
+ retryWeight += sameRecords * 5;
356
395
  }
357
396
  if (amount > 5000) {
358
- immediateWeight -= 10;
359
- retryWeight += 10;
360
- fraudWeight += 10;
397
+ immediateWeight -= sameRecords * 5;
398
+ retryWeight += sameRecords * 5;
399
+ fraudWeight += sameRecords * 5;
361
400
  }
362
401
  if (amount > 10000) {
363
- fraudWeight += 20;
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
- id: crypto.randomUUID(),
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
- id: string;
3
+ uid: string;
4
4
  type: EventType;
5
5
  createdAt: number;
6
6
  data: Record<string, any>;
@@ -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;
@@ -23,3 +23,6 @@ export async function hashToString(input) {
23
23
  export function signPayload(payload, secret) {
24
24
  return crypto.createHmac('sha256', secret).update(payload).digest('hex');
25
25
  }
26
+ export function genReqId() {
27
+ return crypto.randomUUID().split('-')[0] ?? '';
28
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henrylabs-interview/payment-processor",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",