Voltar para Central de Manuais
# Integração Cora — Fluxo de Aplicação (Faturamento + Contas a Pagar)
> Status: Proposta (design) · Autor: análise técnica · Base de código: Sisconnect 2.0 (FastAPI/DDD)
> Objetivo: a **Cora** assume os recebíveis (cobrança Pix + Boleto, hoje no Itaú) e passa a
> **executar os pagamentos** de Contas a Pagar, integrada ao motor financeiro que já existe.
---
## 0. TL;DR
- **Recebíveis:** a emissão de boleto no `faturar()` deixa de chamar o **Itaú** e passa a chamar a
**Cora** (`POST /v2/invoices/`), que devolve **Boleto + Pix** no mesmo título.
- **E-mail ao cliente:** hoje anexa um "resumo" de boleto extraindo a linha digitável do JSON do Itaú.
Com a Cora, anexa o **PDF real do boleto** (`bank_slip.url`) e inclui **Pix copia-e-cola + QR** no corpo.
- **Baixa automática:** o **webhook da Cora** (`invoice.paid`) dá baixa na `Transacao` + `ContaReceber`
e lança a **entrada** no `FluxoCaixa` — hoje a baixa de recebível é manual.
- **Contas a Pagar:** a `liquidar_conta()` hoje só mexe no **ledger interno**. Com a Cora, ela **inicia
o pagamento** (`POST /transfers/initiate`), que **exige aprovação no app Cora**, e a baixa efetiva
ocorre na confirmação (webhook).
- Tudo em **centavos** (ADR-001), atrás de **feature flags** por provedor, com rollout em fases.
---
## 1. Princípios e invariantes
| Princípio | Como se aplica |
|---|---|
| **Centavos inteiros** (ADR-001) | Cora usa centavos em `amount`/`balance`/extrato — casa direto com o schema (`Integer`). |
| **`id_banco` como discriminador** | `Transacao.id_banco` / `ContaPagar.id_conta_bancaria` apontam para uma `ContaBancaria` (`app_contas`). **A conta Cora é um registro em `app_contas`.** |
| **Feature flag por provedor** | `COBRANCA_PROVIDER=itau\|cora` e `PAGAMENTO_PROVIDER=interno\|cora`. Itaú/manual seguem como rede de segurança. |
| **Idempotência** | Emissão usa `Idempotency-Key`; `code` = referência interna (`CR-<id_transacao>`); webhook é idempotente. |
| **Strangler Fig** (ADR-009) | Cora nasce no 2.0; nada do legado PHP é tocado. |
---
## 2. Estado atual — mapa ponto a ponto
### 2.1 Recebíveis — `FaturamentoService.faturar()`
Arquivo: `app/services/faturamento_service.py` · Gatilho: `POST /api/v1/faturamento/processar`
(flags `gerar_boleto`, `enviar_email`).
| # | Passo | O que faz | Observação p/ Cora |
|---|---|---|---|
| 1 | Validar | Carrega CT-es (`ids_ctes`) e o `cliente` (tomador) com cidade/UF | Fonte dos dados do sacado da cobrança Cora |
| 2 | Criar fatura | `ContaReceber(valor=total_ctes, valor_pago=total_adt, situacao, data_vencimento)` | Cabeça do recebível |
| 3 | Vincular CT-es | Cria `ContaReceberCte` por CT-e | — |
| 4 | Baixar adiantamentos | Marca `AdiantamentoCliente.id_conta_receber` | — |
| 5 | **Transação/boleto** | Cria `Transacao` (valor, venc., `nosso_numero=zfill(8)`, juros/multa). **Se `gerar_boleto`:** monta payload Itaú (`build_registrar_boleto_payload`) e chama `ItauClient().registrar_boleto()`. Sucesso → `status="registrado"`, `json_envio`, `json_retorno` | **← PONTO DE TROCA PRINCIPAL** |
| 6 | Marcar fila | `_marcar_fila_como_faturada` | — |
| 7 | **E-mail** | Se `enviar_email`, `FaturamentoEmailService.enviar_email_fatura()` | Ver 2.2 |
| — | Commit / rollback | **Qualquer falha (boleto ou e-mail) → `rollback` de TUDO** | Risco: falha/timeout da Cora aborta a fatura inteira |
> ⚠️ **Invariante crítico:** a emissão do boleto e o envio de e-mail estão dentro da **mesma transação
> de banco**. Se a Cora falhar/der timeout, a fatura inteira é revertida (política herdada do legado).
### 2.2 E-mail ao cliente — `FaturamentoEmailService.enviar_email_fatura()`
Arquivo: `app/services/faturamento_email_service.py`
| # | Passo | O que faz | Observação p/ Cora |
|---|---|---|---|
| 1 | Regra de notificação | `pode_enviar_notificacao_faturamento` (flag cliente/grupo) | Mantém |
| 2 | Destinatários | Contatos com `id_setor_cargo == 2` (Financeiro); CC solicitantes; BCC financeiro@/ti@ | Mantém |
| 3 | Corpo | Renderiza `emails/email_fatura.html` (valor, vencimento, cedente) | **Adicionar bloco Pix (copia-e-cola + QR)** |
| 4 | Anexos CT-e | XML + DACTE de cada CT-e | Mantém |
| 5 | Anexo demonstrativo | PDF `documentos/fatura_pdf.html` | Mantém |
| 6 | **Anexo boleto** | Para cada `Transacao` `registrado`: `_extrair_linha_digitavel(json_retorno)` (busca chaves com "linha"+"digit") e gera `documentos/boleto_resumo_pdf.html` | **← QUEBRA COM CORA** |
> 🐛 **Incompatibilidade detectada:** `_extrair_linha_digitavel()` procura chaves contendo *"linha"* e
> *"digit"* (formato Itaú). A Cora devolve `payment_options.bank_slip.digitable` e `pix.emv` — **não casa**.
> Sem adaptação, o e-mail Cora sai **sem linha digitável**. Além disso a Cora já fornece o **PDF pronto**
> do boleto (`bank_slip.url`), melhor que o "resumo" gerado localmente.
### 2.3 Contas a Pagar — `PagarService`
Arquivo: `app/services/pagar_service.py`
| Método | O que faz | Observação p/ Cora |
|---|---|---|
| `criar_conta` | Cria `ContaPagar` (parcelas `relativedelta(months=i)`, centavos) | Origem da obrigação |
| `liquidar_conta` | Marca `situacao=pago/parcial`, cria `ContaPagarLancamento`, **atualiza `ContaBancaria.saldo_inicial`** e lança `FluxoCaixa(tipo='saida', origem='baixa_pagar')`. **Não paga de verdade — só ledger interno.** | **← PONTO DE TROCA (pagamento real)** |
| `estornar_conta` | Reverte saldo + `FluxoCaixa(entrada, origem='estorno_pagar')` | Espelhar em cancelamento Cora |
### 2.4 Motor bancário interno (já existe!)
Arquivo: `app/models/financeiro_auxiliar.py`
- **`ContaBancaria`** (`app_contas`): `banco`, `agencia`, `conta`, **`chave_pix`**, `titular`, `documento_titular`, `saldo_inicial` (centavos). → **A conta Cora vira um registro aqui.**
- **`FluxoCaixa`** (`app_fluxo_caixa`): `tipo_movimento` (entrada/saída), `origem`, `saldo_anterior/novo`, links p/ conta pagar/receber. → **Espelho interno do extrato Cora** (base da conciliação).
---
## 3. Pontos de integração (seams)
| Seam | Arquivo | Hoje (Itaú/manual) | Com Cora |
|---|---|---|---|
| **S1 — Emitir boleto** | `faturamento_service.py` passo 5 | `ItauClient().registrar_boleto()` | `CoraClient().emitir_cobranca()` (Boleto+Pix) |
| **S2 — Linha digitável no e-mail** | `faturamento_email_service.py` | `_extrair_linha_digitavel(json)` | `bank_slip.digitable` + anexar `bank_slip.url` + bloco Pix |
| **S3 — Baixa de recebível** | *(hoje manual)* | — | Webhook `invoice.paid` → baixa + `FluxoCaixa` entrada |
| **S4 — Pagar conta a pagar** | `pagar_service.liquidar_conta()` | Só ledger interno | `POST /transfers/initiate` + aprovação + webhook |
| **S5 — Conciliação** | *(hoje inexistente)* | — | `GET /bank-statement/statement` ↔ `FluxoCaixa` |
---
## 4. Fluxo de aplicação proposto
### 4.1 Emissão de cobrança (recebível)
```mermaid
sequenceDiagram
participant U as Usuário (Faturamento)
participant API as POST /faturamento/processar
participant FS as FaturamentoService.faturar()
participant PROV as CobrancaProvider (flag)
participant CORA as Cora API
participant DB as MySQL
U->>API: id_tomador, ids_ctes, gerar_boleto, enviar_email
API->>FS: FaturarRequest
FS->>DB: cria ContaReceber + ContaReceberCte + Transacao (nosso_numero)
alt gerar_boleto
FS->>PROV: emitir(cliente, valor, venc, juros/multa)
alt PROVIDER=cora
PROV->>CORA: POST /v2/invoices/ (Idempotency-Key, code=CR-<trans.id>)
CORA-->>PROV: id, pix.emv, bank_slip{digitable,url,our_number}
else PROVIDER=itau
PROV->>CORA: (ItauClient.registrar_boleto — legado)
end
PROV-->>FS: ok, dados_boleto
FS->>DB: Transacao.status=registrado, id_banco=<conta Cora>,\n nosso_numero=our_number, id_externo=invoice_id, json_retorno
end
opt enviar_email
FS->>FS: FaturamentoEmailService (ver 4.2)
end
FS->>DB: COMMIT (ou ROLLBACK em qualquer falha)
```
**Onde grava os dados da Cora na `Transacao`:**
- `id_banco` = id da `ContaBancaria` "Cora" (`app_contas`).
- `nosso_numero` = `payment_options.bank_slip.our_number`.
- `json_envio` / `json_retorno` = payload e resposta completos (contêm `emv`, `digitable`, `url`).
- `id_externo` **(coluna nova)** = `invoice_id` da Cora (índice p/ o webhook localizar rápido).
### 4.2 E-mail com boleto **+ Pix**
```mermaid
flowchart TD
A[enviar_email_fatura] --> B{Transacao registrada?}
B -- sim --> C[Ler json_retorno]
C --> D{Provider}
D -- cora --> E[digitable = payment_options.bank_slip.digitable]
E --> F[Baixar PDF boleto de bank_slip.url]
E --> G[emv = pix.emv → gerar QR PNG]
D -- itau --> H[_extrair_linha_digitavel legado + boleto_resumo_pdf]
F --> I[Anexar PDF boleto real]
G --> J[Inserir Pix copia-e-cola + QR no corpo]
H --> I
I --> K[enviar_email_html_com_anexos]
J --> K
```
Mudanças concretas:
- Nova função `extrair_dados_cobranca(json_retorno, provider)` → `{digitable, boleto_url, pix_emv}`.
- Anexar **PDF real** da Cora (download de `bank_slip.url`) em vez do `boleto_resumo_pdf.html`.
- Template `emails/email_fatura.html`: bloco condicional **Pix** (copia-e-cola + QR inline PNG).
### 4.3 Webhook de pagamento → baixa automática (recebível)
```mermaid
sequenceDiagram
participant CORA as Cora
participant WH as POST /api/v1/cora/webhook
participant SVC as BaixaRecebivelService
participant DB as MySQL
CORA->>WH: {event: invoice.paid, resource_id, code}
WH->>SVC: valida secret + evento
SVC->>DB: Transacao por id_externo (ou code=CR-<id>)
alt já baixada
SVC-->>WH: 200 (idempotente, ignora)
else
SVC->>DB: Transacao.status=pago, data_pagamento, valor_pago
SVC->>DB: ContaReceber.valor_pago += ; situacao=pago se quitado
SVC->>DB: FluxoCaixa(entrada, origem='baixa_receber', id_conta_bancaria=Cora)
SVC->>DB: ContaBancaria(Cora).saldo_inicial += valor
SVC->>DB: ContaReceberTransacaoLog(evento='pago_webhook', payload)
SVC-->>WH: 200
end
```
### 4.4 Contas a Pagar via Cora (com aprovação)
```mermaid
sequenceDiagram
participant U as Usuário (Contas a Pagar)
participant API as POST /pagar/liquidar
participant PS as liquidar_conta()
participant PROV as PagamentoProvider (flag)
participant CORA as Cora
participant APP as App Cora (aprovador)
participant WH as Webhook Cora
participant DB as MySQL
U->>API: id_conta_pagar, valor, id_conta_bancaria
API->>PS: ContaPagarBaixa
alt PAGAMENTO_PROVIDER=cora
PS->>PROV: resolver destino (fornecedor: chave_pix / dados bancários)
PROV->>CORA: POST /transfers/initiate (amount, destination, code)
CORA-->>PROV: status=INITIATED
PS->>DB: ContaPagar.situacao='aguardando_aprovacao', id_externo
APP->>CORA: aprova pagamento (manual)
CORA->>WH: transfer.paid
WH->>DB: situacao='pago', data_baixa, FluxoCaixa(saida,'baixa_pagar')
else PAGAMENTO_PROVIDER=interno
PS->>DB: baixa só no ledger (comportamento atual)
end
```
> A API **só inicia** a transferência; a **aprovação é manual no app Cora**. Portanto **não** se baixa a
> conta antes do webhook de confirmação — usa-se o estado intermediário `aguardando_aprovacao`.
### 4.5 Conciliação (extrato ↔ FluxoCaixa)
- Job/tela lê `GET /bank-statement/statement` da conta Cora e compara com `FluxoCaixa`.
- Marca lançamentos conciliados; sinaliza divergências (valor/data/ausência) para revisão.
- A tela **Banco Cora** já exibe o extrato real (entradas/saídas/saldo) — base visual da conciliação.
---
## 5. Modelo de dados — mudanças mínimas
| Tabela | Mudança | Motivo |
|---|---|---|
| `app_contas_receber_transacoes` | **+`id_externo` VARCHAR(60) NULL (index)** | `invoice_id` da Cora p/ o webhook localizar a baixa |
| `app_contas_receber_transacoes` | `id_banco` = id da `ContaBancaria` Cora | discriminador de provedor |
| `app_contas_pagar` | **+`id_externo` VARCHAR(60) NULL** e situação `aguardando_aprovacao` | rastrear a transferência Cora |
| `app_contas` | 1 registro "Cora" (com `chave_pix`, `documento_titular`) | conta bancária da Cora no motor |
| `app_fornecedores` | usar `chave_pix`/dados bancários já existentes | destino do pagamento |
Migrations em `app/migrations/mysql/` (mesmo padrão das existentes). `json_retorno`/`json_envio` já guardam o corpo completo — nada de coluna nova pra Pix/boleto.
---
## 6. Componentes novos (código)
```
app/integrations/cora_client.py (já existe: cobrança + saldo + extrato) ← +transferência
app/services/cora_boleto_payload.py (novo: cliente → payload /v2/invoices)
app/services/cobranca_provider.py (novo: seleciona itau|cora por flag) ← S1
app/services/pagamento_provider.py (novo: interno|cora) ← S4
app/services/baixa_recebivel_service.py (novo: baixa via webhook) ← S3
app/services/conciliacao_service.py (novo: extrato ↔ FluxoCaixa) ← S5
```
Adaptações: `faturamento_service.py` (S1), `faturamento_email_service.py` (S2),
`pagar_service.py` (S4), `api/v1/endpoints/cora.py` (webhook → serviços de baixa),
`emails/email_fatura.html` (bloco Pix).
---
## 7. Rollout por fases (cada uma atrás de flag, testável em stage)
| Fase | Entrega | Flag | Depende de |
|---|---|---|---|
| **0** ✅ | Tela **Banco Cora** (saldo/extrato/cobranças, read-only) | — | — |
| **1** | `CobrancaProvider` + `faturar()` emitindo na Cora; grava `Transacao` | `COBRANCA_PROVIDER=cora` (default `itau`) | **Auth Cora OK** |
| **2** | E-mail com **boleto real + Pix/QR** | herda Fase 1 | Fase 1 |
| **3** | **Webhook → baixa automática** de recebível + `FluxoCaixa` | — | Fase 1 |
| **4** | **Contas a pagar** via Cora (initiate + aprovação + webhook) | `PAGAMENTO_PROVIDER=cora` | Auth Cora OK |
| **5** | **Conciliação** extrato ↔ FluxoCaixa | — | Fase 3/4 |
---
## 8. Riscos e mitigações
| Risco | Mitigação |
|---|---|
| **Auth Cora 401** (bloqueio atual de emissão de token) | Destravar com suporte Cora **antes** da Fase 1. Nada além da Fase 0 funciona sem isso. |
| Falha/timeout da Cora aborta a fatura inteira (rollback do `faturar()`) | Emissão idempotente (`Idempotency-Key`); avaliar emitir **fora** da transação crítica (grava `Transacao` pendente e emite com retry/async) — trade-off vs. paridade legado, decidir por ADR. |
| Pagamento AP exige **aprovação manual** no app Cora | Estado `aguardando_aprovacao`; baixa efetiva **só** no webhook `transfer.paid`. |
| Divergência entre saldo interno e saldo Cora | Fase 5 (conciliação) + a tela Banco Cora mostra o saldo **real** da Cora. |
| Webhook duplicado / reentrante | Baixa idempotente (checa `status`/`id_externo` antes de aplicar). |
| Unidade do `balance` do endpoint `/saldo` (string) | Fonte de verdade = `end.balance` do extrato (centavos, documentado); `/saldo` é best-effort. |
---
## 9. Decisões em aberto (para alinhar)
1. **Emitir dentro ou fora da transação do `faturar()`?** (paridade legado × resiliência)
2. **Boleto + Pix sempre juntos**, ou por preferência do cliente (`app_formas_pagamento`)?
3. **Contas a pagar:** só Pix por chave, ou também TED por dados bancários do fornecedor?
4. Manter **Itaú como fallback** por quanto tempo após a virada?