import { FastifyInstance } from "fastify"; import prisma from "../../config/database"; import { requirePermission } from "../../middleware/auth"; import { logAudit } from "../../services/audit"; import { success, error, parseId } from "../../utils/response"; import { parsePagination, buildPaginationMeta } from "../../utils/pagination"; import { parseBody } from "../../schemas/common"; import { CreateInvoiceSchema, UpdateInvoiceSchema, } from "../../schemas/invoices.schema"; import { markOverdueInvoices, listInvoices, getNextInvoiceNumberFormatted, getInvoiceStats, getOrderDataForInvoice, getInvoice, createInvoice, updateInvoice, deleteInvoice, } from "../../services/invoices.service"; import { nasFinancialsManager } from "../../services/nas-financials-manager"; export default async function invoicesRoutes( fastify: FastifyInstance, ): Promise { // Auto-update overdue invoices on GET requests only (matches PHP behavior) fastify.addHook("onRequest", async (request) => { if (request.method !== "GET") return; await markOverdueInvoices(); }); // GET /api/admin/invoices fastify.get( "/", { preHandler: requirePermission("invoices.view") }, async (request, reply) => { const query = request.query as Record; const { page, limit, skip, order, search } = parsePagination(query); const result = await listInvoices({ page, limit, skip, sort: String(query.sort || ""), order, search, status: query.status ? String(query.status) : undefined, customer_id: query.customer_id ? Number(query.customer_id) : undefined, month: query.month ? Number(query.month) : undefined, year: query.year ? Number(query.year) : undefined, }); return reply.send({ success: true, data: result.data, pagination: buildPaginationMeta(result.total, page, limit), }); }, ); // GET /api/admin/invoices/next-number fastify.get( "/next-number", { preHandler: requirePermission("invoices.create") }, async (_request, reply) => { const result = await getNextInvoiceNumberFormatted(); return success(reply, result); }, ); // GET /api/admin/invoices/stats fastify.get( "/stats", { preHandler: requirePermission("invoices.view") }, async (request, reply) => { const query = request.query as Record; const month = query.month ? Number(query.month) : undefined; const year = query.year ? Number(query.year) : undefined; const stats = await getInvoiceStats(month, year); return success(reply, stats); }, ); // GET /api/admin/invoices/order-data/:id fastify.get<{ Params: { id: string } }>( "/order-data/:id", { preHandler: requirePermission("invoices.create") }, async (request, reply) => { const orderId = parseId(request.params.id, reply); if (orderId === null) return; const result = await getOrderDataForInvoice(orderId); if (!result) return error(reply, "Objednávka nenalezena", 404); return success(reply, result); }, ); // GET /api/admin/invoices/:id fastify.get<{ Params: { id: string } }>( "/:id", { preHandler: requirePermission("invoices.view") }, async (request, reply) => { const id = parseId(request.params.id, reply); if (id === null) return; const invoice = await getInvoice(id); if (!invoice) return error(reply, "Faktura nenalezena", 404); return success(reply, invoice); }, ); // POST /api/admin/invoices fastify.post( "/", { preHandler: requirePermission("invoices.create") }, async (request, reply) => { const parsed = parseBody(CreateInvoiceSchema, request.body); if ("error" in parsed) return error(reply, parsed.error, 400); const body = parsed.data; const invoice = await createInvoice(body); await logAudit({ request, authData: request.authData, action: "create", entityType: "invoice", entityId: invoice.id, description: `Vytvořena faktura ${invoice.invoice_number}`, }); // Return both invoice_id and id for frontend compatibility return success( reply, { id: invoice.id, invoice_id: invoice.id, invoice_number: invoice.invoice_number, }, 201, "Faktura byla vystavena", ); }, ); // PUT /api/admin/invoices/:id fastify.put<{ Params: { id: string } }>( "/:id", { preHandler: requirePermission("invoices.edit") }, async (request, reply) => { const id = parseId(request.params.id, reply); if (id === null) return; const parsed = parseBody(UpdateInvoiceSchema, request.body); if ("error" in parsed) return error(reply, parsed.error, 400); const body = parsed.data; const result = await updateInvoice(id, body); if ("error" in result) { if (result.error === "not_found") return error(reply, "Faktura nenalezena", 404); if (result.error === "invalid_transition") return error( reply, `Neplatný přechod stavu z "${result.currentStatus}" na "${result.newStatus}"`, 400, ); return error(reply, "Neznámá chyba", 500); } await logAudit({ request, authData: request.authData, action: "update", entityType: "invoice", entityId: id, description: `Upravena faktura ${result.invoice_number}`, }); return success(reply, { id }, 200, "Faktura byla aktualizována"); }, ); // DELETE /api/admin/invoices/:id fastify.delete<{ Params: { id: string } }>( "/:id", { preHandler: requirePermission("invoices.delete") }, async (request, reply) => { const id = parseId(request.params.id, reply); if (id === null) return; const existing = await deleteInvoice(id); if (!existing) return error(reply, "Faktura nenalezena", 404); // Delete PDF from NAS if (existing.invoice_number && existing.issue_date) { const d = new Date(existing.issue_date); const relPath = `Vydané/${d.getFullYear()}/${String(d.getMonth() + 1).padStart(2, "0")}/${existing.invoice_number}.pdf`; nasFinancialsManager.deleteIssuedInvoice(relPath); } await logAudit({ request, authData: request.authData, action: "delete", entityType: "invoice", entityId: id, description: `Smazána faktura ${existing.invoice_number}`, }); return success(reply, null, 200, "Faktura smazána"); }, ); // GET /api/admin/invoices/:id/file — serve PDF from NAS fastify.get<{ Params: { id: string } }>( "/:id/file", { preHandler: requirePermission("invoices.view") }, async (request, reply) => { const id = parseId(request.params.id, reply); if (id === null) return; const invoice = await prisma.invoices.findUnique({ where: { id }, select: { invoice_number: true, issue_date: true }, }); if (!invoice?.invoice_number || !invoice.issue_date) return error(reply, "Faktura nenalezena", 404); const d = new Date(invoice.issue_date); const relPath = `Vydané/${d.getFullYear()}/${String(d.getMonth() + 1).padStart(2, "0")}/${invoice.invoice_number}.pdf`; const file = nasFinancialsManager.readIssuedInvoice(relPath); if (!file) return error(reply, "PDF soubor nenalezen", 404); return reply .type("application/pdf") .header( "Content-Disposition", `inline; filename="${invoice.invoice_number}.pdf"`, ) .send(file.data); }, ); }