@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.
- package/dist/resources/checkout.d.ts +2 -11
- package/dist/resources/checkout.js +32 -30
- package/dist/resources/embedded.js +13 -16
- package/dist/utils/crypto.d.ts +2 -1
- package/dist/utils/crypto.js +9 -5
- package/dist/utils/store.d.ts +1 -1
- package/package.json +1 -1
|
@@ -12,7 +12,7 @@ interface CheckoutCreateSuccessImmediate extends CheckoutCreateGeneric {
|
|
|
12
12
|
status: 'success';
|
|
13
13
|
substatus: '201-immediate';
|
|
14
14
|
data: {
|
|
15
|
-
checkoutId:
|
|
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:
|
|
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 {
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
};
|
|
156
|
-
const paymentToken =
|
|
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;
|
package/dist/utils/crypto.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export declare function generateID(length?: number): number;
|
|
2
|
-
export declare function
|
|
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;
|
package/dist/utils/crypto.js
CHANGED
|
@@ -7,12 +7,16 @@ export function generateID(length = 12) {
|
|
|
7
7
|
}
|
|
8
8
|
return parseInt(digits.join(''));
|
|
9
9
|
}
|
|
10
|
-
export function
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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');
|
package/dist/utils/store.d.ts
CHANGED