Fluxo de Liquidacao
Este guia descreve o processo completo de liquidacao de boletos na plataforma FluxiQ NPC, incluindo o processamento automatico de arquivos ACMP615 e a reconciliacao de pagamentos.
Ciclos de Liquidacao
A Nuclea processa liquidacoes em dois ciclos diarios:
| Ciclo | Horario Nuclea | Processamento FluxiQ NPC | Descricao |
|---|---|---|---|
| Manha | 08:30 | 08:35 | Pagamentos do dia anterior (tarde) |
| Tarde | 16:30 | 16:35 | Pagamentos do dia atual (manha) |
Dias Uteis
A liquidacao ocorre apenas em dias uteis. Nos finais de semana e feriados bancarios, os pagamentos acumulam para o proximo dia util.
Fluxo Automatico
O diagrama abaixo ilustra o fluxo completo de processamento de liquidacao:
┌─────────────────────────────────────────────────────────────────────────────┐
│ Fluxo de Liquidacao Automatico │
└─────────────────────────────────────────────────────────────────────────────┘
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ TRIGGER │─────▶│ DOWNLOAD │─────▶│ PARSE │─────▶│ MATCH │
│ 08:35 │ │ FTP │ │ ACMP615 │ │ BOLETOS │
│ 16:35 │ │ │ │ │ │ │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
│
▼
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ WEBHOOK │◀─────│ UPDATE │◀─────│ RECONCILE│◀─────│ NOTIFY │
│ CENTRAL │ │ STATUS │ │ AMOUNTS │ │ PAYMENTS │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
Eventos gerados:
- payment_received (para cada boleto)
- settlement_completed (ao final do ciclo)Detalhamento do Fluxo
- Trigger (08:35 / 16:35): Job Oban disparado automaticamente
- Download FTP: Conexao ao servidor FTP da Nuclea para baixar arquivo ACMP615
- Parse ACMP615: Leitura e interpretacao dos registros do arquivo
- Match Boletos: Associacao dos pagamentos aos boletos cadastrados
- Notify Payments: Geracao de eventos individuais para cada pagamento
- Reconcile Amounts: Verificacao de valores (original vs pago)
- Update Status: Atualizacao do status dos boletos para
settled - Webhook Central: Envio de webhooks para a Central
Disparo Manual
Para situacoes especiais (reprocessamento, testes), voce pode disparar manualmente:
curl -X POST "https://api.pixconnect.com.br/api/v1/central/settlements/trigger" \
-H "X-API-Key: pk_live_abc123def456" \
-H "Content-Type: application/json" \
-d '{
"cycle": 1,
"date": "2026-02-15"
}'async function triggerSettlement(cycle = 1, date = null) {
const payload = { cycle };
if (date) payload.date = date;
const response = await fetch(
"https://api.pixconnect.com.br/api/v1/central/settlements/trigger",
{
method: "POST",
headers: {
"X-API-Key": "pk_live_abc123def456",
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
}
);
const data = await response.json();
console.log("Job enfileirado:", data.data.job_id);
return data;
}
// Disparar ciclo da manha para data especifica
triggerSettlement(1, "2026-02-15");import requests
def trigger_settlement(cycle=1, date=None):
url = "https://api.pixconnect.com.br/api/v1/central/settlements/trigger"
headers = {
"X-API-Key": "pk_live_abc123def456",
"Content-Type": "application/json"
}
payload = {"cycle": cycle}
if date:
payload["date"] = date
response = requests.post(url, headers=headers, json=payload)
data = response.json()
print(f"Job enfileirado: {data['data']['job_id']}")
return data
# Disparar ciclo da manha para data especifica
trigger_settlement(1, "2026-02-15")Resposta:
{
"success": true,
"data": {
"job_id": 12345,
"date": "2026-02-15",
"cycle": 1,
"status": "queued"
}
}Uso do Disparo Manual
Use o disparo manual apenas para:
- Reprocessamento de arquivos com erro
- Testes em ambiente sandbox
- Recuperacao de dados historicos
O processamento automatico e o metodo recomendado para producao.
Recebendo Notificacoes
Evento settlement_completed
Enviado quando um ciclo de liquidacao e concluido:
app.post("/webhooks/pixconnect", async (req, res) => {
const { event, data } = req.body;
if (!validateSignature(req)) {
return res.status(401).json({ error: "Assinatura invalida" });
}
if (event === "settlement_completed") {
console.log("Ciclo de liquidacao concluido!");
console.log("Settlement ID:", data.settlement_id);
console.log("Data:", data.cycle_date);
console.log("Ciclo:", data.cycle_type);
console.log("Total boletos:", data.total_boletos);
console.log("Valor total:", data.total_valor / 100);
// Processar resumo
const { summary } = data;
console.log("Pagos:", summary.pagos, "- R$", summary.valor_pagos / 100);
console.log("Rejeitados:", summary.rejeitados, "- R$", summary.valor_rejeitados / 100);
// Salvar registro do ciclo
await saveSettlementCycle({
id: data.settlement_id,
date: data.cycle_date,
type: data.cycle_type,
total_boletos: data.total_boletos,
total_valor: data.total_valor,
summary: summary,
arquivo: data.arquivo_acmp615,
completed_at: data.completed_at
});
// Notificar equipe financeira
await notifyFinanceTeam(data);
}
res.status(200).json({ received: true });
});Payload do evento:
{
"event": "settlement_completed",
"timestamp": "2026-02-04T08:45:00Z",
"data": {
"settlement_id": "stl_abc123def456",
"cycle_date": "2026-02-04",
"cycle_type": "morning",
"total_boletos": 47,
"total_valor": 1523450,
"arquivo_acmp615": "ACMP615.20260204.083000",
"status": "completed",
"started_at": "2026-02-04T08:35:00Z",
"completed_at": "2026-02-04T08:45:00Z",
"summary": {
"pagos": 45,
"valor_pagos": 1485000,
"rejeitados": 2,
"valor_rejeitados": 38450
}
}
}Evento payment_received
Enviado para cada pagamento individual processado:
app.post("/webhooks/pixconnect", async (req, res) => {
const { event, data } = req.body;
if (!validateSignature(req)) {
return res.status(401).json({ error: "Assinatura invalida" });
}
if (event === "payment_received") {
console.log("Pagamento processado:", data.nosso_numero);
console.log("Valor:", data.valor_pago / 100);
console.log("Data credito:", data.data_credito);
// Atualizar boleto no banco de dados
await updateBoletoSettlement(data.nosso_numero, {
status: "settled",
valor_pago: data.valor_pago,
data_credito: data.data_credito,
settlement_id: data.settlement_id,
banco_recebedor: data.banco_recebedor,
sequencial_arquivo: data.sequencial_arquivo
});
// Registrar na contabilidade
await createAccountingEntry({
type: "credit",
amount: data.valor_pago,
reference: data.nosso_numero,
settlement_id: data.settlement_id,
description: `Liquidacao boleto ${data.nosso_numero}`
});
}
res.status(200).json({ received: true });
});Reconciliacao Contabil
Estrutura do Arquivo ACMP615
O arquivo ACMP615 segue o padrao FEBRABAN com tres tipos de registro:
| Tipo | Nome | Descricao |
|---|---|---|
00 | Header | Informacoes do arquivo (data, hora, remetente) |
20 | Detalhe | Registros de pagamento individual |
99 | Trailer | Totalizadores e contagem de registros |
Registro Header (Tipo 00)
| Posicao | Tamanho | Campo | Descricao |
|---|---|---|---|
| 1-2 | 2 | tipo_registro | "00" |
| 3-10 | 8 | data_geracao | Data de geracao (YYYYMMDD) |
| 11-16 | 6 | hora_geracao | Hora de geracao (HHMMSS) |
| 17-24 | 8 | ispb_remetente | ISPB do remetente |
| 25-30 | 6 | sequencial_arquivo | Numero sequencial do arquivo |
Registro Detalhe (Tipo 20)
| Posicao | Tamanho | Campo | Descricao |
|---|---|---|---|
| 1-2 | 2 | tipo_registro | "20" |
| 3-13 | 11 | nosso_numero | Identificador do boleto |
| 14-30 | 17 | valor_pago | Valor pago (centavos, zeros a esquerda) |
| 31-38 | 8 | data_pagamento | Data do pagamento (YYYYMMDD) |
| 39-46 | 8 | data_credito | Data do credito (YYYYMMDD) |
| 47-49 | 3 | banco_recebedor | Codigo COMPE do banco |
| 50-53 | 4 | agencia_recebedora | Agencia recebedora |
| 54-55 | 2 | codigo_movimento | Codigo do movimento |
Registro Trailer (Tipo 99)
| Posicao | Tamanho | Campo | Descricao |
|---|---|---|---|
| 1-2 | 2 | tipo_registro | "99" |
| 3-8 | 6 | qtd_registros | Quantidade de registros tipo 20 |
| 9-26 | 18 | valor_total | Soma dos valores (centavos) |
Codigo de Reconciliacao
Exemplo de reconciliacao dos pagamentos com seus registros:
class SettlementReconciliation {
constructor(settlementId) {
this.settlementId = settlementId;
this.matched = [];
this.unmatched = [];
this.discrepancies = [];
}
async reconcile(payments, boletos) {
const boletoMap = new Map(
boletos.map(b => [b.nosso_numero, b])
);
for (const payment of payments) {
const boleto = boletoMap.get(payment.nosso_numero);
if (!boleto) {
// Pagamento sem boleto correspondente
this.unmatched.push({
payment,
reason: "BOLETO_NOT_FOUND"
});
continue;
}
// Verificar valores
const valorOriginal = boleto.amount_cents;
const valorPago = payment.valor_pago;
if (valorPago !== valorOriginal) {
// Discrepancia de valor
this.discrepancies.push({
nosso_numero: payment.nosso_numero,
valor_original: valorOriginal,
valor_pago: valorPago,
diferenca: valorPago - valorOriginal,
reason: this.classifyDiscrepancy(valorOriginal, valorPago, boleto)
});
}
this.matched.push({
nosso_numero: payment.nosso_numero,
boleto_id: boleto.id,
valor_original: valorOriginal,
valor_pago: valorPago,
data_pagamento: payment.data_pagamento,
data_credito: payment.data_credito
});
}
return this.generateReport();
}
classifyDiscrepancy(valorOriginal, valorPago, boleto) {
const diferenca = valorPago - valorOriginal;
if (diferenca > 0) {
// Pagamento maior - pode ser juros/multa
return "JUROS_MULTA";
} else if (diferenca < 0) {
// Pagamento menor - pode ser desconto
const percentual = Math.abs(diferenca / valorOriginal) * 100;
if (percentual <= 10) {
return "DESCONTO_PONTUALIDADE";
}
return "PAGAMENTO_PARCIAL";
}
return "VALOR_EXATO";
}
generateReport() {
const totalMatched = this.matched.reduce((sum, m) => sum + m.valor_pago, 0);
const totalUnmatched = this.unmatched.reduce((sum, u) => sum + u.payment.valor_pago, 0);
return {
settlement_id: this.settlementId,
generated_at: new Date().toISOString(),
summary: {
total_payments: this.matched.length + this.unmatched.length,
matched: this.matched.length,
unmatched: this.unmatched.length,
with_discrepancy: this.discrepancies.length,
valor_matched: totalMatched,
valor_unmatched: totalUnmatched,
match_rate: (this.matched.length / (this.matched.length + this.unmatched.length) * 100).toFixed(2) + "%"
},
matched: this.matched,
unmatched: this.unmatched,
discrepancies: this.discrepancies
};
}
}
// Uso
async function processSettlement(settlementData) {
const payments = settlementData.payments;
const boletos = await fetchBoletosFromDatabase(
payments.map(p => p.nosso_numero)
);
const reconciliation = new SettlementReconciliation(settlementData.settlement_id);
const report = await reconciliation.reconcile(payments, boletos);
console.log("Reconciliacao concluida:");
console.log("Taxa de match:", report.summary.match_rate);
return report;
}Tratando Registros Nao Conciliados
Quando um pagamento nao encontra boleto correspondente:
async function handleUnmatchedPayments(unmatchedPayments) {
for (const { payment, reason } of unmatchedPayments) {
console.warn(`Pagamento nao conciliado: ${payment.nosso_numero}`);
console.warn(`Motivo: ${reason}`);
console.warn(`Valor: R$ ${payment.valor_pago / 100}`);
// Registrar para investigacao
await createInvestigationTicket({
type: "UNMATCHED_PAYMENT",
nosso_numero: payment.nosso_numero,
valor: payment.valor_pago,
data_pagamento: payment.data_pagamento,
settlement_id: payment.settlement_id,
raw_record: payment.acmp615_record
});
// Tentar busca alternativa
const possibleMatch = await searchAlternativeMatches(payment);
if (possibleMatch) {
console.log(`Possivel match encontrado: ${possibleMatch.nosso_numero}`);
await flagForManualReview(payment, possibleMatch);
}
}
}
async function searchAlternativeMatches(payment) {
// Buscar por valor similar
const candidates = await db.query(`
SELECT * FROM boletos
WHERE amount_cents = $1
AND status = 'registered'
AND due_date >= $2
ORDER BY due_date ASC
LIMIT 5
`, [payment.valor_pago, payment.data_pagamento]);
return candidates.length > 0 ? candidates[0] : null;
}Geracao de Relatorio Diario
Exemplo de relatorio consolidado diario:
async function generateDailySettlementReport(date) {
// Buscar ciclos do dia
const cycles = await db.query(`
SELECT * FROM settlement_cycles
WHERE cycle_date = $1
ORDER BY cycle_type ASC
`, [date]);
// Buscar pagamentos
const payments = await db.query(`
SELECT * FROM payments
WHERE DATE(paid_at) = $1
`, [date]);
// Consolidar dados
const report = {
date: date,
generated_at: new Date().toISOString(),
cycles: cycles.map(c => ({
type: c.cycle_type,
started_at: c.started_at,
completed_at: c.completed_at,
total_boletos: c.total_boletos,
total_valor: c.total_valor,
status: c.status
})),
totals: {
total_boletos: payments.length,
total_valor: payments.reduce((sum, p) => sum + p.valor_pago, 0),
por_canal: groupByChannel(payments),
por_banco: groupByBank(payments)
},
reconciliation: {
matched: payments.filter(p => p.reconciliation_status === "matched").length,
unmatched: payments.filter(p => p.reconciliation_status === "unmatched").length,
discrepancies: payments.filter(p => p.has_discrepancy).length
}
};
// Formatar valores em reais
report.totals.total_valor_formatted = formatCurrency(report.totals.total_valor);
// Salvar relatorio
await saveReport("settlement_daily", date, report);
// Enviar para equipe financeira
await sendReportEmail(report, ["financeiro@empresa.com.br"]);
return report;
}
function groupByChannel(payments) {
return payments.reduce((acc, p) => {
const channel = p.canal_pagamento || "unknown";
if (!acc[channel]) {
acc[channel] = { count: 0, valor: 0 };
}
acc[channel].count++;
acc[channel].valor += p.valor_pago;
return acc;
}, {});
}
function groupByBank(payments) {
return payments.reduce((acc, p) => {
const bank = p.banco_recebedor || "unknown";
if (!acc[bank]) {
acc[bank] = { count: 0, valor: 0 };
}
acc[bank].count++;
acc[bank].valor += p.valor_pago;
return acc;
}, {});
}
function formatCurrency(cents) {
return (cents / 100).toLocaleString("pt-BR", {
style: "currency",
currency: "BRL"
});
}Monitoramento e Alertas
Metricas Recomendadas
Configure alertas para as seguintes situacoes:
| Metrica | Limite | Acao |
|---|---|---|
| Taxa de match | < 95% | Alerta amarelo - investigar |
| Taxa de match | < 90% | Alerta vermelho - acao imediata |
| Tempo processamento | > 15 min | Verificar performance FTP |
| Arquivo nao encontrado | 30 min apos ciclo | Contatar Nuclea |
| Discrepancias valor | > 5% dos boletos | Revisar regras de juros/desconto |
Exemplo de Monitoramento
class SettlementMonitor {
constructor() {
this.thresholds = {
matchRate: { warning: 95, critical: 90 },
processingTime: { warning: 10, critical: 15 }, // minutos
discrepancyRate: { warning: 3, critical: 5 } // percentual
};
}
async checkSettlementHealth(settlementId) {
const settlement = await getSettlement(settlementId);
const alerts = [];
// Verificar taxa de match
const matchRate = (settlement.matched / settlement.total_boletos) * 100;
if (matchRate < this.thresholds.matchRate.critical) {
alerts.push({
severity: "critical",
metric: "match_rate",
value: matchRate,
message: `Taxa de match critica: ${matchRate.toFixed(2)}%`
});
} else if (matchRate < this.thresholds.matchRate.warning) {
alerts.push({
severity: "warning",
metric: "match_rate",
value: matchRate,
message: `Taxa de match abaixo do esperado: ${matchRate.toFixed(2)}%`
});
}
// Verificar tempo de processamento
const processingTime = (
new Date(settlement.completed_at) - new Date(settlement.started_at)
) / 60000; // minutos
if (processingTime > this.thresholds.processingTime.critical) {
alerts.push({
severity: "critical",
metric: "processing_time",
value: processingTime,
message: `Tempo de processamento critico: ${processingTime.toFixed(0)} min`
});
}
// Verificar discrepancias
const discrepancyRate = (settlement.discrepancies / settlement.total_boletos) * 100;
if (discrepancyRate > this.thresholds.discrepancyRate.warning) {
alerts.push({
severity: "warning",
metric: "discrepancy_rate",
value: discrepancyRate,
message: `Taxa de discrepancia: ${discrepancyRate.toFixed(2)}%`
});
}
// Enviar alertas
for (const alert of alerts) {
await this.sendAlert(alert, settlementId);
}
return {
settlement_id: settlementId,
status: alerts.length === 0 ? "healthy" : "needs_attention",
metrics: {
match_rate: matchRate,
processing_time: processingTime,
discrepancy_rate: discrepancyRate
},
alerts
};
}
async sendAlert(alert, settlementId) {
console.log(`[${alert.severity.toUpperCase()}] ${alert.message}`);
if (alert.severity === "critical") {
// Enviar SMS/chamada para plantao
await sendCriticalAlert({
settlement_id: settlementId,
alert
});
}
// Registrar no sistema de monitoramento
await logAlert(alert, settlementId);
}
}
// Uso apos cada ciclo
async function postSettlementCheck(settlementId) {
const monitor = new SettlementMonitor();
const health = await monitor.checkSettlementHealth(settlementId);
console.log("Status da liquidacao:", health.status);
if (health.alerts.length > 0) {
console.log("Alertas:");
health.alerts.forEach(a => console.log(` - ${a.message}`));
}
return health;
}Checklist de Implementacao
Configuracao Inicial
- [ ] Credenciais FTP da Nuclea configuradas
- [ ] Horarios de processamento definidos (08:35, 16:35)
- [ ] Endpoint de webhook configurado na Central
- [ ] Banco de dados preparado para armazenar ciclos
Webhooks
- [ ] Handler para
settlement_completed - [ ] Handler para
payment_received - [ ] Validacao de assinatura HMAC
- [ ] Resposta em ate 30 segundos
Reconciliacao
- [ ] Parser de arquivo ACMP615 implementado
- [ ] Logica de match por
nosso_numero - [ ] Tratamento de discrepancias de valor
- [ ] Fila para pagamentos nao conciliados
Contabilidade
- [ ] Integracao com sistema contabil
- [ ] Lancamentos automaticos de credito
- [ ] Relatorios diarios configurados
- [ ] Auditoria de valores
Monitoramento
- [ ] Alertas de taxa de match
- [ ] Alertas de tempo de processamento
- [ ] Dashboard de acompanhamento
- [ ] Logs para debugging
Operacao
- [ ] Procedimento para reprocessamento manual
- [ ] Procedimento para pagamentos nao conciliados
- [ ] Contato com suporte Nuclea configurado
- [ ] Documentacao interna atualizada
Proximos Passos
- Fluxo de Boleto - Ciclo de vida completo do boleto
- Eventos de Webhook - Referencia de eventos
- API de Liquidacao - Referencia da API