From 65634edcdf85deaef4c6cbc530d46e67f0306d8b Mon Sep 17 00:00:00 2001 From: Diego Freitas Date: Sat, 8 Aug 2026 14:36:33 -0300 Subject: [PATCH] =?UTF-8?q?ajuste=20na=20regra=20de=20cria=C3=A7=C3=A3o=20?= =?UTF-8?q?do=20movimento=20estorno=20e=20quita=C3=A7=C3=A3o=20de=20cr?= =?UTF-8?q?=C3=A9dito=20considerando=20estorno?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../quitacoesCredito.controller.js | 10 ++- .../services/quitacoesCredito.service.js | 65 +++++++++++++++---- .../movimentos/components/MovimentoForm.tsx | 33 +++++++--- .../pages/QuitacoesCreditoPage.tsx | 39 +++++++++-- .../types/quitacaoCreditoTypes.ts | 4 +- 5 files changed, 120 insertions(+), 31 deletions(-) diff --git a/financeiro-api/src/modules/quitacoesCredito/controllers/quitacoesCredito.controller.js b/financeiro-api/src/modules/quitacoesCredito/controllers/quitacoesCredito.controller.js index d5f42c0..0fbf8fa 100644 --- a/financeiro-api/src/modules/quitacoesCredito/controllers/quitacoesCredito.controller.js +++ b/financeiro-api/src/modules/quitacoesCredito/controllers/quitacoesCredito.controller.js @@ -147,7 +147,13 @@ async function criar(req, res) { return res.status(201).json({ ok: true, message: 'Crédito quitado com sucesso.', - data: resultado.quitacao, + data: { + ...(resultado.quitacao || {}), + contasQuitadas: resultado.contasQuitadas, + valorQuitado: resultado.valorQuitado, + impactoSaldo: resultado.impactoSaldo || [], + }, + // Mantidos no topo por compatibilidade com consumidores antigos. contasQuitadas: resultado.contasQuitadas, valorQuitado: resultado.valorQuitado, impactoSaldo: resultado.impactoSaldo || [], @@ -238,4 +244,4 @@ module.exports = { criar, listarHistorico, buscarPorId, -}; +}; \ No newline at end of file diff --git a/financeiro-api/src/modules/quitacoesCredito/services/quitacoesCredito.service.js b/financeiro-api/src/modules/quitacoesCredito/services/quitacoesCredito.service.js index be3359c..4507dae 100644 --- a/financeiro-api/src/modules/quitacoesCredito/services/quitacoesCredito.service.js +++ b/financeiro-api/src/modules/quitacoesCredito/services/quitacoesCredito.service.js @@ -112,6 +112,19 @@ function idsUnicos(ids = []) { )]; } +function contaEhSaidaAberta(conta) { + return conta?.movimento === 'Saida' && conta?.status === 'A pagar'; +} + +function contaEhEstornoRecebido(conta) { + return (conta?.movimento === 'Entrada' || conta?.movimento === 'Estorno') && conta?.status === 'Recebido'; +} + +function valorContaNaQuitacao(conta) { + const valor = Number(conta?.valor || 0); + return contaEhEstornoRecebido(conta) ? -valor : valor; +} + async function buscarBancoParaUpdate(connection, idbancos, idUsuario) { const [rows] = await connection.query( ` @@ -205,10 +218,18 @@ async function listarBancosPagamento(idUsuario) { function montarWhereContasPreview(filtros = {}) { const where = [ 'cp.deleted_at IS NULL', - "cp.movimento = 'Saida'", - "cp.status = 'A pagar'", + `( + (cp.movimento = 'Saida' AND cp.status = 'A pagar') + OR + (cp.movimento IN ('Entrada', 'Estorno') AND cp.status = 'Recebido') + )`, 'cp.idbancos = ?', 'b.idusuarios = ?', + `NOT EXISTS ( + SELECT 1 + FROM contasquitadasref cqr + WHERE cqr.idcontasapagar = cp.idcontasapagar + )`, ]; const params = [ @@ -305,6 +326,11 @@ async function previewQuitacao(idUsuario, filtros = {}) { [...params, limite, offset] ); + const contas = rows.map((conta) => ({ + ...conta, + valor_quitacao: arredondarMoeda(valorContaNaQuitacao(conta)), + })); + const [countRows] = await pool.query( ` SELECT COUNT(*) AS total @@ -318,7 +344,12 @@ async function previewQuitacao(idUsuario, filtros = {}) { ` SELECT COUNT(*) AS quantidade, - COALESCE(SUM(cp.valor), 0) AS valorTotal, + COALESCE(SUM( + CASE + WHEN cp.movimento IN ('Entrada', 'Estorno') AND cp.status = 'Recebido' THEN -cp.valor + ELSE cp.valor + END + ), 0) AS valorTotal, MIN(cp.datavencimento) AS primeiroVencimento, MAX(cp.datavencimento) AS ultimoVencimento ${FROM_CONTA_JOIN} @@ -333,7 +364,7 @@ async function previewQuitacao(idUsuario, filtros = {}) { return { bancoCredito, - contas: rows, + contas, pagination: { total, limite, @@ -343,8 +374,11 @@ async function previewQuitacao(idUsuario, filtros = {}) { }, summary: { quantidade: Number(resumo.quantidade || 0), + totalAberto: arredondarMoeda(resumo.valorTotal || 0), valorTotal: arredondarMoeda(resumo.valorTotal || 0), + saldoAtualCredito: arredondarMoeda(bancoCredito.saldo || 0), saldoCreditoAtual: arredondarMoeda(bancoCredito.saldo || 0), + saldoAposQuitacaoTotal: arredondarMoeda(Number(bancoCredito.saldo || 0) - Number(resumo.valorTotal || 0)), saldoCreditoAposQuitacaoTotal: arredondarMoeda(Number(bancoCredito.saldo || 0) - Number(resumo.valorTotal || 0)), primeiroVencimento: resumo.primeiroVencimento || null, ultimoVencimento: resumo.ultimoVencimento || null, @@ -366,6 +400,11 @@ async function buscarContasSelecionadasParaUpdate(connection, ids, idBancoCredit WHERE cp.idcontasapagar IN (${placeholders}) AND cp.idbancos = ? AND b.idusuarios = ? + AND NOT EXISTS ( + SELECT 1 + FROM contasquitadasref cqr + WHERE cqr.idcontasapagar = cp.idcontasapagar + ) FOR UPDATE `, [ @@ -475,16 +514,16 @@ async function criarQuitacaoCredito(idUsuario, dados = {}) { throw criarErro(`A conta ${conta.idcontasapagar} não pertence ao banco de crédito selecionado.`); } - if (conta.movimento !== 'Saida') { - throw criarErro(`A conta ${conta.idcontasapagar} não é uma saída de crédito.`); - } - - if (conta.status !== 'A pagar') { - throw criarErro(`A conta ${conta.idcontasapagar} não está em aberto para pagamento.`); + if (!contaEhSaidaAberta(conta) && !contaEhEstornoRecebido(conta)) { + throw criarErro( + `A conta ${conta.idcontasapagar} não é uma saída em aberto nem um estorno recebido.` + ); } } - const valorTotal = arredondarMoeda(contasAntes.reduce((total, conta) => total + Number(conta.valor || 0), 0)); + const valorTotal = arredondarMoeda( + contasAntes.reduce((total, conta) => total + valorContaNaQuitacao(conta), 0) + ); if (valorTotal <= 0) { throw criarErro('O valor total da quitação deve ser maior que zero.'); @@ -555,6 +594,8 @@ async function criarQuitacaoCredito(idUsuario, dados = {}) { idbancos_p = ?, update_date = NOW() WHERE idcontasapagar IN (${placeholders}) + AND movimento = 'Saida' + AND status = 'A pagar' `, [ dataQuitacao, @@ -837,4 +878,4 @@ module.exports = { criarQuitacaoCredito, listarHistorico, buscarQuitacaoPorId, -}; +}; \ No newline at end of file diff --git a/financeiro-web/src/features/movimentos/components/MovimentoForm.tsx b/financeiro-web/src/features/movimentos/components/MovimentoForm.tsx index 8d8678e..d874379 100644 --- a/financeiro-web/src/features/movimentos/components/MovimentoForm.tsx +++ b/financeiro-web/src/features/movimentos/components/MovimentoForm.tsx @@ -59,7 +59,12 @@ type FormSectionProps = { }; function hojeISO() { - return new Date().toISOString().slice(0, 10); + const hoje = new Date(); + const ano = hoje.getFullYear(); + const mes = String(hoje.getMonth() + 1).padStart(2, '0'); + const dia = String(hoje.getDate()).padStart(2, '0'); + + return `${ano}-${mes}-${dia}`; } function calcularVencimentoCreditoProximoMes(diaVencimento: number, dataBaseISO?: string) { @@ -79,13 +84,11 @@ function calcularVencimentoCreditoProximoMes(diaVencimento: number, dataBaseISO? const diaSeguro = Math.min(Number(diaVencimento), ultimoDiaProximoMes); - return new Date( - proximoMes.getFullYear(), - proximoMes.getMonth(), - diaSeguro - ) - .toISOString() - .slice(0, 10); + const anoVencimento = proximoMes.getFullYear(); + const mesVencimento = String(proximoMes.getMonth() + 1).padStart(2, '0'); + const diaVencimentoFormatado = String(diaSeguro).padStart(2, '0'); + + return `${anoVencimento}-${mesVencimento}-${diaVencimentoFormatado}`; } function toNumberOrNull(value: string): number | null { @@ -287,6 +290,10 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) { return ['Recebido', 'A receber']; } + if (movimento === 'Estorno') { + return ['Recebido']; + } + if (movimento === 'Saida' || movimento === 'Sangria') { return ['Pago', 'A pagar']; } @@ -393,7 +400,7 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) { useEffect(() => { if (isEdit) return; - if (movimento !== 'Saida') return; + if (movimento !== 'Saida' && movimento !== 'Estorno') return; if (!bancoSelecionado) return; if (Number(bancoSelecionado.debito) !== 0) return; if (!bancoSelecionado.dia_vencimento) return; @@ -405,9 +412,15 @@ export function MovimentoForm({ mode, initialData }: MovimentoFormProps) { setDataVencimento(novaDataVencimento); - if (status === 'Pago') { + if (movimento === 'Saida' && status === 'Pago') { setStatus('A pagar'); setDataBaixa(''); + return; + } + + if (movimento === 'Estorno') { + setStatus('Recebido'); + setDataBaixa(dataEntrada || hojeISO()); } }, [ isEdit, diff --git a/financeiro-web/src/features/quitacoesCredito/pages/QuitacoesCreditoPage.tsx b/financeiro-web/src/features/quitacoesCredito/pages/QuitacoesCreditoPage.tsx index 90f3b62..90149b0 100644 --- a/financeiro-web/src/features/quitacoesCredito/pages/QuitacoesCreditoPage.tsx +++ b/financeiro-web/src/features/quitacoesCredito/pages/QuitacoesCreditoPage.tsx @@ -102,6 +102,23 @@ function impactoColor(valorDelta: number) { return valorDelta >= 0 ? 'success.main' : 'error.main'; } +function valorContaQuitacao(conta: ContaCreditoQuitacao) { + const ehEstornoRecebido = + (conta.movimento === 'Entrada' || conta.movimento === 'Estorno') && + conta.status === 'Recebido'; + + return Number( + conta.valor_quitacao ?? + (ehEstornoRecebido + ? -Number(conta.valor || 0) + : Number(conta.valor || 0)) + ); +} + +function contaEhEstorno(conta: ContaCreditoQuitacao) { + return valorContaQuitacao(conta) < 0; +} + export function QuitacoesCreditoPage() { const [bancosCredito, setBancosCredito] = useState([]); const [bancosPagamento, setBancosPagamento] = useState([]); @@ -142,7 +159,7 @@ export function QuitacoesCreditoPage() { }, [contas, idsSelecionados]); const totalSelecionado = useMemo(() => { - return contasSelecionadas.reduce((total, conta) => total + Number(conta.valor || 0), 0); + return contasSelecionadas.reduce((total, conta) => total + valorContaQuitacao(conta), 0); }, [contasSelecionadas]); const todasSelecionadas = contas.length > 0 && idsSelecionados.length === contas.length; @@ -627,8 +644,12 @@ export function QuitacoesCreditoPage() { sx={{ margin: 0 }} /> - - {formatarValor(conta.valor)} + + {formatarValor(valorContaQuitacao(conta))} @@ -639,6 +660,9 @@ export function QuitacoesCreditoPage() { variant="outlined" color={statusChipColor(conta.status) as any} /> + {contaEhEstorno(conta) && ( + + )} {formatarData(conta.datavencimento)} - - {formatarValor(conta.valor)} + + {formatarValor(valorContaQuitacao(conta))} @@ -967,4 +994,4 @@ export function QuitacoesCreditoPage() { ); -} +} \ No newline at end of file diff --git a/financeiro-web/src/features/quitacoesCredito/types/quitacaoCreditoTypes.ts b/financeiro-web/src/features/quitacoesCredito/types/quitacaoCreditoTypes.ts index 579d564..a6a8ff5 100644 --- a/financeiro-web/src/features/quitacoesCredito/types/quitacaoCreditoTypes.ts +++ b/financeiro-web/src/features/quitacoesCredito/types/quitacaoCreditoTypes.ts @@ -15,6 +15,8 @@ export type ContaCreditoQuitacao = { datavencimento: string | null; databaixa: string | null; valor: number; + /** Valor com sinal usado na quitação: saídas positivas e estornos negativos. */ + valor_quitacao?: number; parcela: number; parcelas: number; status: string; @@ -107,4 +109,4 @@ export type ApiResponse = { ok: boolean; message?: string; data: T; -}; +}; \ No newline at end of file