import type { Hono } from "hono"; import * as XLSX from "xlsx"; import type { AuthUser } from "./auth.ts"; import { tenantScope } from "./auth.ts"; import { requireCoreAuth } from "./scope.ts"; import { requirePermission } from "./permissions.ts"; import { callCoreFn } from "./rpc.ts"; import { respondApiError, respondRpc, routeLabel } from "./http_errors.ts"; import type { Db } from "./db.ts"; type App = Hono<{ Variables: { user: AuthUser; db: Db } }>; function tid(c: { get: (k: "user") => AuthUser }): number { return tenantScope(c.get("user")) ?? 0; } function num(v: unknown): number { const n = Number(String(v ?? "").replace(/[$,\s]/g, "")); return Number.isFinite(n) ? n : 0; } function mapInvoiceRow(raw: Record) { const r: Record = {}; for (const [k, v] of Object.entries(raw)) { r[k.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase().trim()] = String(v ?? "").trim(); } const pick = (...keys: string[]) => keys.map((k) => r[k]).find((v) => v) ?? ""; return { direction: pick("direccion", "dirección", "tipo") || "recibida", rfc: pick("rfc"), supplier_name: pick("proveedor", "nombre"), uuid: pick("uuid"), serie: pick("serie"), folio: pick("folio"), invoice_date: pick("fecha", "invoice_date"), description: pick("descripcion", "descripción", "concepto"), quantity: num(pick("cantidad")), unit: pick("unidad"), amount: num(pick("importe", "subtotal")), iva: num(pick("iva")), total: num(pick("total")), project_code: pick("obra", "clave obra", "proyecto"), item_code: pick("clave concepto", "concepto presupuesto", "partida"), }; } export function registerInvoiceRoutes(app: App) { app.get("/v1/invoices", ...requireCoreAuth, requirePermission("invoices.view"), async (c) => { const db = c.get("db"); const env = await callCoreFn(db, "core.fn_invoice_list", { tenant_id: tid(c), direction: c.req.query("direction"), supplier_id: c.req.query("supplier_id"), rfc: c.req.query("rfc"), uuid: c.req.query("uuid"), folio: c.req.query("folio"), q: c.req.query("q"), amount_min: c.req.query("amount_min"), amount_max: c.req.query("amount_max"), date_from: c.req.query("date_from"), date_to: c.req.query("date_to"), project_id: c.req.query("project_id"), classified: c.req.query("classified"), }, { route: routeLabel(c) }); return respondRpc(c, env); }); app.get("/v1/invoices/:id", ...requireCoreAuth, requirePermission("invoices.view"), async (c) => { const db = c.get("db"); return respondRpc(c, await callCoreFn(db, "core.fn_invoice_get", { id: Number(c.req.param("id")) }, { route: routeLabel(c) })); }); app.post("/v1/invoices/import", ...requireCoreAuth, requirePermission("invoices.create"), async (c) => { const body = await c.req.parseBody(); const companyId = Number(body.company_id); const file = body.file; if (!(file instanceof File)) { return respondApiError(c, "VALIDATION", "Adjunte el Excel de facturas", { route: routeLabel(c) }); } const wb = XLSX.read(new Uint8Array(await file.arrayBuffer()), { type: "array", cellDates: true }); const sheet = wb.Sheets[wb.SheetNames[0]]; const raw = XLSX.utils.sheet_to_json>(sheet, { defval: "", raw: false }); const rows = raw.map(mapInvoiceRow).map((row) => { const date = row.invoice_date; const iso = /^\d{4}-\d{2}-\d{2}/.test(date) ? date.slice(0, 10) : /^\d{2}\/\d{2}\/\d{4}/.test(date) ? `${date.slice(6, 10)}-${date.slice(3, 5)}-${date.slice(0, 2)}` : date; return { ...row, invoice_date: iso }; }); const db = c.get("db"); return respondRpc(c, await callCoreFn(db, "core.fn_invoice_import", { tenant_id: tid(c), company_id: companyId, rows, }, { route: routeLabel(c) })); }); app.post("/v1/invoices/:id/classify", ...requireCoreAuth, requirePermission("invoices.update"), async (c) => { const body = await c.req.json>(); const db = c.get("db"); return respondRpc(c, await callCoreFn(db, "core.fn_invoice_classify", { ...body, id: Number(c.req.param("id")), tenant_id: tid(c), }, { route: routeLabel(c) })); }); }