Skip to content

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:

CicloHorario NucleaProcessamento FluxiQ NPCDescricao
Manha08:3008:35Pagamentos do dia anterior (tarde)
Tarde16:3016:35Pagamentos 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

  1. Trigger (08:35 / 16:35): Job Oban disparado automaticamente
  2. Download FTP: Conexao ao servidor FTP da Nuclea para baixar arquivo ACMP615
  3. Parse ACMP615: Leitura e interpretacao dos registros do arquivo
  4. Match Boletos: Associacao dos pagamentos aos boletos cadastrados
  5. Notify Payments: Geracao de eventos individuais para cada pagamento
  6. Reconcile Amounts: Verificacao de valores (original vs pago)
  7. Update Status: Atualizacao do status dos boletos para settled
  8. Webhook Central: Envio de webhooks para a Central

Disparo Manual

Para situacoes especiais (reprocessamento, testes), voce pode disparar manualmente:

bash
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"
  }'
javascript
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");
python
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:

json
{
  "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:

javascript
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:

json
{
  "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:

javascript
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:

TipoNomeDescricao
00HeaderInformacoes do arquivo (data, hora, remetente)
20DetalheRegistros de pagamento individual
99TrailerTotalizadores e contagem de registros

Registro Header (Tipo 00)

PosicaoTamanhoCampoDescricao
1-22tipo_registro"00"
3-108data_geracaoData de geracao (YYYYMMDD)
11-166hora_geracaoHora de geracao (HHMMSS)
17-248ispb_remetenteISPB do remetente
25-306sequencial_arquivoNumero sequencial do arquivo

Registro Detalhe (Tipo 20)

PosicaoTamanhoCampoDescricao
1-22tipo_registro"20"
3-1311nosso_numeroIdentificador do boleto
14-3017valor_pagoValor pago (centavos, zeros a esquerda)
31-388data_pagamentoData do pagamento (YYYYMMDD)
39-468data_creditoData do credito (YYYYMMDD)
47-493banco_recebedorCodigo COMPE do banco
50-534agencia_recebedoraAgencia recebedora
54-552codigo_movimentoCodigo do movimento

Registro Trailer (Tipo 99)

PosicaoTamanhoCampoDescricao
1-22tipo_registro"99"
3-86qtd_registrosQuantidade de registros tipo 20
9-2618valor_totalSoma dos valores (centavos)

Codigo de Reconciliacao

Exemplo de reconciliacao dos pagamentos com seus registros:

javascript
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:

javascript
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:

javascript
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:

MetricaLimiteAcao
Taxa de match< 95%Alerta amarelo - investigar
Taxa de match< 90%Alerta vermelho - acao imediata
Tempo processamento> 15 minVerificar performance FTP
Arquivo nao encontrado30 min apos cicloContatar Nuclea
Discrepancias valor> 5% dos boletosRevisar regras de juros/desconto

Exemplo de Monitoramento

javascript
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

Documentação da API FluxiQ NPC