import type { Context, Hono } from "hono"; import type { AuthUser } from "./auth.ts"; import { tenantScope } from "./auth.ts"; import { requireCoreAuth } from "./scope.ts"; import type { Db } from "./db.ts"; import { callCoreFn } from "./rpc.ts"; import { respondRpc, routeLabel } from "./http_errors.ts"; import { requireAnyPermission, requirePermission } from "./permissions.ts"; import { encryptBytes, decryptBytes } from "./docs_crypto.ts"; import { expenseDocKey, getObject, putObject, s3Configured } from "./storage.ts"; import { randomBytes } from "node:crypto"; type App = Hono<{ Variables: { user: AuthUser; db: Db } }>; function tid(c: { get: (k: "user") => AuthUser }): number { return tenantScope(c.get("user")) ?? 0; } function actor(c: Context): { id: number; name: string } { const u = c.get("user"); return { id: u.id, name: u.display_name }; } export function registerExpenseRoutes(app: App) { app.get("/v1/expenses", ...requireCoreAuth, requireAnyPermission("view_expenses", "manage_expenses"), async (c) => { const db = c.get("db"); const env = await callCoreFn(db, "core.fn_expense_list", { tenant_id: tid(c), project_id: c.req.query("project_id"), company_id: c.req.query("company_id"), date_from: c.req.query("date_from"), date_to: c.req.query("date_to"), budget_item_id: c.req.query("budget_item_id"), source: c.req.query("source"), }, { route: routeLabel(c) }); return respondRpc(c, env); }); app.get("/v1/expenses/:id", ...requireCoreAuth, requireAnyPermission("view_expenses", "manage_expenses"), async (c) => { const db = c.get("db"); const env = await callCoreFn(db, "core.fn_expense_get", { id: Number(c.req.param("id")) }, { route: routeLabel(c) }); return respondRpc(c, env); }); app.post("/v1/expenses", ...requireCoreAuth, requirePermission("manage_expenses"), async (c) => { const body = await c.req.json>(); const db = c.get("db"); const a = actor(c); const env = await callCoreFn(db, "core.fn_expense_create", { ...body, tenant_id: tid(c), created_by_id: a.id, created_by_name: a.name, }, { route: routeLabel(c) }); return respondRpc(c, env); }); app.patch("/v1/expenses/:id", ...requireCoreAuth, requirePermission("manage_expenses"), async (c) => { const body = await c.req.json>(); const db = c.get("db"); const env = await callCoreFn(db, "core.fn_expense_update", { ...body, id: Number(c.req.param("id")), tenant_id: tid(c), }, { route: routeLabel(c) }); return respondRpc(c, env); }); app.delete("/v1/expenses/:id", ...requireCoreAuth, requirePermission("manage_expenses"), async (c) => { const db = c.get("db"); const env = await callCoreFn(db, "core.fn_expense_void", { id: Number(c.req.param("id")) }, { route: routeLabel(c) }); return respondRpc(c, env); }); app.post("/v1/expenses/:id/attachments", ...requireCoreAuth, requirePermission("manage_expenses"), async (c) => { const expenseId = Number(c.req.param("id")); const form = await c.req.formData(); const file = form.get("file"); if (!(file instanceof File)) { const env = await callCoreFn(c.get("db"), "core.fn_expense_attachment_add", { expense_id: expenseId, ...JSON.parse(String(form.get("meta") ?? "{}")), }, { route: routeLabel(c) }); return respondRpc(c, env); } const bytes = new Uint8Array(await file.arrayBuffer()); const { iv, cipher } = await encryptBytes(bytes); const storageName = `${Date.now()}-${randomBytes(8).toString("hex")}`; if (s3Configured()) { await putObject(expenseDocKey(expenseId, storageName), cipher); } const db = c.get("db"); const a = actor(c); const env = await callCoreFn(db, "core.fn_expense_attachment_add", { expense_id: expenseId, original_name: file.name, mime: file.type || "application/pdf", size_bytes: bytes.length, sha256: "", iv, storage_name: storageName, uploaded_by_id: a.id, uploaded_by_name: a.name, }, { route: routeLabel(c) }); return respondRpc(c, env); }); app.get("/v1/expenses/:id/attachments/:attId", ...requireCoreAuth, requireAnyPermission("view_expenses", "manage_expenses"), async (c) => { const expenseId = Number(c.req.param("id")); const attId = Number(c.req.param("attId")); const db = c.get("db"); const row = await db.prepare( "SELECT storage_name, iv, mime, original_name FROM expense_attachments WHERE id = ? AND expense_id = ?", ).get(attId, expenseId) as { storage_name: string; iv: string; mime: string; original_name: string } | undefined; if (!row) return respondRpc(c, { ok: false, code: "NOT_FOUND", layer: "api", message: "Comprobante no encontrado", data: null, errors: null }); const enc = await getObject(expenseDocKey(expenseId, row.storage_name)); const plain = await decryptBytes(row.iv, enc); c.header("Content-Type", row.mime); c.header("Content-Disposition", `inline; filename="${row.original_name}"`); return c.body(plain.buffer as ArrayBuffer); }); app.get("/v1/iva/summary", ...requireCoreAuth, requireAnyPermission("view_expenses", "manage_expenses"), async (c) => { const db = c.get("db"); const env = await callCoreFn(db, "core.fn_iva_period_summary", { tenant_id: tid(c), company_id: c.req.query("company_id"), year: c.req.query("year"), month: c.req.query("month"), }, { route: routeLabel(c) }); return respondRpc(c, env); }); app.get("/v1/cost-settings", ...requireCoreAuth, requireAnyPermission("view_expenses", "manage_cost_settings"), async (c) => { const db = c.get("db"); const env = await callCoreFn(db, "core.fn_cost_settings_get", { tenant_id: tid(c) }, { route: routeLabel(c) }); return respondRpc(c, env); }); app.put("/v1/cost-settings", ...requireCoreAuth, requirePermission("manage_cost_settings"), async (c) => { const body = await c.req.json>(); const db = c.get("db"); const env = await callCoreFn(db, "core.fn_cost_settings_save", { ...body, tenant_id: tid(c) }, { route: routeLabel(c) }); return respondRpc(c, env); }); }