/** * Periodic ticket auto-close (server/utils/ticketAutoClose.ts): tickets waiting on the * merchant (IsMerchantAction) without activity for TICKET_AUTOCLOSE_DAYS (default 10) * are closed, annotated in the chat history and the merchant is mailed. * Mirrors listing-outbox-flush.ts (no Nitro cron infra here). * * Env: TICKET_AUTOCLOSE_ENABLED — default ON only in production builds; a dev server * runs against the PROD iDempiere, so it must opt in explicitly with =1. * TICKET_AUTOCLOSE_DAYS (default 10), TICKET_AUTOCLOSE_INTERVAL_MS (default 6 h), * TICKET_AUTOCLOSE_DRY_RUN=1 logs what would be closed without writing / mailing. * Only PM2 instance 0 runs it (NODE_APP_INSTANCE) so a cluster never double-closes. * Manual / verification run: POST /api/requests/requests/auto-close { dryRun, days }. */ import { runTicketAutoClose, DEFAULT_INACTIVE_DAYS } from '../utils/ticketAutoClose' export default defineNitroPlugin(() => { if (import.meta.prerender) return const flag = String(process.env.TICKET_AUTOCLOSE_ENABLED ?? '').toLowerCase() const enabled = flag ? !(flag === '0' || flag === 'false') : !import.meta.dev if (!enabled) { console.log('[ticket-auto-close] timer disabled' + (flag ? ' (TICKET_AUTOCLOSE_ENABLED=0)' : ' (dev server — set TICKET_AUTOCLOSE_ENABLED=1 to opt in)')) return } const inst = process.env.NODE_APP_INSTANCE if (inst !== undefined && inst !== '0') return const config: any = useRuntimeConfig() if (!config.api?.idempieretoken) { console.warn('[ticket-auto-close] no iDempiere service token (api.idempieretoken) — timer disabled, the manual route still works') return } const days = Math.max(1, Number(process.env.TICKET_AUTOCLOSE_DAYS) || DEFAULT_INACTIVE_DAYS) const every = Math.max(60_000, Number(process.env.TICKET_AUTOCLOSE_INTERVAL_MS) || 6 * 3_600_000) const dryRun = process.env.TICKET_AUTOCLOSE_DRY_RUN === '1' || process.env.TICKET_AUTOCLOSE_DRY_RUN === 'true' const run = () => runTicketAutoClose({ days, dryRun, trigger: 'timer' }).then(s => { if (s.scanned || s.error) { console.log(`[ticket-auto-close] run ${s.runId}${dryRun ? ' (dry run)' : ''}: ${s.scanned} candidate(s), ${s.closed} closed, ${s.skipped} skipped, ${s.errors} error(s), ${s.durationMs} ms${s.error ? ' ERROR ' + s.error : ''}`) for (const it of s.items) if (it.action !== 'skipped') console.log(`[ticket-auto-close] #${it.docNo} (${it.orgName || it.orgId}) ${it.action}, ${it.inactiveDays} d inactive, mail ${it.emailSent ? 'sent to ' + it.email : 'not sent'}${it.reason ? ' — ' + it.reason : ''}`) } }).catch((e: any) => console.error('[ticket-auto-close] timer run failed:', e?.message || e)) setTimeout(run, 60_000).unref() setInterval(run, every).unref() console.log(`[ticket-auto-close] timer armed every ${Math.round(every / 60_000)} min, ${days} d inactivity${dryRun ? ', DRY RUN' : ''}`) })