@henrylabs-interview/payment-processor 0.1.13 → 0.1.15

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/dist/index.d.ts CHANGED
@@ -11,7 +11,7 @@ export declare class PaymentProcessor {
11
11
  export declare class EmbeddedCheckout {
12
12
  private checkoutId;
13
13
  constructor(config: {
14
- checkoutId: number;
14
+ checkoutId: string;
15
15
  });
16
16
  /**
17
17
  * Renders the embedded checkout UI
@@ -19,5 +19,5 @@ export declare class EmbeddedCheckout {
19
19
  * @param callbackFn - Callback function to handle the payment token
20
20
  * @returns - Whether the UI rendered successfully
21
21
  */
22
- render(containerElementId: string, callbackFn: (paymentToken: string) => void): boolean;
22
+ render(containerElementId: string, callbackFn: (paymentToken: string) => void): Promise<boolean>;
23
23
  }
@@ -12,7 +12,7 @@ interface CheckoutCreateSuccessImmediate extends CheckoutCreateGeneric {
12
12
  status: 'success';
13
13
  substatus: '201-immediate';
14
14
  data: {
15
- checkoutId: number;
15
+ checkoutId: string;
16
16
  paymentMethodOptions: string[];
17
17
  };
18
18
  }
@@ -54,7 +54,7 @@ interface CheckoutConfirmSuccessImmediate extends CheckoutConfirmGeneric {
54
54
  status: 'success';
55
55
  substatus: '201-immediate';
56
56
  data: {
57
- confirmationId: number;
57
+ confirmationId: string;
58
58
  amount: number;
59
59
  currency: string;
60
60
  customerId?: string;
@@ -1,5 +1,5 @@
1
1
  import { readHistory, writeHistory } from '../utils/store';
2
- import { generateID, generateTimeBasedID, hashToNumber, signPayload } from '../utils/crypto';
2
+ import { generateTimeBasedID, hashToString, signPayload } from '../utils/crypto';
3
3
  import { INTERNAL_WEBHOOKS } from './webhooks';
4
4
  import { sleep } from '../utils/async';
5
5
  // checkoutId -> { historyRecordId }
@@ -14,7 +14,7 @@ export class Checkout {
14
14
  */
15
15
  async create(params) {
16
16
  await sleep(Math.random() * 100);
17
- const hashId = this.buildHistoryHash(params);
17
+ const hashId = await this.buildHistoryHash(params);
18
18
  const history = await readHistory();
19
19
  const sameRecords = history.filter((v) => v.id === hashId);
20
20
  // ---------------------------------------
@@ -24,7 +24,7 @@ export class Checkout {
24
24
  // ---------------------------------------
25
25
  // Phase 2: Business Logic
26
26
  // ---------------------------------------
27
- const response = validationFailure ?? this.processCreateDecision(params, hashId, sameRecords.length);
27
+ const response = validationFailure ?? (await this.processCreateDecision(params, hashId, sameRecords.length));
28
28
  // ---------------------------------------
29
29
  // Phase 3: Webhook Scheduling
30
30
  // ---------------------------------------
@@ -91,7 +91,7 @@ export class Checkout {
91
91
  }
92
92
  return null;
93
93
  }
94
- processCreateDecision(params, hashId, duplicateCount) {
94
+ async processCreateDecision(params, hashId, duplicateCount) {
95
95
  const resCase = this.determineResponseCase(params.amount, duplicateCount);
96
96
  if (resCase === 'failure-retry') {
97
97
  return {
@@ -109,7 +109,7 @@ export class Checkout {
109
109
  message: 'Potential fraud detected with this purchase',
110
110
  };
111
111
  }
112
- const checkoutId = this.createCheckoutRecord(hashId);
112
+ const checkoutId = await this.createCheckoutRecord(hashId);
113
113
  if (resCase === 'success-deferred') {
114
114
  return {
115
115
  status: 'success',
@@ -129,8 +129,8 @@ export class Checkout {
129
129
  },
130
130
  };
131
131
  }
132
- createCheckoutRecord(hashId) {
133
- const checkoutId = generateTimeBasedID('checkout');
132
+ async createCheckoutRecord(hashId) {
133
+ const checkoutId = `cki_${await generateTimeBasedID('checkout')}`;
134
134
  INTERNAL_CHECKOUTS[`${checkoutId}`] = {
135
135
  historyRecordId: hashId,
136
136
  };
@@ -138,12 +138,12 @@ export class Checkout {
138
138
  }
139
139
  scheduleCreateWebhook(hashId, response) {
140
140
  const webhookDelay = Math.random() * 3000;
141
- setTimeout(() => {
141
+ setTimeout(async () => {
142
142
  // Deferred flow resolves later
143
143
  if (response.status === 'success' && response.substatus === '202-deferred') {
144
144
  const isSuccess = Math.random() > 0.35;
145
145
  if (isSuccess) {
146
- const checkoutId = this.createCheckoutRecord(hashId);
146
+ const checkoutId = await this.createCheckoutRecord(hashId);
147
147
  this.sendWebhookResponse('checkout.create', {
148
148
  status: 'success',
149
149
  substatus: '201-immediate',
@@ -169,8 +169,8 @@ export class Checkout {
169
169
  this.sendWebhookResponse('checkout.create', response);
170
170
  }, webhookDelay);
171
171
  }
172
- buildHistoryHash(params) {
173
- return hashToNumber(JSON.stringify({
172
+ async buildHistoryHash(params) {
173
+ return await hashToString(JSON.stringify({
174
174
  type: 'HISTORY_RECORD',
175
175
  amount: params.amount,
176
176
  currency: params.currency,
@@ -179,7 +179,7 @@ export class Checkout {
179
179
  }
180
180
  ///
181
181
  async validateConfirm(params) {
182
- if (`${params.checkoutId}`.length !== 12) {
182
+ if (`${params.checkoutId}`.length !== 20 || !`${params.checkoutId}`.startsWith('cki_')) {
183
183
  return {
184
184
  status: 'failure',
185
185
  substatus: '500-error',
@@ -195,16 +195,18 @@ export class Checkout {
195
195
  message: 'Expired checkout ID',
196
196
  };
197
197
  }
198
- if (params.type === 'embedded' && params.data.paymentToken.length !== 12) {
199
- return {
200
- status: 'failure',
201
- substatus: '500-error',
202
- code: 500,
203
- message: 'Invalid payment token',
204
- };
198
+ if (params.type === 'embedded') {
199
+ if (params.data.paymentToken.length !== 20 || !params.data.paymentToken.startsWith('pmt_')) {
200
+ return {
201
+ status: 'failure',
202
+ substatus: '500-error',
203
+ code: 500,
204
+ message: 'Invalid payment token',
205
+ };
206
+ }
205
207
  }
206
208
  if (params.type === 'embedded') {
207
- const paymentToken = `pmt_${generateTimeBasedID('payment-token')}`;
209
+ const paymentToken = `pmt_${await generateTimeBasedID('payment-token')}`;
208
210
  if (params.data.paymentToken !== paymentToken) {
209
211
  return {
210
212
  status: 'failure',
@@ -291,12 +293,13 @@ export class Checkout {
291
293
  message: 'Missing history record',
292
294
  };
293
295
  }
294
- const confirmationId = hashToNumber(JSON.stringify({
295
- type: 'CONFIRMATION_ID',
296
- amount: record.amount,
297
- currency: record.currency,
298
- customerId: record.customerId,
299
- }));
296
+ const confirmationId = 'cof_' +
297
+ (await hashToString(JSON.stringify({
298
+ type: 'CONFIRMATION_ID',
299
+ amount: record.amount,
300
+ currency: record.currency,
301
+ customerId: record.customerId,
302
+ })));
300
303
  return {
301
304
  status: 'success',
302
305
  substatus: '201-immediate',
@@ -1 +1 @@
1
- export declare function renderEmbeddedCheckout(containerElementId: string, checkoutId: number, callbackFn: (paymentToken: string) => void): boolean;
1
+ export declare function renderEmbeddedCheckout(containerElementId: string, checkoutId: string, callbackFn: (paymentToken: string) => void): Promise<boolean>;
@@ -1,15 +1,15 @@
1
1
  import { generateTimeBasedID } from '../utils/crypto';
2
- export function renderEmbeddedCheckout(containerElementId, checkoutId, callbackFn) {
2
+ export async function renderEmbeddedCheckout(containerElementId, checkoutId, callbackFn) {
3
3
  // Ensure browser environment
4
4
  if (typeof window === 'undefined' || typeof document === 'undefined') {
5
5
  console.warn('EmbeddedCheckoutUI can only be used in a browser environment.');
6
6
  return false;
7
7
  }
8
- if (checkoutId.toString().length !== 12) {
8
+ if (checkoutId.toString().length !== 20 && !checkoutId.startsWith('cki_')) {
9
9
  console.warn(`Invalid checkout ID: "${checkoutId}".`);
10
10
  return false;
11
11
  }
12
- if (`${checkoutId}` === `${generateTimeBasedID('checkout')}`) {
12
+ if (`${checkoutId}` === `cki_${await generateTimeBasedID('checkout')}`) {
13
13
  console.warn(`Expired checkout ID: "${checkoutId}".`);
14
14
  return false;
15
15
  }
@@ -143,7 +143,7 @@ export function renderEmbeddedCheckout(containerElementId, checkoutId, callbackF
143
143
  </div>
144
144
  `;
145
145
  const button = container.querySelector('#confirm-btn');
146
- button?.addEventListener('click', () => {
146
+ button?.addEventListener('click', async () => {
147
147
  const number = (container.querySelector('#card-number')?.value ?? '').replace(/\s+/g, '');
148
148
  const expMonth = Number(container.querySelector('#exp-month')?.value);
149
149
  const expYear = Number(container.querySelector('#exp-year')?.value);
@@ -155,7 +155,7 @@ export function renderEmbeddedCheckout(containerElementId, checkoutId, callbackF
155
155
  // expYear: expYear > 2000 ? expYear : expYear + 2000,
156
156
  // cvc: cvc,
157
157
  // };
158
- const paymentToken = `pmt_${generateTimeBasedID('payment-token')}`;
158
+ const paymentToken = `pmt_${await generateTimeBasedID('payment-token')}`;
159
159
  callbackFn(paymentToken);
160
160
  });
161
161
  return true;
@@ -1,4 +1,4 @@
1
1
  export declare function generateID(length?: number): number;
2
- export declare function generateTimeBasedID(extra?: string): number;
3
- export declare function hashToNumber(input: string, length?: number): number;
2
+ export declare function generateTimeBasedID(extra?: string): Promise<string>;
3
+ export declare function hashToString(input: string): Promise<string>;
4
4
  export declare function signPayload(payload: string, secret: string): string;
@@ -7,18 +7,23 @@ export function generateID(length = 12) {
7
7
  }
8
8
  return parseInt(digits.join(''));
9
9
  }
10
- export function generateTimeBasedID(extra = '') {
10
+ export async function generateTimeBasedID(extra = '') {
11
11
  const now = Date.now(); // current time in ms
12
12
  const oneMinute = 60 * 1000;
13
13
  const ms = Math.floor(now / oneMinute) * oneMinute;
14
- return hashToNumber(`${ms}---${extra}`);
14
+ return await hashToString(`${ms}---${extra}`);
15
15
  }
16
- export function hashToNumber(input, length = 12) {
17
- if (length > 16) {
18
- throw new Error('Length is greater than max length of 16!');
19
- }
20
- const hash = crypto.createHash('sha256').update(input).digest('hex');
21
- return parseInt(hash.slice(0, length), 16);
16
+ async function sha256(input) {
17
+ const encoder = new TextEncoder();
18
+ const data = encoder.encode(input);
19
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
20
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
21
+ return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
22
+ }
23
+ export async function hashToString(input) {
24
+ const hash = await sha256(input);
25
+ // 16 hex characters = 64 bits
26
+ return hash.slice(0, 16);
22
27
  }
23
28
  export function signPayload(payload, secret) {
24
29
  return crypto.createHmac('sha256', secret).update(payload).digest('hex');
@@ -1,5 +1,5 @@
1
1
  export type HistoryRecord = {
2
- id: number;
2
+ id: string;
3
3
  amount: number;
4
4
  currency: 'USD' | 'EUR' | 'JPY';
5
5
  customerId: string | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henrylabs-interview/payment-processor",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",