mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 23:33:18 +00:00
<!-- 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> <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> </div>
125 lines
4.6 KiB
PL/PgSQL
125 lines
4.6 KiB
PL/PgSQL
--liquibase formatted sql
|
|
-- PANELS · core · cierre de almacén al concluir proyecto
|
|
|
|
--changeset panel:core-026a-fn-project-update-warehouse splitStatements:false
|
|
CREATE OR REPLACE FUNCTION core.fn_project_update(payload jsonb)
|
|
RETURNS jsonb
|
|
LANGUAGE plpgsql
|
|
SECURITY INVOKER
|
|
SET search_path = core
|
|
AS $$
|
|
DECLARE
|
|
v_id bigint := (payload->>'id')::bigint;
|
|
v_cur projects%ROWTYPE;
|
|
v_name text;
|
|
v_address text;
|
|
v_theme_id text;
|
|
v_status text;
|
|
v_company_id integer;
|
|
v_contract_amount numeric(14,2);
|
|
v_start_date date;
|
|
v_end_date date;
|
|
v_resident_name text;
|
|
v_siroc text;
|
|
v_payroll_tax_pct numeric(5,2);
|
|
v_close_env jsonb;
|
|
BEGIN
|
|
IF v_id IS NULL THEN
|
|
RETURN core.rpc_err(
|
|
'VALIDATION',
|
|
'fn_project_update: id es obligatorio',
|
|
jsonb_build_object('fn', 'fn_project_update', 'field', 'id')
|
|
);
|
|
END IF;
|
|
SELECT * INTO v_cur FROM projects WHERE id = v_id;
|
|
IF NOT FOUND THEN
|
|
RETURN core.rpc_err(
|
|
'NOT_FOUND',
|
|
format('fn_project_update: proyecto id=%s no encontrado', v_id),
|
|
jsonb_build_object('fn', 'fn_project_update', 'id', v_id)
|
|
);
|
|
END IF;
|
|
v_name := coalesce(nullif(btrim(payload->>'name'), ''), v_cur.name);
|
|
v_address := CASE WHEN payload ? 'address' THEN coalesce(nullif(btrim(payload->>'address'), ''), '') ELSE v_cur.address END;
|
|
v_theme_id := coalesce(nullif(btrim(payload->>'theme_id'), ''), v_cur.theme_id);
|
|
v_status := CASE
|
|
WHEN payload ? 'status' AND btrim(payload->>'status') IN ('activo', 'pausado', 'concluido', 'cancelado')
|
|
THEN btrim(payload->>'status')
|
|
ELSE v_cur.status
|
|
END;
|
|
v_company_id := coalesce((payload->>'company_id')::integer, v_cur.company_id);
|
|
v_contract_amount := CASE
|
|
WHEN payload ? 'contract_amount' THEN (payload->>'contract_amount')::numeric(14,2)
|
|
ELSE v_cur.contract_amount
|
|
END;
|
|
v_start_date := CASE
|
|
WHEN payload ? 'start_date' THEN nullif(btrim(payload->>'start_date'), '')::date
|
|
ELSE v_cur.start_date
|
|
END;
|
|
v_end_date := CASE
|
|
WHEN payload ? 'end_date' THEN nullif(btrim(payload->>'end_date'), '')::date
|
|
ELSE v_cur.end_date
|
|
END;
|
|
v_resident_name := CASE
|
|
WHEN payload ? 'resident_name' THEN coalesce(nullif(btrim(payload->>'resident_name'), ''), '')
|
|
ELSE v_cur.resident_name
|
|
END;
|
|
v_siroc := CASE
|
|
WHEN payload ? 'siroc' THEN upper(coalesce(nullif(btrim(payload->>'siroc'), ''), ''))
|
|
ELSE v_cur.siroc
|
|
END;
|
|
v_payroll_tax_pct := coalesce((payload->>'payroll_tax_pct')::numeric(5,2), v_cur.payroll_tax_pct);
|
|
IF v_company_id IS NULL THEN
|
|
RETURN core.rpc_err(
|
|
'VALIDATION',
|
|
'fn_project_update: company_id es obligatorio',
|
|
jsonb_build_object('fn', 'fn_project_update', 'field', 'company_id', 'id', v_id)
|
|
);
|
|
END IF;
|
|
IF NOT EXISTS (SELECT 1 FROM companies WHERE id = v_company_id) THEN
|
|
RETURN core.rpc_err(
|
|
'VALIDATION',
|
|
format('fn_project_update: empresa id=%s no encontrada', v_company_id),
|
|
jsonb_build_object('fn', 'fn_project_update', 'company_id', v_company_id, 'id', v_id)
|
|
);
|
|
END IF;
|
|
IF v_start_date IS NOT NULL AND v_end_date IS NOT NULL AND v_end_date < v_start_date THEN
|
|
RETURN core.rpc_err(
|
|
'VALIDATION',
|
|
'fn_project_update: la fecha de término no puede ser anterior al inicio',
|
|
jsonb_build_object('fn', 'fn_project_update', 'field', 'end_date', 'id', v_id)
|
|
);
|
|
END IF;
|
|
IF v_status IN ('concluido', 'cancelado') AND v_cur.status NOT IN ('concluido', 'cancelado') THEN
|
|
v_close_env := core.fn_warehouse_close_project(jsonb_build_object(
|
|
'project_id', v_id,
|
|
'force', COALESCE((payload->>'force_warehouse_close')::boolean, false),
|
|
'closed_by_name', coalesce(nullif(btrim(payload->>'closed_by_name'), ''), 'sistema'),
|
|
'close_notes', coalesce(nullif(btrim(payload->>'warehouse_close_notes'), ''), 'Cierre automático al concluir obra')
|
|
));
|
|
IF NOT COALESCE((v_close_env->>'ok')::boolean, false) THEN
|
|
RETURN v_close_env;
|
|
END IF;
|
|
END IF;
|
|
UPDATE projects SET
|
|
name = v_name,
|
|
address = v_address,
|
|
theme_id = v_theme_id,
|
|
status = v_status,
|
|
company_id = v_company_id,
|
|
contract_amount = v_contract_amount,
|
|
start_date = v_start_date,
|
|
end_date = v_end_date,
|
|
resident_name = v_resident_name,
|
|
siroc = v_siroc,
|
|
payroll_tax_pct = v_payroll_tax_pct
|
|
WHERE id = v_id;
|
|
RETURN core.rpc_ok(
|
|
jsonb_build_object('id', v_id, 'code', v_cur.code, 'status', v_status),
|
|
format('Proyecto %s actualizado', v_cur.code),
|
|
jsonb_build_object('fn', 'fn_project_update', 'id', v_id, 'code', v_cur.code)
|
|
);
|
|
EXCEPTION WHEN OTHERS THEN
|
|
RETURN core.rpc_from_exception('fn_project_update', SQLSTATE, SQLERRM);
|
|
END;
|
|
$$;
|