@henrylabs-interview/payment-processor 0.1.12 → 0.1.14

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.
@@ -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;
@@ -64,15 +64,6 @@ interface CheckoutConfirmFailure extends CheckoutConfirmGeneric {
64
64
  status: 'failure';
65
65
  substatus: '500-error' | '502-fraud' | '503-retry';
66
66
  }
67
- export declare const INTERNAL_CHECKOUTS: Record<string, {
68
- historyRecordId: number;
69
- }>;
70
- export declare const INTERNAL_CARDS: Record<string, {
71
- number: string;
72
- expMonth: number;
73
- expYear: number;
74
- cvc: string;
75
- }>;
76
67
  export declare class Checkout {
77
68
  /**
78
69
  * Create a new checkout session
@@ -1,11 +1,9 @@
1
1
  import { readHistory, writeHistory } from '../utils/store';
2
- import { generateID, 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 }
6
- export const INTERNAL_CHECKOUTS = {};
7
- // paymentToken -> card details
8
- export const INTERNAL_CARDS = {};
6
+ const INTERNAL_CHECKOUTS = {};
9
7
  export class Checkout {
10
8
  /**
11
9
  * Create a new checkout session
@@ -132,7 +130,7 @@ export class Checkout {
132
130
  };
133
131
  }
134
132
  createCheckoutRecord(hashId) {
135
- const checkoutId = generateID();
133
+ const checkoutId = `cki_${generateTimeBasedID('checkout')}`;
136
134
  INTERNAL_CHECKOUTS[`${checkoutId}`] = {
137
135
  historyRecordId: hashId,
138
136
  };
@@ -172,7 +170,7 @@ export class Checkout {
172
170
  }, webhookDelay);
173
171
  }
174
172
  buildHistoryHash(params) {
175
- return hashToNumber(JSON.stringify({
173
+ return hashToString(JSON.stringify({
176
174
  type: 'HISTORY_RECORD',
177
175
  amount: params.amount,
178
176
  currency: params.currency,
@@ -181,7 +179,7 @@ export class Checkout {
181
179
  }
182
180
  ///
183
181
  async validateConfirm(params) {
184
- if (!INTERNAL_CHECKOUTS[`${params.checkoutId}`]) {
182
+ if (`${params.checkoutId}`.length !== 20 || !`${params.checkoutId}`.startsWith('cki_')) {
185
183
  return {
186
184
  status: 'failure',
187
185
  substatus: '500-error',
@@ -189,9 +187,16 @@ export class Checkout {
189
187
  message: 'Invalid checkout ID',
190
188
  };
191
189
  }
190
+ if (!INTERNAL_CHECKOUTS[`${params.checkoutId}`]) {
191
+ return {
192
+ status: 'failure',
193
+ substatus: '503-retry',
194
+ code: 504,
195
+ message: 'Expired checkout ID',
196
+ };
197
+ }
192
198
  if (params.type === 'embedded') {
193
- const stored = INTERNAL_CARDS[`${params.data.paymentToken}`];
194
- if (!stored) {
199
+ if (params.data.paymentToken.length !== 20 || !params.data.paymentToken.startsWith('pmt_')) {
195
200
  return {
196
201
  status: 'failure',
197
202
  substatus: '500-error',
@@ -200,6 +205,17 @@ export class Checkout {
200
205
  };
201
206
  }
202
207
  }
208
+ if (params.type === 'embedded') {
209
+ const paymentToken = `pmt_${generateTimeBasedID('payment-token')}`;
210
+ if (params.data.paymentToken !== paymentToken) {
211
+ return {
212
+ status: 'failure',
213
+ substatus: '503-retry',
214
+ code: 503,
215
+ message: 'Expired payment token',
216
+ };
217
+ }
218
+ }
203
219
  if (params.type === 'raw-card') {
204
220
  const { number, expMonth, expYear, cvc } = params.data;
205
221
  if (!this.isValidCardNumber(number)) {
@@ -230,21 +246,6 @@ export class Checkout {
230
246
  return null;
231
247
  }
232
248
  async processConfirmDecision(params) {
233
- if (params.type === 'raw-card') {
234
- // Add new card to internal storage
235
- const cardDetails = {
236
- number: params.data.number,
237
- expMonth: params.data.expMonth,
238
- expYear: params.data.expYear > 2000 ? params.data.expYear : params.data.expYear + 2000,
239
- cvc: params.data.cvc,
240
- };
241
- const paymentToken = 'pmt_' +
242
- hashToNumber(JSON.stringify({
243
- type: 'PAYMENT_TOKEN',
244
- ...cardDetails,
245
- }));
246
- INTERNAL_CARDS[`${paymentToken}`] = cardDetails;
247
- }
248
249
  const decision = Math.random();
249
250
  if (decision > 0.85) {
250
251
  return {
@@ -292,12 +293,13 @@ export class Checkout {
292
293
  message: 'Missing history record',
293
294
  };
294
295
  }
295
- const confirmationId = hashToNumber(JSON.stringify({
296
- type: 'CONFIRMATION_ID',
297
- amount: record.amount,
298
- currency: record.currency,
299
- customerId: record.customerId,
300
- }));
296
+ const confirmationId = 'cof_' +
297
+ hashToString(JSON.stringify({
298
+ type: 'CONFIRMATION_ID',
299
+ amount: record.amount,
300
+ currency: record.currency,
301
+ customerId: record.customerId,
302
+ }));
301
303
  return {
302
304
  status: 'success',
303
305
  substatus: '201-immediate',
@@ -1,16 +1,18 @@
1
- import { hashToNumber } from '../utils/crypto';
2
- import { INTERNAL_CARDS, INTERNAL_CHECKOUTS } from './checkout';
1
+ import { generateTimeBasedID } from '../utils/crypto';
3
2
  export function renderEmbeddedCheckout(containerElementId, checkoutId, callbackFn) {
4
3
  // Ensure browser environment
5
4
  if (typeof window === 'undefined' || typeof document === 'undefined') {
6
5
  console.warn('EmbeddedCheckoutUI can only be used in a browser environment.');
7
6
  return false;
8
7
  }
9
- const { historyRecordId } = INTERNAL_CHECKOUTS[`${checkoutId}`] ?? {};
10
- if (!historyRecordId) {
8
+ if (checkoutId.toString().length !== 20 && !checkoutId.startsWith('cki_')) {
11
9
  console.warn(`Invalid checkout ID: "${checkoutId}".`);
12
10
  return false;
13
11
  }
12
+ if (`${checkoutId}` === `cki_${generateTimeBasedID('checkout')}`) {
13
+ console.warn(`Expired checkout ID: "${checkoutId}".`);
14
+ return false;
15
+ }
14
16
  const container = document.getElementById(containerElementId);
15
17
  if (!container) {
16
18
  console.warn(`Container element "${containerElementId}" not found.`);
@@ -147,18 +149,13 @@ export function renderEmbeddedCheckout(containerElementId, checkoutId, callbackF
147
149
  const expYear = Number(container.querySelector('#exp-year')?.value);
148
150
  const cvc = container.querySelector('#cvc')?.value ?? '';
149
151
  // Add new card to internal storage
150
- const cardDetails = {
151
- number: number,
152
- expMonth: expMonth,
153
- expYear: expYear > 2000 ? expYear : expYear + 2000,
154
- cvc: cvc,
155
- };
156
- const paymentToken = 'pmt_' +
157
- hashToNumber(JSON.stringify({
158
- type: 'PAYMENT_TOKEN',
159
- ...cardDetails,
160
- }));
161
- INTERNAL_CARDS[`${paymentToken}`] = cardDetails;
152
+ // const cardDetails = {
153
+ // number: number,
154
+ // expMonth: expMonth,
155
+ // expYear: expYear > 2000 ? expYear : expYear + 2000,
156
+ // cvc: cvc,
157
+ // };
158
+ const paymentToken = `pmt_${generateTimeBasedID('payment-token')}`;
162
159
  callbackFn(paymentToken);
163
160
  });
164
161
  return true;
@@ -1,3 +1,4 @@
1
1
  export declare function generateID(length?: number): number;
2
- export declare function hashToNumber(input: string, length?: number): number;
2
+ export declare function generateTimeBasedID(extra?: string): string;
3
+ export declare function hashToString(input: string): string;
3
4
  export declare function signPayload(payload: string, secret: string): string;
@@ -7,12 +7,16 @@ export function generateID(length = 12) {
7
7
  }
8
8
  return parseInt(digits.join(''));
9
9
  }
10
- export function hashToNumber(input, length = 12) {
11
- if (length > 16) {
12
- throw new Error('Length is greater than max length of 16!');
13
- }
10
+ export function generateTimeBasedID(extra = '') {
11
+ const now = Date.now(); // current time in ms
12
+ const oneMinute = 60 * 1000;
13
+ const ms = Math.floor(now / oneMinute) * oneMinute;
14
+ return hashToString(`${ms}---${extra}`);
15
+ }
16
+ export function hashToString(input) {
14
17
  const hash = crypto.createHash('sha256').update(input).digest('hex');
15
- return parseInt(hash.slice(0, length), 16);
18
+ // 16 hex characters = 64 bits
19
+ return hash.slice(0, 16);
16
20
  }
17
21
  export function signPayload(payload, secret) {
18
22
  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.12",
3
+ "version": "0.1.14",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",