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 { respondApiError, respondRpc, routeLabel } from "./http_errors.ts"; import { requirePermission } from "./permissions.ts"; import { denyUnlessProjectScope, denyUnlessWarehouseCentral } from "./scope_enforcement.ts"; import { getUserScope } from "./user_scope.ts"; 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 }; } async function filterWarehouseList(c: Context, env: Awaited>) { const user = c.get("user") as AuthUser; if (!env.ok || !env.data || user.realm === "platform" || user.is_owner) return env; const scope = await getUserScope(user); const warehouses = (env.data as { warehouses?: Array<{ id: number; kind?: string; project_id?: number | null }> }).warehouses ?? []; const filtered = warehouses.filter((wh) => { if (wh.kind === "central") return scope.warehouseCentral; if (wh.kind === "project") { if (!scope.warehouseProjects) return false; if (scope.allProjects) return true; return scope.projectIds.includes(Number(wh.project_id)); } return true; }); return { ...env, data: { ...(env.data as object), warehouses: filtered } }; } export function registerWarehouseRoutes(app: App) { app.get("/v1/warehouses", ...requireCoreAuth, requirePermission("warehouse.view"), async (c) => { const db = c.get("db"); const env = await callCoreFn(db, "core.fn_warehouse_list", { tenant_id: tid(c), include_closed: c.req.query("include_closed") === "true", }, { route: routeLabel(c) }); return respondRpc(c, await filterWarehouseList(c, env)); }); app.post("/v1/warehouses/central/ensure", ...requireCoreAuth, requirePermission("warehouse.update"), async (c) => { if (!await denyUnlessWarehouseCentral(c)) return; const db = c.get("db"); const env = await callCoreFn(db, "core.fn_warehouse_ensure_central", { tenant_id: tid(c) }, { route: routeLabel(c) }); return respondRpc(c, env); }); app.post("/v1/projects/:id/warehouse/open", ...requireCoreAuth, requirePermission("warehouse.update"), async (c) => { const projectId = Number(c.req.param("id")); if (!await denyUnlessProjectScope(c, projectId)) return; const db = c.get("db"); const env = await callCoreFn(db, "core.fn_warehouse_open_project", { tenant_id: tid(c), project_id: projectId, }, { route: routeLabel(c) }); return respondRpc(c, env); }); app.post("/v1/projects/:id/warehouse/close", ...requireCoreAuth, requirePermission("warehouse.update"), async (c) => { const projectId = Number(c.req.param("id")); if (!await denyUnlessProjectScope(c, projectId)) return; const body = await c.req.json<{ force?: boolean; close_notes?: string; transfer_remaining?: boolean }>().catch( () => ({} as { force?: boolean; close_notes?: string; transfer_remaining?: boolean }), ); const db = c.get("db"); const a = actor(c); const env = await callCoreFn(db, "core.fn_warehouse_close_project", { project_id: projectId, force: body.force, transfer_remaining: body.transfer_remaining, closed_by_name: a.name, close_notes: body.close_notes, }, { route: routeLabel(c) }); return respondRpc(c, env); }); app.get("/v1/warehouses/:id/stock", ...requireCoreAuth, requirePermission("warehouse.view"), async (c) => { const db = c.get("db"); const whId = Number(c.req.param("id")); const wh = await db.prepare("SELECT kind, project_id FROM warehouses WHERE id = ?").get(whId) as | { kind: string; project_id: number | null } | undefined; if (!wh) return respondApiError(c, "NOT_FOUND", "Almacén no encontrado", { route: routeLabel(c), warehouse_id: whId }); if (wh.kind === "central") { if (!await denyUnlessWarehouseCentral(c)) return; } else if (!await denyUnlessProjectScope(c, wh.project_id)) return; const env = await callCoreFn(db, "core.fn_warehouse_stock_list", { tenant_id: tid(c), warehouse_id: whId, global: false, }, { route: routeLabel(c) }); return respondRpc(c, env); }); app.get("/v1/warehouses/stock/global", ...requireCoreAuth, requirePermission("warehouse.view"), async (c) => { if (!await denyUnlessWarehouseCentral(c)) return; const db = c.get("db"); const env = await callCoreFn(db, "core.fn_warehouse_stock_list", { tenant_id: tid(c), global: true, }, { route: routeLabel(c) }); return respondRpc(c, env); }); app.get("/v1/warehouses/:id/movements", ...requireCoreAuth, requirePermission("warehouse.view"), async (c) => { const db = c.get("db"); const whId = Number(c.req.param("id")); const wh = await db.prepare("SELECT kind, project_id FROM warehouses WHERE id = ?").get(whId) as | { kind: string; project_id: number | null } | undefined; if (!wh) return respondApiError(c, "NOT_FOUND", "Almacén no encontrado", { route: routeLabel(c), warehouse_id: whId }); if (wh.kind === "central") { if (!await denyUnlessWarehouseCentral(c)) return; } else if (!await denyUnlessProjectScope(c, wh.project_id)) return; const env = await callCoreFn(db, "core.fn_warehouse_movement_list", { tenant_id: tid(c), warehouse_id: whId, material_id: c.req.query("material_id"), }, { route: routeLabel(c) }); return respondRpc(c, env); }); app.get("/v1/warehouses/movements/global", ...requireCoreAuth, requirePermission("warehouse.view"), async (c) => { if (!await denyUnlessWarehouseCentral(c)) return; const db = c.get("db"); const env = await callCoreFn(db, "core.fn_warehouse_movement_list", { tenant_id: tid(c), global: true, material_id: c.req.query("material_id"), }, { route: routeLabel(c) }); return respondRpc(c, env); }); app.get("/v1/warehouse-materials", ...requireCoreAuth, requirePermission("warehouse.view"), async (c) => { const db = c.get("db"); const env = await callCoreFn(db, "core.fn_warehouse_material_list", { tenant_id: tid(c), }, { route: routeLabel(c) }); return respondRpc(c, env); }); app.post("/v1/warehouse-materials", ...requireCoreAuth, requirePermission("warehouse.create"), async (c) => { const body = await c.req.json>(); const db = c.get("db"); const env = await callCoreFn(db, "core.fn_warehouse_material_upsert", { ...body, tenant_id: tid(c) }, { route: routeLabel(c) }); return respondRpc(c, env); }); app.patch("/v1/warehouse-materials/:id", ...requireCoreAuth, requirePermission("warehouse.update"), async (c) => { const body = await c.req.json>(); const db = c.get("db"); const env = await callCoreFn(db, "core.fn_warehouse_material_upsert", { ...body, id: Number(c.req.param("id")), tenant_id: tid(c), }, { route: routeLabel(c) }); return respondRpc(c, env); }); app.post("/v1/warehouses/:id/entries", ...requireCoreAuth, requirePermission("warehouse.create"), async (c) => { const whId = Number(c.req.param("id")); const db = c.get("db"); const wh = await db.prepare("SELECT kind, project_id FROM warehouses WHERE id = ?").get(whId) as | { kind: string; project_id: number | null } | undefined; if (!wh) return respondApiError(c, "NOT_FOUND", "Almacén no encontrado", { route: routeLabel(c), warehouse_id: whId }); if (wh.kind === "central") { if (!await denyUnlessWarehouseCentral(c)) return; } else if (!await denyUnlessProjectScope(c, wh.project_id)) return; const body = await c.req.json>(); const a = actor(c); const env = await callCoreFn(db, "core.fn_warehouse_entry_create", { ...body, tenant_id: tid(c), warehouse_id: whId, created_by_id: a.id, created_by_name: a.name, }, { route: routeLabel(c) }); return respondRpc(c, env); }); app.post("/v1/warehouses/:id/exits", ...requireCoreAuth, requirePermission("warehouse.update"), async (c) => { const whId = Number(c.req.param("id")); const db = c.get("db"); const wh = await db.prepare("SELECT kind, project_id FROM warehouses WHERE id = ?").get(whId) as | { kind: string; project_id: number | null } | undefined; if (!wh) return respondApiError(c, "NOT_FOUND", "Almacén no encontrado", { route: routeLabel(c), warehouse_id: whId }); if (wh.kind === "central") { if (!await denyUnlessWarehouseCentral(c)) return; } else if (!await denyUnlessProjectScope(c, wh.project_id)) return; const body = await c.req.json>(); const a = actor(c); const env = await callCoreFn(db, "core.fn_warehouse_exit_create", { ...body, tenant_id: tid(c), warehouse_id: whId, created_by_id: a.id, created_by_name: a.name, }, { route: routeLabel(c) }); return respondRpc(c, env); }); app.post("/v1/warehouse-transfers", ...requireCoreAuth, requirePermission("warehouse.update"), async (c) => { const body = await c.req.json>(); const db = c.get("db"); const a = actor(c); const env = await callCoreFn(db, "core.fn_warehouse_transfer_create", { ...body, tenant_id: tid(c), created_by_name: a.name, }, { route: routeLabel(c) }); return respondRpc(c, env); }); }