const config = require('./config'); const apiClient = require('./api-client'); function parseOptionalNonNegativeNumber(value) { if (value === undefined || value === null || value === '') return null; const parsed = Number(value); if (!Number.isFinite(parsed) || parsed < 0) return null; return parsed; } class AbandonedCallPoller { constructor(database, printer) { this.db = database; this.printer = printer; this.intervalId = null; this.isPolling = false; } async start() { console.log('Starting abandoned call poller...'); this.poll(); this.scheduleNextPoll(); } scheduleNextPoll() { const appConfig = config.getAll(); const interval = parseInt(appConfig.abandonedCallPollingInterval, 10) || 30000; if (this.intervalId) { clearTimeout(this.intervalId); } this.intervalId = setTimeout(() => { this.poll(); this.scheduleNextPoll(); }, interval); } async poll() { if (this.isPolling) return; this.isPolling = true; try { const appConfig = config.getAll(); if (!appConfig.authToken || !appConfig.selectedBotId) { this.isPolling = false; return; } if (apiClient.isTokenNearExpiry(appConfig.tokenExpiry, 7)) { const refreshed = await apiClient.ensureValidToken(); if (!refreshed) { this.isPolling = false; return; } Object.assign(appConfig, config.getAll()); } const result = await apiClient.getAbandonedCalls( appConfig.authToken, appConfig.selectedBotId, { limit: 200 } ); if (result.error) { console.error('Abandoned calls poll error:', result.message); this.isPolling = false; return; } const calls = result.calls || []; for (const call of calls) { this.db.cacheAbandonedCall(call.id, call); } // Also fetch callback queue for the management page cache const queueResult = await apiClient.getAbandonedCallbackQueue( appConfig.authToken, appConfig.selectedBotId, 50 ); if (!queueResult.error && queueResult.queue) { for (const call of queueResult.queue) { this.db.cacheAbandonedCall(call.id, call); } } // Print new abandoned calls that haven't been printed yet await this.printNewAbandonedCalls(calls, appConfig); // Clean old cache periodically this.db.cleanOldAbandonedCallCache(7); } catch (error) { console.error('Abandoned call poll error:', error.message); } this.isPolling = false; } async printNewAbandonedCalls(calls, appConfig) { const printerConfigs = this.db.getAbandonedCallPrinters(); if (!printerConfigs || printerConfigs.length === 0) return; const configuredCooldown = parseOptionalNonNegativeNumber(appConfig.abandonedCallPrintCooldown); const cooldownSeconds = configuredCooldown === null ? 0 : configuredCooldown; const minScoreForPrint = parseOptionalNonNegativeNumber(appConfig.abandonedCallMinScoreForPrint); let lastPrintTime = this.db.getLastAbandonedCallPrintTime(); for (const call of calls) { if (!call || !call.id) continue; if (this.db.hasAbandonedCallPrint(call.id)) continue; const score = Number(call.callback_score) || 0; if (minScoreForPrint !== null && score < minScoreForPrint) { console.log(`Abandoned call #${call.id}: skipping print (score ${score} below configured minimum ${minScoreForPrint})`); continue; } const now = Math.floor(Date.now() / 1000); if (lastPrintTime && (now - lastPrintTime) < cooldownSeconds) { console.log(`Abandoned call #${call.id}: skipping print (cooldown active)`); continue; } try { const result = await this.printer.printAbandonedCallReceipt(call, printerConfigs); const printedCount = result && typeof result.successCount === 'number' ? result.successCount : (result && result.success ? printerConfigs.length : 0); if (result && result.success && printedCount > 0) { this.db.addAbandonedCallPrint(call.id, printedCount); lastPrintTime = Math.floor(Date.now() / 1000); console.log(`Abandoned call #${call.id}: printed on ${printedCount} printer(s)`); } else { const message = result && result.error ? result.error : 'No printers succeeded'; console.error(`Abandoned call #${call.id}: print failed: ${message}`); } } catch (err) { console.error(`Abandoned call #${call.id}: print failed:`, err.message); } } } stop() { if (this.intervalId) { clearTimeout(this.intervalId); this.intervalId = null; } console.log('Abandoned call poller stopped'); } restart() { this.stop(); this.start(); } } module.exports = AbandonedCallPoller;