From 66287cff29360298249a263c1b35db316666da70 Mon Sep 17 00:00:00 2001 From: Diego Freitas Date: Tue, 2 Jun 2026 10:05:23 -0300 Subject: [PATCH] =?UTF-8?q?Adicionado=20gera=C3=A7=C3=A3o=20de=20movimento?= =?UTF-8?q?s=20fixos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/movimentos.controller.js | 12 +- .../movimentosFixosGeracao.service.js | 441 ++++++++++++++++++ 2 files changed, 452 insertions(+), 1 deletion(-) create mode 100644 financeiro-api/src/modules/movimentosFixos/services/movimentosFixosGeracao.service.js diff --git a/financeiro-api/src/modules/movimentos/controllers/movimentos.controller.js b/financeiro-api/src/modules/movimentos/controllers/movimentos.controller.js index 201d305..cade1e6 100644 --- a/financeiro-api/src/modules/movimentos/controllers/movimentos.controller.js +++ b/financeiro-api/src/modules/movimentos/controllers/movimentos.controller.js @@ -1,4 +1,5 @@ const movimentosService = require('../services/movimentos.service'); +const movimentosFixosGeracaoService = require('../../movimentosFixos/services/movimentosFixosGeracao.service'); const MOVIMENTOS_VALIDOS = ['Entrada', 'Saida', 'Sangria', 'Estorno']; const STATUS_VALIDOS = ['Pago', 'A pagar', 'Recebido', 'A receber']; @@ -120,6 +121,14 @@ function tratarErro(error, res, mensagemPadrao) { async function listar(req, res) { try { + const idUsuario = req.user.idusuarios || req.user.id; + + const geracaoFixos = await movimentosFixosGeracaoService.gerarParaIntervalo({ + dataInicio: req.query.dataInicio, + dataFim: req.query.dataFim, + idUsuario, + }); + const resultado = await movimentosService.listarMovimentos({ limite: req.query.limite, offset: req.query.offset, @@ -144,7 +153,7 @@ async function listar(req, res) { orderBy: req.query.orderBy, orderDirection: req.query.orderDirection, - idusuarios: req.user.idusuarios || req.user.id, + idusuarios: idUsuario, }); return res.json({ @@ -152,6 +161,7 @@ async function listar(req, res) { data: resultado.data, pagination: resultado.pagination, summary: resultado.summary, + movimentosFixosGerados: geracaoFixos, }); } catch (error) { console.error('Erro ao listar movimentos:', error); diff --git a/financeiro-api/src/modules/movimentosFixos/services/movimentosFixosGeracao.service.js b/financeiro-api/src/modules/movimentosFixos/services/movimentosFixosGeracao.service.js new file mode 100644 index 0000000..27575c6 --- /dev/null +++ b/financeiro-api/src/modules/movimentosFixos/services/movimentosFixosGeracao.service.js @@ -0,0 +1,441 @@ +const pool = require('../../../database/mysql'); + +function normalizarDataOuNull(valor) { + if (!valor) return null; + + const texto = String(valor).slice(0, 10); + + if (!/^\d{4}-\d{2}-\d{2}$/.test(texto)) { + return null; + } + + return texto; +} + +function hojeDataString() { + return new Date().toISOString().slice(0, 10); +} + +function competenciaAtual() { + return hojeDataString().slice(0, 7); +} + +function primeiroDiaCompetencia(competencia) { + return `${competencia}-01`; +} + +function ultimoDiaDoMes(ano, mes) { + return new Date(ano, mes, 0).getDate(); +} + +function montarDataVencimento(competencia, diaVencimento) { + const [anoStr, mesStr] = competencia.split('-'); + const ano = Number(anoStr); + const mes = Number(mesStr); + + const ultimoDia = ultimoDiaDoMes(ano, mes); + const dia = Math.min(Math.max(Number(diaVencimento || 1), 1), ultimoDia); + + return `${competencia}-${String(dia).padStart(2, '0')}`; +} + +function addMesesCompetencia(competencia, meses) { + const [anoStr, mesStr] = competencia.split('-'); + const data = new Date(Number(anoStr), Number(mesStr) - 1, 1); + + data.setMonth(data.getMonth() + meses); + + const ano = data.getFullYear(); + const mes = String(data.getMonth() + 1).padStart(2, '0'); + + return `${ano}-${mes}`; +} + +function listarCompetenciasEntre(dataInicio, dataFim) { + const inicio = normalizarDataOuNull(dataInicio) || hojeDataString(); + const fim = normalizarDataOuNull(dataFim) || inicio; + + let competenciaInicio = inicio.slice(0, 7); + const competenciaFim = fim.slice(0, 7); + const atual = competenciaAtual(); + + // Regra anti-retroativo: + // se o filtro começa no passado, começa no mês atual. + if (competenciaInicio < atual) { + competenciaInicio = atual; + } + + if (competenciaInicio > competenciaFim) { + return []; + } + + const competencias = []; + let cursor = competenciaInicio; + + while (cursor <= competenciaFim) { + competencias.push(cursor); + cursor = addMesesCompetencia(cursor, 1); + } + + return competencias; +} + +function statusInicialPorMovimento(movimento) { + if (movimento === 'Entrada' || movimento === 'Estorno') { + return 'A receber'; + } + + if (movimento === 'Saida') { + return 'A pagar'; + } + + if (movimento === 'Sangria') { + return 'Pago'; + } + + return 'A pagar'; +} + +function dataBaixaInicialPorMovimento(movimento, dataVencimento) { + if (movimento === 'Sangria') { + return dataVencimento; + } + + return null; +} + +async function buscarFixosDoUsuario(connection, idUsuario) { + const [rows] = await connection.query( + ` + SELECT + mf.idmovimentosfixos, + mf.movimento, + mf.descricao, + mf.valor, + mf.dia_vencimento, + mf.idcentrodecustos, + mf.idbancos + FROM movimentosfixos mf + INNER JOIN bancos b ON b.idbancos = mf.idbancos + WHERE mf.habilitado = 1 + AND mf.gerar_automatico = 1 + AND b.idusuarios = ? + ORDER BY mf.idmovimentosfixos ASC + `, + [idUsuario] + ); + + return rows; +} + +async function jaExisteMovimentoFixo(connection, fixo, competencia) { + const [rowsGeracao] = await connection.query( + ` + SELECT idmovimentosfixosgeracoes, idcontasapagar, status + FROM movimentosfixosgeracoes + WHERE idmovimentosfixos = ? + AND competencia = ? + LIMIT 1 + FOR UPDATE + `, + [fixo.idmovimentosfixos, competencia] + ); + + if (rowsGeracao.length > 0) { + return true; + } + + const [rowsMovimentoVinculado] = await connection.query( + ` + SELECT idcontasapagar + FROM contasapagar + WHERE idmovimentosfixos = ? + AND competencia = ? + AND deleted_at IS NULL + LIMIT 1 + FOR UPDATE + `, + [fixo.idmovimentosfixos, competencia] + ); + + if (rowsMovimentoVinculado.length > 0) { + await connection.query( + ` + INSERT INTO movimentosfixosgeracoes ( + idmovimentosfixos, + competencia, + idcontasapagar, + status, + mensagem, + insert_date, + update_date + ) VALUES (?, ?, ?, 'existente', 'Movimento fixo já existia vinculado em contasapagar.', NOW(), NOW()) + ON DUPLICATE KEY UPDATE + idcontasapagar = VALUES(idcontasapagar), + status = VALUES(status), + mensagem = VALUES(mensagem), + update_date = NOW() + `, + [fixo.idmovimentosfixos, competencia, rowsMovimentoVinculado[0].idcontasapagar] + ); + + return true; + } + + const [rowsMovimentoLegado] = await connection.query( + ` + SELECT idcontasapagar + FROM contasapagar + WHERE deleted_at IS NULL + AND idmovimentosfixos IS NULL + AND competencia IS NULL + AND movimento = ? + AND descricao = ? + AND idbancos = ? + AND ( + (? IS NULL AND idcentrodecustos IS NULL) + OR idcentrodecustos = ? + ) + AND DATE(datavencimento) BETWEEN ? AND ? + ORDER BY idcontasapagar DESC + LIMIT 1 + FOR UPDATE + `, + [ + fixo.movimento, + fixo.descricao, + fixo.idbancos, + fixo.idcentrodecustos || null, + fixo.idcentrodecustos || null, + `${competencia}-01`, + `${competencia}-31`, + ] + ); + + if (rowsMovimentoLegado.length > 0) { + const idContasPagar = rowsMovimentoLegado[0].idcontasapagar; + + await connection.query( + ` + UPDATE contasapagar + SET + idmovimentosfixos = ?, + competencia = ?, + origem = COALESCE(origem, 'movimento_fixo_legado'), + update_date = NOW() + WHERE idcontasapagar = ? + `, + [fixo.idmovimentosfixos, competencia, idContasPagar] + ); + + await connection.query( + ` + INSERT INTO movimentosfixosgeracoes ( + idmovimentosfixos, + competencia, + idcontasapagar, + status, + mensagem, + insert_date, + update_date + ) VALUES (?, ?, ?, 'existente', 'Movimento legado vinculado automaticamente ao fixo.', NOW(), NOW()) + ON DUPLICATE KEY UPDATE + idcontasapagar = VALUES(idcontasapagar), + status = VALUES(status), + mensagem = VALUES(mensagem), + update_date = NOW() + `, + [fixo.idmovimentosfixos, competencia, idContasPagar] + ); + + return true; + } + + return false; +} + +async function inserirMovimentoFixoGerado(connection, fixo, competencia, idUsuario) { + const dataEntrada = primeiroDiaCompetencia(competencia); + const dataVencimento = montarDataVencimento(competencia, fixo.dia_vencimento); + const status = statusInicialPorMovimento(fixo.movimento); + const dataBaixa = dataBaixaInicialPorMovimento(fixo.movimento, dataVencimento); + + const [result] = await connection.query( + ` + INSERT INTO contasapagar ( + movimento, + descricao, + dataentrada, + datavencimento, + databaixa, + valor, + parcela, + parcelas, + status, + idcentrodecustos, + idbancos, + idusuarios_cad, + idusuarios_baixa, + idclientes, + idveiculosdetalhes, + idbancos_p, + idmovimentosfixos, + competencia, + grupo_parcelamento, + origem, + saldo_processado, + observacao, + insert_date, + update_date, + deleted_at + ) VALUES (?, ?, ?, ?, ?, ?, 1, 1, ?, ?, ?, ?, ?, NULL, NULL, NULL, ?, ?, NULL, 'movimento_fixo', 0, NULL, NOW(), NOW(), NULL) + `, + [ + fixo.movimento, + fixo.descricao, + dataEntrada, + dataVencimento, + dataBaixa, + Number(fixo.valor || 0), + status, + fixo.idcentrodecustos || null, + fixo.idbancos || null, + idUsuario, + dataBaixa ? idUsuario : null, + fixo.idmovimentosfixos, + competencia, + ] + ); + + const idContasPagar = result.insertId; + + await connection.query( + ` + INSERT INTO movimentosfixos_geracoes ( + idmovimentosfixos, + competencia, + idcontasapagar, + status, + mensagem, + insert_date, + update_date + ) VALUES (?, ?, ?, 'gerado', 'Movimento gerado automaticamente.', NOW(), NOW()) + ON DUPLICATE KEY UPDATE + idcontasapagar = VALUES(idcontasapagar), + status = VALUES(status), + mensagem = VALUES(mensagem), + update_date = NOW() + `, + [fixo.idmovimentosfixos, competencia, idContasPagar] + ); + + await connection.query( + ` + UPDATE movimentosfixos + SET + ultimo_mes_gerado = CASE + WHEN ultimo_mes_gerado IS NULL OR ultimo_mes_gerado < ? THEN ? + ELSE ultimo_mes_gerado + END, + update_date = NOW() + WHERE idmovimentosfixos = ? + `, + [competencia, competencia, fixo.idmovimentosfixos] + ); + + return idContasPagar; +} + +async function gerarParaIntervalo(opcoes = {}) { + const idUsuario = Number(opcoes.idUsuario || opcoes.idusuarios || 0); + + if (!idUsuario) { + return { + gerados: 0, + ignorados: 0, + competencias: [], + detalhes: [], + }; + } + + const competencias = listarCompetenciasEntre(opcoes.dataInicio, opcoes.dataFim); + + if (competencias.length === 0) { + return { + gerados: 0, + ignorados: 0, + competencias: [], + detalhes: [], + }; + } + + const connection = await pool.getConnection(); + + try { + await connection.beginTransaction(); + + const fixos = await buscarFixosDoUsuario(connection, idUsuario); + + let gerados = 0; + let ignorados = 0; + const detalhes = []; + + for (const competencia of competencias) { + for (const fixo of fixos) { + const existe = await jaExisteMovimentoFixo( + connection, + fixo, + competencia + ); + + if (existe) { + ignorados++; + + detalhes.push({ + idmovimentosfixos: fixo.idmovimentosfixos, + competencia, + status: 'ignorado', + mensagem: 'Já existia geração ou movimento vinculado.', + }); + + continue; + } + + const idcontasapagar = await inserirMovimentoFixoGerado( + connection, + fixo, + competencia, + idUsuario + ); + + gerados++; + + detalhes.push({ + idmovimentosfixos: fixo.idmovimentosfixos, + competencia, + idcontasapagar, + status: 'gerado', + }); + } + } + + await connection.commit(); + + return { + gerados, + ignorados, + competencias, + detalhes, + }; + } catch (error) { + await connection.rollback(); + + throw error; + } finally { + connection.release(); + } +} + +module.exports = { + gerarParaIntervalo, +}; \ No newline at end of file