elpaypg 2.0.6 → 2.0.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.
- package/package.json +1 -1
- package/public/views/checkout.html +33 -15
- package/src/index.js +138 -137
- package/src/router.js +65 -5
- package/src/routes/admin.routes.js +3 -5
- package/src/routes/checkout.routes.js +4 -19
- package/src/sse.js +44 -0
package/package.json
CHANGED
|
@@ -970,21 +970,16 @@
|
|
|
970
970
|
let currentSelectedMethod = 'UPI';
|
|
971
971
|
|
|
972
972
|
function hideAllContent() {
|
|
973
|
-
document.getElementById('viewUpiContent').hidden = true;
|
|
974
|
-
const vCod = document.getElementById('viewCodContent');
|
|
975
|
-
if (
|
|
976
|
-
const
|
|
977
|
-
if (
|
|
978
|
-
const
|
|
979
|
-
if (
|
|
980
|
-
document.getElementById('
|
|
981
|
-
document.getElementById('
|
|
982
|
-
document.getElementById('
|
|
983
|
-
document.getElementById('viewQueuedContent').hidden = true;
|
|
984
|
-
const vBlocked = document.getElementById('viewBlockedContent');
|
|
985
|
-
if (vBlocked) vBlocked.hidden = true;
|
|
986
|
-
const vDup = document.getElementById('viewDuplicateSessionContent');
|
|
987
|
-
if (vDup) vDup.hidden = true;
|
|
973
|
+
const u = document.getElementById('viewUpiContent'); if (u) u.hidden = true;
|
|
974
|
+
const vCod = document.getElementById('viewCodContent'); if (vCod) vCod.hidden = true;
|
|
975
|
+
const vCash = document.getElementById('viewCashContent'); if (vCash) vCash.hidden = true;
|
|
976
|
+
const vCard = document.getElementById('viewCardContent'); if (vCard) vCard.hidden = true;
|
|
977
|
+
const p = document.getElementById('viewProcessingContent'); if (p) p.hidden = true;
|
|
978
|
+
const s = document.getElementById('viewSuccessContent'); if (s) s.hidden = true;
|
|
979
|
+
const f = document.getElementById('viewFailedContent'); if (f) f.hidden = true;
|
|
980
|
+
const q = document.getElementById('viewQueuedContent'); if (q) q.hidden = true;
|
|
981
|
+
const vBlocked = document.getElementById('viewBlockedContent'); if (vBlocked) vBlocked.hidden = true;
|
|
982
|
+
const vDup = document.getElementById('viewDuplicateSessionContent'); if (vDup) vDup.hidden = true;
|
|
988
983
|
}
|
|
989
984
|
|
|
990
985
|
function formatTime(totalSec) {
|
|
@@ -1432,6 +1427,21 @@
|
|
|
1432
1427
|
}
|
|
1433
1428
|
|
|
1434
1429
|
return { deviceId: null, deviceToken: null };
|
|
1430
|
+
},
|
|
1431
|
+
|
|
1432
|
+
async reset() {
|
|
1433
|
+
this.currentDeviceId = null;
|
|
1434
|
+
this.currentDeviceToken = null;
|
|
1435
|
+
try { localStorage.removeItem('ypg_device_id'); localStorage.removeItem('ypg_device_token'); } catch {}
|
|
1436
|
+
try {
|
|
1437
|
+
const db = await this.openDb();
|
|
1438
|
+
if (db) {
|
|
1439
|
+
const tx = db.transaction(this.storeName, 'readwrite');
|
|
1440
|
+
const store = tx.objectStore(this.storeName);
|
|
1441
|
+
store.delete('primary_device');
|
|
1442
|
+
await new Promise((resolve) => { tx.oncomplete = () => resolve(); tx.onerror = () => resolve(); });
|
|
1443
|
+
}
|
|
1444
|
+
} catch {}
|
|
1435
1445
|
}
|
|
1436
1446
|
};
|
|
1437
1447
|
|
|
@@ -1459,6 +1469,14 @@
|
|
|
1459
1469
|
const res = await fetch(`${API_BASE}/api/v1/checkout/${PAYMENT_ID}/state`, { headers });
|
|
1460
1470
|
const data = await res.json().catch(() => ({}));
|
|
1461
1471
|
|
|
1472
|
+
if (data.code === 'DEVICE_REVOKED' || data.code === 'DEVICE_TOKEN_INVALID' || data.code === 'DEVICE_ID_REQUIRED' || data.code === 'DEVICE_CREDENTIALS_REQUIRED') {
|
|
1473
|
+
if (!window._deviceResetAttempted) {
|
|
1474
|
+
window._deviceResetAttempted = true;
|
|
1475
|
+
await DeviceIdentity.reset();
|
|
1476
|
+
return loadState();
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1462
1480
|
if (res.status === 409 || data.error === 'DUPLICATE_SESSION_DETECTED') {
|
|
1463
1481
|
renderDuplicateSession(data);
|
|
1464
1482
|
dismissSplashScreen();
|
package/src/index.js
CHANGED
|
@@ -1,137 +1,138 @@
|
|
|
1
|
-
import { EventEmitter } from 'node:events';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { PaymentStatus } from './vendor/common/index.js';
|
|
5
|
-
import {
|
|
6
|
-
getDatabase,
|
|
7
|
-
ensureDatabaseInitialized,
|
|
8
|
-
runMigrations,
|
|
9
|
-
MerchantRepository,
|
|
10
|
-
ReceivingAccountRepository,
|
|
11
|
-
CustomerRepository,
|
|
12
|
-
CustomerSessionRepository,
|
|
13
|
-
PaymentSessionRepository,
|
|
14
|
-
ObservationRepository,
|
|
15
|
-
SupportTicketRepository,
|
|
16
|
-
AuditLogRepository,
|
|
17
|
-
DeviceRepository
|
|
18
|
-
} from './vendor/db/index.js';
|
|
19
|
-
import {
|
|
20
|
-
QueueEngine,
|
|
21
|
-
TimingEngine,
|
|
22
|
-
AttributionEngine,
|
|
23
|
-
RecoveryEngine,
|
|
24
|
-
WebhookEngine,
|
|
25
|
-
PaymentVerificationPipeline
|
|
26
|
-
} from './vendor/core/index.js';
|
|
27
|
-
import { createRouter } from './router.js';
|
|
28
|
-
|
|
29
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
30
|
-
const __dirname = path.dirname(__filename);
|
|
31
|
-
|
|
32
|
-
export async function elpay(options = {}) {
|
|
33
|
-
const db = options.db || getDatabase(options.databaseUrl);
|
|
34
|
-
ensureDatabaseInitialized(db);
|
|
35
|
-
const { defaultMerchant } = runMigrations(db);
|
|
36
|
-
|
|
37
|
-
const merchantRepo = new MerchantRepository(db);
|
|
38
|
-
const accountRepo = new ReceivingAccountRepository(db);
|
|
39
|
-
const customerRepo = new CustomerRepository(db);
|
|
40
|
-
const customerSessionRepo = new CustomerSessionRepository(db, customerRepo);
|
|
41
|
-
const sessionRepo = new PaymentSessionRepository(db);
|
|
42
|
-
const obsRepo = new ObservationRepository(db);
|
|
43
|
-
const ticketRepo = new SupportTicketRepository(db);
|
|
44
|
-
const auditRepo = new AuditLogRepository(db);
|
|
45
|
-
const deviceRepo = new DeviceRepository(db);
|
|
46
|
-
|
|
47
|
-
const eventEmitter = new EventEmitter();
|
|
48
|
-
|
|
49
|
-
const parseWindowMs = (raw, fallback) => {
|
|
50
|
-
if (raw === undefined || raw === null || String(raw).trim() === '') return fallback;
|
|
51
|
-
const value = Number(raw);
|
|
52
|
-
if (!Number.isFinite(value) || value <= 0 || value > 24 * 60 * 60 * 1000) return fallback;
|
|
53
|
-
return Math.floor(value);
|
|
54
|
-
};
|
|
55
|
-
const activeDurationMs = parseWindowMs(process.env.PAYMENT_ACTIVE_WINDOW_MS, 300000);
|
|
56
|
-
const processingDurationMs = parseWindowMs(process.env.PAYMENT_PROCESSING_WINDOW_MS, 30000);
|
|
57
|
-
|
|
58
|
-
const queueEngine = new QueueEngine({ sessionRepo, accountRepo, eventEmitter, auditRepo, activeDurationMs, processingDurationMs });
|
|
59
|
-
const timingEngine = new TimingEngine({ sessionRepo, queueEngine, eventEmitter, auditRepo, activeDurationMs, processingDurationMs });
|
|
60
|
-
const attributionEngine = new AttributionEngine({ sessionRepo, obsRepo, ticketRepo, auditRepo, eventEmitter });
|
|
61
|
-
const recoveryEngine = new RecoveryEngine({ db, sessionRepo, accountRepo, queueEngine, timingEngine, auditRepo, eventEmitter });
|
|
62
|
-
const webhookEngine = new WebhookEngine({ db, merchantRepo, maxRetries: Number(process.env.WEBHOOK_MAX_RETRIES || 5) });
|
|
63
|
-
|
|
64
|
-
const checkoutBaseUrl = options.baseUrl || process.env.BASE_CHECKOUT_URL || 'http://localhost:3000';
|
|
65
|
-
|
|
66
|
-
const handleSlotFreedAndPromote = (receivingAccountId) => {
|
|
67
|
-
if (!receivingAccountId) return null;
|
|
68
|
-
const promoted = queueEngine.promoteNext(receivingAccountId);
|
|
69
|
-
if (promoted) {
|
|
70
|
-
if (promoted.expiresAt) timingEngine.startActiveTimer(promoted.id, promoted.expiresAt);
|
|
71
|
-
}
|
|
72
|
-
return promoted;
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
eventEmitter.on('PAYMENT.SUCCESS', (data) => {
|
|
76
|
-
const session = sessionRepo.findById(data.paymentId);
|
|
77
|
-
if (session) {
|
|
78
|
-
webhookEngine.dispatch('SUCCESS', session).catch(() => {});
|
|
79
|
-
handleSlotFreedAndPromote(session.receivingAccountId);
|
|
80
|
-
}
|
|
81
|
-
if (options.onSuccess) options.onSuccess(data);
|
|
82
|
-
});
|
|
83
|
-
eventEmitter.on('PAYMENT.FAILED', (data) => {
|
|
84
|
-
const session = sessionRepo.findById(data.paymentId);
|
|
85
|
-
if (session) {
|
|
86
|
-
webhookEngine.dispatch('FAILED', session).catch(() => {});
|
|
87
|
-
handleSlotFreedAndPromote(session.receivingAccountId);
|
|
88
|
-
}
|
|
89
|
-
if (options.onFailed) options.onFailed(data);
|
|
90
|
-
});
|
|
91
|
-
eventEmitter.on('PAYMENT.EXPIRED', (data) => {
|
|
92
|
-
const session = sessionRepo.findById(data.paymentId);
|
|
93
|
-
if (session) {
|
|
94
|
-
webhookEngine.dispatch('EXPIRED', session).catch(() => {});
|
|
95
|
-
handleSlotFreedAndPromote(session.receivingAccountId);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
eventEmitter.on('PAYMENT.CANCELLED', (data) => {
|
|
99
|
-
const session = sessionRepo.findById(data.paymentId);
|
|
100
|
-
if (session) {
|
|
101
|
-
webhookEngine.dispatch('CANCELLED', session).catch(() => {});
|
|
102
|
-
handleSlotFreedAndPromote(session.receivingAccountId);
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
recoveryEngine.recover();
|
|
107
|
-
|
|
108
|
-
const pipeline = new PaymentVerificationPipeline({
|
|
109
|
-
db, sessionRepo, obsRepo, customerRepo, attributionEngine, auditRepo, merchantRepo,
|
|
110
|
-
server: null, eventEmitter
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
const ctx = {
|
|
114
|
-
db, merchantRepo, accountRepo, customerRepo, customerSessionRepo, sessionRepo, deviceRepo,
|
|
115
|
-
obsRepo, ticketRepo, auditRepo, queueEngine, timingEngine, attributionEngine, pipeline,
|
|
116
|
-
recoveryEngine, eventEmitter, defaultMerchant, checkoutBaseUrl, webhookEngine,
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
export
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { PaymentStatus } from './vendor/common/index.js';
|
|
5
|
+
import {
|
|
6
|
+
getDatabase,
|
|
7
|
+
ensureDatabaseInitialized,
|
|
8
|
+
runMigrations,
|
|
9
|
+
MerchantRepository,
|
|
10
|
+
ReceivingAccountRepository,
|
|
11
|
+
CustomerRepository,
|
|
12
|
+
CustomerSessionRepository,
|
|
13
|
+
PaymentSessionRepository,
|
|
14
|
+
ObservationRepository,
|
|
15
|
+
SupportTicketRepository,
|
|
16
|
+
AuditLogRepository,
|
|
17
|
+
DeviceRepository
|
|
18
|
+
} from './vendor/db/index.js';
|
|
19
|
+
import {
|
|
20
|
+
QueueEngine,
|
|
21
|
+
TimingEngine,
|
|
22
|
+
AttributionEngine,
|
|
23
|
+
RecoveryEngine,
|
|
24
|
+
WebhookEngine,
|
|
25
|
+
PaymentVerificationPipeline
|
|
26
|
+
} from './vendor/core/index.js';
|
|
27
|
+
import { createRouter } from './router.js';
|
|
28
|
+
|
|
29
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
30
|
+
const __dirname = path.dirname(__filename);
|
|
31
|
+
|
|
32
|
+
export async function elpay(options = {}) {
|
|
33
|
+
const db = options.db || getDatabase(options.databaseUrl);
|
|
34
|
+
ensureDatabaseInitialized(db);
|
|
35
|
+
const { defaultMerchant } = runMigrations(db);
|
|
36
|
+
|
|
37
|
+
const merchantRepo = new MerchantRepository(db);
|
|
38
|
+
const accountRepo = new ReceivingAccountRepository(db);
|
|
39
|
+
const customerRepo = new CustomerRepository(db);
|
|
40
|
+
const customerSessionRepo = new CustomerSessionRepository(db, customerRepo);
|
|
41
|
+
const sessionRepo = new PaymentSessionRepository(db);
|
|
42
|
+
const obsRepo = new ObservationRepository(db);
|
|
43
|
+
const ticketRepo = new SupportTicketRepository(db);
|
|
44
|
+
const auditRepo = new AuditLogRepository(db);
|
|
45
|
+
const deviceRepo = new DeviceRepository(db);
|
|
46
|
+
|
|
47
|
+
const eventEmitter = new EventEmitter();
|
|
48
|
+
|
|
49
|
+
const parseWindowMs = (raw, fallback) => {
|
|
50
|
+
if (raw === undefined || raw === null || String(raw).trim() === '') return fallback;
|
|
51
|
+
const value = Number(raw);
|
|
52
|
+
if (!Number.isFinite(value) || value <= 0 || value > 24 * 60 * 60 * 1000) return fallback;
|
|
53
|
+
return Math.floor(value);
|
|
54
|
+
};
|
|
55
|
+
const activeDurationMs = parseWindowMs(process.env.PAYMENT_ACTIVE_WINDOW_MS, 300000);
|
|
56
|
+
const processingDurationMs = parseWindowMs(process.env.PAYMENT_PROCESSING_WINDOW_MS, 30000);
|
|
57
|
+
|
|
58
|
+
const queueEngine = new QueueEngine({ sessionRepo, accountRepo, eventEmitter, auditRepo, activeDurationMs, processingDurationMs });
|
|
59
|
+
const timingEngine = new TimingEngine({ sessionRepo, queueEngine, eventEmitter, auditRepo, activeDurationMs, processingDurationMs });
|
|
60
|
+
const attributionEngine = new AttributionEngine({ sessionRepo, obsRepo, ticketRepo, auditRepo, eventEmitter });
|
|
61
|
+
const recoveryEngine = new RecoveryEngine({ db, sessionRepo, accountRepo, queueEngine, timingEngine, auditRepo, eventEmitter });
|
|
62
|
+
const webhookEngine = new WebhookEngine({ db, merchantRepo, maxRetries: Number(process.env.WEBHOOK_MAX_RETRIES || 5) });
|
|
63
|
+
|
|
64
|
+
const checkoutBaseUrl = options.baseUrl || process.env.BASE_CHECKOUT_URL || 'http://localhost:3000';
|
|
65
|
+
|
|
66
|
+
const handleSlotFreedAndPromote = (receivingAccountId) => {
|
|
67
|
+
if (!receivingAccountId) return null;
|
|
68
|
+
const promoted = queueEngine.promoteNext(receivingAccountId);
|
|
69
|
+
if (promoted) {
|
|
70
|
+
if (promoted.expiresAt) timingEngine.startActiveTimer(promoted.id, promoted.expiresAt);
|
|
71
|
+
}
|
|
72
|
+
return promoted;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
eventEmitter.on('PAYMENT.SUCCESS', (data) => {
|
|
76
|
+
const session = sessionRepo.findById(data.paymentId);
|
|
77
|
+
if (session) {
|
|
78
|
+
webhookEngine.dispatch('SUCCESS', session).catch(() => {});
|
|
79
|
+
handleSlotFreedAndPromote(session.receivingAccountId);
|
|
80
|
+
}
|
|
81
|
+
if (options.onSuccess) options.onSuccess(data);
|
|
82
|
+
});
|
|
83
|
+
eventEmitter.on('PAYMENT.FAILED', (data) => {
|
|
84
|
+
const session = sessionRepo.findById(data.paymentId);
|
|
85
|
+
if (session) {
|
|
86
|
+
webhookEngine.dispatch('FAILED', session).catch(() => {});
|
|
87
|
+
handleSlotFreedAndPromote(session.receivingAccountId);
|
|
88
|
+
}
|
|
89
|
+
if (options.onFailed) options.onFailed(data);
|
|
90
|
+
});
|
|
91
|
+
eventEmitter.on('PAYMENT.EXPIRED', (data) => {
|
|
92
|
+
const session = sessionRepo.findById(data.paymentId);
|
|
93
|
+
if (session) {
|
|
94
|
+
webhookEngine.dispatch('EXPIRED', session).catch(() => {});
|
|
95
|
+
handleSlotFreedAndPromote(session.receivingAccountId);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
eventEmitter.on('PAYMENT.CANCELLED', (data) => {
|
|
99
|
+
const session = sessionRepo.findById(data.paymentId);
|
|
100
|
+
if (session) {
|
|
101
|
+
webhookEngine.dispatch('CANCELLED', session).catch(() => {});
|
|
102
|
+
handleSlotFreedAndPromote(session.receivingAccountId);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
recoveryEngine.recover();
|
|
107
|
+
|
|
108
|
+
const pipeline = new PaymentVerificationPipeline({
|
|
109
|
+
db, sessionRepo, obsRepo, customerRepo, attributionEngine, auditRepo, merchantRepo,
|
|
110
|
+
server: null, eventEmitter
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const ctx = {
|
|
114
|
+
db, merchantRepo, accountRepo, customerRepo, customerSessionRepo, sessionRepo, deviceRepo,
|
|
115
|
+
obsRepo, ticketRepo, auditRepo, queueEngine, timingEngine, attributionEngine, pipeline,
|
|
116
|
+
recoveryEngine, eventEmitter, defaultMerchant, checkoutBaseUrl, webhookEngine,
|
|
117
|
+
baseUrl: options.baseUrl || process.env.BASE_URL || 'http://localhost:5000',
|
|
118
|
+
otpCooldownMs: options.otpCooldownMs,
|
|
119
|
+
theme: options.theme || {}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const router = createRouter(ctx);
|
|
123
|
+
|
|
124
|
+
const destroy = () => {
|
|
125
|
+
timingEngine.destroy?.();
|
|
126
|
+
attributionEngine.destroy?.();
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
router,
|
|
131
|
+
on: (event, cb) => eventEmitter.on(event, cb),
|
|
132
|
+
init: async () => {},
|
|
133
|
+
destroy,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export { createRouter };
|
|
138
|
+
export default { elpay };
|
package/src/router.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
|
+
import { server } from './sse.js';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import fs from 'node:fs';
|
|
5
6
|
import express from 'express';
|
|
7
|
+
import { getInternalServiceToken } from './vendor/common/index.js';
|
|
6
8
|
import { registerPaymentRoutes } from './routes/payments.routes.js';
|
|
7
9
|
import { registerCheckoutRoutes } from './routes/checkout.routes.js';
|
|
8
10
|
import { registerPollerRoutes } from './routes/pollers.routes.js';
|
|
@@ -12,15 +14,60 @@ import { registerDeviceRoutes } from './routes/device.routes.js';
|
|
|
12
14
|
|
|
13
15
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
16
|
|
|
17
|
+
function loadTemplateWithPartials() {
|
|
18
|
+
const viewsDir = path.resolve(__dirname, '..', 'public', 'views');
|
|
19
|
+
const partialsDir = path.resolve(viewsDir, 'partials');
|
|
20
|
+
let content = fs.readFileSync(path.join(viewsDir, 'checkout.html'), 'utf8');
|
|
21
|
+
content = content.replace(/<!--\s*include:partials\/([a-zA-Z0-9_\-.]+)\s*-->/g, (match, filename) => {
|
|
22
|
+
const partialPath = path.resolve(partialsDir, filename);
|
|
23
|
+
const relative = path.relative(partialsDir, partialPath);
|
|
24
|
+
const isSecure = relative && !relative.startsWith('..') && !path.isAbsolute(relative);
|
|
25
|
+
if (isSecure && fs.existsSync(partialPath)) {
|
|
26
|
+
return fs.readFileSync(partialPath, 'utf8');
|
|
27
|
+
}
|
|
28
|
+
return match;
|
|
29
|
+
});
|
|
30
|
+
return content;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function renderTemplate(template, values) {
|
|
34
|
+
const escapeHtml = (v) => (v === null || v === undefined) ? '' : String(v).replace(/[&<>"']/g, (ch) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[ch]);
|
|
35
|
+
const escapeJs = (v) => (v === null || v === undefined) ? '' : String(v).replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/&/g, '\\u0026');
|
|
36
|
+
const scriptKeys = new Set(['PAYMENT_ID', 'API_BASE_URL', 'AMOUNT']);
|
|
37
|
+
return template.replace(/\{\{([A-Z0-9_]+)\}\}/g, (match, key) => {
|
|
38
|
+
if (!Object.prototype.hasOwnProperty.call(values, key)) return match;
|
|
39
|
+
const value = values[key];
|
|
40
|
+
return scriptKeys.has(key) ? escapeJs(value) : escapeHtml(value);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function serializeForScript(value) {
|
|
45
|
+
return JSON.stringify(value === undefined ? null : value)
|
|
46
|
+
.replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/&/g, '\\u0026')
|
|
47
|
+
.replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function fetchCheckoutState(ctx, paymentId) {
|
|
51
|
+
try {
|
|
52
|
+
const base = (ctx.baseUrl || ctx.checkoutBaseUrl || 'http://127.0.0.1:5000').replace(/\/$/, '');
|
|
53
|
+
const token = getInternalServiceToken();
|
|
54
|
+
const res = await fetch(`${base}/api/v1/checkout/${encodeURIComponent(paymentId)}/state`, {
|
|
55
|
+
headers: { Authorization: `Bearer ${token}`, 'X-Internal-Render': 'true' },
|
|
56
|
+
signal: AbortSignal.timeout(4000)
|
|
57
|
+
});
|
|
58
|
+
if (res.ok) return await res.json();
|
|
59
|
+
} catch {}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
15
63
|
export function createRouter(ctx = {}) {
|
|
16
64
|
const router = Router();
|
|
17
65
|
router.use(express.static(path.join(__dirname, '..', 'public')));
|
|
18
66
|
|
|
19
|
-
router.get('/checkout/:id', (req, res) => {
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
.replace(/\{\{PAYMENT_ID\}\}/g, req.params.id);
|
|
67
|
+
router.get('/checkout/:id', async (req, res) => {
|
|
68
|
+
const paymentId = req.params.id;
|
|
69
|
+
const state = await fetchCheckoutState(ctx, paymentId);
|
|
70
|
+
const html = renderCheckoutPage(ctx, paymentId, state);
|
|
24
71
|
res.type('html').send(html);
|
|
25
72
|
});
|
|
26
73
|
|
|
@@ -50,4 +97,17 @@ export function createRouter(ctx = {}) {
|
|
|
50
97
|
return router;
|
|
51
98
|
}
|
|
52
99
|
|
|
100
|
+
function renderCheckoutPage(ctx, paymentId, state) {
|
|
101
|
+
const template = loadTemplateWithPartials();
|
|
102
|
+
const values = {
|
|
103
|
+
PAYMENT_ID: paymentId,
|
|
104
|
+
API_BASE_URL: '',
|
|
105
|
+
APP_NAME: state?.branding?.appName || 'ELPAY PG',
|
|
106
|
+
MERCHANT_NAME: state?.merchantName || 'Merchant Checkout',
|
|
107
|
+
AMOUNT: state?.amount || '0.00',
|
|
108
|
+
INITIAL_STATE_JSON: serializeForScript(state || {})
|
|
109
|
+
};
|
|
110
|
+
return renderTemplate(template, values);
|
|
111
|
+
}
|
|
112
|
+
|
|
53
113
|
export default createRouter;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import crypto from 'node:crypto';
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { server } from '../sse.js';
|
|
2
3
|
import { Router } from 'express';
|
|
3
4
|
import { PaymentStatus, PaymentMethod, SellingMode, AuditActorType, isValidVpa, toRupeesString, deriveCloudflareRoutes, resolveRuntimeUrls, resolveGmailRedirectUri, createOAuthState, validateOutboundWebhookUrl } from '../vendor/common/index.js';
|
|
4
5
|
import { PaymentVerificationPipeline } from '../vendor/core/index.js';
|
|
@@ -1120,10 +1121,7 @@ export function registerAdminRoutes(router, ctx) {
|
|
|
1120
1121
|
|
|
1121
1122
|
// GET /api/v1/admin/stream (SSE for Live Admin Counter & Queue Events)
|
|
1122
1123
|
router.get('/api/v1/admin/stream', async (req, res) => {
|
|
1123
|
-
|
|
1124
|
-
res.write('event: connected\ndata: {"status":"connected"}\n\n');
|
|
1125
|
-
const _iv = setInterval(() => { try { res.write(': keepalive\n\n'); } catch {} }, 15000);
|
|
1126
|
-
req.on('close', () => clearInterval(_iv));
|
|
1124
|
+
server.initSse('admin_counter_events', res);
|
|
1127
1125
|
});
|
|
1128
1126
|
|
|
1129
1127
|
// --- WHATSAPP BERRY PROTOCOL MERCHANT SESSION CONFIGURATION ---
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Router } from 'express';
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { server } from '../sse.js';
|
|
2
3
|
import { toRupeesString, buildUpiUri, PaymentStatus, PaymentMethod, UPI_PROVIDERS, isInternalServiceRequest, isValidTransition } from '../vendor/common/index.js';
|
|
3
4
|
import { validateSessionDeviceAccess, verifyRegisteredDeviceCredentials } from '../vendor/core/index.js';
|
|
4
5
|
import { isPaymentOwner } from '../auth-guard.js';
|
|
@@ -1961,15 +1962,7 @@ export function registerCheckoutRoutes(router, ctx) {
|
|
|
1961
1962
|
res.status(deviceAccess.statusCode || 409).json(deviceAccess.body);
|
|
1962
1963
|
return;
|
|
1963
1964
|
}
|
|
1964
|
-
|
|
1965
|
-
'Content-Type': 'text/event-stream',
|
|
1966
|
-
'Cache-Control': 'no-cache',
|
|
1967
|
-
'Connection': 'keep-alive',
|
|
1968
|
-
'Access-Control-Allow-Origin': '*'
|
|
1969
|
-
});
|
|
1970
|
-
res.write('event: connected\ndata: {"status":"connected"}\n\n');
|
|
1971
|
-
const interval = setInterval(() => { try { res.write(': keepalive\n\n'); } catch {} }, 15000);
|
|
1972
|
-
req.on('close', () => clearInterval(interval));
|
|
1965
|
+
server.initSse(req.params.id, res);
|
|
1973
1966
|
});
|
|
1974
1967
|
|
|
1975
1968
|
router.get('/api/v1/checkout/:id/events', async (req, res) => {
|
|
@@ -1983,15 +1976,7 @@ export function registerCheckoutRoutes(router, ctx) {
|
|
|
1983
1976
|
res.status(deviceAccess.statusCode || 409).json(deviceAccess.body);
|
|
1984
1977
|
return;
|
|
1985
1978
|
}
|
|
1986
|
-
|
|
1987
|
-
'Content-Type': 'text/event-stream',
|
|
1988
|
-
'Cache-Control': 'no-cache',
|
|
1989
|
-
'Connection': 'keep-alive',
|
|
1990
|
-
'Access-Control-Allow-Origin': '*'
|
|
1991
|
-
});
|
|
1992
|
-
res.write('event: connected\ndata: {"status":"connected"}\n\n');
|
|
1993
|
-
const interval = setInterval(() => { try { res.write(': keepalive\n\n'); } catch {} }, 15000);
|
|
1994
|
-
req.on('close', () => clearInterval(interval));
|
|
1979
|
+
server.initSse(req.params.id, res);
|
|
1995
1980
|
});
|
|
1996
1981
|
}
|
|
1997
1982
|
|
package/src/sse.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const channels = new Map();
|
|
2
|
+
|
|
3
|
+
export const server = {
|
|
4
|
+
initSse(topic, res) {
|
|
5
|
+
const headers = {
|
|
6
|
+
'Content-Type': 'text/event-stream',
|
|
7
|
+
'Cache-Control': 'no-cache',
|
|
8
|
+
'Connection': 'keep-alive',
|
|
9
|
+
'Access-Control-Allow-Origin': '*'
|
|
10
|
+
};
|
|
11
|
+
res.writeHead(200, headers);
|
|
12
|
+
res.write('event: connected\ndata: {"status":"connected"}\n\n');
|
|
13
|
+
|
|
14
|
+
if (!channels.has(topic)) channels.set(topic, new Set());
|
|
15
|
+
const set = channels.get(topic);
|
|
16
|
+
set.add(res);
|
|
17
|
+
|
|
18
|
+
const ping = setInterval(() => {
|
|
19
|
+
try { res.write(': keepalive\n\n'); } catch { clearInterval(ping); }
|
|
20
|
+
}, 15000);
|
|
21
|
+
if (ping.unref) ping.unref();
|
|
22
|
+
|
|
23
|
+
res.on('close', () => {
|
|
24
|
+
clearInterval(ping);
|
|
25
|
+
set.delete(res);
|
|
26
|
+
if (set.size === 0) channels.delete(topic);
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
broadcastSse(topic, event, data) {
|
|
31
|
+
const set = channels.get(topic);
|
|
32
|
+
if (!set || set.size === 0) return;
|
|
33
|
+
let safeJson;
|
|
34
|
+
try { safeJson = JSON.stringify(data, (k, v) => typeof v === 'bigint' ? v.toString() : v); } catch { safeJson = '{}'; }
|
|
35
|
+
const named = `event: ${event}\ndata: ${safeJson}\n\n`;
|
|
36
|
+
const def = `data: ${JSON.stringify({ type: event, ...data, event })}\n\n`;
|
|
37
|
+
const payload = named + def;
|
|
38
|
+
for (const r of set) {
|
|
39
|
+
try { r.write(payload); } catch { set.delete(r); }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default server;
|