@henrylabs-interview/payment-processor 0.1.9 → 0.1.10

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
@@ -1,6 +1,6 @@
1
1
  import { Checkout } from './resources/checkout';
2
2
  import { Webhooks } from './resources/webhooks';
3
- export default class HenryPaymentProcessorSDK {
3
+ export declare class PaymentProcessorSDK {
4
4
  checkout: Checkout;
5
5
  webhooks: Webhooks;
6
6
  private VALID_API_KEYS;
@@ -8,3 +8,16 @@ export default class HenryPaymentProcessorSDK {
8
8
  apiKey: string;
9
9
  });
10
10
  }
11
+ export declare class EmbeddedCheckoutUI {
12
+ private checkoutId;
13
+ constructor(config: {
14
+ checkoutId: string;
15
+ });
16
+ /**
17
+ * Renders the embedded checkout UI
18
+ * @param containerElementId - The ID of the container element where the checkout UI will be rendered
19
+ * @param callbackFn - Callback function to handle the payment token
20
+ * @returns - Whether the UI rendered successfully
21
+ */
22
+ render(containerElementId: string, callbackFn: (paymentToken: string) => void): boolean;
23
+ }
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Checkout } from './resources/checkout';
2
+ import { renderEmbeddedCheckout } from './resources/embedded';
2
3
  import { Webhooks } from './resources/webhooks';
3
- export default class HenryPaymentProcessorSDK {
4
+ export class PaymentProcessorSDK {
4
5
  checkout;
5
6
  webhooks;
6
7
  VALID_API_KEYS = ['824c951e-dfac-4342-8e03', '3f67e17a-880a-463b-a667', '78254d40-623a-48c5-b83f'];
@@ -15,3 +16,21 @@ export default class HenryPaymentProcessorSDK {
15
16
  this.webhooks = new Webhooks();
16
17
  }
17
18
  }
19
+ export class EmbeddedCheckoutUI {
20
+ checkoutId;
21
+ constructor(config) {
22
+ if (!config.checkoutId) {
23
+ throw new Error('Henry: checkoutId is required');
24
+ }
25
+ this.checkoutId = config.checkoutId;
26
+ }
27
+ /**
28
+ * Renders the embedded checkout UI
29
+ * @param containerElementId - The ID of the container element where the checkout UI will be rendered
30
+ * @param callbackFn - Callback function to handle the payment token
31
+ * @returns - Whether the UI rendered successfully
32
+ */
33
+ render(containerElementId, callbackFn) {
34
+ return renderEmbeddedCheckout(containerElementId, this.checkoutId, callbackFn);
35
+ }
36
+ }
@@ -64,6 +64,15 @@ 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
+ }>;
67
76
  export declare class Checkout {
68
77
  /**
69
78
  * Create a new checkout session
@@ -1,11 +1,11 @@
1
- import { readHistory } from '../utils/store';
1
+ import { readHistory, writeHistory } from '../utils/store';
2
2
  import { generateID, hashToNumber, signPayload } from '../utils/crypto';
3
3
  import { INTERNAL_WEBHOOKS } from './webhooks';
4
4
  import { sleep } from '../utils/async';
5
5
  // checkoutId -> { historyRecordId }
6
- const INTERNAL_CHECKOUTS = {};
6
+ export const INTERNAL_CHECKOUTS = {};
7
7
  // paymentToken -> card details
8
- const INTERNAL_CARDS = {};
8
+ export const INTERNAL_CARDS = {};
9
9
  export class Checkout {
10
10
  /**
11
11
  * Create a new checkout session
@@ -31,6 +31,13 @@ export class Checkout {
31
31
  // Phase 3: Webhook Scheduling
32
32
  // ---------------------------------------
33
33
  this.scheduleCreateWebhook(hashId, response);
34
+ // Add to history records
35
+ await writeHistory({
36
+ id: hashId,
37
+ amount: params.amount,
38
+ currency: params.currency,
39
+ customerId: params.customerId ?? null,
40
+ });
34
41
  // Simulate API latency
35
42
  await sleep(Math.random() * 2000);
36
43
  return response;
@@ -126,7 +133,7 @@ export class Checkout {
126
133
  }
127
134
  createCheckoutRecord(hashId) {
128
135
  const checkoutId = generateID();
129
- INTERNAL_CHECKOUTS[checkoutId] = {
136
+ INTERNAL_CHECKOUTS[`${checkoutId}`] = {
130
137
  historyRecordId: hashId,
131
138
  };
132
139
  return checkoutId;
@@ -174,7 +181,7 @@ export class Checkout {
174
181
  }
175
182
  ///
176
183
  async validateConfirm(params) {
177
- if (!INTERNAL_CHECKOUTS[params.checkoutId]) {
184
+ if (!INTERNAL_CHECKOUTS[`${params.checkoutId}`]) {
178
185
  return {
179
186
  status: 'failure',
180
187
  substatus: '500-error',
@@ -183,7 +190,7 @@ export class Checkout {
183
190
  };
184
191
  }
185
192
  if (params.type === 'embedded') {
186
- const stored = INTERNAL_CARDS[params.data.paymentToken];
193
+ const stored = INTERNAL_CARDS[`${params.data.paymentToken}`];
187
194
  if (!stored) {
188
195
  return {
189
196
  status: 'failure',
@@ -223,6 +230,21 @@ export class Checkout {
223
230
  return null;
224
231
  }
225
232
  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
+ }
226
248
  const decision = Math.random();
227
249
  if (decision > 0.85) {
228
250
  return {
@@ -251,7 +273,7 @@ export class Checkout {
251
273
  return this.buildInstantConfirmSuccess(params.checkoutId);
252
274
  }
253
275
  async buildInstantConfirmSuccess(checkoutId) {
254
- const { historyRecordId } = INTERNAL_CHECKOUTS[checkoutId] ?? {};
276
+ const { historyRecordId } = INTERNAL_CHECKOUTS[`${checkoutId}`] ?? {};
255
277
  if (!historyRecordId) {
256
278
  return {
257
279
  status: 'failure',
@@ -0,0 +1 @@
1
+ export declare function renderEmbeddedCheckout(containerElementId: string, checkoutId: string, callbackFn: (paymentToken: string) => void): boolean;
@@ -0,0 +1,165 @@
1
+ import { hashToNumber } from '../utils/crypto';
2
+ import { INTERNAL_CARDS, INTERNAL_CHECKOUTS } from './checkout';
3
+ export function renderEmbeddedCheckout(containerElementId, checkoutId, callbackFn) {
4
+ // Ensure browser environment
5
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
6
+ console.warn('EmbeddedCheckoutUI can only be used in a browser environment.');
7
+ return false;
8
+ }
9
+ const { historyRecordId } = INTERNAL_CHECKOUTS[`${checkoutId}`] ?? {};
10
+ if (!historyRecordId) {
11
+ console.warn(`Invalid checkout ID: "${checkoutId}".`);
12
+ return false;
13
+ }
14
+ const container = document.getElementById(containerElementId);
15
+ if (!container) {
16
+ console.warn(`Container element "${containerElementId}" not found.`);
17
+ return false;
18
+ }
19
+ container.innerHTML = `
20
+ <div style="
21
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
22
+ background: #ffffff;
23
+ border-radius: 16px;
24
+ box-shadow: 0 10px 30px rgba(0,0,0,0.08);
25
+ padding: 24px;
26
+ max-width: 360px;
27
+ width: 100%;
28
+ box-sizing: border-box;
29
+ ">
30
+ <h3 style="
31
+ margin: 0 0 20px 0;
32
+ font-size: 18px;
33
+ font-weight: 600;
34
+ color: #111827;
35
+ ">
36
+ Secure Checkout
37
+ </h3>
38
+
39
+ <div style="display:flex; flex-direction:column; gap:14px;">
40
+
41
+ <div style="display:flex; flex-direction:column;">
42
+ <label style="font-size:12px; color:#6b7280; margin-bottom:6px;">
43
+ Card Number
44
+ </label>
45
+ <input id="card-number" type="text" placeholder="4242 4242 4242 4242"
46
+ style="
47
+ padding: 10px 12px;
48
+ border-radius: 10px;
49
+ border: 1px solid #e5e7eb;
50
+ font-size: 14px;
51
+ outline: none;
52
+ transition: border 0.2s, box-shadow 0.2s;
53
+ "
54
+ />
55
+ </div>
56
+
57
+ <div style="display:flex; gap:10px;">
58
+
59
+ <div style="flex:1; display:flex; flex-direction:column;">
60
+ <label style="font-size:12px; color:#6b7280; margin-bottom:6px;">
61
+ Exp. Month
62
+ </label>
63
+ <input id="exp-month" type="number" placeholder="12" min="1" max="12"
64
+ style="
65
+ padding: 10px 12px;
66
+ border-radius: 10px;
67
+ border: 1px solid #e5e7eb;
68
+ font-size: 14px;
69
+ outline: none;
70
+ transition: border 0.2s, box-shadow 0.2s;
71
+ "
72
+ />
73
+ </div>
74
+
75
+ <div style="flex:1; display:flex; flex-direction:column;">
76
+ <label style="font-size:12px; color:#6b7280; margin-bottom:6px;">
77
+ Exp. Year
78
+ </label>
79
+ <input id="exp-year" type="number" placeholder="2030"
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
+ />
89
+ </div>
90
+
91
+ </div>
92
+
93
+ <div style="display:flex; flex-direction:column;">
94
+ <label style="font-size:12px; color:#6b7280; margin-bottom:6px;">
95
+ CVC
96
+ </label>
97
+ <input id="cvc" type="text" placeholder="123"
98
+ style="
99
+ padding: 10px 12px;
100
+ border-radius: 10px;
101
+ border: 1px solid #e5e7eb;
102
+ font-size: 14px;
103
+ outline: none;
104
+ transition: border 0.2s, box-shadow 0.2s;
105
+ "
106
+ />
107
+ </div>
108
+
109
+ <button id="confirm-btn"
110
+ style="
111
+ margin-top: 8px;
112
+ padding: 12px;
113
+ border-radius: 12px;
114
+ border: none;
115
+ font-size: 14px;
116
+ font-weight: 600;
117
+ background: linear-gradient(135deg, #6366f1, #4f46e5);
118
+ color: white;
119
+ cursor: pointer;
120
+ transition: transform 0.05s ease, box-shadow 0.2s ease;
121
+ box-shadow: 0 4px 14px rgba(79,70,229,0.3);
122
+ "
123
+ onmouseover="this.style.boxShadow='0 6px 18px rgba(79,70,229,0.4)'"
124
+ onmouseout="this.style.boxShadow='0 4px 14px rgba(79,70,229,0.3)'"
125
+ onmousedown="this.style.transform='scale(0.98)'"
126
+ onmouseup="this.style.transform='scale(1)'"
127
+ >
128
+ Confirm Payment
129
+ </button>
130
+
131
+ <p style="
132
+ font-size: 11px;
133
+ color: #9ca3af;
134
+ text-align: center;
135
+ margin-top: 8px;
136
+ ">
137
+ 🔒 Payments are securely processed
138
+ </p>
139
+
140
+ </div>
141
+ </div>
142
+ `;
143
+ const button = container.querySelector('#confirm-btn');
144
+ button?.addEventListener('click', () => {
145
+ const number = (container.querySelector('#card-number')?.value ?? '').replace(/\s+/g, '');
146
+ const expMonth = Number(container.querySelector('#exp-month')?.value);
147
+ const expYear = Number(container.querySelector('#exp-year')?.value);
148
+ const cvc = container.querySelector('#cvc')?.value ?? '';
149
+ // 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;
162
+ callbackFn(paymentToken);
163
+ });
164
+ return true;
165
+ }
@@ -5,7 +5,6 @@ export type HistoryRecord = {
5
5
  customerId: string | null;
6
6
  updatedAt: number;
7
7
  createdAt: number;
8
- confirmed: boolean;
9
8
  };
10
9
  /**
11
10
  * Reads all history records from the JSON store.
@@ -14,4 +13,4 @@ export declare function readHistory(): Promise<HistoryRecord[]>;
14
13
  /**
15
14
  * Writes a new record to the JSON store.
16
15
  */
17
- export declare function writeHistory(params: Omit<HistoryRecord, 'updatedAt'>): Promise<HistoryRecord>;
16
+ export declare function writeHistory(params: Omit<HistoryRecord, 'createdAt' | 'updatedAt'>): Promise<HistoryRecord>;
@@ -22,11 +22,28 @@ export async function readHistory() {
22
22
  */
23
23
  export async function writeHistory(params) {
24
24
  const history = await readHistory();
25
- const newRecord = {
26
- updatedAt: new Date().getTime(),
27
- ...params,
28
- };
29
- history.push(newRecord);
25
+ const now = Date.now();
26
+ const existingIndex = history.findIndex((v) => v.id === params.id);
27
+ let record;
28
+ if (existingIndex !== -1) {
29
+ // Replace existing record
30
+ record = {
31
+ ...history[existingIndex],
32
+ ...params,
33
+ createdAt: history[existingIndex]?.createdAt ?? now,
34
+ updatedAt: now,
35
+ };
36
+ history[existingIndex] = record;
37
+ }
38
+ else {
39
+ // Add new record
40
+ record = {
41
+ ...params,
42
+ createdAt: now,
43
+ updatedAt: now,
44
+ };
45
+ history.push(record);
46
+ }
30
47
  await Bun.write(path, JSON.stringify(history, null, 2));
31
- return newRecord;
48
+ return record;
32
49
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henrylabs-interview/payment-processor",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",