elpaypg 2.0.6 → 2.0.7
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 +34 -13
- 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,19 +970,17 @@
|
|
|
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
|
-
|
|
984
|
-
const vBlocked = document.getElementById('viewBlockedContent');
|
|
985
|
-
if (vBlocked) vBlocked.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;
|
|
983
|
+
}
|
|
986
984
|
const vDup = document.getElementById('viewDuplicateSessionContent');
|
|
987
985
|
if (vDup) vDup.hidden = true;
|
|
988
986
|
}
|
|
@@ -1432,6 +1430,21 @@
|
|
|
1432
1430
|
}
|
|
1433
1431
|
|
|
1434
1432
|
return { deviceId: null, deviceToken: null };
|
|
1433
|
+
},
|
|
1434
|
+
|
|
1435
|
+
async reset() {
|
|
1436
|
+
this.currentDeviceId = null;
|
|
1437
|
+
this.currentDeviceToken = null;
|
|
1438
|
+
try { localStorage.removeItem('ypg_device_id'); localStorage.removeItem('ypg_device_token'); } catch {}
|
|
1439
|
+
try {
|
|
1440
|
+
const db = await this.openDb();
|
|
1441
|
+
if (db) {
|
|
1442
|
+
const tx = db.transaction(this.storeName, 'readwrite');
|
|
1443
|
+
const store = tx.objectStore(this.storeName);
|
|
1444
|
+
store.delete('primary_device');
|
|
1445
|
+
await new Promise((resolve) => { tx.oncomplete = () => resolve(); tx.onerror = () => resolve(); });
|
|
1446
|
+
}
|
|
1447
|
+
} catch {}
|
|
1435
1448
|
}
|
|
1436
1449
|
};
|
|
1437
1450
|
|
|
@@ -1459,6 +1472,14 @@
|
|
|
1459
1472
|
const res = await fetch(`${API_BASE}/api/v1/checkout/${PAYMENT_ID}/state`, { headers });
|
|
1460
1473
|
const data = await res.json().catch(() => ({}));
|
|
1461
1474
|
|
|
1475
|
+
if (data.code === 'DEVICE_REVOKED' || data.code === 'DEVICE_TOKEN_INVALID' || data.code === 'DEVICE_ID_REQUIRED' || data.code === 'DEVICE_CREDENTIALS_REQUIRED') {
|
|
1476
|
+
if (!window._deviceResetAttempted) {
|
|
1477
|
+
window._deviceResetAttempted = true;
|
|
1478
|
+
await DeviceIdentity.reset();
|
|
1479
|
+
return loadState();
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1462
1483
|
if (res.status === 409 || data.error === 'DUPLICATE_SESSION_DETECTED') {
|
|
1463
1484
|
renderDuplicateSession(data);
|
|
1464
1485
|
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;
|