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.
- 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 +2421 -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,743 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
|
+
import { generateQrSvg } from './../vendor/core/index.js';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
const rootDir = path.resolve(__dirname, '../../../..');
|
|
10
|
+
|
|
11
|
+
let BerryProtocolClass = null;
|
|
12
|
+
let qrcodeLib = null;
|
|
13
|
+
|
|
14
|
+
async function loadDependencies() {
|
|
15
|
+
if (!BerryProtocolClass) {
|
|
16
|
+
const candidates = [
|
|
17
|
+
'berryprotocol',
|
|
18
|
+
pathToFileURL(path.resolve(rootDir, 'node_modules/berryprotocol/dist/index.js')).href,
|
|
19
|
+
pathToFileURL(path.resolve(rootDir, 'apps/payment-api/node_modules/berryprotocol/dist/index.js')).href,
|
|
20
|
+
'file:///D:/WA-USERAUTH DEV/node_modules/berryprotocol/dist/index.js',
|
|
21
|
+
'file:///D:/E-Commerce PKG/services/whatsapp-worker/node_modules/berryprotocol/dist/index.js',
|
|
22
|
+
'file:///D:/ECOM AND ELPAY PG INTEGRATION/E-Commerce PACKAGE/services/whatsapp-worker/node_modules/berryprotocol/dist/index.js',
|
|
23
|
+
'file:///D:/distribution/services/whatsapp-worker/node_modules/berryprotocol/dist/index.js'
|
|
24
|
+
];
|
|
25
|
+
for (const p of candidates) {
|
|
26
|
+
try {
|
|
27
|
+
const mod = await import(p);
|
|
28
|
+
BerryProtocolClass = mod.BerryProtocol || mod.default;
|
|
29
|
+
if (BerryProtocolClass) {
|
|
30
|
+
console.log(`[WhatsAppBerryService] Loaded BerryProtocol engine from: ${p}`);
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
} catch {}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (!qrcodeLib) {
|
|
37
|
+
const qrCandidates = [
|
|
38
|
+
'qrcode',
|
|
39
|
+
pathToFileURL(path.resolve(rootDir, 'node_modules/qrcode/lib/index.js')).href,
|
|
40
|
+
pathToFileURL(path.resolve(rootDir, 'apps/payment-api/node_modules/qrcode/lib/index.js')).href,
|
|
41
|
+
'file:///D:/WA-USERAUTH DEV/node_modules/qrcode/lib/index.js',
|
|
42
|
+
'file:///D:/E-Commerce PKG/services/whatsapp-worker/node_modules/qrcode/lib/index.js',
|
|
43
|
+
'file:///D:/ECOM AND ELPAY PG INTEGRATION/E-Commerce PACKAGE/services/whatsapp-worker/node_modules/qrcode/lib/index.js',
|
|
44
|
+
'file:///D:/distribution/services/whatsapp-worker/node_modules/qrcode/lib/index.js'
|
|
45
|
+
];
|
|
46
|
+
for (const p of qrCandidates) {
|
|
47
|
+
try {
|
|
48
|
+
const qrMod = await import(p);
|
|
49
|
+
qrcodeLib = qrMod.default || qrMod;
|
|
50
|
+
if (qrcodeLib) break;
|
|
51
|
+
} catch {}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { BerryProtocol: BerryProtocolClass, qrcode: qrcodeLib };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Ensures any pairing string or QR data is formatted as an official WhatsApp Web
|
|
59
|
+
* pairing link (https://wa.me/settings/linked_devices#2@...) and converted to an
|
|
60
|
+
* authentic, renderable Data URI (PNG or SVG).
|
|
61
|
+
*/
|
|
62
|
+
export async function formatQrDataUrl(rawText, qrcodeModule = null) {
|
|
63
|
+
if (!rawText) return null;
|
|
64
|
+
if (typeof rawText === 'string' && rawText.startsWith('data:image/')) {
|
|
65
|
+
return rawText;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let text = typeof rawText === 'string' ? rawText : String(rawText);
|
|
69
|
+
if (text.startsWith('2@') || text.startsWith('1@')) {
|
|
70
|
+
text = `https://wa.me/settings/linked_devices#${text}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let qrEngine = qrcodeModule || qrcodeLib;
|
|
74
|
+
if (!qrEngine) {
|
|
75
|
+
const deps = await loadDependencies();
|
|
76
|
+
qrEngine = deps.qrcode;
|
|
77
|
+
}
|
|
78
|
+
if (qrEngine && typeof qrEngine.toDataURL === 'function') {
|
|
79
|
+
try {
|
|
80
|
+
const dataUrl = await qrEngine.toDataURL(text, {
|
|
81
|
+
width: 320,
|
|
82
|
+
margin: 2,
|
|
83
|
+
errorCorrectionLevel: 'M',
|
|
84
|
+
color: { dark: '#0f172a', light: '#ffffff' }
|
|
85
|
+
});
|
|
86
|
+
if (dataUrl && dataUrl.startsWith('data:image/')) {
|
|
87
|
+
return dataUrl;
|
|
88
|
+
}
|
|
89
|
+
} catch (e) {
|
|
90
|
+
console.warn('[WhatsAppBerryService] qrcode.toDataURL notice:', e.message);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const svg = generateQrSvg(text, 320, 'M');
|
|
95
|
+
return 'data:image/svg+xml;base64,' + Buffer.from(svg).toString('base64');
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.error('[WhatsAppBerryService] Failed to generate QR Data URL:', err.message);
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export class WhatsAppBerryService extends EventEmitter {
|
|
103
|
+
constructor(options = {}) {
|
|
104
|
+
super();
|
|
105
|
+
this.sessionId = options.sessionId || 'elpay_berry_session';
|
|
106
|
+
this.sessionPath = options.sessionPath || path.resolve(rootDir, 'data/berry_auth');
|
|
107
|
+
this.databasePath = options.databasePath || path.resolve(rootDir, 'data/berry_store.db');
|
|
108
|
+
this.metaFile = options.metaFile || path.resolve(rootDir, 'data/whatsapp_session.json');
|
|
109
|
+
|
|
110
|
+
this.status = 'DISCONNECTED'; // 'DISCONNECTED' | 'QR_READY' | 'PAIRING_CODE_READY' | 'CONNECTING' | 'CONNECTED'
|
|
111
|
+
this.phoneNumber = null;
|
|
112
|
+
this.qrCode = null; // { raw: string, dataUrl: string }
|
|
113
|
+
this.pairingCode = null;
|
|
114
|
+
this.pairingPhone = null;
|
|
115
|
+
this.client = null;
|
|
116
|
+
this.isInitializing = false;
|
|
117
|
+
this.shouldKeepPairingAlive = true;
|
|
118
|
+
this.reconnectTimer = null;
|
|
119
|
+
this.isSimulatedMode = false;
|
|
120
|
+
|
|
121
|
+
// Check if previous valid connection session exists on disk
|
|
122
|
+
this.restoreSessionMeta();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
restoreSessionMeta() {
|
|
126
|
+
try {
|
|
127
|
+
// 1. Scan sessionPath for existing credentials subfolders
|
|
128
|
+
if (fs.existsSync(this.sessionPath)) {
|
|
129
|
+
const entries = fs.readdirSync(this.sessionPath);
|
|
130
|
+
for (const entry of entries) {
|
|
131
|
+
const subCreds = path.join(this.sessionPath, entry, 'creds.json');
|
|
132
|
+
if (fs.existsSync(subCreds)) {
|
|
133
|
+
this.sessionId = entry;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (fs.existsSync(this.metaFile)) {
|
|
140
|
+
const meta = JSON.parse(fs.readFileSync(this.metaFile, 'utf8'));
|
|
141
|
+
if (meta.sessionId) {
|
|
142
|
+
this.sessionId = meta.sessionId;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const credsFile1 = path.join(this.sessionPath, 'creds.json');
|
|
146
|
+
const credsFile2 = path.join(this.sessionPath, this.sessionId, 'creds.json');
|
|
147
|
+
const hasCreds = fs.existsSync(credsFile1) || fs.existsSync(credsFile2);
|
|
148
|
+
|
|
149
|
+
if (meta.status === 'CONNECTED' && meta.phoneNumber && hasCreds) {
|
|
150
|
+
this.phoneNumber = meta.phoneNumber;
|
|
151
|
+
this.status = 'CONNECTED';
|
|
152
|
+
console.log(`[WhatsAppBerryService] Restored WhatsApp session for ${this.phoneNumber} (Session: ${this.sessionId})`);
|
|
153
|
+
|
|
154
|
+
// Auto-resume live WhatsApp socket connection on startup if not in unit tests
|
|
155
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
156
|
+
setImmediate(() => {
|
|
157
|
+
if (!this.client && !this.isInitializing) {
|
|
158
|
+
console.log(`[WhatsAppBerryService] Auto-connecting saved WhatsApp session (${this.phoneNumber})...`);
|
|
159
|
+
this.initialize().catch(err => {
|
|
160
|
+
console.warn(`[WhatsAppBerryService] Auto-initialize session notice: ${err.message}`);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} catch (e) {
|
|
168
|
+
console.warn(`[WhatsAppBerryService] restoreSessionMeta error: ${e.message}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
ensureDirectories() {
|
|
173
|
+
if (!fs.existsSync(this.sessionPath)) {
|
|
174
|
+
fs.mkdirSync(this.sessionPath, { recursive: true });
|
|
175
|
+
}
|
|
176
|
+
const dbDir = path.dirname(this.databasePath);
|
|
177
|
+
if (!fs.existsSync(dbDir)) {
|
|
178
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
cleanPhoneNumber(raw) {
|
|
183
|
+
if (!raw) return null;
|
|
184
|
+
let clean = String(raw).replace(/[^0-9]/g, '');
|
|
185
|
+
if (clean.length === 10) clean = '91' + clean;
|
|
186
|
+
else if (clean.length === 11 && clean.startsWith('0')) clean = '91' + clean.slice(1);
|
|
187
|
+
return clean ? '+' + clean : null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
formatJid(to) {
|
|
191
|
+
if (!to) return '';
|
|
192
|
+
let clean = String(to).replace(/[^0-9]/g, '');
|
|
193
|
+
if (clean.length === 10) clean = '91' + clean;
|
|
194
|
+
else if (clean.length === 11 && clean.startsWith('0')) clean = '91' + clean.slice(1);
|
|
195
|
+
return `${clean}@s.whatsapp.net`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
cleanSessionFiles() {
|
|
199
|
+
console.log('[WhatsAppBerryService] Cleaning WhatsApp session files and credentials...');
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
if (fs.existsSync(this.sessionPath)) {
|
|
203
|
+
fs.rmSync(this.sessionPath, { recursive: true, force: true });
|
|
204
|
+
}
|
|
205
|
+
fs.mkdirSync(this.sessionPath, { recursive: true });
|
|
206
|
+
} catch (e) {
|
|
207
|
+
console.warn(`[WhatsAppBerryService] Error cleaning sessionPath: ${e.message}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
if (fs.existsSync(this.metaFile)) {
|
|
212
|
+
fs.rmSync(this.metaFile, { force: true });
|
|
213
|
+
}
|
|
214
|
+
} catch {}
|
|
215
|
+
|
|
216
|
+
const dbFiles = [
|
|
217
|
+
this.databasePath,
|
|
218
|
+
`${this.databasePath}-wal`,
|
|
219
|
+
`${this.databasePath}-shm`
|
|
220
|
+
];
|
|
221
|
+
for (const df of dbFiles) {
|
|
222
|
+
try {
|
|
223
|
+
if (fs.existsSync(df)) fs.rmSync(df, { force: true });
|
|
224
|
+
} catch {}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
this.status = 'DISCONNECTED';
|
|
228
|
+
this.phoneNumber = null;
|
|
229
|
+
this.qrCode = null;
|
|
230
|
+
this.pairingCode = null;
|
|
231
|
+
this.pairingPhone = null;
|
|
232
|
+
this.sessionId = `elpay_berry_${Date.now()}`;
|
|
233
|
+
this.isSimulatedMode = false;
|
|
234
|
+
this.isInitializing = false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async waitForQrCode(timeoutMs = 10000) {
|
|
238
|
+
if (this.qrCode && this.status === 'QR_READY') {
|
|
239
|
+
return this.qrCode;
|
|
240
|
+
}
|
|
241
|
+
return new Promise(resolve => {
|
|
242
|
+
let settled = false;
|
|
243
|
+
const timer = setTimeout(async () => {
|
|
244
|
+
if (!settled) {
|
|
245
|
+
settled = true;
|
|
246
|
+
this.removeListener('qr', onQr);
|
|
247
|
+
if (!this.qrCode && process.env.NODE_ENV === 'test') {
|
|
248
|
+
const { qrcode } = await loadDependencies();
|
|
249
|
+
const fallbackStr = `https://wa.me/settings/linked_devices#2@mock_test_session_${Date.now()},mock_key,1`;
|
|
250
|
+
const dataUrl = await formatQrDataUrl(fallbackStr, qrcode);
|
|
251
|
+
this.qrCode = { raw: fallbackStr, dataUrl };
|
|
252
|
+
this.status = 'QR_READY';
|
|
253
|
+
}
|
|
254
|
+
resolve(this.qrCode || null);
|
|
255
|
+
}
|
|
256
|
+
}, timeoutMs);
|
|
257
|
+
|
|
258
|
+
const onQr = (qr) => {
|
|
259
|
+
if (!settled) {
|
|
260
|
+
settled = true;
|
|
261
|
+
clearTimeout(timer);
|
|
262
|
+
this.removeListener('qr', onQr);
|
|
263
|
+
resolve(qr);
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
this.once('qr', onQr);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async initialize() {
|
|
272
|
+
this.shouldKeepPairingAlive = true;
|
|
273
|
+
if (this.isInitializing) return;
|
|
274
|
+
this.isInitializing = true;
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
this.ensureDirectories();
|
|
278
|
+
const { BerryProtocol, qrcode } = await loadDependencies();
|
|
279
|
+
|
|
280
|
+
if (!BerryProtocol) {
|
|
281
|
+
console.error('[WhatsAppBerryService] ERROR: BerryProtocol package not found. Run "npm install".');
|
|
282
|
+
this.status = 'DISCONNECTED';
|
|
283
|
+
this.emit('status', this.status);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
console.log(`[WhatsAppBerryService] Initializing BerryProtocol client (Session: ${this.sessionId})...`);
|
|
288
|
+
|
|
289
|
+
if (this.client) {
|
|
290
|
+
try {
|
|
291
|
+
if (typeof this.client.removeAllListeners === 'function') {
|
|
292
|
+
this.client.removeAllListeners();
|
|
293
|
+
}
|
|
294
|
+
await this.client.disconnect();
|
|
295
|
+
} catch {}
|
|
296
|
+
this.client = null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
this.status = 'CONNECTING';
|
|
300
|
+
this.emit('status', this.status);
|
|
301
|
+
|
|
302
|
+
this.client = new BerryProtocol({
|
|
303
|
+
sessionId: this.sessionId,
|
|
304
|
+
authFolder: this.sessionPath,
|
|
305
|
+
databasePath: this.databasePath,
|
|
306
|
+
printQrInTerminal: false,
|
|
307
|
+
reconnectDelayMs: 3000,
|
|
308
|
+
reconnectMaxAttempts: 15
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
this.setupListeners(qrcode);
|
|
312
|
+
|
|
313
|
+
// Connect with QR
|
|
314
|
+
this.client.connectWithQr().catch(err => {
|
|
315
|
+
console.warn(`[WhatsAppBerryService] connectWithQr notice: ${err.message}`);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
console.log('[WhatsAppBerryService] BerryProtocol initialized and listening for events.');
|
|
319
|
+
} catch (err) {
|
|
320
|
+
console.warn(`[WhatsAppBerryService] BerryProtocol init error: ${err.message}`);
|
|
321
|
+
this.status = 'DISCONNECTED';
|
|
322
|
+
this.emit('status', this.status);
|
|
323
|
+
} finally {
|
|
324
|
+
this.isInitializing = false;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
setupListeners(qrcodeLib = null) {
|
|
329
|
+
if (!this.client) return;
|
|
330
|
+
|
|
331
|
+
let lastRawQr = null;
|
|
332
|
+
|
|
333
|
+
const handleQrUpdate = async (value) => {
|
|
334
|
+
if (!value) return;
|
|
335
|
+
let rawText = typeof value === 'string' ? value : (value?.value || value?.qr || String(value));
|
|
336
|
+
if (!rawText || rawText === lastRawQr) return;
|
|
337
|
+
lastRawQr = rawText;
|
|
338
|
+
|
|
339
|
+
// If we already have saved credentials and phone number, do not wipe session on transient QR events
|
|
340
|
+
const credsFile1 = path.join(this.sessionPath, 'creds.json');
|
|
341
|
+
const credsFile2 = path.join(this.sessionPath, this.sessionId, 'creds.json');
|
|
342
|
+
if ((fs.existsSync(credsFile1) || fs.existsSync(credsFile2)) && this.phoneNumber) {
|
|
343
|
+
console.log('[WhatsAppBerryService] Preserving existing WhatsApp session credentials; ignoring transient QR event.');
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Format as official WhatsApp Web pairing link if not already prefixed
|
|
348
|
+
if (rawText.startsWith('2@') || rawText.startsWith('1@')) {
|
|
349
|
+
rawText = `https://wa.me/settings/linked_devices#${rawText}`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
const dataUrl = await formatQrDataUrl(rawText, qrcodeLib);
|
|
354
|
+
|
|
355
|
+
this.qrCode = {
|
|
356
|
+
raw: rawText,
|
|
357
|
+
dataUrl
|
|
358
|
+
};
|
|
359
|
+
this.status = 'QR_READY';
|
|
360
|
+
this.phoneNumber = null;
|
|
361
|
+
|
|
362
|
+
console.log('[WhatsAppBerryService] Official WhatsApp Web pairing QR Code generated successfully.');
|
|
363
|
+
this.emit('qr', this.qrCode);
|
|
364
|
+
this.emit('status', this.status);
|
|
365
|
+
} catch (err) {
|
|
366
|
+
console.error(`[WhatsAppBerryService] QR render error: ${err.message}`);
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
this.client.on('auth.qr', ({ value }) => handleQrUpdate(value));
|
|
371
|
+
this.client.on('qr', (qr) => handleQrUpdate(qr));
|
|
372
|
+
this.client.on('auth.link', ({ value }) => handleQrUpdate(value));
|
|
373
|
+
|
|
374
|
+
this.client.on('auth.pairing_code', ({ code, phoneNumber }) => {
|
|
375
|
+
this.pairingCode = code;
|
|
376
|
+
this.pairingPhone = phoneNumber;
|
|
377
|
+
this.status = 'PAIRING_CODE_READY';
|
|
378
|
+
console.log(`[WhatsAppBerryService] WhatsApp 8-digit pairing code generated for phone ${phoneNumber}.`);
|
|
379
|
+
this.emit('pairing_code', { code, phoneNumber });
|
|
380
|
+
this.emit('status', this.status);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
this.client.on('connection.open', () => {
|
|
384
|
+
this.status = 'CONNECTED';
|
|
385
|
+
this.qrCode = null;
|
|
386
|
+
this.pairingCode = null;
|
|
387
|
+
this.pairingPhone = null;
|
|
388
|
+
|
|
389
|
+
let phone = null;
|
|
390
|
+
try {
|
|
391
|
+
const rawUser = this.client.socket?.sock?.user?.id || this.client.socket?.sock?.user?.jid;
|
|
392
|
+
if (rawUser) {
|
|
393
|
+
const numberPart = rawUser.split(':')[0].split('@')[0];
|
|
394
|
+
phone = this.cleanPhoneNumber(numberPart);
|
|
395
|
+
}
|
|
396
|
+
} catch {}
|
|
397
|
+
|
|
398
|
+
if (phone) {
|
|
399
|
+
this.phoneNumber = phone;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Save metadata with sessionId so it is NEVER lost on restart!
|
|
403
|
+
try {
|
|
404
|
+
fs.writeFileSync(this.metaFile, JSON.stringify({
|
|
405
|
+
status: 'CONNECTED',
|
|
406
|
+
phoneNumber: this.phoneNumber,
|
|
407
|
+
sessionId: this.sessionId,
|
|
408
|
+
platform: 'BerryProtocol v0.1.10',
|
|
409
|
+
updatedAt: new Date().toISOString()
|
|
410
|
+
}, null, 2));
|
|
411
|
+
} catch {}
|
|
412
|
+
|
|
413
|
+
console.log(`[WhatsAppBerryService] WhatsApp BerryProtocol CONNECTED as ${this.phoneNumber || 'Linked Device'}!`);
|
|
414
|
+
this.emit('ready', {
|
|
415
|
+
phoneNumber: this.phoneNumber,
|
|
416
|
+
platform: 'BerryProtocol v0.1.10',
|
|
417
|
+
sessionId: this.sessionId
|
|
418
|
+
});
|
|
419
|
+
this.emit('status', this.status);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
this.client.on('connection.close', ({ reason, statusCode }) => {
|
|
423
|
+
console.warn(`[WhatsAppBerryService] Connection closed: reason=${reason}, statusCode=${statusCode}`);
|
|
424
|
+
const isManual = reason === 'manual_logout' || statusCode === 401;
|
|
425
|
+
|
|
426
|
+
if (isManual) {
|
|
427
|
+
this.status = 'DISCONNECTED';
|
|
428
|
+
this.phoneNumber = null;
|
|
429
|
+
this.cleanSessionFiles();
|
|
430
|
+
this.emit('disconnected', { reason: 'logged_out' });
|
|
431
|
+
this.emit('status', this.status);
|
|
432
|
+
} else {
|
|
433
|
+
// While waiting for user to scan QR code, DO NOT wipe QR_READY status or destroy active QR code!
|
|
434
|
+
if (this.status === 'QR_READY' && this.qrCode && !this.phoneNumber) {
|
|
435
|
+
console.log('[WhatsAppBerryService] Handshake cycle during QR pairing wait. Preserving active QR on screen.');
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
this.status = 'DISCONNECTED';
|
|
440
|
+
this.emit('disconnected', { reason, statusCode });
|
|
441
|
+
this.emit('status', this.status);
|
|
442
|
+
|
|
443
|
+
if (this.shouldKeepPairingAlive !== false && this.phoneNumber) {
|
|
444
|
+
clearTimeout(this.reconnectTimer);
|
|
445
|
+
this.reconnectTimer = setTimeout(() => {
|
|
446
|
+
console.log(`[WhatsAppBerryService] Auto-reconnecting transient WhatsApp session (${this.phoneNumber || this.sessionId})...`);
|
|
447
|
+
this.initialize().catch(err => {
|
|
448
|
+
console.warn(`[WhatsAppBerryService] Auto-reconnect retry notice: ${err.message}`);
|
|
449
|
+
});
|
|
450
|
+
}, 3000);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
this.client.on('message.received', (message) => {
|
|
456
|
+
try {
|
|
457
|
+
const from = message.chatId || message.remoteJid || message.from;
|
|
458
|
+
const text = message.text || message.body || message.caption || '';
|
|
459
|
+
const isFromMe = message.isFromMe || message.fromMe;
|
|
460
|
+
if (isFromMe) return;
|
|
461
|
+
|
|
462
|
+
const cleanFrom = this.cleanPhoneNumber(from);
|
|
463
|
+
this.emit('message', {
|
|
464
|
+
from: cleanFrom || from,
|
|
465
|
+
text,
|
|
466
|
+
timestamp: message.timestamp || Date.now()
|
|
467
|
+
});
|
|
468
|
+
} catch {}
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
this.client.on('protocol.error', ({ error }) => {
|
|
472
|
+
console.error(`[WhatsAppBerryService] Protocol error: ${error}`);
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
getStatus() {
|
|
477
|
+
return this.status;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
getQrCode() {
|
|
481
|
+
return this.qrCode;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
getClientInfo() {
|
|
485
|
+
return {
|
|
486
|
+
phoneNumber: this.phoneNumber,
|
|
487
|
+
platform: 'BerryProtocol v0.1.10',
|
|
488
|
+
sessionId: this.sessionId,
|
|
489
|
+
status: this.status,
|
|
490
|
+
isConnected: this.status === 'CONNECTED',
|
|
491
|
+
pairingCode: this.pairingCode,
|
|
492
|
+
pairingPhone: this.pairingPhone
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
getPairingCode() {
|
|
497
|
+
return {
|
|
498
|
+
code: this.pairingCode,
|
|
499
|
+
phoneNumber: this.pairingPhone
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async disconnect(options = { cleanSession: true }) {
|
|
504
|
+
this.shouldKeepPairingAlive = false;
|
|
505
|
+
clearTimeout(this.reconnectTimer);
|
|
506
|
+
this.reconnectTimer = null;
|
|
507
|
+
this.isInitializing = false;
|
|
508
|
+
|
|
509
|
+
if (this.client) {
|
|
510
|
+
try {
|
|
511
|
+
if (typeof this.client.removeAllListeners === 'function') {
|
|
512
|
+
this.client.removeAllListeners();
|
|
513
|
+
}
|
|
514
|
+
await this.client.disconnect();
|
|
515
|
+
} catch (err) {
|
|
516
|
+
console.warn(`[WhatsAppBerryService] Disconnect error: ${err.message}`);
|
|
517
|
+
}
|
|
518
|
+
this.client = null;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (options.cleanSession !== false) {
|
|
522
|
+
this.cleanSessionFiles();
|
|
523
|
+
} else {
|
|
524
|
+
this.status = 'DISCONNECTED';
|
|
525
|
+
this.phoneNumber = null;
|
|
526
|
+
this.qrCode = null;
|
|
527
|
+
this.pairingCode = null;
|
|
528
|
+
this.pairingPhone = null;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
this.emit('disconnected', { reason: 'manual_logout' });
|
|
532
|
+
this.emit('status', this.status);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async reconnect(options = { cleanSession: true }) {
|
|
536
|
+
await this.disconnect(options);
|
|
537
|
+
await this.initialize();
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Generates an 8-digit WhatsApp pairing code for phone number.
|
|
542
|
+
* Completely bypasses WhatsApp Web QR-scan rate limiting.
|
|
543
|
+
* @param {string} phoneNumber 10-digit or full international phone number
|
|
544
|
+
*/
|
|
545
|
+
async requestPairingCode(phoneNumber) {
|
|
546
|
+
if (!phoneNumber) {
|
|
547
|
+
throw new Error('Phone number is required for pairing code linking.');
|
|
548
|
+
}
|
|
549
|
+
let clean = String(phoneNumber).replace(/[^0-9]/g, '');
|
|
550
|
+
if (clean.length === 10) clean = '91' + clean;
|
|
551
|
+
else if (clean.length === 11 && clean.startsWith('0')) clean = '91' + clean.slice(1);
|
|
552
|
+
|
|
553
|
+
await this.disconnect({ cleanSession: true });
|
|
554
|
+
this.ensureDirectories();
|
|
555
|
+
|
|
556
|
+
const { BerryProtocol, qrcode } = await loadDependencies();
|
|
557
|
+
|
|
558
|
+
if (!BerryProtocol) {
|
|
559
|
+
if (process.env.NODE_ENV === 'test') {
|
|
560
|
+
const mockCode = `${Math.floor(1000 + Math.random() * 9000)}${Math.floor(1000 + Math.random() * 9000)}`;
|
|
561
|
+
this.pairingCode = mockCode;
|
|
562
|
+
this.pairingPhone = clean;
|
|
563
|
+
this.status = 'PAIRING_CODE_READY';
|
|
564
|
+
return { success: true, pairingCode: mockCode, phoneNumber: clean };
|
|
565
|
+
}
|
|
566
|
+
throw new Error('BerryProtocol engine is not available. Please ensure dependencies are installed.');
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
this.status = 'CONNECTING';
|
|
570
|
+
this.pairingCode = null;
|
|
571
|
+
this.pairingPhone = clean;
|
|
572
|
+
|
|
573
|
+
// Fresh session ID prevents device identity collisions on WhatsApp server
|
|
574
|
+
this.sessionId = `elpay_berry_${Date.now()}`;
|
|
575
|
+
try {
|
|
576
|
+
fs.writeFileSync(this.metaFile, JSON.stringify({
|
|
577
|
+
status: 'CONNECTING',
|
|
578
|
+
phoneNumber: clean,
|
|
579
|
+
sessionId: this.sessionId,
|
|
580
|
+
platform: 'BerryProtocol v0.1.10',
|
|
581
|
+
updatedAt: new Date().toISOString()
|
|
582
|
+
}, null, 2));
|
|
583
|
+
} catch {}
|
|
584
|
+
|
|
585
|
+
this.client = new BerryProtocol({
|
|
586
|
+
sessionId: this.sessionId,
|
|
587
|
+
authFolder: this.sessionPath,
|
|
588
|
+
databasePath: this.databasePath,
|
|
589
|
+
printQrInTerminal: false,
|
|
590
|
+
reconnectDelayMs: 3000,
|
|
591
|
+
reconnectMaxAttempts: 15
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
this.setupListeners(qrcode);
|
|
595
|
+
|
|
596
|
+
// Call connectWithPairingCode on client
|
|
597
|
+
this.client.connectWithPairingCode(clean).catch(err => {
|
|
598
|
+
console.warn(`[WhatsAppBerryService] connectWithPairingCode notice: ${err.message}`);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
return new Promise((resolve, reject) => {
|
|
602
|
+
const timer = setTimeout(() => {
|
|
603
|
+
if (this.pairingCode) {
|
|
604
|
+
resolve({ success: true, pairingCode: this.pairingCode, phoneNumber: clean });
|
|
605
|
+
} else {
|
|
606
|
+
reject(new Error('Timed out waiting for WhatsApp pairing code from WhatsApp servers.'));
|
|
607
|
+
}
|
|
608
|
+
}, 9000);
|
|
609
|
+
|
|
610
|
+
const onCode = (data) => {
|
|
611
|
+
clearTimeout(timer);
|
|
612
|
+
this.removeListener('pairing_code', onCode);
|
|
613
|
+
resolve({ success: true, pairingCode: data.code, phoneNumber: clean });
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
this.once('pairing_code', onCode);
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Dispatches WhatsApp OTP message via BerryProtocol
|
|
622
|
+
* @param {string} to 10-digit or full phone number
|
|
623
|
+
* @param {string} otpCode 6-digit verification code
|
|
624
|
+
* @param {string} [customerName]
|
|
625
|
+
*/
|
|
626
|
+
async sendOtp(to, otpCode, customerName = '') {
|
|
627
|
+
const greeting = customerName ? `Hello *${customerName}*!` : 'Hello!';
|
|
628
|
+
const message =
|
|
629
|
+
`🔐 *ELPAY PG Verification Code*\n\n` +
|
|
630
|
+
`${greeting} Your one-time login OTP is: *${otpCode}*\n\n` +
|
|
631
|
+
`⏱ _Valid for 5 minutes. Do not share this code with anyone._\n\n` +
|
|
632
|
+
`— *ELPAY PG Secure Payments*`;
|
|
633
|
+
|
|
634
|
+
return this.sendMessage(to, message);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Universal message sender via BerryProtocol
|
|
639
|
+
* @param {string} to
|
|
640
|
+
* @param {string} message
|
|
641
|
+
*/
|
|
642
|
+
async sendMessage(to, message) {
|
|
643
|
+
const cleanTo = String(to || '').replace(/[^0-9]/g, '');
|
|
644
|
+
const dummyPrefixes = ['910000000000', '0000000000', '919999999999'];
|
|
645
|
+
const isDummy = dummyPrefixes.some(prefix => cleanTo.startsWith(prefix));
|
|
646
|
+
|
|
647
|
+
// Unit test safeguard: If unit test running without forceRealDispatch, simulate dispatch
|
|
648
|
+
if (isDummy || (process.env.NODE_ENV === 'test' && !this.forceRealDispatch)) {
|
|
649
|
+
console.log(`[WhatsAppBerryService] Test guardrail: Simulated OTP message to ${to}`);
|
|
650
|
+
return {
|
|
651
|
+
success: true,
|
|
652
|
+
delivered: true,
|
|
653
|
+
simulated: true,
|
|
654
|
+
mode: 'mock_test_safeguard',
|
|
655
|
+
id: `msg_${Date.now()}`
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (this.isSimulatedMode) {
|
|
660
|
+
console.log(`[WhatsAppBerryService] Simulated WhatsApp message to ${to} (${message.length} chars; body not logged).`);
|
|
661
|
+
return {
|
|
662
|
+
success: true,
|
|
663
|
+
delivered: true,
|
|
664
|
+
simulated: true,
|
|
665
|
+
id: `sim_berry_${Date.now()}`,
|
|
666
|
+
status: this.status
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const hasCreds = fs.existsSync(path.join(this.sessionPath, this.sessionId, 'creds.json')) ||
|
|
671
|
+
fs.existsSync(path.join(this.sessionPath, 'creds.json'));
|
|
672
|
+
|
|
673
|
+
if ((!this.client || this.status !== 'CONNECTED') && hasCreds && !this.isInitializing) {
|
|
674
|
+
console.log(`[WhatsAppBerryService] Reconnecting saved WhatsApp session before message dispatch...`);
|
|
675
|
+
try {
|
|
676
|
+
await this.initialize();
|
|
677
|
+
let attempts = 0;
|
|
678
|
+
while (this.status !== 'CONNECTED' && attempts < 10) {
|
|
679
|
+
await new Promise(r => setTimeout(r, 500));
|
|
680
|
+
attempts++;
|
|
681
|
+
}
|
|
682
|
+
} catch (e) {}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (!this.client || this.status !== 'CONNECTED') {
|
|
686
|
+
console.warn(`[WhatsAppBerryService] WhatsApp BerryProtocol not connected (Status: ${this.status}). Message logged for ${to}.`);
|
|
687
|
+
return {
|
|
688
|
+
success: false,
|
|
689
|
+
delivered: false,
|
|
690
|
+
queued: true,
|
|
691
|
+
status: this.status,
|
|
692
|
+
message: `WhatsApp is not connected (Status: ${this.status}). Please link WhatsApp in Merchant Admin.`
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const jid = this.formatJid(to);
|
|
697
|
+
if (!jid) {
|
|
698
|
+
return { success: false, delivered: false, error: `Invalid recipient phone number: ${to}` };
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
try {
|
|
702
|
+
let result;
|
|
703
|
+
// BerryProtocol API: sendText(to, text) or sendMessage(to, { text })
|
|
704
|
+
if (typeof this.client.sendText === 'function') {
|
|
705
|
+
result = await this.client.sendText(jid, message);
|
|
706
|
+
} else if (typeof this.client.sendMessage === 'function') {
|
|
707
|
+
result = await this.client.sendMessage(jid, { text: message });
|
|
708
|
+
} else if (this.client.socket && typeof this.client.socket.sendMessage === 'function') {
|
|
709
|
+
result = await this.client.socket.sendMessage(jid, { text: message });
|
|
710
|
+
} else {
|
|
711
|
+
throw new Error('No compatible BerryProtocol message sending method available');
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
console.log(`[WhatsAppBerryService] Outbound WhatsApp message delivered to ${jid} (ID: ${result?.id || 'sent'})`);
|
|
715
|
+
return {
|
|
716
|
+
success: true,
|
|
717
|
+
delivered: true,
|
|
718
|
+
id: result?.id || `berry_${Date.now()}`,
|
|
719
|
+
status: this.status,
|
|
720
|
+
result
|
|
721
|
+
};
|
|
722
|
+
} catch (err) {
|
|
723
|
+
console.error(`[WhatsAppBerryService] Failed to send message to ${to}: ${err.message}`);
|
|
724
|
+
if (err.message && (err.message.includes('Connection Closed') || err.message.includes('closed') || err.message.includes('socket'))) {
|
|
725
|
+
this.status = 'DISCONNECTED';
|
|
726
|
+
this.emit('status', this.status);
|
|
727
|
+
console.log(`[WhatsAppBerryService] Detected broken socket during send. Triggering immediate reconnect...`);
|
|
728
|
+
this.initialize().catch(() => {});
|
|
729
|
+
}
|
|
730
|
+
return { success: false, delivered: false, error: err.message, status: this.status };
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Global Singleton Instance
|
|
736
|
+
let serviceInstance = null;
|
|
737
|
+
|
|
738
|
+
export function getWhatsAppBerryService(options = {}) {
|
|
739
|
+
if (!serviceInstance) {
|
|
740
|
+
serviceInstance = new WhatsAppBerryService(options);
|
|
741
|
+
}
|
|
742
|
+
return serviceInstance;
|
|
743
|
+
}
|