@henrylabs-interview/payment-processor 0.2.6 → 0.2.8

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.
@@ -92,8 +92,6 @@ export declare class Checkout {
92
92
  private processConfirmDecision;
93
93
  private buildInstantConfirmSuccess;
94
94
  private scheduleConfirmWebhook;
95
- private isValidCardNumber;
96
- private isValidExpiry;
97
95
  private determineResponseCase;
98
96
  private sendWebhookResponse;
99
97
  }
@@ -2,6 +2,7 @@ import { readHistory, writeHistory } from '../utils/store';
2
2
  import { generateTimeBasedID, hashToString, signPayload } from '../utils/crypto';
3
3
  import { INTERNAL_WEBHOOKS } from './webhooks';
4
4
  import { sleep } from '../utils/async';
5
+ import { isValidCardNumber, isValidExpiry } from '../utils/card';
5
6
  // checkoutId -> { historyRecordId }
6
7
  const INTERNAL_CHECKOUTS = {};
7
8
  export class Checkout {
@@ -218,7 +219,7 @@ export class Checkout {
218
219
  }
219
220
  if (params.type === 'raw-card') {
220
221
  const { number, expMonth, expYear, cvc } = params.data;
221
- if (!this.isValidCardNumber(number)) {
222
+ if (!isValidCardNumber(number)) {
222
223
  return {
223
224
  status: 'failure',
224
225
  substatus: '502-fraud',
@@ -226,7 +227,7 @@ export class Checkout {
226
227
  message: 'Invalid card number',
227
228
  };
228
229
  }
229
- if (!this.isValidExpiry(expMonth, expYear)) {
230
+ if (!isValidExpiry(expMonth, expYear)) {
230
231
  return {
231
232
  status: 'failure',
232
233
  substatus: '503-retry',
@@ -337,35 +338,6 @@ export class Checkout {
337
338
  }, webhookDelay);
338
339
  }
339
340
  //
340
- isValidCardNumber(number) {
341
- // Test card is valid
342
- if (number === '4242424242424242')
343
- return true;
344
- // Cards must be 13-19 digits
345
- if (!/^\d{13,19}$/.test(number))
346
- return false;
347
- // Luhn check
348
- let sum = 0;
349
- let shouldDouble = false;
350
- for (let i = number.length - 1; i >= 0; i--) {
351
- let digit = parseInt(number[i] ?? '');
352
- if (shouldDouble) {
353
- digit *= 2;
354
- if (digit > 9)
355
- digit -= 9;
356
- }
357
- sum += digit;
358
- shouldDouble = !shouldDouble;
359
- }
360
- return sum % 10 === 0;
361
- }
362
- isValidExpiry(month, year) {
363
- if (month < 1 || month > 12)
364
- return false;
365
- const now = new Date();
366
- const expiry = new Date(year, month - 1);
367
- return expiry > now;
368
- }
369
341
  determineResponseCase(amount, sameRecords) {
370
342
  // --- Base probabilities ---
371
343
  let immediateWeight = 65;
@@ -1,11 +1,13 @@
1
+ import { isValidCardNumber, isValidExpiry } from '../utils/card';
1
2
  import { generateTimeBasedID } from '../utils/crypto';
2
3
  export async function renderEmbeddedCheckout(containerElementId, checkoutId, callbackFn) {
3
- // Ensure browser environment
4
+ // --- Environment Guard ---
4
5
  if (typeof window === 'undefined' || typeof document === 'undefined') {
5
6
  console.warn('EmbeddedCheckoutUI can only be used in a browser environment.');
6
7
  return false;
7
8
  }
8
- if (checkoutId.toString().length !== 20 || !checkoutId.startsWith('cki_')) {
9
+ // --- Checkout Validation ---
10
+ if (checkoutId.length !== 20 || !checkoutId.startsWith('cki_')) {
9
11
  console.warn(`Invalid checkout ID: "${checkoutId}".`);
10
12
  return false;
11
13
  }
@@ -18,6 +20,7 @@ export async function renderEmbeddedCheckout(containerElementId, checkoutId, cal
18
20
  console.warn(`Container element "${containerElementId}" not found.`);
19
21
  return false;
20
22
  }
23
+ // --- Render UI ---
21
24
  container.innerHTML = `
22
25
  <div style="
23
26
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
@@ -25,161 +28,147 @@ export async function renderEmbeddedCheckout(containerElementId, checkoutId, cal
25
28
  border-radius: 16px;
26
29
  box-shadow: 0 10px 30px rgba(0,0,0,0.08);
27
30
  padding: 24px;
28
- max-width: 360px;
31
+ max-width: 400px;
29
32
  width: 100%;
30
33
  box-sizing: border-box;
31
34
  ">
32
- <h3 style="
33
- margin: 0 0 20px 0;
34
- font-size: 18px;
35
- font-weight: 600;
36
- color: #111827;
37
- ">
35
+ <h3 style="margin:0 0 20px 0; font-size:18px; font-weight:600; color:#111827;">
38
36
  "Secure" Checkout
39
37
  </h3>
40
38
 
41
39
  <div style="display:flex; flex-direction:column; gap:14px;">
42
40
 
43
- <div style="display:flex; flex-direction:column;">
44
- <label style="font-size:12px; color:#6b7280; margin-bottom:6px;">
45
- Card Number
46
- </label>
47
- <input
48
- id="card-number"
49
- type="text"
50
- placeholder="4242 4242 4242 4242"
51
- maxlength="16"
52
- inputmode="numeric"
53
- pattern="[0-9]*"
54
- style="
55
- padding: 10px 12px;
56
- border-radius: 10px;
57
- border: 1px solid #e5e7eb;
58
- font-size: 14px;
59
- outline: none;
60
- transition: border 0.2s, box-shadow 0.2s;
61
- "
62
- />
63
- </div>
64
-
41
+ ${inputBlock('card-number', 'Card Number', '4242 4242 4242 4242')}
65
42
  <div style="display:flex; gap:10px;">
66
-
67
- <div style="flex:1; display:flex; flex-direction:column;">
68
- <label style="font-size:12px; color:#6b7280; margin-bottom:6px;">
69
- Exp. Month
70
- </label>
71
- <input
72
- id="exp-month"
73
- type="text"
74
- placeholder="12"
75
- min="1"
76
- max="12"
77
- maxlength="2"
78
- inputmode="numeric"
79
- pattern="[0-9]*"
80
- style="
81
- padding: 10px 12px;
82
- border-radius: 10px;
83
- border: 1px solid #e5e7eb;
84
- font-size: 14px;
85
- outline: none;
86
- transition: border 0.2s, box-shadow 0.2s;
87
- "
88
- />
43
+ <div style="flex:1;">
44
+ ${inputBlock('exp-month', 'Exp. Month', '12')}
89
45
  </div>
90
-
91
- <div style="flex:1; display:flex; flex-direction:column;">
92
- <label style="font-size:12px; color:#6b7280; margin-bottom:6px;">
93
- Exp. Year
94
- </label>
95
- <input
96
- id="exp-year"
97
- type="text"
98
- placeholder="2030"
99
- maxlength="4"
100
- inputmode="numeric"
101
- pattern="[0-9]*"
102
- style="
103
- padding: 10px 12px;
104
- border-radius: 10px;
105
- border: 1px solid #e5e7eb;
106
- font-size: 14px;
107
- outline: none;
108
- transition: border 0.2s, box-shadow 0.2s;
109
- "
110
- />
46
+ <div style="flex:1;">
47
+ ${inputBlock('exp-year', 'Exp. Year', '2030')}
111
48
  </div>
112
-
113
- </div>
114
-
115
- <div style="display:flex; flex-direction:column;">
116
- <label style="font-size:12px; color:#6b7280; margin-bottom:6px;">
117
- CVC
118
- </label>
119
- <input id="cvc"
120
- type="text"
121
- placeholder="123"
122
- maxlength="4"
123
- inputmode="numeric"
124
- pattern="[0-9]*"
125
- style="
126
- padding: 10px 12px;
127
- border-radius: 10px;
128
- border: 1px solid #e5e7eb;
129
- font-size: 14px;
130
- outline: none;
131
- transition: border 0.2s, box-shadow 0.2s;
132
- "
133
- />
134
49
  </div>
50
+ ${inputBlock('cvc', 'CVC', '123')}
135
51
 
136
52
  <button id="confirm-btn"
137
53
  style="
138
- margin-top: 8px;
139
- padding: 12px;
140
- border-radius: 12px;
141
- border: none;
142
- font-size: 14px;
143
- font-weight: 600;
54
+ margin-top:8px;
55
+ padding:12px;
56
+ border-radius:12px;
57
+ border:none;
58
+ font-size:14px;
59
+ font-weight:600;
144
60
  background: linear-gradient(135deg, #6366f1, #4f46e5);
145
- color: white;
146
- cursor: pointer;
147
- transition: transform 0.05s ease, box-shadow 0.2s ease;
61
+ color:white;
62
+ cursor:pointer;
63
+ transition: all 0.15s ease;
148
64
  box-shadow: 0 4px 14px rgba(79,70,229,0.3);
149
- "
150
- onmouseover="this.style.boxShadow='0 6px 18px rgba(79,70,229,0.4)'"
151
- onmouseout="this.style.boxShadow='0 4px 14px rgba(79,70,229,0.3)'"
152
- onmousedown="this.style.transform='scale(0.98)'"
153
- onmouseup="this.style.transform='scale(1)'"
154
- >
65
+ ">
155
66
  Confirm Payment
156
67
  </button>
157
68
 
158
- <p style="
159
- font-size: 11px;
160
- color: #9ca3af;
161
- text-align: center;
162
- margin-top: 8px;
163
- ">
69
+ <p style="font-size:11px; color:#9ca3af; text-align:center; margin-top:8px;">
164
70
  🔒 Payments are securely processed
165
71
  </p>
166
72
 
167
73
  </div>
168
74
  </div>
169
- `;
75
+ `;
76
+ function inputBlock(id, label, placeholder) {
77
+ return `
78
+ <div style="display:flex; flex-direction:column;">
79
+ <label style="font-size:12px; color:#6b7280; margin-bottom:6px;">
80
+ ${label}
81
+ </label>
82
+ <input
83
+ id="${id}"
84
+ type="tel"
85
+ placeholder="${placeholder}"
86
+ inputmode="numeric"
87
+ style="
88
+ padding:10px 12px;
89
+ border-radius:10px;
90
+ border:1px solid #e5e7eb;
91
+ font-size:14px;
92
+ outline:none;
93
+ transition:border 0.2s, box-shadow 0.2s;
94
+ "
95
+ />
96
+ <div id="${id}-error"
97
+ style="font-size:12px; color:#ef4444; margin-top:4px; display:none;">
98
+ </div>
99
+ </div>
100
+ `;
101
+ }
102
+ // --- DOM Refs ---
103
+ const cardNumberInput = container.querySelector('#card-number');
104
+ const expMonthInput = container.querySelector('#exp-month');
105
+ const expYearInput = container.querySelector('#exp-year');
106
+ const cvcInput = container.querySelector('#cvc');
170
107
  const button = container.querySelector('#confirm-btn');
171
- button?.addEventListener('click', async () => {
172
- const number = (container.querySelector('#card-number')?.value ?? '').replace(/\s+/g, '');
173
- const expMonth = Number(container.querySelector('#exp-month')?.value);
174
- const expYear = Number(container.querySelector('#exp-year')?.value);
175
- const cvc = container.querySelector('#cvc')?.value ?? '';
176
- // Add new card to internal storage
177
- // const cardDetails = {
178
- // number: number,
179
- // expMonth: expMonth,
180
- // expYear: expYear > 2000 ? expYear : expYear + 2000,
181
- // cvc: cvc,
182
- // };
108
+ // --- Helpers ---
109
+ function showError(input, message) {
110
+ input.style.border = '1px solid #ef4444';
111
+ const error = container?.querySelector(`#${input.id}-error`);
112
+ if (error) {
113
+ error.textContent = message;
114
+ error.style.display = 'block';
115
+ }
116
+ }
117
+ function clearError(input) {
118
+ input.style.border = '1px solid #e5e7eb';
119
+ const error = container?.querySelector(`#${input.id}-error`);
120
+ if (error) {
121
+ error.textContent = '';
122
+ error.style.display = 'none';
123
+ }
124
+ }
125
+ function setLoading(state) {
126
+ button.disabled = state;
127
+ button.style.opacity = state ? '0.6' : '1';
128
+ button.textContent = state ? 'Processing...' : 'Confirm Payment';
129
+ }
130
+ function clearAllErrors() {
131
+ [cardNumberInput, expMonthInput, expYearInput, cvcInput].forEach(clearError);
132
+ }
133
+ // --- Formatting ---
134
+ cardNumberInput.addEventListener('input', () => {
135
+ let value = cardNumberInput.value.replace(/\D/g, '').slice(0, 16);
136
+ value = value.replace(/(.{4})/g, '$1 ').trim();
137
+ cardNumberInput.value = value;
138
+ clearError(cardNumberInput);
139
+ });
140
+ [expMonthInput, expYearInput, cvcInput].forEach((input) => input.addEventListener('input', () => clearError(input)));
141
+ // --- Submit ---
142
+ button.addEventListener('click', async () => {
143
+ clearAllErrors();
144
+ const number = cardNumberInput.value.replace(/\s+/g, '');
145
+ const expMonth = Number(expMonthInput.value);
146
+ const expYear = Number(expYearInput.value);
147
+ const cvc = cvcInput.value;
148
+ let hasError = false;
149
+ if (!number) {
150
+ showError(cardNumberInput, 'Card number required');
151
+ hasError = true;
152
+ }
153
+ else if (!isValidCardNumber(number)) {
154
+ showError(cardNumberInput, 'Invalid card number');
155
+ hasError = true;
156
+ }
157
+ if (!expMonthInput.value || expMonth < 1 || expMonth > 12) {
158
+ showError(expMonthInput, 'Invalid month');
159
+ hasError = true;
160
+ }
161
+ if (!expYearInput.value || !isValidExpiry(expMonth, expYear)) {
162
+ showError(expYearInput, 'Card expired');
163
+ hasError = true;
164
+ }
165
+ if (!cvc || !/^\d{3,4}$/.test(cvc)) {
166
+ showError(cvcInput, 'Invalid CVC');
167
+ hasError = true;
168
+ }
169
+ if (hasError)
170
+ return;
171
+ setLoading(true);
183
172
  const paymentToken = `pmt_${await generateTimeBasedID('payment-token')}`;
184
173
  callbackFn(paymentToken);
185
174
  });
@@ -0,0 +1,2 @@
1
+ export declare function isValidCardNumber(number: string): boolean;
2
+ export declare function isValidExpiry(month: number, year: number): boolean;
@@ -0,0 +1,29 @@
1
+ export function isValidCardNumber(number) {
2
+ // Test card is valid
3
+ if (number === '4242424242424242')
4
+ return true;
5
+ // Cards must be 13-19 digits
6
+ if (!/^\d{13,19}$/.test(number))
7
+ return false;
8
+ // Luhn check
9
+ let sum = 0;
10
+ let shouldDouble = false;
11
+ for (let i = number.length - 1; i >= 0; i--) {
12
+ let digit = parseInt(number[i] ?? '');
13
+ if (shouldDouble) {
14
+ digit *= 2;
15
+ if (digit > 9)
16
+ digit -= 9;
17
+ }
18
+ sum += digit;
19
+ shouldDouble = !shouldDouble;
20
+ }
21
+ return sum % 10 === 0;
22
+ }
23
+ export function isValidExpiry(month, year) {
24
+ if (month < 1 || month > 12)
25
+ return false;
26
+ const now = new Date();
27
+ const expiry = new Date(year, month - 1);
28
+ return expiry > now;
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henrylabs-interview/payment-processor",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",