2026-03-01 17:10:03 -05:00
|
|
|
const config = require('./config');
|
|
|
|
|
const apiClient = require('./api-client');
|
|
|
|
|
|
2026-05-01 10:48:11 -04:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-01 17:10:03 -05:00
|
|
|
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,
|
2026-05-01 10:48:11 -04:00
|
|
|
{ limit: 200 }
|
2026-03-01 17:10:03 -05:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
2026-05-01 10:48:11 -04:00
|
|
|
const configuredCooldown = parseOptionalNonNegativeNumber(appConfig.abandonedCallPrintCooldown);
|
|
|
|
|
const cooldownSeconds = configuredCooldown === null ? 0 : configuredCooldown;
|
|
|
|
|
const minScoreForPrint = parseOptionalNonNegativeNumber(appConfig.abandonedCallMinScoreForPrint);
|
|
|
|
|
let lastPrintTime = this.db.getLastAbandonedCallPrintTime();
|
2026-03-01 17:10:03 -05:00
|
|
|
|
|
|
|
|
for (const call of calls) {
|
2026-05-01 10:48:11 -04:00
|
|
|
if (!call || !call.id) continue;
|
2026-03-01 17:10:03 -05:00
|
|
|
if (this.db.hasAbandonedCallPrint(call.id)) continue;
|
|
|
|
|
|
2026-05-01 10:48:11 -04:00
|
|
|
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;
|
|
|
|
|
}
|
2026-03-01 17:10:03 -05:00
|
|
|
|
2026-05-01 10:48:11 -04:00
|
|
|
const now = Math.floor(Date.now() / 1000);
|
2026-03-01 17:10:03 -05:00
|
|
|
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);
|
2026-05-01 10:48:11 -04:00
|
|
|
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}`);
|
|
|
|
|
}
|
2026-03-01 17:10:03 -05:00
|
|
|
} 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;
|