From 9b0ad44b77eaa1ab7b3470d01f5311f355681f1b Mon Sep 17 00:00:00 2001 From: Lucas Ansei Date: Thu, 26 Feb 2026 09:43:43 -0300 Subject: [PATCH 1/3] feat(meta-orcamento-consolidado): adicionando view de orcamento consolidado e triggers --- ...-atualiza-meta-orcamento-consolidado.pgsql | 344 ++++++++++++++++++ .../migration.sql | 33 ++ backend/prisma/schema.prisma | 31 ++ backend/src/meta/entities/meta.entity.ts | 25 ++ backend/src/meta/meta.service.ts | 42 +++ 5 files changed, 475 insertions(+) create mode 100644 backend/prisma/manual-copy/0056-atualiza-meta-orcamento-consolidado.pgsql create mode 100644 backend/prisma/migrations/20260226121720_meta_orcamento_consolidado/migration.sql diff --git a/backend/prisma/manual-copy/0056-atualiza-meta-orcamento-consolidado.pgsql b/backend/prisma/manual-copy/0056-atualiza-meta-orcamento-consolidado.pgsql new file mode 100644 index 0000000000..da45670e61 --- /dev/null +++ b/backend/prisma/manual-copy/0056-atualiza-meta-orcamento-consolidado.pgsql @@ -0,0 +1,344 @@ +-- Função auxiliar para extrair projeto_atividade da dotacao +CREATE OR REPLACE FUNCTION f_extrair_projeto_atividade(dotacao TEXT) +RETURNS TEXT AS $$ +DECLARE + partes TEXT[]; + codigo TEXT; +BEGIN + -- Split da dotação por ponto + partes := string_to_array(dotacao, '.'); + + -- Verifica se tem pelo menos 7 campos (índices 0-6) + IF array_length(partes, 1) >= 7 THEN + -- Junta os campos 6 e 7 (índices 5 e 6 em array 1-based do PostgreSQL) + codigo := partes[6] || partes[7]; + RETURN codigo; + ELSE + RETURN ''; + END IF; +END; +$$ LANGUAGE plpgsql IMMUTABLE; + +-- Função auxiliar para classificar projeto_atividade +CREATE OR REPLACE FUNCTION f_classificar_projeto_atividade(dotacao TEXT) +RETURNS TEXT AS $$ +DECLARE + projeto_atividade TEXT; + primeiro_digito CHAR(1); + digito_numerico INT; +BEGIN + -- Extrai o projeto_atividade da dotação + projeto_atividade := f_extrair_projeto_atividade(dotacao); + + -- Se não conseguiu extrair, retorna desconhecido + IF projeto_atividade = '' OR projeto_atividade IS NULL THEN + RETURN 'desconhecido'; + END IF; + + -- Extrai o primeiro dígito do projeto_atividade + primeiro_digito := substring(projeto_atividade from 1 for 1); + + -- Tenta converter para número + BEGIN + digito_numerico := primeiro_digito::INT; + EXCEPTION WHEN OTHERS THEN + -- Se não for número, retorna 'desconhecido' + RETURN 'desconhecido'; + END; + + -- Classifica baseado no primeiro dígito + IF digito_numerico = 0 THEN + RETURN 'operacao_especial'; + ELSIF digito_numerico % 2 = 1 THEN + RETURN 'projeto'; + ELSE + RETURN 'atividade'; + END IF; +END; +$$ LANGUAGE plpgsql IMMUTABLE; + +-- Função para recalcular consolidado de uma meta +CREATE OR REPLACE FUNCTION f_refresh_meta_orcamento_consolidado(p_meta_id INT) +RETURNS VOID AS $$ +DECLARE + v_total_previsao DECIMAL(19,2); + v_total_empenhado DECIMAL(19,2); + v_total_liquidado DECIMAL(19,2); + v_total_previsao_projeto DECIMAL(19,2); + v_total_empenhado_projeto DECIMAL(19,2); + v_total_liquidado_projeto DECIMAL(19,2); + v_total_previsao_atividade DECIMAL(19,2); + v_total_empenhado_atividade DECIMAL(19,2); + v_total_liquidado_atividade DECIMAL(19,2); + v_total_previsao_operacao DECIMAL(19,2); + v_total_empenhado_operacao DECIMAL(19,2); + v_total_liquidado_operacao DECIMAL(19,2); +BEGIN + -- Calcula totais de planejado + -- Inclui orçamentos diretos da meta, de iniciativas e de atividades + WITH planejado_com_classificacao AS ( + SELECT + op.valor_planejado, + f_classificar_projeto_atividade(op.dotacao) as tipo + FROM orcamento_planejado op + LEFT JOIN iniciativa i ON i.id = op.iniciativa_id + LEFT JOIN atividade a ON a.id = op.atividade_id + LEFT JOIN iniciativa ia ON ia.id = a.iniciativa_id + WHERE ( + op.meta_id = p_meta_id OR + i.meta_id = p_meta_id OR + ia.meta_id = p_meta_id + ) + AND op.removido_em IS NULL + ) + SELECT + COALESCE(SUM(valor_planejado), 0), + COALESCE(SUM(CASE WHEN tipo = 'projeto' THEN valor_planejado ELSE 0 END), 0), + COALESCE(SUM(CASE WHEN tipo = 'atividade' THEN valor_planejado ELSE 0 END), 0), + COALESCE(SUM(CASE WHEN tipo = 'operacao_especial' THEN valor_planejado ELSE 0 END), 0) + INTO + v_total_previsao, + v_total_previsao_projeto, + v_total_previsao_atividade, + v_total_previsao_operacao + FROM planejado_com_classificacao; + + -- Calcula totais de realizado + -- Inclui orçamentos diretos da meta, de iniciativas e de atividades + WITH realizado_com_classificacao AS ( + SELECT + orc.soma_valor_empenho, + orc.soma_valor_liquidado, + f_classificar_projeto_atividade(orc.dotacao) as tipo + FROM orcamento_realizado orc + LEFT JOIN iniciativa i ON i.id = orc.iniciativa_id + LEFT JOIN atividade a ON a.id = orc.atividade_id + LEFT JOIN iniciativa ia ON ia.id = a.iniciativa_id + WHERE ( + orc.meta_id = p_meta_id OR + i.meta_id = p_meta_id OR + ia.meta_id = p_meta_id + ) + AND orc.removido_em IS NULL + ) + SELECT + COALESCE(SUM(soma_valor_empenho), 0), + COALESCE(SUM(soma_valor_liquidado), 0), + COALESCE(SUM(CASE WHEN tipo = 'projeto' THEN soma_valor_empenho ELSE 0 END), 0), + COALESCE(SUM(CASE WHEN tipo = 'projeto' THEN soma_valor_liquidado ELSE 0 END), 0), + COALESCE(SUM(CASE WHEN tipo = 'atividade' THEN soma_valor_empenho ELSE 0 END), 0), + COALESCE(SUM(CASE WHEN tipo = 'atividade' THEN soma_valor_liquidado ELSE 0 END), 0), + COALESCE(SUM(CASE WHEN tipo = 'operacao_especial' THEN soma_valor_empenho ELSE 0 END), 0), + COALESCE(SUM(CASE WHEN tipo = 'operacao_especial' THEN soma_valor_liquidado ELSE 0 END), 0) + INTO + v_total_empenhado, + v_total_liquidado, + v_total_empenhado_projeto, + v_total_liquidado_projeto, + v_total_empenhado_atividade, + v_total_liquidado_atividade, + v_total_empenhado_operacao, + v_total_liquidado_operacao + FROM realizado_com_classificacao; + + -- Upsert na tabela consolidada + INSERT INTO meta_orcamento_consolidado ( + meta_id, + total_previsao, + total_empenhado, + total_liquidado, + total_previsao_projeto, + total_empenhado_projeto, + total_liquidado_projeto, + total_previsao_atividade, + total_empenhado_atividade, + total_liquidado_atividade, + total_previsao_operacao_especial, + total_empenhado_operacao_especial, + total_liquidado_operacao_especial, + atualizado_em + ) VALUES ( + p_meta_id, + v_total_previsao, + v_total_empenhado, + v_total_liquidado, + v_total_previsao_projeto, + v_total_empenhado_projeto, + v_total_liquidado_projeto, + v_total_previsao_atividade, + v_total_empenhado_atividade, + v_total_liquidado_atividade, + v_total_previsao_operacao, + v_total_empenhado_operacao, + v_total_liquidado_operacao, + NOW() + ) + ON CONFLICT (meta_id) DO UPDATE SET + total_previsao = EXCLUDED.total_previsao, + total_empenhado = EXCLUDED.total_empenhado, + total_liquidado = EXCLUDED.total_liquidado, + total_previsao_projeto = EXCLUDED.total_previsao_projeto, + total_empenhado_projeto = EXCLUDED.total_empenhado_projeto, + total_liquidado_projeto = EXCLUDED.total_liquidado_projeto, + total_previsao_atividade = EXCLUDED.total_previsao_atividade, + total_empenhado_atividade = EXCLUDED.total_empenhado_atividade, + total_liquidado_atividade = EXCLUDED.total_liquidado_atividade, + total_previsao_operacao_especial = EXCLUDED.total_previsao_operacao_especial, + total_empenhado_operacao_especial = EXCLUDED.total_empenhado_operacao_especial, + total_liquidado_operacao_especial = EXCLUDED.total_liquidado_operacao_especial, + atualizado_em = NOW(); +END; +$$ LANGUAGE plpgsql; + +-- Trigger para OrcamentoPlanejado +CREATE OR REPLACE FUNCTION tg_orcamento_planejado_refresh_consolidado() +RETURNS TRIGGER AS $$ +DECLARE + v_meta_id INT; + v_old_meta_id INT; +BEGIN + IF TG_OP = 'DELETE' THEN + -- Busca a meta_id considerando meta, iniciativa e atividade + IF OLD.meta_id IS NOT NULL THEN + v_meta_id := OLD.meta_id; + ELSIF OLD.iniciativa_id IS NOT NULL THEN + SELECT meta_id INTO v_meta_id FROM iniciativa WHERE id = OLD.iniciativa_id; + ELSIF OLD.atividade_id IS NOT NULL THEN + SELECT i.meta_id INTO v_meta_id + FROM atividade a + JOIN iniciativa i ON i.id = a.iniciativa_id + WHERE a.id = OLD.atividade_id; + END IF; + + IF v_meta_id IS NOT NULL THEN + PERFORM f_refresh_meta_orcamento_consolidado(v_meta_id); + END IF; + RETURN OLD; + ELSE + -- Busca a meta_id do registro novo + IF NEW.meta_id IS NOT NULL THEN + v_meta_id := NEW.meta_id; + ELSIF NEW.iniciativa_id IS NOT NULL THEN + SELECT meta_id INTO v_meta_id FROM iniciativa WHERE id = NEW.iniciativa_id; + ELSIF NEW.atividade_id IS NOT NULL THEN + SELECT i.meta_id INTO v_meta_id + FROM atividade a + JOIN iniciativa i ON i.id = a.iniciativa_id + WHERE a.id = NEW.atividade_id; + END IF; + + IF v_meta_id IS NOT NULL THEN + PERFORM f_refresh_meta_orcamento_consolidado(v_meta_id); + END IF; + + -- Se o meta_id/iniciativa_id/atividade_id mudou, atualiza o antigo também + IF TG_OP = 'UPDATE' THEN + IF OLD.meta_id IS NOT NULL THEN + v_old_meta_id := OLD.meta_id; + ELSIF OLD.iniciativa_id IS NOT NULL THEN + SELECT meta_id INTO v_old_meta_id FROM iniciativa WHERE id = OLD.iniciativa_id; + ELSIF OLD.atividade_id IS NOT NULL THEN + SELECT i.meta_id INTO v_old_meta_id + FROM atividade a + JOIN iniciativa i ON i.id = a.iniciativa_id + WHERE a.id = OLD.atividade_id; + END IF; + + IF v_old_meta_id IS NOT NULL AND v_old_meta_id != v_meta_id THEN + PERFORM f_refresh_meta_orcamento_consolidado(v_old_meta_id); + END IF; + END IF; + RETURN NEW; + END IF; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS tg_orcamento_planejado_refresh_consolidado ON orcamento_planejado; +CREATE TRIGGER tg_orcamento_planejado_refresh_consolidado + AFTER INSERT OR UPDATE OR DELETE ON orcamento_planejado + FOR EACH ROW + EXECUTE FUNCTION tg_orcamento_planejado_refresh_consolidado(); + +-- Trigger para OrcamentoRealizado +CREATE OR REPLACE FUNCTION tg_orcamento_realizado_refresh_consolidado() +RETURNS TRIGGER AS $$ +DECLARE + v_meta_id INT; + v_old_meta_id INT; +BEGIN + IF TG_OP = 'DELETE' THEN + -- Busca a meta_id considerando meta, iniciativa e atividade + IF OLD.meta_id IS NOT NULL THEN + v_meta_id := OLD.meta_id; + ELSIF OLD.iniciativa_id IS NOT NULL THEN + SELECT meta_id INTO v_meta_id FROM iniciativa WHERE id = OLD.iniciativa_id; + ELSIF OLD.atividade_id IS NOT NULL THEN + SELECT i.meta_id INTO v_meta_id + FROM atividade a + JOIN iniciativa i ON i.id = a.iniciativa_id + WHERE a.id = OLD.atividade_id; + END IF; + + IF v_meta_id IS NOT NULL THEN + PERFORM f_refresh_meta_orcamento_consolidado(v_meta_id); + END IF; + RETURN OLD; + ELSE + -- Busca a meta_id do registro novo + IF NEW.meta_id IS NOT NULL THEN + v_meta_id := NEW.meta_id; + ELSIF NEW.iniciativa_id IS NOT NULL THEN + SELECT meta_id INTO v_meta_id FROM iniciativa WHERE id = NEW.iniciativa_id; + ELSIF NEW.atividade_id IS NOT NULL THEN + SELECT i.meta_id INTO v_meta_id + FROM atividade a + JOIN iniciativa i ON i.id = a.iniciativa_id + WHERE a.id = NEW.atividade_id; + END IF; + + IF v_meta_id IS NOT NULL THEN + PERFORM f_refresh_meta_orcamento_consolidado(v_meta_id); + END IF; + + -- Se o meta_id/iniciativa_id/atividade_id mudou, atualiza o antigo também + IF TG_OP = 'UPDATE' THEN + IF OLD.meta_id IS NOT NULL THEN + v_old_meta_id := OLD.meta_id; + ELSIF OLD.iniciativa_id IS NOT NULL THEN + SELECT meta_id INTO v_old_meta_id FROM iniciativa WHERE id = OLD.iniciativa_id; + ELSIF OLD.atividade_id IS NOT NULL THEN + SELECT i.meta_id INTO v_old_meta_id + FROM atividade a + JOIN iniciativa i ON i.id = a.iniciativa_id + WHERE a.id = OLD.atividade_id; + END IF; + + IF v_old_meta_id IS NOT NULL AND v_old_meta_id != v_meta_id THEN + PERFORM f_refresh_meta_orcamento_consolidado(v_old_meta_id); + END IF; + END IF; + RETURN NEW; + END IF; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS tg_orcamento_realizado_refresh_consolidado ON orcamento_realizado; +CREATE TRIGGER tg_orcamento_realizado_refresh_consolidado + AFTER INSERT OR UPDATE OR DELETE ON orcamento_realizado + FOR EACH ROW + EXECUTE FUNCTION tg_orcamento_realizado_refresh_consolidado(); + +-- Popular dados existentes +INSERT INTO meta_orcamento_consolidado (meta_id) +SELECT DISTINCT id FROM meta WHERE removido_em IS NULL +ON CONFLICT (meta_id) DO NOTHING; + +-- Recalcular todos os consolidados existentes +DO $$ +DECLARE + rec RECORD; +BEGIN + FOR rec IN SELECT id FROM meta WHERE removido_em IS NULL LOOP + PERFORM f_refresh_meta_orcamento_consolidado(rec.id); + END LOOP; +END $$; \ No newline at end of file diff --git a/backend/prisma/migrations/20260226121720_meta_orcamento_consolidado/migration.sql b/backend/prisma/migrations/20260226121720_meta_orcamento_consolidado/migration.sql new file mode 100644 index 0000000000..f25ee4a526 --- /dev/null +++ b/backend/prisma/migrations/20260226121720_meta_orcamento_consolidado/migration.sql @@ -0,0 +1,33 @@ + +-- CreateTable +CREATE TABLE "meta_orcamento_consolidado" ( + "meta_id" INTEGER NOT NULL PRIMARY KEY, + + -- Totais gerais + "total_previsao" DECIMAL(19,2) NOT NULL DEFAULT 0, + "total_empenhado" DECIMAL(19,2) NOT NULL DEFAULT 0, + "total_liquidado" DECIMAL(19,2) NOT NULL DEFAULT 0, + + -- Totais de Projetos (primeiro dígito ímpar) + "total_previsao_projeto" DECIMAL(19,2) NOT NULL DEFAULT 0, + "total_empenhado_projeto" DECIMAL(19,2) NOT NULL DEFAULT 0, + "total_liquidado_projeto" DECIMAL(19,2) NOT NULL DEFAULT 0, + + -- Totais de Atividades (primeiro dígito par e != 0) + "total_previsao_atividade" DECIMAL(19,2) NOT NULL DEFAULT 0, + "total_empenhado_atividade" DECIMAL(19,2) NOT NULL DEFAULT 0, + "total_liquidado_atividade" DECIMAL(19,2) NOT NULL DEFAULT 0, + + -- Totais de Operações Especiais (primeiro dígito = 0) + "total_previsao_operacao_especial" DECIMAL(19,2) NOT NULL DEFAULT 0, + "total_empenhado_operacao_especial" DECIMAL(19,2) NOT NULL DEFAULT 0, + "total_liquidado_operacao_especial" DECIMAL(19,2) NOT NULL DEFAULT 0, + + "atualizado_em" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "meta_orcamento_consolidado_meta_id_fkey" + FOREIGN KEY ("meta_id") REFERENCES "meta"("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateIndex +CREATE INDEX "meta_orcamento_consolidado_atualizado_em_idx" ON "meta_orcamento_consolidado"("atualizado_em"); diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 7886a69728..e40a3e1ff6 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -1440,11 +1440,42 @@ model Meta { view_ps_dashboard_meta_stats view_ps_dashboard_meta_stats[] ViewPsDashboardConsolidado view_ps_dashboard_consolidado[] vinculosDistribuicaoRecursos DistribuicaoRecursoVinculo[] + MetaOrcamentoConsolidado MetaOrcamentoConsolidado? @@index([pdm_id]) @@map("meta") } +model MetaOrcamentoConsolidado { + meta_id Int @id + meta Meta @relation(fields: [meta_id], references: [id], onDelete: Cascade) + + // Totais gerais + total_previsao Decimal @default(0) @db.Decimal(19, 2) + total_empenhado Decimal @default(0) @db.Decimal(19, 2) + total_liquidado Decimal @default(0) @db.Decimal(19, 2) + + // Totais de Projetos (primeiro dígito ímpar) + total_previsao_projeto Decimal @default(0) @db.Decimal(19, 2) + total_empenhado_projeto Decimal @default(0) @db.Decimal(19, 2) + total_liquidado_projeto Decimal @default(0) @db.Decimal(19, 2) + + // Totais de Atividades (primeiro dígito par e != 0) + total_previsao_atividade Decimal @default(0) @db.Decimal(19, 2) + total_empenhado_atividade Decimal @default(0) @db.Decimal(19, 2) + total_liquidado_atividade Decimal @default(0) @db.Decimal(19, 2) + + // Totais de Operações Especiais (primeiro dígito = 0) + total_previsao_operacao_especial Decimal @default(0) @db.Decimal(19, 2) + total_empenhado_operacao_especial Decimal @default(0) @db.Decimal(19, 2) + total_liquidado_operacao_especial Decimal @default(0) @db.Decimal(19, 2) + + atualizado_em DateTime @default(now()) @db.Timestamptz(6) + + @@index([atualizado_em]) + @@map("meta_orcamento_consolidado") +} + model MetaTag { id Int @id @default(autoincrement()) meta_id Int diff --git a/backend/src/meta/entities/meta.entity.ts b/backend/src/meta/entities/meta.entity.ts index d6fa8db0ed..5ff00d61e3 100644 --- a/backend/src/meta/entities/meta.entity.ts +++ b/backend/src/meta/entities/meta.entity.ts @@ -33,6 +33,30 @@ export class MetaIniAtvTag { download_token: string | null; } +export class MetaOrcamentoConsolidado { + // Totais gerais + total_previsao: string; + total_empenhado: string; + total_liquidado: string; + + // Totais de Projetos (primeiro dígito ímpar do projeto_atividade) + total_previsao_projeto: string; + total_empenhado_projeto: string; + total_liquidado_projeto: string; + + // Totais de Atividades (primeiro dígito par e != 0) + total_previsao_atividade: string; + total_empenhado_atividade: string; + total_liquidado_atividade: string; + + // Totais de Operações Especiais (primeiro dígito = 0) + total_previsao_operacao_especial: string; + total_empenhado_operacao_especial: string; + total_liquidado_operacao_especial: string; + + atualizado_em: Date; +} + export class MetaItemDto extends ResumoDetalheOrigensDto { id: number; status: string; @@ -50,6 +74,7 @@ export class MetaItemDto extends ResumoDetalheOrigensDto { tags: MetaIniAtvTag[]; cronograma: CronogramaAtrasoGrau | null; geolocalizacao: GeolocalizacaoDto[]; + orcamento: MetaOrcamentoConsolidado | null; pode_editar: boolean; ps_tecnico_cp: CreatePSEquipeTecnicoCPDto; ps_ponto_focal: CreatePSEquipePontoFocalDto; diff --git a/backend/src/meta/meta.service.ts b/backend/src/meta/meta.service.ts index 8302f4e5ae..1456eaba79 100644 --- a/backend/src/meta/meta.service.ts +++ b/backend/src/meta/meta.service.ts @@ -627,6 +627,26 @@ export class MetaService { }, } : undefined, + MetaOrcamentoConsolidado: + filters?.id !== undefined + ? { + select: { + total_previsao: true, + total_empenhado: true, + total_liquidado: true, + total_previsao_projeto: true, + total_empenhado_projeto: true, + total_liquidado_projeto: true, + total_previsao_atividade: true, + total_empenhado_atividade: true, + total_liquidado_atividade: true, + total_previsao_operacao_especial: true, + total_empenhado_operacao_especial: true, + total_liquidado_operacao_especial: true, + atualizado_em: true, + }, + } + : undefined, origem_cache: true, }, }); @@ -773,6 +793,27 @@ export class MetaService { resumoOrigem = await CompromissoOrigemHelper.buscaOrigensComDetalhes('meta', dbMeta.id, this.prisma); } + const orcamento = dbMeta.MetaOrcamentoConsolidado + ? { + total_previsao: dbMeta.MetaOrcamentoConsolidado.total_previsao.toString(), + total_empenhado: dbMeta.MetaOrcamentoConsolidado.total_empenhado.toString(), + total_liquidado: dbMeta.MetaOrcamentoConsolidado.total_liquidado.toString(), + total_previsao_projeto: dbMeta.MetaOrcamentoConsolidado.total_previsao_projeto.toString(), + total_empenhado_projeto: dbMeta.MetaOrcamentoConsolidado.total_empenhado_projeto.toString(), + total_liquidado_projeto: dbMeta.MetaOrcamentoConsolidado.total_liquidado_projeto.toString(), + total_previsao_atividade: dbMeta.MetaOrcamentoConsolidado.total_previsao_atividade.toString(), + total_empenhado_atividade: dbMeta.MetaOrcamentoConsolidado.total_empenhado_atividade.toString(), + total_liquidado_atividade: dbMeta.MetaOrcamentoConsolidado.total_liquidado_atividade.toString(), + total_previsao_operacao_especial: + dbMeta.MetaOrcamentoConsolidado.total_previsao_operacao_especial.toString(), + total_empenhado_operacao_especial: + dbMeta.MetaOrcamentoConsolidado.total_empenhado_operacao_especial.toString(), + total_liquidado_operacao_especial: + dbMeta.MetaOrcamentoConsolidado.total_liquidado_operacao_especial.toString(), + atualizado_em: dbMeta.MetaOrcamentoConsolidado.atualizado_em, + } + : null; + ret.push({ origens_extra: resumoOrigem, id: dbMeta.id, @@ -791,6 +832,7 @@ export class MetaService { tags: tags, cronograma: metaCronograma, geolocalizacao: 'get' in geolocalizacao ? geolocalizacao.get(dbMeta.id) || [] : [], + orcamento: orcamento, pode_editar: podeEditar, // TODO (lembrar, ps_tecnico_cp: { equipes: dbMeta.PdmPerfil.filter((r) => r.tipo == 'CP').map((r) => r.equipe_id), From 7cd04424a8dabe6eb664d2219debebb6a254ebd5 Mon Sep 17 00:00:00 2001 From: Lucas Ansei Date: Thu, 26 Feb 2026 14:43:04 -0300 Subject: [PATCH 2/3] fix: ajuste quando meta_id for null --- .../0056-atualiza-meta-orcamento-consolidado.pgsql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/prisma/manual-copy/0056-atualiza-meta-orcamento-consolidado.pgsql b/backend/prisma/manual-copy/0056-atualiza-meta-orcamento-consolidado.pgsql index da45670e61..6112ded5d9 100644 --- a/backend/prisma/manual-copy/0056-atualiza-meta-orcamento-consolidado.pgsql +++ b/backend/prisma/manual-copy/0056-atualiza-meta-orcamento-consolidado.pgsql @@ -244,7 +244,7 @@ BEGIN WHERE a.id = OLD.atividade_id; END IF; - IF v_old_meta_id IS NOT NULL AND v_old_meta_id != v_meta_id THEN + IF v_old_meta_id IS NOT NULL AND v_old_meta_id IS DISTINCT FROM v_meta_id THEN PERFORM f_refresh_meta_orcamento_consolidado(v_old_meta_id); END IF; END IF; @@ -313,7 +313,7 @@ BEGIN WHERE a.id = OLD.atividade_id; END IF; - IF v_old_meta_id IS NOT NULL AND v_old_meta_id != v_meta_id THEN + IF v_old_meta_id IS NOT NULL AND v_old_meta_id IS DISTINCT FROM v_meta_id THEN PERFORM f_refresh_meta_orcamento_consolidado(v_old_meta_id); END IF; END IF; From 0db06f3f46a7842717472b3e2a41d5db6d6cf83e Mon Sep 17 00:00:00 2001 From: Renato Cron Date: Thu, 26 Feb 2026 19:16:55 -0300 Subject: [PATCH 3/3] =?UTF-8?q?feat(meta=5Forcamento=5Fconsolidado):=20adi?= =?UTF-8?q?ciona=20processamento=20ass=C3=ADncrono=20com=20fila=20e=20dedu?= =?UTF-8?q?plica=C3=A7=C3=A3o=20para=20rec=C3=A1lculo=20Inclui=20procedure?= =?UTF-8?q?=20e=20fun=C3=A7=C3=A3o=20PL/pgSQL=20para=20enfileirar=20tarefa?= =?UTF-8?q?s=20de=20rec=C3=A1lculo=20da=20meta=20orcamento=20consolidado?= =?UTF-8?q?=20com=20deduplica=C3=A7=C3=A3o=20por=20txid,=20atualiza=20trig?= =?UTF-8?q?gers=20para=20usar=20essa=20fila,=20adiciona=20suporte=20no=20s?= =?UTF-8?q?chema=20Prisma,=20cria=20m=C3=B3dulo,=20servi=C3=A7o=20e=20DTO?= =?UTF-8?q?=20no=20backend=20para=20executar=20a=20tarefa=20de=20forma=20r?= =?UTF-8?q?esiliente=20com=20retries,=20e=20integra=20o=20novo=20servi?= =?UTF-8?q?=C3=A7o=20ao=20sistema=20de=20tasks=20existente.=20Isso=20melho?= =?UTF-8?q?ra=20a=20escalabilidade=20e=20evita=20execu=C3=A7=C3=B5es=20red?= =?UTF-8?q?undantes=20do=20rec=C3=A1lculo,=20garantindo=20maior=20efici?= =?UTF-8?q?=C3=AAncia=20e=20consist=C3=AAncia.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...-atualiza-meta-orcamento-consolidado.pgsql | 46 ++++++++++++--- backend/prisma/schema.prisma | 1 + ...-refresh-meta-orcamento-consolidado.dto.ts | 6 ++ ...fresh-meta-orcamento-consolidado.module.ts | 10 ++++ ...resh-meta-orcamento-consolidado.service.ts | 57 +++++++++++++++++++ backend/src/task/task.module.ts | 2 + backend/src/task/task.parseParams.ts | 4 ++ backend/src/task/task.service.ts | 10 +++- 8 files changed, 127 insertions(+), 9 deletions(-) create mode 100644 backend/src/task/refresh_meta_orcamento_consolidado/dto/create-refresh-meta-orcamento-consolidado.dto.ts create mode 100644 backend/src/task/refresh_meta_orcamento_consolidado/refresh-meta-orcamento-consolidado.module.ts create mode 100644 backend/src/task/refresh_meta_orcamento_consolidado/refresh-meta-orcamento-consolidado.service.ts diff --git a/backend/prisma/manual-copy/0056-atualiza-meta-orcamento-consolidado.pgsql b/backend/prisma/manual-copy/0056-atualiza-meta-orcamento-consolidado.pgsql index 6112ded5d9..671441c876 100644 --- a/backend/prisma/manual-copy/0056-atualiza-meta-orcamento-consolidado.pgsql +++ b/backend/prisma/manual-copy/0056-atualiza-meta-orcamento-consolidado.pgsql @@ -8,9 +8,7 @@ BEGIN -- Split da dotação por ponto partes := string_to_array(dotacao, '.'); - -- Verifica se tem pelo menos 7 campos (índices 0-6) IF array_length(partes, 1) >= 7 THEN - -- Junta os campos 6 e 7 (índices 5 e 6 em array 1-based do PostgreSQL) codigo := partes[6] || partes[7]; RETURN codigo; ELSE @@ -190,6 +188,38 @@ BEGIN END; $$ LANGUAGE plpgsql; +-- Procedure para enfileirar recálculo assíncrono (com deduplicação por txid) +CREATE OR REPLACE PROCEDURE add_refresh_meta_orcamento_consolidado_task(p_meta_id INTEGER) +AS $$ +DECLARE + current_txid bigint; +BEGIN + current_txid := txid_current(); + IF NOT EXISTS ( + SELECT 1 FROM task_queue + WHERE "type" = 'refresh_meta_orcamento_consolidado' + AND status = 'pending' + AND (params->>'meta_id')::INTEGER = p_meta_id + AND (params->>'current_txid')::bigint = current_txid + AND criado_em = now() + ) THEN + INSERT INTO task_queue ("type", params) + VALUES ( + 'refresh_meta_orcamento_consolidado', + json_build_object('meta_id', p_meta_id, 'current_txid', current_txid) + ); + END IF; +END; +$$ LANGUAGE plpgsql; + +-- Wrapper function para uso nos triggers (triggers usam PERFORM em funções, não CALL em procedures) +CREATE OR REPLACE FUNCTION f_add_refresh_meta_orcamento_consolidado_task(p_meta_id INTEGER) +RETURNS VOID AS $$ +BEGIN + CALL add_refresh_meta_orcamento_consolidado_task(p_meta_id); +END; +$$ LANGUAGE plpgsql; + -- Trigger para OrcamentoPlanejado CREATE OR REPLACE FUNCTION tg_orcamento_planejado_refresh_consolidado() RETURNS TRIGGER AS $$ @@ -211,7 +241,7 @@ BEGIN END IF; IF v_meta_id IS NOT NULL THEN - PERFORM f_refresh_meta_orcamento_consolidado(v_meta_id); + PERFORM f_add_refresh_meta_orcamento_consolidado_task(v_meta_id); END IF; RETURN OLD; ELSE @@ -228,7 +258,7 @@ BEGIN END IF; IF v_meta_id IS NOT NULL THEN - PERFORM f_refresh_meta_orcamento_consolidado(v_meta_id); + PERFORM f_add_refresh_meta_orcamento_consolidado_task(v_meta_id); END IF; -- Se o meta_id/iniciativa_id/atividade_id mudou, atualiza o antigo também @@ -245,7 +275,7 @@ BEGIN END IF; IF v_old_meta_id IS NOT NULL AND v_old_meta_id IS DISTINCT FROM v_meta_id THEN - PERFORM f_refresh_meta_orcamento_consolidado(v_old_meta_id); + PERFORM f_add_refresh_meta_orcamento_consolidado_task(v_old_meta_id); END IF; END IF; RETURN NEW; @@ -280,7 +310,7 @@ BEGIN END IF; IF v_meta_id IS NOT NULL THEN - PERFORM f_refresh_meta_orcamento_consolidado(v_meta_id); + PERFORM f_add_refresh_meta_orcamento_consolidado_task(v_meta_id); END IF; RETURN OLD; ELSE @@ -297,7 +327,7 @@ BEGIN END IF; IF v_meta_id IS NOT NULL THEN - PERFORM f_refresh_meta_orcamento_consolidado(v_meta_id); + PERFORM f_add_refresh_meta_orcamento_consolidado_task(v_meta_id); END IF; -- Se o meta_id/iniciativa_id/atividade_id mudou, atualiza o antigo também @@ -314,7 +344,7 @@ BEGIN END IF; IF v_old_meta_id IS NOT NULL AND v_old_meta_id IS DISTINCT FROM v_meta_id THEN - PERFORM f_refresh_meta_orcamento_consolidado(v_old_meta_id); + PERFORM f_add_refresh_meta_orcamento_consolidado_task(v_old_meta_id); END IF; END IF; RETURN NEW; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index e40a3e1ff6..7bc19b71c9 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -5188,6 +5188,7 @@ enum task_type { gerar_preview_documento gerar_thumbnail_imagem refresh_demanda + refresh_meta_orcamento_consolidado } enum task_status { diff --git a/backend/src/task/refresh_meta_orcamento_consolidado/dto/create-refresh-meta-orcamento-consolidado.dto.ts b/backend/src/task/refresh_meta_orcamento_consolidado/dto/create-refresh-meta-orcamento-consolidado.dto.ts new file mode 100644 index 0000000000..638a32b521 --- /dev/null +++ b/backend/src/task/refresh_meta_orcamento_consolidado/dto/create-refresh-meta-orcamento-consolidado.dto.ts @@ -0,0 +1,6 @@ +import { IsInt } from 'class-validator'; + +export class CreateRefreshMetaOrcamentoConsolidadoDto { + @IsInt() + meta_id: number; +} diff --git a/backend/src/task/refresh_meta_orcamento_consolidado/refresh-meta-orcamento-consolidado.module.ts b/backend/src/task/refresh_meta_orcamento_consolidado/refresh-meta-orcamento-consolidado.module.ts new file mode 100644 index 0000000000..d16048da1a --- /dev/null +++ b/backend/src/task/refresh_meta_orcamento_consolidado/refresh-meta-orcamento-consolidado.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { PrismaModule } from '../../prisma/prisma.module'; +import { RefreshMetaOrcamentoConsolidadoService } from './refresh-meta-orcamento-consolidado.service'; + +@Module({ + imports: [PrismaModule], + providers: [RefreshMetaOrcamentoConsolidadoService], + exports: [RefreshMetaOrcamentoConsolidadoService], +}) +export class RefreshMetaOrcamentoConsolidadoModule {} diff --git a/backend/src/task/refresh_meta_orcamento_consolidado/refresh-meta-orcamento-consolidado.service.ts b/backend/src/task/refresh_meta_orcamento_consolidado/refresh-meta-orcamento-consolidado.service.ts new file mode 100644 index 0000000000..b80ac035d9 --- /dev/null +++ b/backend/src/task/refresh_meta_orcamento_consolidado/refresh-meta-orcamento-consolidado.service.ts @@ -0,0 +1,57 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { RetryOperation } from '../../common/RetryOperation'; +import { PrismaService } from '../../prisma/prisma.service'; +import { TaskableService } from '../entities/task.entity'; +import { CreateRefreshMetaOrcamentoConsolidadoDto } from './dto/create-refresh-meta-orcamento-consolidado.dto'; + +@Injectable() +export class RefreshMetaOrcamentoConsolidadoService implements TaskableService { + private readonly logger = new Logger(RefreshMetaOrcamentoConsolidadoService.name); + constructor(private readonly prisma: PrismaService) {} + + async executeJob(inputParams: CreateRefreshMetaOrcamentoConsolidadoDto, taskId: string): Promise { + const before = Date.now(); + + this.logger.verbose(`Refreshing meta orcamento consolidado ${inputParams.meta_id}...`); + + const task = await this.prisma.task_queue.findFirstOrThrow({ where: { id: +taskId } }); + + await this.prisma.$queryRaw`UPDATE task_queue + SET status='completed', output = '{"duplicated": true}' + WHERE type = 'refresh_meta_orcamento_consolidado' + AND status='pending' AND id != ${task.id} + AND (params->>'meta_id')::int = ${inputParams.meta_id}::int + AND criado_em = (select criado_em from task_queue where id = ${task.id}) + `; + + await RetryOperation( + 5, + async () => { + await this.prisma.$transaction(async (tx) => { + await tx.$queryRaw`SELECT f_refresh_meta_orcamento_consolidado(${inputParams.meta_id}::int)`; + + await tx.$queryRaw`DELETE FROM task_queue + WHERE type = 'refresh_meta_orcamento_consolidado' + AND status IN ('pending', 'completed') + AND id != ${task.id} + AND (params->>'meta_id')::int = ${inputParams.meta_id}::int + AND criado_em < (SELECT criado_em FROM task_queue WHERE id = ${task.id})`; + }); + }, + async (error) => { + this.logger.error(`Erro ao recalcular meta orcamento consolidado: ${error}`); + throw error; + } + ); + + const took = Date.now() - before; + return { + success: true, + took, + }; + } + + outputToJson(executeOutput: any, _inputParams: any, _taskId: string): JSON { + return JSON.stringify(executeOutput) as any; + } +} diff --git a/backend/src/task/task.module.ts b/backend/src/task/task.module.ts index 55b8a6ad6b..7e9f7cdced 100644 --- a/backend/src/task/task.module.ts +++ b/backend/src/task/task.module.ts @@ -8,6 +8,7 @@ import { AeNotaModule } from './aviso_email_nota/ae_nota.module'; import { EchoModule } from './echo/echo.module'; import { ImportacaoParlamentarModule } from './importacao_parlamentar/parlamentar.module'; import { RefreshDemandaModule } from './refresh_demanda/refresh-demanda.module'; +import { RefreshMetaOrcamentoConsolidadoModule } from './refresh_meta_orcamento_consolidado/refresh-meta-orcamento-consolidado.module'; import { RefreshIndicadorModule } from './refresh_indicador/refresh-indicador.module'; import { RefreshMetaModule } from './refresh_meta/refresh-meta.module'; import { RefreshMvModule } from './refresh_mv/refresh-mv.module'; @@ -35,6 +36,7 @@ import { TaskService } from './task.service'; forwardRef(() => RunReportModule), forwardRef(() => RefreshVariavelModule), forwardRef(() => RefreshDemandaModule), + forwardRef(() => RefreshMetaOrcamentoConsolidadoModule), forwardRef(() => RunUpdateModule), ], controllers: [TaskController], diff --git a/backend/src/task/task.parseParams.ts b/backend/src/task/task.parseParams.ts index 6d01421729..cf7466d141 100644 --- a/backend/src/task/task.parseParams.ts +++ b/backend/src/task/task.parseParams.ts @@ -9,6 +9,7 @@ import { CreateImportacaoParlamentarDto } from './importacao_parlamentar/dto/cre import { CreateRefreshDemandaDto } from '../sysadmin/dto/demanda/create-refresh-demanda.dto'; import { CreateRefreshIndicadorDto } from './refresh_indicador/dto/create-refresh-indicador.dto'; import { CreateRefreshMetaDto } from './refresh_meta/dto/create-refresh-mv.dto'; +import { CreateRefreshMetaOrcamentoConsolidadoDto } from './refresh_meta_orcamento_consolidado/dto/create-refresh-meta-orcamento-consolidado.dto'; import { CreateRefreshMvDto } from './refresh_mv/dto/create-refresh-mv.dto'; import { CreateRefreshTransferenciaDto } from './refresh_transferencia/dto/create-refresh-transferencia.dto'; import { CreateRefreshVariavelDto } from './refresh_variavel/dto/create-refresh-variavel.dto'; @@ -69,6 +70,9 @@ export function ParseParams(taskType: task_type, value: any): any { case 'refresh_demanda': theClass = CreateRefreshDemandaDto; break; + case 'refresh_meta_orcamento_consolidado': + theClass = CreateRefreshMetaOrcamentoConsolidadoDto; + break; default: taskType satisfies never; } diff --git a/backend/src/task/task.service.ts b/backend/src/task/task.service.ts index ee9db31001..c747b8bc32 100644 --- a/backend/src/task/task.service.ts +++ b/backend/src/task/task.service.ts @@ -22,6 +22,7 @@ import { EchoService } from './echo/echo.service'; import { TaskSingleDto, TaskableService } from './entities/task.entity'; import { ImportacaoParlamentarService } from './importacao_parlamentar/parlamentar.service'; import { RefreshDemandaService } from './refresh_demanda/refresh-demanda.service'; +import { RefreshMetaOrcamentoConsolidadoService } from './refresh_meta_orcamento_consolidado/refresh-meta-orcamento-consolidado.service'; import { RefreshIndicadorService } from './refresh_indicador/refresh-indicador.service'; import { RefreshMetaService } from './refresh_meta/refresh-meta.service'; import { RefreshMvService } from './refresh_mv/refresh-mv.service'; @@ -210,7 +211,10 @@ export class TaskService { private readonly thumbnailService: ThumbnailService, // @Inject(forwardRef(() => RefreshDemandaService)) - private readonly refreshDemandaService: RefreshDemandaService + private readonly refreshDemandaService: RefreshDemandaService, + // + @Inject(forwardRef(() => RefreshMetaOrcamentoConsolidadoService)) + private readonly refreshMetaOrcamentoConsolidadoService: RefreshMetaOrcamentoConsolidadoService ) { this.enabled = IsCrontabEnabled('task'); this.logger.debug(`task crontab enabled? ${this.enabled}`); @@ -698,6 +702,7 @@ export class TaskService { refresh_transferencia: true, // tbm só chama função no banco refresh_variavel: true, // tbm só chama função no banco refresh_demanda: true, // tbm só chama função no banco + refresh_meta_orcamento_consolidado: true, // tbm só chama função no banco aviso_email: true, // tbm só chama função no banco aviso_email_cronograma_tp: true, // tbm só chama função no banco aviso_email_nota: true, // tbm só chama função no banco @@ -785,6 +790,9 @@ export class TaskService { case 'refresh_demanda': service = this.refreshDemandaService; break; + case 'refresh_meta_orcamento_consolidado': + service = this.refreshMetaOrcamentoConsolidadoService; + break; default: task_type satisfies never; }