@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 +2 -2
- package/dist/resources/checkout.d.ts +2 -2
- package/dist/resources/checkout.js +29 -26
- package/dist/resources/embedded.d.ts +1 -1
- package/dist/resources/embedded.js +5 -5
- package/dist/utils/crypto.d.ts +2 -2
- package/dist/utils/crypto.js +13 -8
- package/dist/utils/store.d.ts +1 -1
- package/package.json +1 -1
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:
|
|
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:
|
|
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;
|
|
@@ -1,5 +1,5 @@
|
|
|
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 }
|
|
@@ -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
|
|
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 !==
|
|
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'
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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 =
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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:
|
|
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 !==
|
|
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}` ===
|
|
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;
|
package/dist/utils/crypto.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export declare function generateID(length?: number): number;
|
|
2
|
-
export declare function generateTimeBasedID(extra?: string):
|
|
3
|
-
export declare function
|
|
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;
|
package/dist/utils/crypto.js
CHANGED
|
@@ -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
|
|
14
|
+
return await hashToString(`${ms}---${extra}`);
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
return
|
|
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');
|
package/dist/utils/store.d.ts
CHANGED