const cron = require("node-cron"); async function claimAndArchiveUnusedInvoices(tx, batchSize, unusedDays) { return tx.$queryRaw` WITH picked AS ( SELECT id FROM "invoice_pool" WHERE linked_reimbursement_id IS NULL AND status IN ('available', 'ocr_failed') AND created_at < NOW() - make_interval(days => ${unusedDays}) ORDER BY created_at ASC LIMIT ${batchSize} FOR UPDATE SKIP LOCKED ) UPDATE "invoice_pool" AS i SET status = 'archived', archived_reason = 'unused_timeout', archived_at = NOW(), linked_item_id = NULL, ocr_locked_at = NULL, ocr_lock_owner = NULL, updated_by = NULL FROM picked WHERE i.id = picked.id AND i.linked_reimbursement_id IS NULL AND i.status IN ('available', 'ocr_failed') RETURNING i.id, i.owner_employee_id, i.status, i.archived_at `; } async function archiveUnusedInvoices(prisma, logger, options = {}) { const batchSize = Math.max(1, Number(options.batchSize ?? 200)); const unusedDays = Math.max(1, Number(options.unusedDays ?? 90)); let total = 0; let round = 0; while (true) { round += 1; const archivedRows = await prisma.$transaction(async (tx) => { return claimAndArchiveUnusedInvoices(tx, batchSize, unusedDays); }); if (!archivedRows.length) { break; } total += archivedRows.length; logger.info( { round, claimed: archivedRows.length, updated: archivedRows.length, archived: archivedRows.length, unusedDays, }, "Unused invoices archived batch processed", ); } if (total > 0) { logger.info({ archived: total, unusedDays }, "Unused invoices auto-archived"); } return total; } function startInvoiceArchiveJob(fastify) { let running = false; const task = cron.schedule(fastify.config.invoiceArchiveCron, async () => { if (running) { return; } running = true; try { await archiveUnusedInvoices(fastify.prisma, fastify.log, { batchSize: fastify.config.invoiceArchiveBatchSize, unusedDays: fastify.config.invoiceArchiveUnusedDays, }); } catch (error) { fastify.log.error({ err: error }, "Invoice archive job failed"); } finally { running = false; } }); fastify.addHook("onClose", async () => { task.stop(); task.destroy(); }); fastify.log.info( { cron: fastify.config.invoiceArchiveCron, unusedDays: fastify.config.invoiceArchiveUnusedDays, }, "Invoice archive job scheduled", ); return task; } module.exports = { archiveUnusedInvoices, startInvoiceArchiveJob, };