elpaypg 0.0.0-stage → 2.0.0

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.
Files changed (90) hide show
  1. package/.env.example +8 -0
  2. package/README.md +176 -2
  3. package/package.json +21 -4
  4. package/public/elpay-checkout.js +769 -0
  5. package/public/elpay-sms-gateway.apk +0 -0
  6. package/public/elpay-sms-gateway.apk.sha256 +1 -0
  7. package/public/elpay.js +785 -0
  8. package/public/fonts/dm-sans.woff2 +0 -0
  9. package/public/fonts/sora.woff2 +0 -0
  10. package/public/views/checkout.html +2839 -0
  11. package/public/views/partials/auth.html +110 -0
  12. package/public/views/partials/footer.html +54 -0
  13. package/public/views/partials/header.html +101 -0
  14. package/public/views/partials/modals.html +179 -0
  15. package/public/views/partials/payment-methods.html +526 -0
  16. package/public/views/partials/sidebar.html +75 -0
  17. package/public/views/partials/splash.html +35 -0
  18. package/src/__tests__/smoke.test.js +8 -0
  19. package/src/auth-guard.js +95 -0
  20. package/src/index.js +137 -0
  21. package/src/router.js +46 -0
  22. package/src/routes/admin.routes.js +2421 -0
  23. package/src/routes/checkout.routes.js +2000 -0
  24. package/src/routes/customer.routes.js +650 -0
  25. package/src/routes/device.routes.js +303 -0
  26. package/src/routes/payments.routes.js +446 -0
  27. package/src/routes/pollers.routes.js +653 -0
  28. package/src/services/observer-poller.service.js +369 -0
  29. package/src/services/tunnel-manager.js +130 -0
  30. package/src/services/whatsapp-berry.service.js +743 -0
  31. package/src/vendor/common/__tests__/common.test.js +297 -0
  32. package/src/vendor/common/__tests__/common.test.ts +160 -0
  33. package/src/vendor/common/contracts.js +64 -0
  34. package/src/vendor/common/contracts.ts +188 -0
  35. package/src/vendor/common/crypto.js +132 -0
  36. package/src/vendor/common/crypto.ts +112 -0
  37. package/src/vendor/common/enums.js +91 -0
  38. package/src/vendor/common/enums.ts +91 -0
  39. package/src/vendor/common/index.js +12 -0
  40. package/src/vendor/common/index.ts +8 -0
  41. package/src/vendor/common/money.js +62 -0
  42. package/src/vendor/common/money.ts +56 -0
  43. package/src/vendor/common/observation.js +27 -0
  44. package/src/vendor/common/observation.ts +52 -0
  45. package/src/vendor/common/runtime-config.js +246 -0
  46. package/src/vendor/common/secrets.js +374 -0
  47. package/src/vendor/common/state-machine.d.ts +1 -0
  48. package/src/vendor/common/state-machine.js +1 -0
  49. package/src/vendor/common/state-transitions.js +85 -0
  50. package/src/vendor/common/tunnel-command.js +59 -0
  51. package/src/vendor/common/upi-providers.js +389 -0
  52. package/src/vendor/common/upi-providers.ts +400 -0
  53. package/src/vendor/common/upi.js +69 -0
  54. package/src/vendor/common/upi.ts +61 -0
  55. package/src/vendor/core/__tests__/core.test.js +683 -0
  56. package/src/vendor/core/__tests__/queue-timing.test.js +237 -0
  57. package/src/vendor/core/__tests__/webhook-engine.test.js +243 -0
  58. package/src/vendor/core/attribution-engine.js +855 -0
  59. package/src/vendor/core/bank-email-parser.js +105 -0
  60. package/src/vendor/core/bank-sms-parser.js +183 -0
  61. package/src/vendor/core/device-security.js +506 -0
  62. package/src/vendor/core/index.js +13 -0
  63. package/src/vendor/core/normalizer.js +165 -0
  64. package/src/vendor/core/qr-lib.js +8 -0
  65. package/src/vendor/core/qr.js +62 -0
  66. package/src/vendor/core/queue-engine.js +153 -0
  67. package/src/vendor/core/recovery-engine.js +159 -0
  68. package/src/vendor/core/state-machine.js +42 -0
  69. package/src/vendor/core/timing-engine.js +272 -0
  70. package/src/vendor/core/verification-pipeline.js +1164 -0
  71. package/src/vendor/core/webhook-engine.js +280 -0
  72. package/src/vendor/db/__tests__/auth-matrix.test.js +564 -0
  73. package/src/vendor/db/__tests__/customer-reset.test.js +95 -0
  74. package/src/vendor/db/__tests__/db.test.js +265 -0
  75. package/src/vendor/db/__tests__/schema-parity.test.js +137 -0
  76. package/src/vendor/db/__tests__/sqlite_verify.test.js +15 -0
  77. package/src/vendor/db/database.js +181 -0
  78. package/src/vendor/db/index.js +12 -0
  79. package/src/vendor/db/migrate.js +210 -0
  80. package/src/vendor/db/repositories/audit-log.repository.js +68 -0
  81. package/src/vendor/db/repositories/customer-session.repository.js +181 -0
  82. package/src/vendor/db/repositories/customer.repository.js +636 -0
  83. package/src/vendor/db/repositories/device.repository.js +285 -0
  84. package/src/vendor/db/repositories/merchant.repository.js +158 -0
  85. package/src/vendor/db/repositories/observation.repository.js +124 -0
  86. package/src/vendor/db/repositories/payment-session.repository.js +618 -0
  87. package/src/vendor/db/repositories/receiving-account.repository.js +244 -0
  88. package/src/vendor/db/repositories/support-ticket.repository.js +140 -0
  89. package/src/vendor/db/schema.sql +266 -0
  90. package/src/vendor/db/sqlite-schema.sql +270 -0
@@ -0,0 +1,272 @@
1
+ import { PaymentStatus } from '../common/index.js';
2
+
3
+ export class TimingEngine {
4
+ /**
5
+ * @param {{
6
+ * sessionRepo: import('../db/index.js').PaymentSessionRepository,
7
+ * queueEngine: import('./queue-engine.js').QueueEngine,
8
+ * eventEmitter?: { emit: (event: string, payload: any) => void },
9
+ * auditRepo?: import('../db/index.js').AuditLogRepository,
10
+ * activeDurationMs?: number,
11
+ * processingDurationMs?: number
12
+ * }} options
13
+ */
14
+ constructor(options) {
15
+ this.sessionRepo = options.sessionRepo;
16
+ this.queueEngine = options.queueEngine;
17
+ this.eventEmitter = options.eventEmitter || { emit: () => {} };
18
+ this.auditRepo = options.auditRepo;
19
+ this.activeDurationMs = options.activeDurationMs || 300000; // 5 minutes
20
+ this.processingDurationMs = options.processingDurationMs || 30000; // 30 seconds
21
+
22
+ /** @type {Map<string, NodeJS.Timeout>} */
23
+ this.activeTimers = new Map();
24
+ /** @type {Map<string, NodeJS.Timeout>} */
25
+ this.processingTimers = new Map();
26
+ }
27
+
28
+ /**
29
+ * Starts Timer 1 (5-minute payment window) for an activated session.
30
+ * @param {string} sessionId
31
+ * @param {Date} expiresAt
32
+ */
33
+ startActiveTimer(sessionId, expiresAt) {
34
+ this.clearTimers(sessionId);
35
+
36
+ const delayMs = Math.max(0, expiresAt.getTime() - Date.now());
37
+
38
+ const timer = setTimeout(() => {
39
+ this.handleActiveTimerExpiration(sessionId);
40
+ }, delayMs);
41
+
42
+ // Unref so timer doesn't prevent process exit if running standalone
43
+ if (timer.unref) timer.unref();
44
+
45
+ this.activeTimers.set(sessionId, timer);
46
+ }
47
+
48
+ /**
49
+ * Executes when Timer 1 reaches 0 without customer clicking "Payment Done".
50
+ * Transitions to EXPIRED and promotes next queued customer.
51
+ * @param {string} sessionId
52
+ */
53
+ handleActiveTimerExpiration(sessionId) {
54
+ this.activeTimers.delete(sessionId);
55
+
56
+ const session = this.sessionRepo.findById(sessionId);
57
+ if (!session) return;
58
+
59
+ // Invariant: Only expire if still in ACTIVE status
60
+ if (session.status !== PaymentStatus.ACTIVE) {
61
+ return;
62
+ }
63
+
64
+ // Transition to EXPIRED and release slot
65
+ this.sessionRepo.transitionToTerminal(sessionId, PaymentStatus.EXPIRED);
66
+
67
+ if (this.auditRepo) {
68
+ this.auditRepo.log({
69
+ merchantId: session.merchantId,
70
+ entityType: 'PAYMENT_SESSION',
71
+ entityId: sessionId,
72
+ action: 'PAYMENT_EXPIRED_TIMEOUT',
73
+ actorType: 'SYSTEM',
74
+ details: { reason: '5-minute payment window elapsed without customer clicking Payment Done' }
75
+ });
76
+ }
77
+
78
+ this.eventEmitter.emit('SESSION.EXPIRED', { sessionId, receivingAccountId: session.receivingAccountId });
79
+ this.eventEmitter.emit('PAYMENT.EXPIRED', {
80
+ paymentId: sessionId,
81
+ orderId: session.orderId,
82
+ merchantId: session.merchantId,
83
+ amountPaise: session.amountPaise
84
+ });
85
+
86
+ // Immediately promote next queued customer
87
+ if (session.receivingAccountId) {
88
+ this.queueEngine.promoteNext(session.receivingAccountId, this.activeDurationMs);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Cancel active timer for session if running.
94
+ * @param {string} sessionId
95
+ */
96
+ cancelActiveTimer(sessionId) {
97
+ const activeTimer = this.activeTimers.get(sessionId);
98
+ if (activeTimer) {
99
+ clearTimeout(activeTimer);
100
+ this.activeTimers.delete(sessionId);
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Customer clicked "PAYMENT DONE — CHECK MY PAYMENT".
106
+ * 1. Cancels Timer 1 (5-minute timer).
107
+ * 2. Starts Timer 2 (30-second processing timer).
108
+ * 3. Customer does NOT get another 5 minutes.
109
+ * @param {string} sessionId
110
+ * @returns {any} Updated session in PROCESSING state
111
+ */
112
+ handlePaymentDone(sessionId) {
113
+ // 1. Cancel Timer 1
114
+ const activeTimer = this.activeTimers.get(sessionId);
115
+ if (activeTimer) {
116
+ clearTimeout(activeTimer);
117
+ this.activeTimers.delete(sessionId);
118
+ }
119
+
120
+ // 2. Transition session to PROCESSING in DB
121
+ const session = this.sessionRepo.transitionToProcessing(sessionId, this.processingDurationMs);
122
+
123
+ if (this.auditRepo) {
124
+ this.auditRepo.log({
125
+ merchantId: session.merchantId,
126
+ entityType: 'PAYMENT_SESSION',
127
+ entityId: sessionId,
128
+ action: 'CUSTOMER_CLICKED_PAYMENT_DONE',
129
+ actorType: 'CUSTOMER',
130
+ details: { processingExpiresAt: session.processingExpiresAt?.toISOString() }
131
+ });
132
+ }
133
+
134
+ this.eventEmitter.emit('SESSION.PROCESSING', {
135
+ sessionId,
136
+ receivingAccountId: session.receivingAccountId,
137
+ processingExpiresAt: session.processingExpiresAt
138
+ });
139
+
140
+ // 3. Start Timer 2 (30-second processing window)
141
+ const delayMs = Math.max(0, session.processingExpiresAt.getTime() - Date.now());
142
+
143
+ const procTimer = setTimeout(() => {
144
+ this.handleProcessingTimerExpiration(sessionId);
145
+ }, delayMs);
146
+
147
+ if (procTimer.unref) procTimer.unref();
148
+ this.processingTimers.set(sessionId, procTimer);
149
+
150
+ return session;
151
+ }
152
+
153
+ /**
154
+ * Executes when Timer 2 (30-second processing window) reaches 0 with no matching observation.
155
+ * Transitions to FAILED and promotes next queued customer.
156
+ * @param {string} sessionId
157
+ */
158
+ handleProcessingTimerExpiration(sessionId) {
159
+ this.processingTimers.delete(sessionId);
160
+
161
+ const session = this.sessionRepo.findById(sessionId);
162
+ if (!session) return;
163
+
164
+ // Invariant: Only fail if still in PROCESSING status (no observer matched it yet)
165
+ if (session.status !== PaymentStatus.PROCESSING) {
166
+ return;
167
+ }
168
+
169
+ // Transition to FAILED and release slot
170
+ this.sessionRepo.transitionToTerminal(sessionId, PaymentStatus.FAILED);
171
+
172
+ if (this.auditRepo) {
173
+ this.auditRepo.log({
174
+ merchantId: session.merchantId,
175
+ entityType: 'PAYMENT_SESSION',
176
+ entityId: sessionId,
177
+ action: 'PAYMENT_FAILED_NO_OBSERVATION',
178
+ actorType: 'SYSTEM',
179
+ details: { reason: '30-second verification window elapsed with no matching observation' }
180
+ });
181
+ }
182
+
183
+ this.eventEmitter.emit('SESSION.FAILED', { sessionId, receivingAccountId: session.receivingAccountId });
184
+ this.eventEmitter.emit('PAYMENT.FAILED', {
185
+ paymentId: sessionId,
186
+ orderId: session.orderId,
187
+ merchantId: session.merchantId,
188
+ amountPaise: session.amountPaise
189
+ });
190
+
191
+ // Immediately promote next queued customer
192
+ if (session.receivingAccountId) {
193
+ this.queueEngine.promoteNext(session.receivingAccountId, this.activeDurationMs);
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Customer explicitly cancels the payment session during ACTIVE window.
199
+ * Immediately releases slot and promotes next queued customer.
200
+ * @param {string} sessionId
201
+ */
202
+ handleCancel(sessionId) {
203
+ const session = this.sessionRepo.findById(sessionId);
204
+ if (!session) return null;
205
+
206
+ if ([PaymentStatus.PROCESSING, PaymentStatus.SUCCESS, PaymentStatus.FAILED, PaymentStatus.EXPIRED, PaymentStatus.CANCELLED, PaymentStatus.SUSPICIOUS].includes(session.status)) {
207
+ return session; // cannot cancel in-flight PROCESSING or already terminal sessions
208
+ }
209
+
210
+ this.clearTimers(sessionId);
211
+
212
+ const updated = this.sessionRepo.transitionToTerminal(sessionId, PaymentStatus.CANCELLED);
213
+
214
+ if (this.auditRepo) {
215
+ this.auditRepo.log({
216
+ merchantId: session.merchantId,
217
+ entityType: 'PAYMENT_SESSION',
218
+ entityId: sessionId,
219
+ action: 'PAYMENT_CANCELLED_BY_USER',
220
+ actorType: 'CUSTOMER',
221
+ details: { previousStatus: session.status }
222
+ });
223
+ }
224
+
225
+ this.eventEmitter.emit('SESSION.CANCELLED', { sessionId, receivingAccountId: session.receivingAccountId });
226
+ this.eventEmitter.emit('PAYMENT.CANCELLED', {
227
+ paymentId: sessionId,
228
+ orderId: session.orderId,
229
+ merchantId: session.merchantId,
230
+ amountPaise: session.amountPaise
231
+ });
232
+
233
+ // Immediately promote next queued customer
234
+ if (session.receivingAccountId) {
235
+ this.queueEngine.promoteNext(session.receivingAccountId, this.activeDurationMs);
236
+ }
237
+
238
+ return updated;
239
+ }
240
+
241
+ /**
242
+ * Clears all in-memory timer handles for a session.
243
+ * @param {string} sessionId
244
+ */
245
+ clearTimers(sessionId) {
246
+ const act = this.activeTimers.get(sessionId);
247
+ if (act) {
248
+ clearTimeout(act);
249
+ this.activeTimers.delete(sessionId);
250
+ }
251
+
252
+ const prc = this.processingTimers.get(sessionId);
253
+ if (prc) {
254
+ clearTimeout(prc);
255
+ this.processingTimers.delete(sessionId);
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Clean up all timers across all sessions (for graceful shutdown).
261
+ */
262
+ destroy() {
263
+ for (const timer of this.activeTimers.values()) {
264
+ clearTimeout(timer);
265
+ }
266
+ for (const timer of this.processingTimers.values()) {
267
+ clearTimeout(timer);
268
+ }
269
+ this.activeTimers.clear();
270
+ this.processingTimers.clear();
271
+ }
272
+ }