import prisma from "../config/database"; import { config } from "../config/env"; import { sendMail } from "./mailer"; import { localDateCzStr, localDateStr } from "../utils/date"; import { getSystemSettings } from "./system-settings"; interface AlertInvoice { id: number; type: "created" | "received"; number: string; counterparty: string; amount: string; currency: string; due_date: string; days_label: string; } function escapeHtml(str: string): string { return str .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function formatAmount(n: number | { toNumber?: () => number }): string { const num = typeof n === "number" ? n : Number(n); return num.toLocaleString("cs-CZ", { minimumFractionDigits: 2, maximumFractionDigits: 2, }); } export async function checkInvoiceAlerts(): Promise { const settings = await getSystemSettings(); const alertEmail = settings.invoice_alert_email || config.email.invoiceAlert; if (!alertEmail) return; const today = new Date(); const todayStr = localDateStr(today); const in3days = new Date(today); in3days.setDate(in3days.getDate() + 3); const in3daysStr = localDateStr(in3days); const alerts: AlertInvoice[] = []; // --- Created invoices (customer owes us) --- const createdInvoices = await prisma.invoices.findMany({ where: { status: { in: ["issued", "overdue"] }, due_date: { not: null }, }, include: { customers: { select: { name: true } }, invoice_items: true, }, }); for (const inv of createdInvoices) { if (!inv.due_date) continue; const dueDateStr = localDateStr(new Date(inv.due_date)); let alertType: string | null = null; let daysLabel = ""; if (dueDateStr === todayStr) { alertType = "due"; daysLabel = "splatnost dnes"; } else if (dueDateStr === in3daysStr) { alertType = "3days"; daysLabel = "splatnost za 3 dny"; } if (!alertType) continue; const alreadySent = await prisma.invoice_alert_log.findUnique({ where: { invoice_type_invoice_id_alert_type: { invoice_type: "created", invoice_id: inv.id, alert_type: alertType, }, }, }); if (alreadySent) continue; const subtotal = inv.invoice_items.reduce( (sum, item) => sum + Number(item.quantity) * Number(item.unit_price), 0, ); alerts.push({ id: inv.id, type: "created", number: inv.invoice_number || `#${inv.id}`, counterparty: inv.customers?.name || "—", amount: formatAmount(subtotal), currency: inv.currency || "CZK", due_date: localDateCzStr(new Date(inv.due_date)), days_label: daysLabel, }); await prisma.invoice_alert_log.create({ data: { invoice_type: "created", invoice_id: inv.id, alert_type: alertType, }, }); } // --- Received invoices (we owe supplier) --- const receivedInvoices = await prisma.received_invoices.findMany({ where: { status: "unpaid", due_date: { not: null }, }, }); for (const inv of receivedInvoices) { if (!inv.due_date) continue; const dueDateStr = localDateStr(new Date(inv.due_date)); let alertType: string | null = null; let daysLabel = ""; if (dueDateStr === todayStr) { alertType = "due"; daysLabel = "splatnost dnes"; } else if (dueDateStr === in3daysStr) { alertType = "3days"; daysLabel = "splatnost za 3 dny"; } if (!alertType) continue; const alreadySent = await prisma.invoice_alert_log.findUnique({ where: { invoice_type_invoice_id_alert_type: { invoice_type: "received", invoice_id: inv.id, alert_type: alertType, }, }, }); if (alreadySent) continue; alerts.push({ id: inv.id, type: "received", number: inv.invoice_number || inv.supplier_name, counterparty: inv.supplier_name, amount: formatAmount(inv.amount), currency: inv.currency, due_date: localDateCzStr(new Date(inv.due_date)), days_label: daysLabel, }); await prisma.invoice_alert_log.create({ data: { invoice_type: "received", invoice_id: inv.id, alert_type: alertType, }, }); } if (alerts.length === 0) return; // --- Build summary email --- const createdAlerts = alerts.filter((a) => a.type === "created"); const receivedAlerts = alerts.filter((a) => a.type === "received"); const subject = `Upozornění na splatnost faktur (${alerts.length})`; const buildTable = (items: AlertInvoice[], title: string) => { if (items.length === 0) return ""; const rows = items .map( (a) => ` ${escapeHtml(a.number)} ${escapeHtml(a.counterparty)} ${a.amount} ${a.currency} ${a.due_date} ${a.days_label} `, ) .join(""); return `

${escapeHtml(title)} (${items.length})

${rows}
Číslo Firma Částka Splatnost Stav
`; }; const html = `

Upozornění na splatnost faktur

Následující faktury se blíží ke splatnosti nebo jsou dnes splatné:

${buildTable(createdAlerts, "Vydané faktury (neuhrazené zákazníkem)")} ${buildTable(receivedAlerts, "Přijaté faktury (k úhradě)")}

Automatické upozornění vygenerováno ${localDateCzStr(today)}.

`; const sent = await sendMail(alertEmail, subject, html); if (!sent) { console.error(`InvoiceAlerts: Failed to send alert to ${alertEmail}`); } else { console.log( `InvoiceAlerts: Sent ${alerts.length} alert(s) to ${alertEmail}`, ); } }