From 1a3fb4927223514f63ef0d1a074239caeeecb1bf Mon Sep 17 00:00:00 2001
From: Alberto Martinez <349769123@users.noreply.cursor.com>
Date: Tue, 8 Sep 2026 21:36:05 +0000
Subject: [PATCH] fix: guardar programa de obra (GET fallaba por agregados SQL
anidados)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Problema
La preview del Excel por partida ya funciona. Al pulsar **Importar** el programa sí se guarda (`Programa importado`), pero al recargar `GET /v1/projects/:id/work-program` aparece:
`Error inesperado en fn_work_program_get (sqlstate=42803): aggregate function calls cannot be nested`
Postgres no permite `jsonb_agg(... SUM(...))` en el mismo SELECT. Eso arma la curva semanal después del import.
## Cambio
Changeset `036`: `SUM` va en una subquery y `jsonb_agg` queda afuera. El import no cambia.
## Cómo probar
1. Aplicar migraciones Liquibase (changeset 036).
2. En una obra con presupuesto, importar el Excel por partida (el de 8 partidas / 9 semanas).
3. Tras **Importar**, la pantalla debe mostrar el programa (semanas y curva), sin el toast rojo.
4. Actualizar / volver a entrar a Programa de obra: sigue cargando.
---
db/core/changelog-master.xml | 1 +
.../036-fn-work-program-get-curve.sql | 92 +++++++++++++++++++
2 files changed, 93 insertions(+)
create mode 100644 db/core/changesets/036-fn-work-program-get-curve.sql
diff --git a/db/core/changelog-master.xml b/db/core/changelog-master.xml
index 5c4f997..d11dbb0 100644
--- a/db/core/changelog-master.xml
+++ b/db/core/changelog-master.xml
@@ -45,5 +45,6 @@
+
diff --git a/db/core/changesets/036-fn-work-program-get-curve.sql b/db/core/changesets/036-fn-work-program-get-curve.sql
new file mode 100644
index 0000000..e6fe33c
--- /dev/null
+++ b/db/core/changesets/036-fn-work-program-get-curve.sql
@@ -0,0 +1,92 @@
+--liquibase formatted sql
+-- PANELS · core · fn_work_program_get: jsonb_agg no puede anidar SUM (42803)
+
+--changeset panel:core-036-fn-work-program-get-curve splitStatements:false
+CREATE OR REPLACE FUNCTION core.fn_work_program_get(payload jsonb)
+RETURNS jsonb
+LANGUAGE plpgsql
+SECURITY INVOKER
+SET search_path = core
+AS $$
+DECLARE
+ v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint;
+ v_program jsonb;
+ v_periods jsonb;
+ v_budget_total numeric(14,2);
+ v_scheduled jsonb;
+ v_cumulative numeric := 0;
+ v_row jsonb;
+ v_arr jsonb := '[]'::jsonb;
+ v_pct_period numeric;
+ v_pct_cum numeric;
+BEGIN
+ IF v_project_id IS NULL THEN
+ RETURN core.rpc_err('VALIDATION', 'fn_work_program_get: project_id es obligatorio',
+ jsonb_build_object('fn', 'fn_work_program_get'));
+ END IF;
+
+ SELECT to_jsonb(wp) INTO v_program
+ FROM work_programs wp WHERE wp.project_id = v_project_id;
+
+ IF v_program IS NULL THEN
+ SELECT COALESCE(SUM(amount), 0) INTO v_budget_total FROM budget_items WHERE project_id = v_project_id;
+ RETURN core.rpc_ok(
+ jsonb_build_object(
+ 'program', NULL,
+ 'periods', '[]'::jsonb,
+ 'budget_subtotal', v_budget_total,
+ 'curve', '[]'::jsonb
+ ),
+ format('Proyecto %s sin programa de obra', v_project_id),
+ jsonb_build_object('fn', 'fn_work_program_get', 'project_id', v_project_id)
+ );
+ END IF;
+
+ SELECT COALESCE(jsonb_agg(to_jsonb(p) ORDER BY p.period_index), '[]'::jsonb) INTO v_periods
+ FROM work_program_periods p WHERE p.program_id = (v_program->>'id')::bigint;
+
+ SELECT COALESCE(SUM(amount), 0) INTO v_budget_total FROM budget_items WHERE project_id = v_project_id;
+
+ -- SUM va en la subquery; jsonb_agg afuera. Postgres no permite agregados anidados
+ -- (sqlstate 42803) y esto petaba el GET justo después de importar por partida.
+ SELECT COALESCE(jsonb_agg(to_jsonb(x) ORDER BY x.period_index), '[]'::jsonb)
+ INTO v_scheduled
+ FROM (
+ SELECT
+ pr.period_index,
+ pr.label,
+ round(COALESCE(SUM(pa.amount), 0), 2) AS amount
+ FROM work_program_periods pr
+ LEFT JOIN work_program_partida_amounts pa ON pa.period_id = pr.id
+ WHERE pr.program_id = (v_program->>'id')::bigint
+ GROUP BY pr.period_index, pr.label
+ ) x;
+
+ FOR v_row IN SELECT value FROM jsonb_array_elements(v_scheduled) LOOP
+ v_pct_period := CASE WHEN v_budget_total > 0
+ THEN round((v_row.value->>'amount')::numeric / v_budget_total, 6) ELSE 0 END;
+ v_cumulative := v_cumulative + (v_row.value->>'amount')::numeric;
+ v_pct_cum := CASE WHEN v_budget_total > 0 THEN round(v_cumulative / v_budget_total, 6) ELSE 0 END;
+ v_arr := v_arr || jsonb_build_array(jsonb_build_object(
+ 'period_index', v_row.value->'period_index',
+ 'label', v_row.value->'label',
+ 'amount', v_row.value->'amount',
+ 'pct_period', v_pct_period,
+ 'pct_cumulative', v_pct_cum
+ ));
+ END LOOP;
+
+ RETURN core.rpc_ok(
+ jsonb_build_object(
+ 'program', v_program,
+ 'periods', v_periods,
+ 'budget_subtotal', v_budget_total,
+ 'curve', v_arr
+ ),
+ format('Programa de obra del proyecto %s', v_project_id),
+ jsonb_build_object('fn', 'fn_work_program_get', 'project_id', v_project_id)
+ );
+EXCEPTION WHEN OTHERS THEN
+ RETURN core.rpc_from_exception('fn_work_program_get', SQLSTATE, SQLERRM);
+END;
+$$;