# Settlement Flow

This guide describes the complete boleto settlement process on the FluxiQ NPC platform, including automatic processing of ACMP615 files and payment reconciliation.

## Settlement Cycles

Nuclea processes settlements in two daily cycles:

| Cycle     | Nuclea Time | FluxiQ NPC Processing | Description |
|-----------|-------------|----------------------|-------------|
| Morning   | 08:30 | 08:35 | Payments from previous day (afternoon) |
| Afternoon | 16:30 | 16:35 | Payments from current day (morning) |

::: info Business Days
Settlement occurs only on business days. On weekends and bank holidays, payments accumulate for the next business day.
:::

## Automatic Flow

The diagram below illustrates the complete settlement processing flow:

```
+-----------------------------------------------------------------------------+
|                       Automatic Settlement Flow                              |
+-----------------------------------------------------------------------------+

  +----------+      +----------+      +----------+      +----------+
  |  TRIGGER |----->| DOWNLOAD |----->|  PARSE   |----->|  MATCH   |
  |  08:35   |      |   FTP    |      | ACMP615  |      | BOLETOS  |
  |  16:35   |      |          |      |          |      |          |
  +----------+      +----------+      +----------+      +----------+
                                                              |
                                                              v
  +----------+      +----------+      +----------+      +----------+
  | WEBHOOK  |<-----| UPDATE   |<-----| RECONCILE|<-----|  NOTIFY  |
  | CENTRAL  |      | STATUS   |      | AMOUNTS  |      | PAYMENTS |
  +----------+      +----------+      +----------+      +----------+

  Events generated:
  - payment_received (for each boleto)
  - settlement_completed (at end of cycle)
```

### Flow Details

1. **Trigger (08:35 / 16:35)**: Oban job triggered automatically
2. **FTP Download**: Connect to Nuclea FTP server to download ACMP615 file
3. **Parse ACMP615**: Read and interpret file records
4. **Match Boletos**: Associate payments with registered boletos
5. **Notify Payments**: Generate individual events for each payment
6. **Reconcile Amounts**: Verify values (original vs paid)
7. **Update Status**: Update boleto status to `settled`
8. **Webhook Central**: Send webhooks to Central

## Manual Trigger

For special situations (reprocessing, testing), you can trigger manually:

::: code-group

```bash [cURL]
curl -X POST "https://api.monetarie_npc.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 [JavaScript]
async function triggerSettlement(cycle = 1, date = null) {
  const payload = { cycle };
  if (date) payload.date = date;

  const response = await fetch(
    "https://api.monetarie_npc.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 enqueued:", data.data.job_id);
  return data;
}

// Trigger morning cycle for specific date
triggerSettlement(1, "2026-02-15");
```

```python [Python]
import requests

def trigger_settlement(cycle=1, date=None):
    url = "https://api.monetarie_npc.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 enqueued: {data['data']['job_id']}")
    return data

# Trigger morning cycle for specific date
trigger_settlement(1, "2026-02-15")
```

:::

**Response:**

```json
{
  "success": true,
  "data": {
    "job_id": 12345,
    "date": "2026-02-15",
    "cycle": 1,
    "status": "queued"
  }
}
```

::: warning Manual Trigger Usage
Use manual trigger only for:
- Reprocessing files with errors
- Testing in sandbox environment
- Recovering historical data

Automatic processing is the recommended method for production.
:::

## Receiving Notifications

### settlement_completed Event

Sent when a settlement cycle is completed:

```javascript
app.post("/webhooks/monetarie_npc", async (req, res) => {
  const { event, data } = req.body;

  if (!validateSignature(req)) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  if (event === "settlement_completed") {
    console.log("Settlement cycle completed!");
    console.log("Settlement ID:", data.settlement_id);
    console.log("Date:", data.cycle_date);
    console.log("Cycle:", data.cycle_type);
    console.log("Total boletos:", data.total_boletos);
    console.log("Total value:", data.total_valor / 100);

    // Process summary
    const { summary } = data;
    console.log("Paid:", summary.pagos, "- R$", summary.valor_pagos / 100);
    console.log("Rejected:", summary.rejeitados, "- R$", summary.valor_rejeitados / 100);

    // Save cycle record
    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
    });

    // Notify finance team
    await notifyFinanceTeam(data);
  }

  res.status(200).json({ received: true });
});
```

**Event payload:**

```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
    }
  }
}
```

### payment_received Event

Sent for each individual payment processed:

```javascript
app.post("/webhooks/monetarie_npc", async (req, res) => {
  const { event, data } = req.body;

  if (!validateSignature(req)) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  if (event === "payment_received") {
    console.log("Payment processed:", data.nosso_numero);
    console.log("Value:", data.valor_pago / 100);
    console.log("Credit date:", data.data_credito);

    // Update boleto in database
    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
    });

    // Create accounting entry
    await createAccountingEntry({
      type: "credit",
      amount: data.valor_pago,
      reference: data.nosso_numero,
      settlement_id: data.settlement_id,
      description: `Boleto settlement ${data.nosso_numero}`
    });
  }

  res.status(200).json({ received: true });
});
```

## Accounting Reconciliation

### ACMP615 File Structure

The ACMP615 file follows the FEBRABAN standard with three record types:

| Type | Name | Description |
|------|------|-------------|
| `00` | Header | File information (date, time, sender) |
| `20` | Detail | Individual payment records |
| `99` | Trailer | Totals and record count |

#### Header Record (Type 00)

| Position | Size | Field | Description |
|----------|------|-------|-------------|
| 1-2 | 2 | tipo_registro | "00" |
| 3-10 | 8 | data_geracao | Generation date (YYYYMMDD) |
| 11-16 | 6 | hora_geracao | Generation time (HHMMSS) |
| 17-24 | 8 | ispb_remetente | Sender ISPB |
| 25-30 | 6 | sequencial_arquivo | File sequence number |

#### Detail Record (Type 20)

| Position | Size | Field | Description |
|----------|------|-------|-------------|
| 1-2 | 2 | tipo_registro | "20" |
| 3-13 | 11 | nosso_numero | Boleto identifier |
| 14-30 | 17 | valor_pago | Amount paid (cents, left-padded zeros) |
| 31-38 | 8 | data_pagamento | Payment date (YYYYMMDD) |
| 39-46 | 8 | data_credito | Credit date (YYYYMMDD) |
| 47-49 | 3 | banco_recebedor | Bank COMPE code |
| 50-53 | 4 | agencia_recebedora | Receiving branch |
| 54-55 | 2 | codigo_movimento | Movement code |

#### Trailer Record (Type 99)

| Position | Size | Field | Description |
|----------|------|-------|-------------|
| 1-2 | 2 | tipo_registro | "99" |
| 3-8 | 6 | qtd_registros | Count of type 20 records |
| 9-26 | 18 | valor_total | Sum of values (cents) |

### Reconciliation Code

Example of reconciling payments with their records:

```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) {
        // Payment without corresponding boleto
        this.unmatched.push({
          payment,
          reason: "BOLETO_NOT_FOUND"
        });
        continue;
      }

      // Verify values
      const valorOriginal = boleto.amount_cents;
      const valorPago = payment.valor_pago;

      if (valorPago !== valorOriginal) {
        // Value discrepancy
        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) {
      // Higher payment - could be interest/penalty
      return "INTEREST_PENALTY";
    } else if (diferenca < 0) {
      // Lower payment - could be discount
      const percentual = Math.abs(diferenca / valorOriginal) * 100;
      if (percentual <= 10) {
        return "EARLY_PAYMENT_DISCOUNT";
      }
      return "PARTIAL_PAYMENT";
    }

    return "EXACT_VALUE";
  }

  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
    };
  }
}

// Usage
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("Reconciliation completed:");
  console.log("Match rate:", report.summary.match_rate);

  return report;
}
```

## Handling Unmatched Records

When a payment doesn't find a corresponding boleto:

```javascript
async function handleUnmatchedPayments(unmatchedPayments) {
  for (const { payment, reason } of unmatchedPayments) {
    console.warn(`Unmatched payment: ${payment.nosso_numero}`);
    console.warn(`Reason: ${reason}`);
    console.warn(`Value: R$ ${payment.valor_pago / 100}`);

    // Create investigation ticket
    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
    });

    // Try alternative search
    const possibleMatch = await searchAlternativeMatches(payment);

    if (possibleMatch) {
      console.log(`Possible match found: ${possibleMatch.nosso_numero}`);
      await flagForManualReview(payment, possibleMatch);
    }
  }
}

async function searchAlternativeMatches(payment) {
  // Search by similar value
  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;
}
```

## Daily Report Generation

Example of consolidated daily report:

```javascript
async function generateDailySettlementReport(date) {
  // Fetch day's cycles
  const cycles = await db.query(`
    SELECT * FROM settlement_cycles
    WHERE cycle_date = $1
    ORDER BY cycle_type ASC
  `, [date]);

  // Fetch payments
  const payments = await db.query(`
    SELECT * FROM payments
    WHERE DATE(paid_at) = $1
  `, [date]);

  // Consolidate data
  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),
      by_channel: groupByChannel(payments),
      by_bank: 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
    }
  };

  // Format values in currency
  report.totals.total_valor_formatted = formatCurrency(report.totals.total_valor);

  // Save report
  await saveReport("settlement_daily", date, report);

  // Send to finance team
  await sendReportEmail(report, ["finance@company.com"]);

  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"
  });
}
```

## Monitoring and Alerts

### Recommended Metrics

Configure alerts for the following situations:

| Metric | Threshold | Action |
|--------|-----------|--------|
| Match rate | < 95% | Yellow alert - investigate |
| Match rate | < 90% | Red alert - immediate action |
| Processing time | > 15 min | Check FTP performance |
| File not found | 30 min after cycle | Contact Nuclea |
| Value discrepancies | > 5% of boletos | Review interest/discount rules |

### Monitoring Example

```javascript
class SettlementMonitor {
  constructor() {
    this.thresholds = {
      matchRate: { warning: 95, critical: 90 },
      processingTime: { warning: 10, critical: 15 }, // minutes
      discrepancyRate: { warning: 3, critical: 5 } // percentage
    };
  }

  async checkSettlementHealth(settlementId) {
    const settlement = await getSettlement(settlementId);
    const alerts = [];

    // Check match rate
    const matchRate = (settlement.matched / settlement.total_boletos) * 100;
    if (matchRate < this.thresholds.matchRate.critical) {
      alerts.push({
        severity: "critical",
        metric: "match_rate",
        value: matchRate,
        message: `Critical match rate: ${matchRate.toFixed(2)}%`
      });
    } else if (matchRate < this.thresholds.matchRate.warning) {
      alerts.push({
        severity: "warning",
        metric: "match_rate",
        value: matchRate,
        message: `Match rate below expected: ${matchRate.toFixed(2)}%`
      });
    }

    // Check processing time
    const processingTime = (
      new Date(settlement.completed_at) - new Date(settlement.started_at)
    ) / 60000; // minutes

    if (processingTime > this.thresholds.processingTime.critical) {
      alerts.push({
        severity: "critical",
        metric: "processing_time",
        value: processingTime,
        message: `Critical processing time: ${processingTime.toFixed(0)} min`
      });
    }

    // Check discrepancies
    const discrepancyRate = (settlement.discrepancies / settlement.total_boletos) * 100;
    if (discrepancyRate > this.thresholds.discrepancyRate.warning) {
      alerts.push({
        severity: "warning",
        metric: "discrepancy_rate",
        value: discrepancyRate,
        message: `Discrepancy rate: ${discrepancyRate.toFixed(2)}%`
      });
    }

    // Send alerts
    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") {
      // Send SMS/call to on-call
      await sendCriticalAlert({
        settlement_id: settlementId,
        alert
      });
    }

    // Log in monitoring system
    await logAlert(alert, settlementId);
  }
}

// Usage after each cycle
async function postSettlementCheck(settlementId) {
  const monitor = new SettlementMonitor();
  const health = await monitor.checkSettlementHealth(settlementId);

  console.log("Settlement status:", health.status);

  if (health.alerts.length > 0) {
    console.log("Alerts:");
    health.alerts.forEach(a => console.log(`  - ${a.message}`));
  }

  return health;
}
```

## Implementation Checklist

### Initial Configuration
- [ ] Nuclea FTP credentials configured
- [ ] Processing times defined (08:35, 16:35)
- [ ] Webhook endpoint configured with Central
- [ ] Database prepared to store cycles

### Webhooks
- [ ] Handler for `settlement_completed`
- [ ] Handler for `payment_received`
- [ ] HMAC signature validation
- [ ] Response within 30 seconds

### Reconciliation
- [ ] ACMP615 file parser implemented
- [ ] Match logic by `nosso_numero`
- [ ] Value discrepancy handling
- [ ] Queue for unmatched payments

### Accounting
- [ ] Accounting system integration
- [ ] Automatic credit entries
- [ ] Daily reports configured
- [ ] Value auditing

### Monitoring
- [ ] Match rate alerts
- [ ] Processing time alerts
- [ ] Monitoring dashboard
- [ ] Debug logs

### Operations
- [ ] Manual reprocessing procedure
- [ ] Unmatched payment procedure
- [ ] Nuclea support contact configured
- [ ] Internal documentation updated

## Next Steps

- [Boleto Flow](/en/guides/boleto-flow) - Complete boleto lifecycle
- [Webhook Events](/en/webhooks/events) - Event reference
- [Settlements API](/en/api/settlements) - API reference
