panels-origin/db/core/changesets/028-fix-cost-settings-ambiguous.sql
Alberto Martinez 41273dfb04 Gastos, almacén y control presupuestal
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## 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)
<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a href="https://cursor.com/agents/bc-eeec3b5c-f789-43e8-a353-0755e4706377?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a href="https://cursor.com/background-agent?bcId=bc-eeec3b5c-f789-43e8-a353-0755e4706377&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>
2026-09-04 04:29:02 +00:00

88 lines
2.5 KiB
PL/PgSQL

--liquibase formatted sql
-- PANELS · core · fix ambigüedad de columnas en _cost_settings (RETURNS TABLE vs tenant_cost_settings)
--changeset panel:core-028a-fix-cost-settings splitStatements:false
CREATE OR REPLACE FUNCTION core._cost_settings(p_tenant_id integer)
RETURNS TABLE (
budget_warn_pct numeric,
budget_critical_pct numeric,
default_iva_rate numeric
)
LANGUAGE plpgsql
STABLE
SET search_path = core
AS $$
BEGIN
RETURN QUERY
SELECT
COALESCE(s.budget_warn_pct, 85::numeric),
COALESCE(s.budget_critical_pct, 100::numeric),
COALESCE(s.default_iva_rate, 0.16::numeric)
FROM (
SELECT t.budget_warn_pct, t.budget_critical_pct, t.default_iva_rate
FROM tenant_cost_settings t
WHERE t.tenant_id = p_tenant_id
) s
UNION ALL
SELECT 85::numeric, 100::numeric, 0.16::numeric
WHERE NOT EXISTS (SELECT 1 FROM tenant_cost_settings WHERE tenant_id = p_tenant_id)
LIMIT 1;
END;
$$;
--changeset panel:core-028b-fix-cost-budget-warning splitStatements:false
CREATE OR REPLACE FUNCTION core._cost_budget_warning(
p_tenant_id integer,
p_budget_item_id bigint,
p_add_subtotal numeric DEFAULT 0,
p_add_qty numeric DEFAULT 0
)
RETURNS jsonb
LANGUAGE plpgsql
STABLE
SET search_path = core
AS $$
DECLARE
v_item record;
v_warn numeric;
v_critical numeric;
v_exec_amt numeric;
v_exec_qty numeric;
v_pct numeric;
v_level text;
BEGIN
IF p_budget_item_id IS NULL THEN
RETURN NULL;
END IF;
SELECT id, description, amount, quantity INTO v_item
FROM budget_items WHERE id = p_budget_item_id;
IF NOT FOUND OR COALESCE(v_item.amount, 0) <= 0 THEN
RETURN NULL;
END IF;
SELECT cs.budget_warn_pct, cs.budget_critical_pct INTO v_warn, v_critical
FROM core._cost_settings(p_tenant_id) cs;
v_exec_amt := core._cost_executed_amount(p_budget_item_id) + COALESCE(p_add_subtotal, 0);
v_exec_qty := core._cost_executed_qty(p_budget_item_id) + COALESCE(p_add_qty, 0);
v_pct := round(v_exec_amt / v_item.amount * 100, 1);
IF v_pct > v_critical THEN
v_level := 'critical';
ELSIF v_pct > v_warn THEN
v_level := 'warn';
ELSE
RETURN NULL;
END IF;
RETURN jsonb_build_object(
'level', v_level,
'budget_item_id', p_budget_item_id,
'pct_after', v_pct,
'executed_amount', v_exec_amt,
'budget_amount', v_item.amount,
'executed_qty', v_exec_qty,
'budget_qty', v_item.quantity,
'message', format(
'Esta operación dejará el concepto "%s" al %s%% del presupuesto (importe)',
left(v_item.description, 80), v_pct
)
);
END;
$$;