@henrylabs-interview/payment-processor 0.1.2 → 0.1.4

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
@@ -1,15 +1,151 @@
1
- # henry-labs/take-home
1
+ # Henry Labs - Interview: Payment Processor
2
2
 
3
- To install dependencies:
3
+ A lightweight payments SDK for creating and confirming checkouts, with built-in webhook support.
4
+
5
+ This SDK simulates a real-world payment processing system, including fraud detection, transient errors, retries, and asynchronous authorization flows.
6
+
7
+ ---
8
+
9
+ ## Installation
4
10
 
5
11
  ```bash
6
12
  bun install
7
13
  ```
8
14
 
9
- To run:
15
+ Build:
10
16
 
11
17
  ```bash
12
- bun run index.ts
18
+ bun run build
19
+ ```
20
+
21
+ ---
22
+
23
+ ## Overview
24
+
25
+ The SDK provides three primary capabilities:
26
+
27
+ - Create a checkout
28
+ - Confirm a checkout
29
+ - Register webhook endpoints
30
+
31
+ The system performs internal validation, fraud screening, and risk checks.
32
+ Depending on system conditions, operations may:
33
+
34
+ - Succeed immediately
35
+ - Succeed asynchronously
36
+ - Require a retry
37
+ - Fail due to risk controls
38
+ - Fail due to temporary system overload
39
+
40
+ Consumers should always handle both synchronous responses and webhook events.
41
+
42
+ ---
43
+
44
+ ## Usage
45
+
46
+ ### Initialize
47
+
48
+ ```ts
49
+ import { PaymentSDK } from 'henry-labs/take-home';
50
+
51
+ const sdk = new PaymentSDK();
52
+ ```
53
+
54
+ ---
55
+
56
+ ## Create a Checkout
57
+
58
+ ```ts
59
+ const response = await sdk.create({
60
+ amount: 1000,
61
+ currency: 'USD',
62
+ customerId: 'cust_123',
63
+ });
64
+ ```
65
+
66
+ ### Behavior
67
+
68
+ Creating a checkout may:
69
+
70
+ - Return immediate approval
71
+ - Return a pending authorization state
72
+ - Return a retryable error
73
+ - Return a fraud-related failure
74
+ - Fail due to temporary internal errors
75
+
76
+ If the checkout is authorized asynchronously, the final outcome will be delivered via webhook.
77
+
78
+ ---
79
+
80
+ ## Confirm a Checkout
81
+
82
+ ```ts
83
+ const response = await sdk.confirm({
84
+ checkoutId: '...',
85
+ type: 'raw-card',
86
+ data: {
87
+ number: '4242424242424242',
88
+ expMonth: 12,
89
+ expYear: 2030,
90
+ cvc: '123',
91
+ },
92
+ });
93
+ ```
94
+
95
+ ### Behavior
96
+
97
+ Confirmation requests are subject to:
98
+
99
+ - Card validation
100
+ - Fraud screening
101
+ - Retry conditions
102
+ - System load conditions
103
+
104
+ Confirmations may resolve immediately or asynchronously.
105
+ Webhook handling is required to receive final outcomes in deferred cases.
106
+
107
+ ---
108
+
109
+ ## Webhooks
110
+
111
+ You can register webhook endpoints to receive event notifications.
112
+
113
+ ```ts
114
+ sdk.webhooks.createEndpoint({
115
+ url: 'https://example.com/webhooks',
116
+ event: 'checkout.confirm',
117
+ secret: 'whsec_...',
118
+ });
13
119
  ```
14
120
 
15
- This project was created using `bun init` in bun v1.3.9. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
121
+ ### Webhook Events
122
+
123
+ Events are emitted for:
124
+
125
+ - Checkout creation
126
+ - Checkout confirmation
127
+ - Success outcomes
128
+ - Failure outcomes
129
+
130
+ Webhooks may be triggered for both synchronous and asynchronous results.
131
+
132
+ All webhook deliveries are signed if a secret is provided.
133
+
134
+ Consumers are responsible for verifying signatures and handling retries idempotently.
135
+
136
+ ---
137
+
138
+ ## Important Notes
139
+
140
+ - Not all operations resolve immediately.
141
+ - Some failures are retryable.
142
+ - The system may simulate temporary overload conditions.
143
+ - Fraud checks may result in blocked transactions.
144
+ - Consumers should implement robust retry and webhook handling logic.
145
+
146
+ ---
147
+
148
+ ## Disclaimer
149
+
150
+ This SDK is intended for evaluation and sandbox purposes only.
151
+ It does not process real payments.
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
- export interface ClientOptions {
2
- apiKey: string;
3
- }
4
- export declare class MySDK {
5
- private apiKey;
6
- constructor(options: ClientOptions);
7
- getData(): Promise<unknown>;
1
+ import { Checkout } from './resources/checkout';
2
+ import { Webhooks } from './resources/webhooks';
3
+ export declare class HenryTakeHomeSDK {
4
+ checkout: Checkout;
5
+ webhooks: Webhooks;
6
+ constructor(config: {
7
+ apiKey: string;
8
+ });
8
9
  }
package/dist/index.js CHANGED
@@ -1,7 +1,20 @@
1
+ var __create = Object.create;
2
+ var __getProtoOf = Object.getPrototypeOf;
1
3
  var __defProp = Object.defineProperty;
2
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
3
5
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
6
  var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __toESM = (mod, isNodeMode, target) => {
8
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
9
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
10
+ for (let key of __getOwnPropNames(mod))
11
+ if (!__hasOwnProp.call(to, key))
12
+ __defProp(to, key, {
13
+ get: () => mod[key],
14
+ enumerable: true
15
+ });
16
+ return to;
17
+ };
5
18
  var __moduleCache = /* @__PURE__ */ new WeakMap;
6
19
  var __toCommonJS = (from) => {
7
20
  var entry = __moduleCache.get(from), desc;
@@ -29,20 +42,442 @@ var __export = (target, all) => {
29
42
  // src/index.ts
30
43
  var exports_src = {};
31
44
  __export(exports_src, {
32
- MySDK: () => MySDK
45
+ HenryTakeHomeSDK: () => HenryTakeHomeSDK
33
46
  });
34
47
  module.exports = __toCommonJS(exports_src);
35
48
 
36
- class MySDK {
37
- apiKey;
38
- constructor(options) {
39
- this.apiKey = options.apiKey;
49
+ // src/utils/store.ts
50
+ var path = new URL("../db-store/history.json", "file:///Users/aaroncassar/Projects/take-home/src/utils/store.ts");
51
+ async function readHistory() {
52
+ const file = Bun.file(path);
53
+ if (!await file.exists()) {
54
+ return [];
55
+ }
56
+ const text = await file.text();
57
+ if (!text)
58
+ return [];
59
+ try {
60
+ return JSON.parse(text);
61
+ } catch {
62
+ return [];
63
+ }
64
+ }
65
+
66
+ // src/utils/crypto.ts
67
+ var import_crypto = __toESM(require("crypto"));
68
+ function generateID(length = 12) {
69
+ const digits = [];
70
+ const bytes = import_crypto.default.randomBytes(length);
71
+ for (let i = 0;i < length; i++) {
72
+ digits.push((bytes[i] ?? 0) % 10);
73
+ }
74
+ return parseInt(digits.join(""));
75
+ }
76
+ function hashToNumber(input, length = 12) {
77
+ if (length > 16) {
78
+ throw new Error("Length is greater than max length of 16!");
79
+ }
80
+ const hash = import_crypto.default.createHash("sha256").update(input).digest("hex");
81
+ return parseInt(hash.slice(0, length), 16);
82
+ }
83
+ function signPayload(payload, secret) {
84
+ return import_crypto.default.createHmac("sha256", secret).update(payload).digest("hex");
85
+ }
86
+
87
+ // src/utils/async.ts
88
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
89
+
90
+ // src/resources/webhooks.ts
91
+ var INTERNAL_WEBHOOKS = [];
92
+
93
+ class Webhooks {
94
+ async createEndpoint(params) {
95
+ for (const event of params.events) {
96
+ await sleep(Math.random() * 100);
97
+ INTERNAL_WEBHOOKS.push({
98
+ url: params.url,
99
+ event,
100
+ secret: params.secret
101
+ });
102
+ }
103
+ return true;
104
+ }
105
+ }
106
+
107
+ // src/resources/checkout.ts
108
+ var INTERNAL_CHECKOUTS = {};
109
+ var INTERNAL_CARDS = {};
110
+
111
+ class Checkout {
112
+ async create(params) {
113
+ await sleep(Math.random() * 100);
114
+ const hashId = this.buildHistoryHash(params);
115
+ const history = await readHistory();
116
+ const sameRecords = history.filter((v) => v.id === hashId);
117
+ const validationFailure = this.validateCreate(params);
118
+ const response = validationFailure ?? this.processCreateDecision(params, hashId, sameRecords.length);
119
+ this.scheduleCreateWebhook(hashId, response);
120
+ await sleep(Math.random() * 2000);
121
+ return response;
122
+ }
123
+ async confirm(params) {
124
+ await sleep(Math.random() * 100);
125
+ const validationFailure = await this.validateConfirm(params);
126
+ const response = validationFailure ?? await this.processConfirmDecision(params);
127
+ this.scheduleConfirmWebhook(params, response);
128
+ await sleep(Math.random() * 2000);
129
+ return response;
130
+ }
131
+ validateCreate(params) {
132
+ if (params.amount <= 0) {
133
+ return {
134
+ status: "failure",
135
+ substatus: "500-error",
136
+ code: 500,
137
+ message: "Invalid amount"
138
+ };
139
+ }
140
+ if (params.currency === "JPY") {
141
+ return {
142
+ status: "failure",
143
+ substatus: "501-not-supported",
144
+ code: 501,
145
+ message: "This currency is currently not supported, please convert first."
146
+ };
147
+ }
148
+ if (Math.random() > 0.85) {
149
+ return {
150
+ status: "failure",
151
+ substatus: "500-error",
152
+ code: 500,
153
+ message: "An internal error occurred when creating checkout"
154
+ };
155
+ }
156
+ return null;
157
+ }
158
+ processCreateDecision(params, hashId, duplicateCount) {
159
+ const resCase = this.determineResponseCase(params.amount, duplicateCount);
160
+ if (resCase === "failure-retry") {
161
+ return {
162
+ status: "failure",
163
+ substatus: "503-retry",
164
+ code: 503,
165
+ message: "Server is busy, please retry the request"
166
+ };
167
+ }
168
+ if (resCase === "failure-fraud") {
169
+ return {
170
+ status: "failure",
171
+ substatus: "502-fraud",
172
+ code: 502,
173
+ message: "Potential fraud detected with this purchase"
174
+ };
175
+ }
176
+ const checkoutId = this.createCheckoutRecord(hashId);
177
+ if (resCase === "success-deferred") {
178
+ return {
179
+ status: "success",
180
+ substatus: "202-deferred",
181
+ code: 202,
182
+ message: "Authorizing, checkout information will likely be returned via webhook"
183
+ };
184
+ }
185
+ return {
186
+ status: "success",
187
+ substatus: "201-immediate",
188
+ code: 201,
189
+ message: "Approved",
190
+ data: {
191
+ checkoutId,
192
+ paymentMethodOptions: ["embedded", "raw-card"]
193
+ }
194
+ };
195
+ }
196
+ createCheckoutRecord(hashId) {
197
+ const checkoutId = generateID();
198
+ INTERNAL_CHECKOUTS[checkoutId] = {
199
+ historyRecordId: hashId
200
+ };
201
+ return checkoutId;
202
+ }
203
+ scheduleCreateWebhook(hashId, response) {
204
+ const webhookDelay = Math.random() * 3000;
205
+ setTimeout(() => {
206
+ if (response.status === "success" && response.substatus === "202-deferred") {
207
+ const isSuccess = Math.random() > 0.35;
208
+ if (isSuccess) {
209
+ const checkoutId = this.createCheckoutRecord(hashId);
210
+ this.sendWebhookResponse("checkout.create", {
211
+ status: "success",
212
+ substatus: "201-immediate",
213
+ code: 201,
214
+ message: "Approved",
215
+ data: {
216
+ checkoutId,
217
+ paymentMethodOptions: ["embedded", "raw-card"]
218
+ }
219
+ });
220
+ } else {
221
+ this.sendWebhookResponse("checkout.create", {
222
+ status: "failure",
223
+ substatus: "502-fraud",
224
+ code: 502,
225
+ message: "Potential fraud detected with this purchase"
226
+ });
227
+ }
228
+ return;
229
+ }
230
+ this.sendWebhookResponse("checkout.create", response);
231
+ }, webhookDelay);
232
+ }
233
+ buildHistoryHash(params) {
234
+ return hashToNumber(JSON.stringify({
235
+ type: "HISTORY_RECORD",
236
+ amount: params.amount,
237
+ currency: params.currency,
238
+ customerId: params.customerId
239
+ }));
240
+ }
241
+ async validateConfirm(params) {
242
+ if (!INTERNAL_CHECKOUTS[params.checkoutId]) {
243
+ return {
244
+ status: "failure",
245
+ substatus: "500-error",
246
+ code: 500,
247
+ message: "Invalid checkout ID"
248
+ };
249
+ }
250
+ if (params.type === "embedded") {
251
+ const stored = INTERNAL_CARDS[params.data.paymentToken];
252
+ if (!stored) {
253
+ return {
254
+ status: "failure",
255
+ substatus: "500-error",
256
+ code: 500,
257
+ message: "Invalid payment token"
258
+ };
259
+ }
260
+ }
261
+ if (params.type === "raw-card") {
262
+ const { number, expMonth, expYear, cvc } = params.data;
263
+ if (!this.isValidCardNumber(number)) {
264
+ return {
265
+ status: "failure",
266
+ substatus: "502-fraud",
267
+ code: 502,
268
+ message: "Invalid card number"
269
+ };
270
+ }
271
+ if (!this.isValidExpiry(expMonth, expYear)) {
272
+ return {
273
+ status: "failure",
274
+ substatus: "503-retry",
275
+ code: 503,
276
+ message: "Card expired"
277
+ };
278
+ }
279
+ if (!/^\d{3,4}$/.test(cvc)) {
280
+ return {
281
+ status: "failure",
282
+ substatus: "502-fraud",
283
+ code: 502,
284
+ message: "Invalid CVC"
285
+ };
286
+ }
287
+ }
288
+ return null;
289
+ }
290
+ async processConfirmDecision(params) {
291
+ const decision = Math.random();
292
+ if (decision > 0.85) {
293
+ return {
294
+ status: "failure",
295
+ substatus: "502-fraud",
296
+ code: 502,
297
+ message: "Potential fraud detected with this purchase"
298
+ };
299
+ }
300
+ if (decision > 0.65) {
301
+ return {
302
+ status: "failure",
303
+ substatus: "503-retry",
304
+ code: 503,
305
+ message: "Server is busy, please retry the request"
306
+ };
307
+ }
308
+ if (decision > 0.35) {
309
+ return {
310
+ status: "success",
311
+ substatus: "202-deferred",
312
+ code: 202,
313
+ message: "Authorizing, purchase information will likely be returned via webhook"
314
+ };
315
+ }
316
+ return this.buildInstantConfirmSuccess(params.checkoutId);
317
+ }
318
+ async buildInstantConfirmSuccess(checkoutId) {
319
+ const { historyRecordId } = INTERNAL_CHECKOUTS[checkoutId] ?? {};
320
+ if (!historyRecordId) {
321
+ return {
322
+ status: "failure",
323
+ substatus: "500-error",
324
+ code: 500,
325
+ message: "Missing history record"
326
+ };
327
+ }
328
+ const history = await readHistory();
329
+ const record = history.find((v) => v.id === historyRecordId);
330
+ if (!record) {
331
+ return {
332
+ status: "failure",
333
+ substatus: "500-error",
334
+ code: 500,
335
+ message: "Missing history record"
336
+ };
337
+ }
338
+ const confirmationId = hashToNumber(JSON.stringify({
339
+ type: "CONFIRMATION_ID",
340
+ amount: record.amount,
341
+ currency: record.currency,
342
+ customerId: record.customerId
343
+ }));
344
+ return {
345
+ status: "success",
346
+ substatus: "201-immediate",
347
+ code: 201,
348
+ message: "Approved",
349
+ data: {
350
+ confirmationId,
351
+ amount: record.amount,
352
+ currency: record.currency,
353
+ customerId: record.customerId ?? undefined
354
+ }
355
+ };
40
356
  }
41
- async getData() {
42
- return fetch("https://api.example.com/data", {
43
- headers: {
44
- Authorization: `Bearer ${this.apiKey}`
357
+ scheduleConfirmWebhook(params, response) {
358
+ const webhookDelay = Math.random() * 3000;
359
+ setTimeout(() => {
360
+ if (response.status === "success" && response.substatus === "202-deferred") {
361
+ const isSuccess = Math.random() > 0.35;
362
+ if (isSuccess) {
363
+ this.buildInstantConfirmSuccess(params.checkoutId).then((finalResponse) => {
364
+ this.sendWebhookResponse("checkout.confirm", finalResponse);
365
+ });
366
+ } else {
367
+ this.sendWebhookResponse("checkout.confirm", {
368
+ status: "failure",
369
+ substatus: "502-fraud",
370
+ code: 502,
371
+ message: "Potential fraud detected with this purchase"
372
+ });
373
+ }
374
+ return;
45
375
  }
46
- }).then((res) => res.json());
376
+ this.sendWebhookResponse("checkout.confirm", response);
377
+ }, webhookDelay);
378
+ }
379
+ isValidCardNumber(number) {
380
+ if (number === "4242424242424242")
381
+ return true;
382
+ if (!/^\d{13,19}$/.test(number))
383
+ return false;
384
+ let sum = 0;
385
+ let shouldDouble = false;
386
+ for (let i = number.length - 1;i >= 0; i--) {
387
+ let digit = parseInt(number[i] ?? "");
388
+ if (shouldDouble) {
389
+ digit *= 2;
390
+ if (digit > 9)
391
+ digit -= 9;
392
+ }
393
+ sum += digit;
394
+ shouldDouble = !shouldDouble;
395
+ }
396
+ return sum % 10 === 0;
397
+ }
398
+ isValidExpiry(month, year) {
399
+ if (month < 1 || month > 12)
400
+ return false;
401
+ const now = new Date;
402
+ const expiry = new Date(year, month - 1);
403
+ return expiry > now;
404
+ }
405
+ determineResponseCase(amount, sameRecords) {
406
+ let immediateWeight = 65;
407
+ let deferredWeight = 20;
408
+ let retryWeight = 10;
409
+ let fraudWeight = 5;
410
+ immediateWeight -= sameRecords * 10;
411
+ deferredWeight += sameRecords * 5;
412
+ retryWeight += sameRecords * 5;
413
+ fraudWeight += sameRecords * 5;
414
+ if (amount > 1000) {
415
+ deferredWeight += 10;
416
+ retryWeight += 5;
417
+ }
418
+ if (amount > 5000) {
419
+ immediateWeight -= 10;
420
+ retryWeight += 10;
421
+ fraudWeight += 10;
422
+ }
423
+ if (amount > 1e4) {
424
+ fraudWeight += 20;
425
+ }
426
+ immediateWeight = Math.max(0, immediateWeight);
427
+ deferredWeight = Math.max(0, deferredWeight);
428
+ retryWeight = Math.max(0, retryWeight);
429
+ fraudWeight = Math.max(0, fraudWeight);
430
+ const total = immediateWeight + deferredWeight + retryWeight + fraudWeight;
431
+ const rand = Math.random() * total;
432
+ if (rand < immediateWeight) {
433
+ return "success-immediate";
434
+ }
435
+ if (rand < immediateWeight + deferredWeight) {
436
+ return "success-deferred";
437
+ }
438
+ if (rand < immediateWeight + deferredWeight + retryWeight) {
439
+ return "failure-retry";
440
+ }
441
+ return "failure-fraud";
442
+ }
443
+ async sendWebhookResponse(baseType, response) {
444
+ const statusSuffix = response.status === "success" ? "success" : "failure";
445
+ const eventType = `${baseType}.${statusSuffix}`;
446
+ const matchingTypes = ["checkout", baseType, eventType];
447
+ const hooks = INTERNAL_WEBHOOKS.filter((w) => matchingTypes.includes(w.event));
448
+ const event = {
449
+ id: crypto.randomUUID(),
450
+ type: eventType,
451
+ createdAt: Date.now(),
452
+ data: response
453
+ };
454
+ const payload = JSON.stringify(event);
455
+ await Promise.allSettled(hooks.map(async (hook) => {
456
+ const headers = {
457
+ "Content-Type": "application/json"
458
+ };
459
+ if (hook.secret) {
460
+ headers["webhook-signature"] = signPayload(payload, hook.secret);
461
+ }
462
+ await fetch(hook.url, {
463
+ method: "POST",
464
+ headers,
465
+ body: payload
466
+ });
467
+ }));
468
+ return true;
469
+ }
470
+ }
471
+
472
+ // src/index.ts
473
+ class HenryTakeHomeSDK {
474
+ checkout;
475
+ webhooks;
476
+ constructor(config) {
477
+ if (!config.apiKey) {
478
+ throw new Error("Henry: apiKey is required");
479
+ }
480
+ this.checkout = new Checkout;
481
+ this.webhooks = new Webhooks;
47
482
  }
48
483
  }
@@ -0,0 +1,100 @@
1
+ type CheckoutCreateResponse = CheckoutCreateSuccessDeferred | CheckoutCreateSuccessImmediate | CheckoutCreateFailure;
2
+ interface CheckoutCreateGeneric {
3
+ status: 'success' | 'failure';
4
+ code: number;
5
+ message: string;
6
+ }
7
+ interface CheckoutCreateSuccessDeferred extends CheckoutCreateGeneric {
8
+ status: 'success';
9
+ substatus: '202-deferred';
10
+ }
11
+ interface CheckoutCreateSuccessImmediate extends CheckoutCreateGeneric {
12
+ status: 'success';
13
+ substatus: '201-immediate';
14
+ data: {
15
+ checkoutId: number;
16
+ paymentMethodOptions: string[];
17
+ };
18
+ }
19
+ interface CheckoutCreateFailure extends CheckoutCreateGeneric {
20
+ status: 'failure';
21
+ substatus: '500-error' | '501-not-supported' | '502-fraud' | '503-retry';
22
+ }
23
+ type CheckoutConfirmParams = CheckoutConfirmParamsEmbedded | CheckoutConfirmParamsRawCard;
24
+ interface CheckoutConfirmParamsGeneric {
25
+ checkoutId: string;
26
+ type: 'embedded' | 'raw-card';
27
+ }
28
+ interface CheckoutConfirmParamsEmbedded extends CheckoutConfirmParamsGeneric {
29
+ type: 'embedded';
30
+ data: {
31
+ paymentToken: string;
32
+ };
33
+ }
34
+ interface CheckoutConfirmParamsRawCard extends CheckoutConfirmParamsGeneric {
35
+ type: 'raw-card';
36
+ data: {
37
+ number: string;
38
+ expMonth: number;
39
+ expYear: number;
40
+ cvc: string;
41
+ };
42
+ }
43
+ type CheckoutConfirmResponse = CheckoutConfirmSuccessDeferred | CheckoutConfirmSuccessImmediate | CheckoutConfirmFailure;
44
+ interface CheckoutConfirmGeneric {
45
+ status: 'success' | 'failure';
46
+ code: number;
47
+ message: string;
48
+ }
49
+ interface CheckoutConfirmSuccessDeferred extends CheckoutConfirmGeneric {
50
+ status: 'success';
51
+ substatus: '202-deferred';
52
+ }
53
+ interface CheckoutConfirmSuccessImmediate extends CheckoutConfirmGeneric {
54
+ status: 'success';
55
+ substatus: '201-immediate';
56
+ data: {
57
+ confirmationId: number;
58
+ amount: number;
59
+ currency: string;
60
+ customerId?: string;
61
+ };
62
+ }
63
+ interface CheckoutConfirmFailure extends CheckoutConfirmGeneric {
64
+ status: 'failure';
65
+ substatus: '500-error' | '502-fraud' | '503-retry';
66
+ }
67
+ export declare class Checkout {
68
+ /**
69
+ * Create a new checkout session
70
+ * @param amount - The amount for the checkout
71
+ * @param currency - The curreny type (note: not all are supported atm)
72
+ * @param customerId - Optional customer ID, used for unique customer identification
73
+ * @returns The response from the checkout creation
74
+ */
75
+ create(params: {
76
+ amount: number;
77
+ currency: 'USD' | 'EUR' | 'JPY';
78
+ customerId?: string;
79
+ }): Promise<CheckoutCreateResponse>;
80
+ /**
81
+ * Confirm a checkout session
82
+ * @param params - Either embedded or raw card checkout confirmation parameters
83
+ * @returns - The response from the checkout confirmation
84
+ */
85
+ confirm(params: CheckoutConfirmParams): Promise<CheckoutConfirmResponse>;
86
+ private validateCreate;
87
+ private processCreateDecision;
88
+ private createCheckoutRecord;
89
+ private scheduleCreateWebhook;
90
+ private buildHistoryHash;
91
+ private validateConfirm;
92
+ private processConfirmDecision;
93
+ private buildInstantConfirmSuccess;
94
+ private scheduleConfirmWebhook;
95
+ private isValidCardNumber;
96
+ private isValidExpiry;
97
+ private determineResponseCase;
98
+ private sendWebhookResponse;
99
+ }
100
+ export {};
@@ -0,0 +1,24 @@
1
+ export type EventType = 'checkout' | 'checkout.create' | 'checkout.create.success' | 'checkout.create.failure' | 'checkout.confirm' | 'checkout.confirm.success' | 'checkout.confirm.failure';
2
+ export interface WebhookEvent {
3
+ id: string;
4
+ type: EventType;
5
+ createdAt: number;
6
+ data: Record<string, any>;
7
+ }
8
+ export declare const INTERNAL_WEBHOOKS: {
9
+ url: string;
10
+ event: EventType;
11
+ secret?: string;
12
+ }[];
13
+ export declare class Webhooks {
14
+ /**
15
+ * Registers a new webhook endpoint for the specified events
16
+ * @param params - The webhook parameters including URL, events, and optional secret
17
+ * @returns A promise that resolves to a boolean indicating successful registration
18
+ */
19
+ createEndpoint(params: {
20
+ url: string;
21
+ events: EventType[];
22
+ secret?: string;
23
+ }): Promise<boolean>;
24
+ }
@@ -0,0 +1 @@
1
+ export declare const sleep: (ms: number) => Promise<unknown>;
@@ -0,0 +1,3 @@
1
+ export declare function generateID(length?: number): number;
2
+ export declare function hashToNumber(input: string, length?: number): number;
3
+ export declare function signPayload(payload: string, secret: string): string;
@@ -0,0 +1,17 @@
1
+ export type HistoryRecord = {
2
+ id: number;
3
+ amount: number;
4
+ currency: 'USD' | 'EUR' | 'JPY';
5
+ customerId: string | null;
6
+ updatedAt: number;
7
+ createdAt: number;
8
+ confirmed: boolean;
9
+ };
10
+ /**
11
+ * Reads all history records from the JSON store.
12
+ */
13
+ export declare function readHistory(): Promise<HistoryRecord[]>;
14
+ /**
15
+ * Writes a new record to the JSON store.
16
+ */
17
+ export declare function writeHistory(params: Omit<HistoryRecord, 'updatedAt'>): Promise<HistoryRecord>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henrylabs-interview/payment-processor",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -17,7 +17,8 @@
17
17
  ],
18
18
  "scripts": {
19
19
  "build": "bun run build.ts && bunx tsc -p tsconfig.build.json",
20
- "dev": "bun run src/index.ts"
20
+ "dev": "bun run src/index.ts",
21
+ "publish": "bun run build && npm publish --access public"
21
22
  },
22
23
  "devDependencies": {
23
24
  "@types/bun": "latest",