2026-05-30 20:38:53 +00:00
const crypto = require ( 'crypto' ) ;
2026-05-30 01:51:05 +00:00
const pool = require ( '../../../database/mysql' ) ;
2026-05-30 20:38:53 +00:00
const movimentosSaldoService = require ( './movimentosSaldo.service' ) ;
2026-05-30 01:51:05 +00:00
const CAMPOS _MOVIMENTO _SELECT = `
cp . idcontasapagar ,
cp . movimento ,
cp . descricao ,
cp . dataentrada ,
cp . datavencimento ,
cp . databaixa ,
cp . valor ,
cp . parcela ,
cp . parcelas ,
cp . status ,
cp . idcentrodecustos ,
cp . idbancos ,
cp . idusuarios _cad ,
cp . idusuarios _baixa ,
cp . idclientes ,
cp . idveiculosdetalhes ,
cp . idbancos _p ,
2026-05-30 20:38:53 +00:00
cp . idmovimentosfixos ,
cp . competencia ,
cp . grupo _parcelamento ,
cp . origem ,
cp . saldo _processado ,
2026-05-30 01:51:05 +00:00
cp . observacao ,
cp . insert _date ,
cp . update _date ,
2026-05-30 20:38:53 +00:00
cp . deleted _at ,
2026-05-30 01:51:05 +00:00
b . descricao AS banco _descricao ,
2026-05-30 20:38:53 +00:00
b . debito AS banco _debito ,
2026-05-30 01:51:05 +00:00
cc . descricao AS centro _custo _descricao ,
c . nome AS cliente _nome ,
2026-05-30 20:38:53 +00:00
bp . descricao AS banco _referencia _descricao ,
bp . debito AS banco _referencia _debito ,
mf . descricao AS movimento _fixo _descricao
2026-05-30 01:51:05 +00:00
` ;
const FROM _MOVIMENTO _JOIN = `
FROM contasapagar cp
LEFT JOIN bancos b ON b . idbancos = cp . idbancos
LEFT JOIN centrodecustos cc ON cc . idcentrodecustos = cp . idcentrodecustos
LEFT JOIN clientes c ON c . idclientes = cp . idclientes
LEFT JOIN bancos bp ON bp . idbancos = cp . idbancos _p
2026-05-30 20:38:53 +00:00
LEFT JOIN movimentosfixos mf ON mf . idmovimentosfixos = cp . idmovimentosfixos
2026-05-30 01:51:05 +00:00
` ;
function normalizarNumeroOuNull ( valor ) {
if ( valor === undefined || valor === null || valor === '' ) {
return null ;
}
2026-05-30 20:38:53 +00:00
const numero = Number ( valor ) ;
if ( ! Number . isFinite ( numero ) ) {
return null ;
}
return numero ;
2026-05-30 01:51:05 +00:00
}
function normalizarTextoOuNull ( valor ) {
if ( valor === undefined || valor === null || valor === '' ) {
return null ;
}
2026-05-30 20:38:53 +00:00
const texto = String ( valor ) . trim ( ) ;
return texto || null ;
2026-05-30 01:51:05 +00:00
}
function limitarNumero ( valor , padrao , minimo , maximo ) {
const numero = Number ( valor ) ;
if ( ! Number . isFinite ( numero ) ) {
return padrao ;
}
if ( numero < minimo ) {
return minimo ;
}
if ( numero > maximo ) {
return maximo ;
}
return numero ;
}
2026-05-30 20:38:53 +00:00
function hojeDataString ( ) {
return new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) ;
}
function extrairCompetencia ( data ) {
if ( ! data ) return null ;
return String ( data ) . slice ( 0 , 7 ) ;
}
function addMesesDataString ( data , meses ) {
if ( ! data ) return null ;
const partes = String ( data ) . slice ( 0 , 10 ) . split ( '-' ) ;
if ( partes . length !== 3 ) {
return data ;
}
const ano = Number ( partes [ 0 ] ) ;
const mes = Number ( partes [ 1 ] ) ;
const dia = Number ( partes [ 2 ] ) ;
const dataBase = new Date ( ano , mes - 1 , dia ) ;
dataBase . setMonth ( dataBase . getMonth ( ) + meses ) ;
const y = dataBase . getFullYear ( ) ;
const m = String ( dataBase . getMonth ( ) + 1 ) . padStart ( 2 , '0' ) ;
const d = String ( dataBase . getDate ( ) ) . padStart ( 2 , '0' ) ;
return ` ${ y } - ${ m } - ${ d } ` ;
}
function statusBaixado ( status ) {
return status === 'Pago' || status === 'Recebido' ;
}
function normalizarMovimentoParaSalvar ( idUsuario , dados , extras = { } ) {
const status = dados . status || null ;
const baixado = statusBaixado ( status ) ;
return {
movimento : dados . movimento ,
descricao : normalizarTextoOuNull ( dados . descricao ) ,
dataentrada : dados . dataentrada || null ,
datavencimento : dados . datavencimento || null ,
databaixa : baixado ? ( dados . databaixa || hojeDataString ( ) ) : null ,
valor : Number ( dados . valor || 0 ) ,
parcela : Number ( dados . parcela || 1 ) ,
parcelas : Number ( dados . parcelas || 1 ) ,
status ,
idcentrodecustos : normalizarNumeroOuNull ( dados . idcentrodecustos ) ,
idbancos : normalizarNumeroOuNull ( dados . idbancos ) ,
idusuarios _cad : extras . idusuarios _cad || dados . idusuarios _cad || idUsuario || null ,
idusuarios _baixa : baixado
? ( normalizarNumeroOuNull ( dados . idusuarios _baixa ) || idUsuario || null )
: null ,
idclientes : normalizarNumeroOuNull ( dados . idclientes ) ,
idveiculosdetalhes : normalizarNumeroOuNull ( dados . idveiculosdetalhes ) ,
idbancos _p : normalizarNumeroOuNull ( dados . idbancos _p ) ,
idmovimentosfixos : normalizarNumeroOuNull ( dados . idmovimentosfixos ) ,
competencia : normalizarTextoOuNull ( dados . competencia ) || extrairCompetencia ( dados . datavencimento ) ,
grupo _parcelamento : normalizarTextoOuNull ( dados . grupo _parcelamento ) || extras . grupo _parcelamento || null ,
origem : normalizarTextoOuNull ( dados . origem ) || extras . origem || 'manual' ,
saldo _processado : Number ( dados . saldo _processado || 0 ) ,
observacao : normalizarTextoOuNull ( dados . observacao ) ,
} ;
}
2026-05-30 01:51:05 +00:00
function resolverCampoData ( dataCampo ) {
const camposPermitidos = {
dataentrada : 'cp.dataentrada' ,
datavencimento : 'cp.datavencimento' ,
databaixa : 'cp.databaixa' ,
insert _date : 'cp.insert_date' ,
update _date : 'cp.update_date' ,
2026-05-30 20:38:53 +00:00
deleted _at : 'cp.deleted_at' ,
2026-05-30 01:51:05 +00:00
} ;
return camposPermitidos [ dataCampo ] || 'cp.datavencimento' ;
}
function resolverOrdenacao ( orderBy ) {
const camposPermitidos = {
2026-05-30 20:38:53 +00:00
idcontasapagar : 'cp.idcontasapagar' ,
2026-05-30 01:51:05 +00:00
dataentrada : 'cp.dataentrada' ,
datavencimento : 'cp.datavencimento' ,
databaixa : 'cp.databaixa' ,
insert _date : 'cp.insert_date' ,
update _date : 'cp.update_date' ,
valor : 'cp.valor' ,
descricao : 'cp.descricao' ,
status : 'cp.status' ,
movimento : 'cp.movimento' ,
2026-05-30 20:38:53 +00:00
competencia : 'cp.competencia' ,
origem : 'cp.origem' ,
saldo _processado : 'cp.saldo_processado' ,
banco _descricao : 'b.descricao' ,
centro _custo _descricao : 'cc.descricao' ,
cliente _nome : 'c.nome' ,
2026-05-30 01:51:05 +00:00
} ;
return camposPermitidos [ orderBy ] || 'cp.datavencimento' ;
}
function resolverDirecao ( orderDirection ) {
return String ( orderDirection || '' ) . toUpperCase ( ) === 'ASC' ? 'ASC' : 'DESC' ;
}
function montarWhereMovimentos ( filtros = { } ) {
const where = [ ] ;
const params = [ ] ;
2026-05-30 20:38:53 +00:00
if ( String ( filtros . incluirExcluidos || '' ) !== '1' ) {
where . push ( 'cp.deleted_at IS NULL' ) ;
}
2026-06-02 01:30:10 +00:00
if ( filtros . idusuarios ) {
where . push ( 'b.idusuarios = ?' ) ;
params . push ( Number ( filtros . idusuarios ) ) ;
}
2026-05-30 01:51:05 +00:00
if ( filtros . movimento ) {
where . push ( 'cp.movimento = ?' ) ;
params . push ( filtros . movimento ) ;
}
if ( filtros . status ) {
where . push ( 'cp.status = ?' ) ;
params . push ( filtros . status ) ;
}
if ( filtros . idbancos ) {
where . push ( 'cp.idbancos = ?' ) ;
params . push ( Number ( filtros . idbancos ) ) ;
}
if ( filtros . idcentrodecustos ) {
where . push ( 'cp.idcentrodecustos = ?' ) ;
params . push ( Number ( filtros . idcentrodecustos ) ) ;
}
if ( filtros . idclientes ) {
where . push ( 'cp.idclientes = ?' ) ;
params . push ( Number ( filtros . idclientes ) ) ;
}
if ( filtros . idbancos _p ) {
where . push ( 'cp.idbancos_p = ?' ) ;
params . push ( Number ( filtros . idbancos _p ) ) ;
}
2026-05-30 20:38:53 +00:00
if ( filtros . idmovimentosfixos ) {
where . push ( 'cp.idmovimentosfixos = ?' ) ;
params . push ( Number ( filtros . idmovimentosfixos ) ) ;
}
if ( filtros . competencia ) {
where . push ( 'cp.competencia = ?' ) ;
params . push ( filtros . competencia ) ;
}
if ( filtros . grupo _parcelamento ) {
where . push ( 'cp.grupo_parcelamento = ?' ) ;
params . push ( filtros . grupo _parcelamento ) ;
}
if ( filtros . origem ) {
where . push ( 'cp.origem = ?' ) ;
params . push ( filtros . origem ) ;
}
if ( filtros . saldo _processado !== undefined && filtros . saldo _processado !== null && filtros . saldo _processado !== '' ) {
where . push ( 'cp.saldo_processado = ?' ) ;
params . push ( Number ( filtros . saldo _processado ) ) ;
}
2026-05-30 01:51:05 +00:00
if ( filtros . referenciaTipo === 'cliente' ) {
where . push ( 'cp.idclientes IS NOT NULL' ) ;
}
if ( filtros . referenciaTipo === 'banco' ) {
where . push ( 'cp.idbancos_p IS NOT NULL' ) ;
}
if ( filtros . referenciaTipo === 'sem_referencia' ) {
where . push ( 'cp.idclientes IS NULL AND cp.idbancos_p IS NULL' ) ;
}
const campoData = resolverCampoData ( filtros . dataCampo ) ;
if ( filtros . dataInicio ) {
where . push ( ` DATE( ${ campoData } ) >= ? ` ) ;
params . push ( filtros . dataInicio ) ;
}
if ( filtros . dataFim ) {
where . push ( ` DATE( ${ campoData } ) <= ? ` ) ;
params . push ( filtros . dataFim ) ;
}
if ( filtros . busca ) {
where . push ( `
(
cp . descricao LIKE ?
OR cp . observacao LIKE ?
2026-05-30 20:38:53 +00:00
OR cp . competencia LIKE ?
OR cp . origem LIKE ?
2026-05-30 01:51:05 +00:00
OR b . descricao LIKE ?
OR cc . descricao LIKE ?
OR c . nome LIKE ?
OR c . cpf _cnpj LIKE ?
OR bp . descricao LIKE ?
2026-05-30 20:38:53 +00:00
OR mf . descricao LIKE ?
2026-05-30 01:51:05 +00:00
)
` );
const termo = ` % ${ String ( filtros . busca ) . trim ( ) } % ` ;
params . push (
termo ,
termo ,
termo ,
termo ,
termo ,
termo ,
2026-05-30 20:38:53 +00:00
termo ,
termo ,
termo ,
2026-05-30 01:51:05 +00:00
termo
) ;
}
const whereSql = where . length > 0 ? ` WHERE ${ where . join ( ' AND ' ) } ` : '' ;
return {
whereSql ,
params ,
} ;
}
async function listarMovimentos ( filtros = { } ) {
const limite = limitarNumero ( filtros . limite , 20 , 1 , 100 ) ;
const page = limitarNumero ( filtros . page , 1 , 1 , 999999 ) ;
const offset = filtros . offset !== undefined
? limitarNumero ( filtros . offset , 0 , 0 , 999999999 )
: ( page - 1 ) * limite ;
const orderBy = resolverOrdenacao ( filtros . orderBy || filtros . dataCampo ) ;
const orderDirection = resolverDirecao ( filtros . orderDirection ) ;
const { whereSql , params } = montarWhereMovimentos ( filtros ) ;
const [ rows ] = await pool . query (
`
2026-05-30 20:38:53 +00:00
SELECT
2026-05-30 01:51:05 +00:00
$ { CAMPOS _MOVIMENTO _SELECT }
$ { FROM _MOVIMENTO _JOIN }
$ { whereSql }
ORDER BY $ { orderBy } $ { orderDirection } , cp . idcontasapagar DESC
LIMIT ? OFFSET ?
` ,
[ ... params , limite , offset ]
) ;
const [ countRows ] = await pool . query (
`
SELECT COUNT ( * ) AS total
$ { FROM _MOVIMENTO _JOIN }
$ { whereSql }
` ,
params
) ;
const [ summaryRows ] = await pool . query (
`
SELECT
COUNT ( * ) AS quantidade ,
COALESCE ( SUM ( CASE WHEN cp . movimento = 'Entrada' THEN cp . valor ELSE 0 END ) , 0 ) AS totalEntradas ,
COALESCE ( SUM ( CASE WHEN cp . movimento = 'Saida' THEN cp . valor ELSE 0 END ) , 0 ) AS totalSaidas ,
COALESCE ( SUM ( CASE WHEN cp . movimento = 'Sangria' THEN cp . valor ELSE 0 END ) , 0 ) AS totalSangrias ,
COALESCE ( SUM ( CASE WHEN cp . movimento = 'Estorno' THEN cp . valor ELSE 0 END ) , 0 ) AS totalEstornos ,
2026-05-30 20:38:53 +00:00
COALESCE ( SUM ( CASE WHEN cp . status IN ( 'Pago' , 'Recebido' ) THEN cp . valor ELSE 0 END ) , 0 ) AS totalBaixado ,
COALESCE ( SUM ( CASE WHEN cp . status IN ( 'A pagar' , 'A receber' ) THEN cp . valor ELSE 0 END ) , 0 ) AS totalAberto ,
COALESCE ( SUM ( CASE WHEN cp . saldo _processado = 1 THEN 1 ELSE 0 END ) , 0 ) AS saldoProcessado ,
2026-05-30 01:51:05 +00:00
COALESCE ( SUM (
2026-05-30 20:38:53 +00:00
CASE
2026-05-30 01:51:05 +00:00
WHEN cp . movimento IN ( 'Entrada' , 'Estorno' ) THEN cp . valor
WHEN cp . movimento IN ( 'Saida' , 'Sangria' ) THEN - cp . valor
ELSE 0
END
) , 0 ) AS saldo
$ { FROM _MOVIMENTO _JOIN }
$ { whereSql }
` ,
params
) ;
const total = Number ( countRows [ 0 ] ? . total || 0 ) ;
const totalPages = Math . max ( 1 , Math . ceil ( total / limite ) ) ;
return {
data : rows ,
pagination : {
total ,
limite ,
offset ,
page : Math . floor ( offset / limite ) + 1 ,
totalPages ,
} ,
summary : {
quantidade : Number ( summaryRows [ 0 ] ? . quantidade || 0 ) ,
totalEntradas : Number ( summaryRows [ 0 ] ? . totalEntradas || 0 ) ,
totalSaidas : Number ( summaryRows [ 0 ] ? . totalSaidas || 0 ) ,
totalSangrias : Number ( summaryRows [ 0 ] ? . totalSangrias || 0 ) ,
totalEstornos : Number ( summaryRows [ 0 ] ? . totalEstornos || 0 ) ,
2026-05-30 20:38:53 +00:00
totalBaixado : Number ( summaryRows [ 0 ] ? . totalBaixado || 0 ) ,
totalAberto : Number ( summaryRows [ 0 ] ? . totalAberto || 0 ) ,
saldoProcessado : Number ( summaryRows [ 0 ] ? . saldoProcessado || 0 ) ,
2026-05-30 01:51:05 +00:00
saldo : Number ( summaryRows [ 0 ] ? . saldo || 0 ) ,
} ,
} ;
}
2026-05-30 20:38:53 +00:00
async function buscarMovimentoPorId ( id , opcoes = { } ) {
const executor = opcoes . connection || pool ;
const incluirExcluidos = opcoes . incluirExcluidos === true ;
2026-06-02 01:30:10 +00:00
const idUsuario = opcoes . idUsuario || null ;
const params = [ id ] ;
let filtroUsuario = '' ;
if ( idUsuario ) {
filtroUsuario = 'AND b.idusuarios = ?' ;
params . push ( Number ( idUsuario ) ) ;
}
2026-05-30 20:38:53 +00:00
const [ rows ] = await executor . query (
2026-05-30 01:51:05 +00:00
`
2026-05-30 20:38:53 +00:00
SELECT
2026-05-30 01:51:05 +00:00
$ { CAMPOS _MOVIMENTO _SELECT }
$ { FROM _MOVIMENTO _JOIN }
WHERE cp . idcontasapagar = ?
2026-05-30 20:38:53 +00:00
$ { incluirExcluidos ? '' : 'AND cp.deleted_at IS NULL' }
2026-06-02 01:30:10 +00:00
$ { filtroUsuario }
2026-05-30 01:51:05 +00:00
LIMIT 1
` ,
2026-06-02 01:30:10 +00:00
params
2026-05-30 01:51:05 +00:00
) ;
return rows [ 0 ] || null ;
}
2026-06-02 01:30:10 +00:00
async function buscarMovimentoPorIdParaUpdate ( connection , id , idUsuario = null ) {
const params = [ id ] ;
let filtroUsuario = '' ;
if ( idUsuario ) {
filtroUsuario = 'AND b.idusuarios = ?' ;
params . push ( Number ( idUsuario ) ) ;
}
2026-05-30 20:38:53 +00:00
const [ rows ] = await connection . query (
`
SELECT
cp . *
FROM contasapagar cp
2026-06-02 01:30:10 +00:00
INNER JOIN bancos b ON b . idbancos = cp . idbancos
2026-05-30 20:38:53 +00:00
WHERE cp . idcontasapagar = ?
2026-06-02 01:30:10 +00:00
AND cp . deleted _at IS NULL
$ { filtroUsuario }
2026-05-30 20:38:53 +00:00
LIMIT 1
FOR UPDATE
` ,
2026-06-02 01:30:10 +00:00
params
2026-05-30 20:38:53 +00:00
) ;
return rows [ 0 ] || null ;
}
async function inserirAuditoriaMovimento ( connection , dados ) {
await connection . query (
`
INSERT INTO contasapagar _auditoria (
idcontasapagar ,
acao ,
dados _antes ,
dados _depois ,
idusuarios ,
insert _date
) VALUES ( ? , ? , ? , ? , ? , NOW ( ) )
` ,
[
dados . idcontasapagar || null ,
dados . acao ,
dados . dados _antes ? JSON . stringify ( dados . dados _antes ) : null ,
dados . dados _depois ? JSON . stringify ( dados . dados _depois ) : null ,
dados . idusuarios || null ,
]
) ;
}
async function inserirMovimento ( connection , movimento ) {
const [ result ] = await connection . query (
2026-05-30 01:51:05 +00:00
`
INSERT INTO contasapagar (
movimento ,
descricao ,
dataentrada ,
datavencimento ,
databaixa ,
valor ,
parcela ,
parcelas ,
status ,
idcentrodecustos ,
idbancos ,
idusuarios _cad ,
idusuarios _baixa ,
idclientes ,
idveiculosdetalhes ,
idbancos _p ,
2026-05-30 20:38:53 +00:00
idmovimentosfixos ,
competencia ,
grupo _parcelamento ,
origem ,
saldo _processado ,
2026-05-30 01:51:05 +00:00
observacao ,
insert _date ,
2026-05-30 20:38:53 +00:00
update _date ,
deleted _at
) VALUES ( ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , NOW ( ) , NOW ( ) , NULL )
2026-05-30 01:51:05 +00:00
` ,
[
2026-05-30 20:38:53 +00:00
movimento . movimento ,
movimento . descricao ,
movimento . dataentrada ,
movimento . datavencimento ,
movimento . databaixa ,
movimento . valor ,
movimento . parcela ,
movimento . parcelas ,
movimento . status ,
movimento . idcentrodecustos ,
movimento . idbancos ,
movimento . idusuarios _cad ,
movimento . idusuarios _baixa ,
movimento . idclientes ,
movimento . idveiculosdetalhes ,
movimento . idbancos _p ,
movimento . idmovimentosfixos ,
movimento . competencia ,
movimento . grupo _parcelamento ,
movimento . origem ,
movimento . saldo _processado ,
movimento . observacao ,
2026-05-30 01:51:05 +00:00
]
) ;
2026-05-30 20:38:53 +00:00
return result . insertId ;
2026-05-30 01:51:05 +00:00
}
2026-05-30 20:38:53 +00:00
async function atualizarMovimentoBase ( connection , id , movimento , saldoProcessado ) {
await connection . query (
2026-05-30 01:51:05 +00:00
`
UPDATE contasapagar
SET
movimento = ? ,
descricao = ? ,
dataentrada = ? ,
datavencimento = ? ,
databaixa = ? ,
valor = ? ,
parcela = ? ,
parcelas = ? ,
status = ? ,
idcentrodecustos = ? ,
idbancos = ? ,
idusuarios _baixa = ? ,
idclientes = ? ,
idveiculosdetalhes = ? ,
idbancos _p = ? ,
2026-05-30 20:38:53 +00:00
idmovimentosfixos = ? ,
competencia = ? ,
grupo _parcelamento = ? ,
origem = ? ,
saldo _processado = ? ,
2026-05-30 01:51:05 +00:00
observacao = ? ,
update _date = NOW ( )
WHERE idcontasapagar = ?
` ,
[
2026-05-30 20:38:53 +00:00
movimento . movimento ,
movimento . descricao ,
movimento . dataentrada ,
movimento . datavencimento ,
movimento . databaixa ,
movimento . valor ,
movimento . parcela ,
movimento . parcelas ,
movimento . status ,
movimento . idcentrodecustos ,
movimento . idbancos ,
movimento . idusuarios _baixa ,
movimento . idclientes ,
movimento . idveiculosdetalhes ,
movimento . idbancos _p ,
movimento . idmovimentosfixos ,
movimento . competencia ,
movimento . grupo _parcelamento ,
movimento . origem ,
saldoProcessado ,
movimento . observacao ,
2026-05-30 01:51:05 +00:00
id ,
]
) ;
2026-05-30 20:38:53 +00:00
}
async function verificarDuplicidade ( dados = { } ) {
const idIgnorar = normalizarNumeroOuNull ( dados . idcontasapagar ) ;
const valor = Number ( dados . valor || 0 ) ;
const idcentrodecustos = normalizarNumeroOuNull ( dados . idcentrodecustos ) ;
const idbancos = normalizarNumeroOuNull ( dados . idbancos ) ;
const params = [
dados . movimento ,
valor ,
idcentrodecustos ,
idbancos ,
dados . datavencimento ,
] ;
let filtroId = '' ;
if ( idIgnorar ) {
filtroId = 'AND idcontasapagar <> ?' ;
params . push ( idIgnorar ) ;
}
const [ rows ] = await pool . query (
`
SELECT
idcontasapagar ,
movimento ,
descricao ,
valor ,
status ,
datavencimento ,
idcentrodecustos ,
idbancos
FROM contasapagar
WHERE deleted _at IS NULL
AND movimento = ?
AND valor = ?
AND (
( ? IS NULL AND idcentrodecustos IS NULL )
OR idcentrodecustos = ?
)
AND (
( ? IS NULL AND idbancos IS NULL )
OR idbancos = ?
)
AND DATE ( datavencimento ) = DATE ( ? )
$ { filtroId }
LIMIT 1
` ,
[
dados . movimento ,
valor ,
idcentrodecustos ,
idcentrodecustos ,
idbancos ,
idbancos ,
dados . datavencimento ,
... ( idIgnorar ? [ idIgnorar ] : [ ] ) ,
]
) ;
return rows [ 0 ] || null ;
}
function criarErroDuplicidade ( movimentoDuplicado ) {
const erro = new Error (
` Já existe um movimento parecido cadastrado: ID ${ movimentoDuplicado . idcontasapagar } , descrição " ${ movimentoDuplicado . descricao } ", situação " ${ movimentoDuplicado . status } ". `
) ;
erro . code = 'DUPLICIDADE_MOVIMENTO' ;
erro . data = movimentoDuplicado ;
return erro ;
}
function montarParcelas ( idUsuario , dados ) {
const parcelas = Number ( dados . parcelas || 1 ) ;
const parcelaInicial = Number ( dados . parcela || 1 ) ;
const gerarParcelas = dados . gerarParcelas !== false && parcelas > parcelaInicial ;
if ( ! gerarParcelas ) {
return {
grupoParcelamento : normalizarTextoOuNull ( dados . grupo _parcelamento ) ,
movimentos : [
normalizarMovimentoParaSalvar ( idUsuario , dados , {
origem : normalizarTextoOuNull ( dados . origem ) || 'manual' ,
} ) ,
] ,
} ;
}
const grupoParcelamento = normalizarTextoOuNull ( dados . grupo _parcelamento ) || crypto . randomUUID ( ) ;
const modoParcelamento = dados . modoParcelamento || 'valor_parcela' ;
const valorInformado = Number ( dados . valor || 0 ) ;
const valorParcela = modoParcelamento === 'valor_total'
? Number ( ( valorInformado / parcelas ) . toFixed ( 2 ) )
: valorInformado ;
const movimentos = [ ] ;
for ( let parcela = parcelaInicial ; parcela <= parcelas ; parcela ++ ) {
const indiceMes = parcela - parcelaInicial ;
const dadosParcela = {
... dados ,
valor : valorParcela ,
parcela ,
parcelas ,
datavencimento : addMesesDataString ( dados . datavencimento , indiceMes ) ,
competencia : normalizarTextoOuNull ( dados . competencia )
? addMesesDataString ( ` ${ dados . competencia } -01 ` , indiceMes ) . slice ( 0 , 7 )
: extrairCompetencia ( addMesesDataString ( dados . datavencimento , indiceMes ) ) ,
} ;
movimentos . push (
normalizarMovimentoParaSalvar ( idUsuario , dadosParcela , {
grupo _parcelamento : grupoParcelamento ,
origem : parcela === parcelaInicial
? ( normalizarTextoOuNull ( dados . origem ) || 'manual' )
: 'parcelamento' ,
} )
) ;
}
return {
grupoParcelamento ,
movimentos ,
} ;
}
async function aplicarSaldoMovimento ( connection , movimento , contexto ) {
2026-06-02 01:30:10 +00:00
const deltas = await movimentosSaldoService . calcularImpactoMovimento ( connection , movimento , contexto ) ;
2026-05-30 20:38:53 +00:00
if ( deltas . length === 0 ) {
return {
saldoProcessado : 0 ,
impactos : [ ] ,
} ;
}
const impactos = await movimentosSaldoService . aplicarDeltasSaldo (
connection ,
deltas ,
contexto
) ;
return {
saldoProcessado : impactos . length > 0 ? 1 : 0 ,
impactos ,
} ;
}
async function criarMovimento ( idUsuario , dados ) {
const duplicado = await verificarDuplicidade ( dados ) ;
if ( duplicado && dados . confirmarDuplicado !== true ) {
throw criarErroDuplicidade ( duplicado ) ;
}
const connection = await pool . getConnection ( ) ;
try {
await connection . beginTransaction ( ) ;
const { movimentos } = montarParcelas ( idUsuario , dados ) ;
const movimentosCriados = [ ] ;
const impactosTodos = [ ] ;
for ( const movimento of movimentos ) {
const idCriado = await inserirMovimento ( connection , movimento ) ;
const movimentoCriado = await buscarMovimentoPorId ( idCriado , {
connection ,
} ) ;
const { saldoProcessado , impactos } = await aplicarSaldoMovimento (
connection ,
movimentoCriado ,
{
idcontasapagar : idCriado ,
tipo _operacao : 'movimento_aplicado' ,
origem : movimentoCriado . origem || 'movimento' ,
idusuarios : idUsuario ,
}
) ;
if ( saldoProcessado === 1 ) {
await connection . query (
`
UPDATE contasapagar
SET saldo _processado = 1
WHERE idcontasapagar = ?
` ,
[ idCriado ]
) ;
}
const movimentoFinal = await buscarMovimentoPorId ( idCriado , {
connection ,
} ) ;
await inserirAuditoriaMovimento ( connection , {
idcontasapagar : idCriado ,
acao : movimento . parcela > 1 ? 'gerar_parcela' : 'criar' ,
dados _antes : null ,
dados _depois : movimentoFinal ,
idusuarios : idUsuario ,
} ) ;
movimentosCriados . push ( movimentoFinal ) ;
impactosTodos . push ( ... impactos ) ;
}
await connection . commit ( ) ;
return {
data : movimentosCriados [ 0 ] ,
parcelasGeradas : movimentosCriados . slice ( 1 ) ,
impactoSaldo : impactosTodos ,
} ;
} catch ( error ) {
await connection . rollback ( ) ;
throw error ;
} finally {
connection . release ( ) ;
}
}
async function atualizarMovimento ( id , idUsuario , dados ) {
const duplicado = await verificarDuplicidade ( {
... dados ,
idcontasapagar : id ,
} ) ;
if ( duplicado && dados . confirmarDuplicado !== true ) {
throw criarErroDuplicidade ( duplicado ) ;
}
const connection = await pool . getConnection ( ) ;
try {
await connection . beginTransaction ( ) ;
2026-06-02 01:30:10 +00:00
const movimentoAntigo = await buscarMovimentoPorIdParaUpdate ( connection , id , idUsuario ) ;
2026-05-30 20:38:53 +00:00
if ( ! movimentoAntigo ) {
await connection . rollback ( ) ;
return null ;
}
const dadosNovos = normalizarMovimentoParaSalvar ( idUsuario , dados , {
idusuarios _cad : movimentoAntigo . idusuarios _cad ,
grupo _parcelamento : dados . grupo _parcelamento || movimentoAntigo . grupo _parcelamento ,
origem : dados . origem || movimentoAntigo . origem || 'manual' ,
} ) ;
const impactosTodos = [ ] ;
if ( Number ( movimentoAntigo . saldo _processado ) === 1 ) {
const deltasAntigos = await movimentosSaldoService . calcularImpactoMovimento (
connection ,
2026-06-02 01:30:10 +00:00
movimentoAntigo ,
{ idusuarios : idUsuario }
2026-05-30 20:38:53 +00:00
) ;
const impactosReversao = await movimentosSaldoService . aplicarDeltasSaldo (
connection ,
movimentosSaldoService . inverterDeltas ( deltasAntigos ) ,
{
idcontasapagar : id ,
tipo _operacao : 'movimento_revertido' ,
origem : 'edicao_movimento' ,
idusuarios : idUsuario ,
}
) ;
impactosTodos . push ( ... impactosReversao ) ;
}
await atualizarMovimentoBase ( connection , id , dadosNovos , 0 ) ;
const movimentoNovo = await buscarMovimentoPorId ( id , {
connection ,
} ) ;
const { saldoProcessado , impactos } = await aplicarSaldoMovimento (
connection ,
movimentoNovo ,
{
idcontasapagar : id ,
tipo _operacao : 'movimento_aplicado' ,
origem : 'edicao_movimento' ,
idusuarios : idUsuario ,
}
) ;
if ( saldoProcessado === 1 ) {
await connection . query (
`
UPDATE contasapagar
SET saldo _processado = 1
WHERE idcontasapagar = ?
` ,
[ id ]
) ;
}
const movimentoFinal = await buscarMovimentoPorId ( id , {
connection ,
} ) ;
await inserirAuditoriaMovimento ( connection , {
idcontasapagar : id ,
acao : 'editar' ,
dados _antes : movimentoAntigo ,
dados _depois : movimentoFinal ,
idusuarios : idUsuario ,
} ) ;
impactosTodos . push ( ... impactos ) ;
await connection . commit ( ) ;
return {
data : movimentoFinal ,
impactoSaldo : impactosTodos ,
} ;
} catch ( error ) {
await connection . rollback ( ) ;
throw error ;
} finally {
connection . release ( ) ;
}
}
async function deletarMovimento ( id , idUsuario ) {
const connection = await pool . getConnection ( ) ;
try {
await connection . beginTransaction ( ) ;
2026-06-02 01:30:10 +00:00
const movimentoAntigo = await buscarMovimentoPorIdParaUpdate ( connection , id , idUsuario ) ;
2026-05-30 20:38:53 +00:00
if ( ! movimentoAntigo ) {
await connection . rollback ( ) ;
return {
ok : false ,
} ;
}
const impactosTodos = [ ] ;
if ( Number ( movimentoAntigo . saldo _processado ) === 1 ) {
const deltasAntigos = await movimentosSaldoService . calcularImpactoMovimento (
connection ,
2026-06-02 01:30:10 +00:00
movimentoAntigo ,
{ idusuarios : idUsuario }
2026-05-30 20:38:53 +00:00
) ;
const impactosReversao = await movimentosSaldoService . aplicarDeltasSaldo (
connection ,
movimentosSaldoService . inverterDeltas ( deltasAntigos ) ,
{
idcontasapagar : id ,
tipo _operacao : 'movimento_revertido' ,
origem : 'exclusao_movimento' ,
idusuarios : idUsuario ,
}
) ;
impactosTodos . push ( ... impactosReversao ) ;
}
await connection . query (
`
UPDATE contasapagar
SET
deleted _at = NOW ( ) ,
saldo _processado = 0 ,
update _date = NOW ( )
WHERE idcontasapagar = ?
` ,
[ id ]
) ;
await inserirAuditoriaMovimento ( connection , {
idcontasapagar : id ,
acao : 'excluir' ,
dados _antes : movimentoAntigo ,
dados _depois : null ,
idusuarios : idUsuario ,
} ) ;
await connection . commit ( ) ;
return {
ok : true ,
impactoSaldo : impactosTodos ,
} ;
} catch ( error ) {
await connection . rollback ( ) ;
throw error ;
} finally {
connection . release ( ) ;
}
}
async function preverImpactoSaldo ( dados , idcontasapagar = null ) {
let movimentoAntigo = null ;
if ( idcontasapagar ) {
movimentoAntigo = await buscarMovimentoPorId ( idcontasapagar ) ;
}
const movimentoNovo = normalizarMovimentoParaSalvar ( null , dados , {
idusuarios _cad : dados . idusuarios _cad || null ,
grupo _parcelamento : dados . grupo _parcelamento || null ,
origem : dados . origem || 'manual' ,
} ) ;
2026-05-30 01:51:05 +00:00
2026-05-30 20:38:53 +00:00
return movimentosSaldoService . preverImpactoSaldo ( movimentoNovo , movimentoAntigo ) ;
2026-05-30 01:51:05 +00:00
}
module . exports = {
listarMovimentos ,
buscarMovimentoPorId ,
2026-05-30 20:38:53 +00:00
verificarDuplicidade ,
preverImpactoSaldo ,
2026-05-30 01:51:05 +00:00
criarMovimento ,
atualizarMovimento ,
2026-05-30 20:38:53 +00:00
deletarMovimento ,
2026-05-30 01:51:05 +00:00
} ;