elpaypg 0.0.0-stage → 2.0.1
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/.env.example +8 -0
- package/README.md +176 -2
- package/package.json +21 -4
- package/public/elpay-checkout.js +769 -0
- package/public/elpay-sms-gateway.apk +0 -0
- package/public/elpay-sms-gateway.apk.sha256 +1 -0
- package/public/elpay.js +785 -0
- package/public/fonts/dm-sans.woff2 +0 -0
- package/public/fonts/sora.woff2 +0 -0
- package/public/views/checkout.html +2839 -0
- package/public/views/partials/auth.html +110 -0
- package/public/views/partials/footer.html +54 -0
- package/public/views/partials/header.html +101 -0
- package/public/views/partials/modals.html +179 -0
- package/public/views/partials/payment-methods.html +526 -0
- package/public/views/partials/sidebar.html +75 -0
- package/public/views/partials/splash.html +35 -0
- package/src/__tests__/smoke.test.js +8 -0
- package/src/auth-guard.js +95 -0
- package/src/index.js +137 -0
- package/src/router.js +46 -0
- package/src/routes/admin.routes.js +2424 -0
- package/src/routes/checkout.routes.js +2000 -0
- package/src/routes/customer.routes.js +650 -0
- package/src/routes/device.routes.js +303 -0
- package/src/routes/payments.routes.js +446 -0
- package/src/routes/pollers.routes.js +653 -0
- package/src/services/observer-poller.service.js +369 -0
- package/src/services/tunnel-manager.js +130 -0
- package/src/services/whatsapp-berry.service.js +743 -0
- package/src/vendor/common/__tests__/common.test.js +297 -0
- package/src/vendor/common/__tests__/common.test.ts +160 -0
- package/src/vendor/common/contracts.js +64 -0
- package/src/vendor/common/contracts.ts +188 -0
- package/src/vendor/common/crypto.js +132 -0
- package/src/vendor/common/crypto.ts +112 -0
- package/src/vendor/common/enums.js +91 -0
- package/src/vendor/common/enums.ts +91 -0
- package/src/vendor/common/index.js +12 -0
- package/src/vendor/common/index.ts +8 -0
- package/src/vendor/common/money.js +62 -0
- package/src/vendor/common/money.ts +56 -0
- package/src/vendor/common/observation.js +27 -0
- package/src/vendor/common/observation.ts +52 -0
- package/src/vendor/common/runtime-config.js +246 -0
- package/src/vendor/common/secrets.js +374 -0
- package/src/vendor/common/state-machine.d.ts +1 -0
- package/src/vendor/common/state-machine.js +1 -0
- package/src/vendor/common/state-transitions.js +85 -0
- package/src/vendor/common/tunnel-command.js +59 -0
- package/src/vendor/common/upi-providers.js +389 -0
- package/src/vendor/common/upi-providers.ts +400 -0
- package/src/vendor/common/upi.js +69 -0
- package/src/vendor/common/upi.ts +61 -0
- package/src/vendor/core/__tests__/core.test.js +683 -0
- package/src/vendor/core/__tests__/queue-timing.test.js +237 -0
- package/src/vendor/core/__tests__/webhook-engine.test.js +243 -0
- package/src/vendor/core/attribution-engine.js +855 -0
- package/src/vendor/core/bank-email-parser.js +105 -0
- package/src/vendor/core/bank-sms-parser.js +183 -0
- package/src/vendor/core/device-security.js +506 -0
- package/src/vendor/core/index.js +13 -0
- package/src/vendor/core/normalizer.js +165 -0
- package/src/vendor/core/qr-lib.js +8 -0
- package/src/vendor/core/qr.js +62 -0
- package/src/vendor/core/queue-engine.js +153 -0
- package/src/vendor/core/recovery-engine.js +159 -0
- package/src/vendor/core/state-machine.js +42 -0
- package/src/vendor/core/timing-engine.js +272 -0
- package/src/vendor/core/verification-pipeline.js +1164 -0
- package/src/vendor/core/webhook-engine.js +280 -0
- package/src/vendor/db/__tests__/auth-matrix.test.js +564 -0
- package/src/vendor/db/__tests__/customer-reset.test.js +95 -0
- package/src/vendor/db/__tests__/db.test.js +265 -0
- package/src/vendor/db/__tests__/schema-parity.test.js +137 -0
- package/src/vendor/db/__tests__/sqlite_verify.test.js +15 -0
- package/src/vendor/db/database.js +181 -0
- package/src/vendor/db/index.js +12 -0
- package/src/vendor/db/migrate.js +210 -0
- package/src/vendor/db/repositories/audit-log.repository.js +68 -0
- package/src/vendor/db/repositories/customer-session.repository.js +181 -0
- package/src/vendor/db/repositories/customer.repository.js +636 -0
- package/src/vendor/db/repositories/device.repository.js +285 -0
- package/src/vendor/db/repositories/merchant.repository.js +158 -0
- package/src/vendor/db/repositories/observation.repository.js +124 -0
- package/src/vendor/db/repositories/payment-session.repository.js +618 -0
- package/src/vendor/db/repositories/receiving-account.repository.js +244 -0
- package/src/vendor/db/repositories/support-ticket.repository.js +140 -0
- package/src/vendor/db/schema.sql +266 -0
- 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
|
+
}
|