From 41273dfb040c088b647b58ab450e821983b708d2 Mon Sep 17 00:00:00 2001 From: Alberto Martinez <349769123@users.noreply.cursor.com> Date: Fri, 4 Sep 2026 04:29:02 +0000 Subject: [PATCH] =?UTF-8?q?Gastos,=20almac=C3=A9n=20y=20control=20presupue?= =?UTF-8?q?stal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Validación de endpoints y SPs Se probaron todos los endpoints nuevos (gastos, almacén, control presupuestal, IAM) y sus RPCs asociados. ### Bugs corregidos en migraciones/SQL 1. **IAM grants en schema core** — `iam-004e` intentaba `GRANT` sobre `core` con rol `iam_owner` (sin permiso). Los grants cruzados se movieron a `core-027-iam-rpc-cross-grants.sql`. 2. **`_cost_settings` ambiguo** — columnas `budget_warn_pct` etc. colisionaban con `RETURNS TABLE` en PL/pgSQL, rompiendo `fn_expense_create` y control presupuestal. Corregido en `core-028-fix-cost-settings-ambiguous.sql`. ### Tests añadidos - `api/cost_modules_test.ts` — CRUD RPC gastos, flujo almacén completo, control presupuestal, IAM permisos - `scripts/crud-smoke-test.sh` — smoke HTTP de todos los endpoints nuevos ### Resultados - `deno test cost_modules_test.ts` — 5/5 OK - `./scripts/crud-smoke-test.sh` — todos los checks OK (gastos CRUD, IVA, cost-settings, almacén, transferencias, cost-control, IAM)
--- api/cost_control_http.ts | 53 ++ api/cost_modules_test.ts | 283 ++++++++++ api/expenses_http.ts | 146 ++++++ api/expenses_test.ts | 10 + api/iam_http.ts | 65 +++ api/main.ts | 8 + api/permissions.ts | 101 ++++ api/storage.ts | 3 + api/warehouse_http.ts | 133 +++++ db/core/changelog-master.xml | 11 + .../changesets/018-schema-expenses-cost.sql | 103 ++++ db/core/changesets/019-schema-warehouse.sql | 94 ++++ .../changesets/020-rls-expenses-warehouse.sql | 50 ++ .../changesets/021-cost-control-helpers.sql | 180 +++++++ db/core/changesets/022-rpc-expenses.sql | 409 +++++++++++++++ db/core/changesets/023-rpc-warehouse.sql | 486 ++++++++++++++++++ db/core/changesets/024-rpc-cost-control.sql | 186 +++++++ .../changesets/025-rpc-payroll-cost-sync.sql | 197 +++++++ .../026-rpc-project-update-warehouse.sql | 125 +++++ .../changesets/027-iam-rpc-cross-grants.sql | 9 + .../028-fix-cost-settings-ambiguous.sql | 88 ++++ db/iam/changelog-master.xml | 2 + .../003-expenses-warehouse-permissions.sql | 27 + .../changesets/004-rpc-users-permissions.sql | 114 ++++ scripts/crud-smoke-test.sh | 139 +++++ web-panel/assets/css/desktop-v2.css | 5 + web-panel/components/BudgetItemPicker.vue | 73 +++ web-panel/components/CostSemaphore.vue | 18 + web-panel/layouts/default.vue | 4 + web-panel/pages/almacen.vue | 235 +++++++++ web-panel/pages/control-presupuesto.vue | 153 ++++++ web-panel/pages/gastos.vue | 235 +++++++++ web-panel/pages/usuarios.vue | 78 +++ 33 files changed, 3823 insertions(+) create mode 100644 api/cost_control_http.ts create mode 100644 api/cost_modules_test.ts create mode 100644 api/expenses_http.ts create mode 100644 api/expenses_test.ts create mode 100644 api/iam_http.ts create mode 100644 api/permissions.ts create mode 100644 api/warehouse_http.ts create mode 100644 db/core/changesets/018-schema-expenses-cost.sql create mode 100644 db/core/changesets/019-schema-warehouse.sql create mode 100644 db/core/changesets/020-rls-expenses-warehouse.sql create mode 100644 db/core/changesets/021-cost-control-helpers.sql create mode 100644 db/core/changesets/022-rpc-expenses.sql create mode 100644 db/core/changesets/023-rpc-warehouse.sql create mode 100644 db/core/changesets/024-rpc-cost-control.sql create mode 100644 db/core/changesets/025-rpc-payroll-cost-sync.sql create mode 100644 db/core/changesets/026-rpc-project-update-warehouse.sql create mode 100644 db/core/changesets/027-iam-rpc-cross-grants.sql create mode 100644 db/core/changesets/028-fix-cost-settings-ambiguous.sql create mode 100644 db/iam/changesets/003-expenses-warehouse-permissions.sql create mode 100644 db/iam/changesets/004-rpc-users-permissions.sql create mode 100644 web-panel/components/BudgetItemPicker.vue create mode 100644 web-panel/components/CostSemaphore.vue create mode 100644 web-panel/pages/almacen.vue create mode 100644 web-panel/pages/control-presupuesto.vue create mode 100644 web-panel/pages/gastos.vue create mode 100644 web-panel/pages/usuarios.vue diff --git a/api/cost_control_http.ts b/api/cost_control_http.ts new file mode 100644 index 0000000..fee67f4 --- /dev/null +++ b/api/cost_control_http.ts @@ -0,0 +1,53 @@ +import type { 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 } from "./permissions.ts"; + +type App = Hono<{ Variables: { user: AuthUser; db: Db } }>; + +function tid(c: { get: (k: "user") => AuthUser }): number { + return tenantScope(c.get("user")) ?? 0; +} + +export function registerCostControlRoutes(app: App) { + app.get("/v1/projects/:id/cost-control/summary", ...requireCoreAuth, requireAnyPermission("view_expenses", "manage_expenses"), async (c) => { + const db = c.get("db"); + const env = await callCoreFn(db, "core.fn_cost_control_project_summary", { + tenant_id: tid(c), + project_id: Number(c.req.param("id")), + }, { route: routeLabel(c) }); + return respondRpc(c, env); + }); + + app.get("/v1/projects/:id/cost-control/items", ...requireCoreAuth, requireAnyPermission("view_expenses", "manage_expenses"), async (c) => { + const db = c.get("db"); + const env = await callCoreFn(db, "core.fn_cost_control_by_item", { + tenant_id: tid(c), + project_id: Number(c.req.param("id")), + }, { route: routeLabel(c) }); + return respondRpc(c, env); + }); + + app.get("/v1/projects/:id/cost-control/chapters", ...requireCoreAuth, requireAnyPermission("view_expenses", "manage_expenses"), async (c) => { + const db = c.get("db"); + const env = await callCoreFn(db, "core.fn_cost_control_by_chapter", { + tenant_id: tid(c), + project_id: Number(c.req.param("id")), + }, { route: routeLabel(c) }); + return respondRpc(c, env); + }); + + app.get("/v1/projects/:id/cost-control/deviations", ...requireCoreAuth, requireAnyPermission("view_expenses", "manage_expenses"), async (c) => { + const db = c.get("db"); + const env = await callCoreFn(db, "core.fn_cost_control_top_deviations", { + tenant_id: tid(c), + project_id: Number(c.req.param("id")), + limit: c.req.query("limit"), + }, { route: routeLabel(c) }); + return respondRpc(c, env); + }); +} diff --git a/api/cost_modules_test.ts b/api/cost_modules_test.ts new file mode 100644 index 0000000..831b3b5 --- /dev/null +++ b/api/cost_modules_test.ts @@ -0,0 +1,283 @@ +import { assert, assertEquals, assertExists } from "jsr:@std/assert@1"; +import type { PgDb } from "./pg.ts"; +import { callCoreFn } from "./rpc.ts"; +import { withTestDb, closeTestPool } from "./test_helpers.ts"; +import { createPool, PgDb as PgDbClass } from "./pg.ts"; + +const TENANT_ID = 999002; + +type Fixture = { companyId: number; projectId: number; budgetItemId: number }; + +async function seedFixture(db: PgDb): Promise