# Dossiê de paridade profunda — PIX Automático / recorrência (pain.009 / 011 / 012 / 013 / 014)

Auditoria READ ONLY. Cada afirmação abaixo cita prova (arquivo:linha ou tabela.coluna). Sem prova = veredito INCONCLUSIVO. Escrita pt-br sem travessão.

Escopo: ciclo completo de recorrência do PIX Automático (Res BCB 505/2025) nas 5 mensagens ISO 20022:

- pain.009 (MndtInitnReq) solicitação de criação de autorização de recorrência
- pain.011 (MndtCxlReq) solicitação de cancelamento de autorização
- pain.012 (MndtAccptncRpt) retorno de aceite/rejeição de autorização (resposta ao 009/011)
- pain.013 (CdtrPmtActvtnReq) solicitação de agendamento de pagamento (parcela)
- pain.014 (CdtrPmtActvtnReqStsRpt) retorno do agendamento (resposta ao 013)

---

## 1. Arquitetura do LEGADO (com prova)

O legado separa em DOIS sistemas:

### 1.1 SPI (gateway BACEN) — recebe e responde ao BACEN

O `SPI.Core` é a cabine que fala com o BACEN. Ela RECEBE pain.009/011/013 (e camt.055) do BACEN e GERA as respostas pain.012/014/camt.029. Ela NÃO guarda a regra de negócio da recorrência: DELEGA por HTTP ao "multiliquidação".

Prova do roteamento de retorno do BACEN por tipo:
- `SPI.Core.Worker.TratamentoRetornoApiBacen.Infrastructure.decompiled.cs:1017-1029` roteia `ImportaPAIN009`, `ImportaPAIN012`, `ImportaPAIN011`, `ImportaPAIN013`.
- `ImportaPAIN009` (`:2690`), `ImportaPAIN011` (`:2280`), `ImportaPAIN012` (`:2343`), `ImportaPAIN013` (`:2800`) fazem parse do XML, montam DTO (`PAIN009DTO`/`PAIN011DTO`/`PAIN012DTO`), numeram a operação e persistem em `MessageFull`/`Payment`.

Prova da geração das respostas ao BACEN:
- `EfetuaValidacaoRecorrencia` (pain.009 → pain.012) em `SPI.Core.Worker.TratamentoRetornoApiBacen.Infrastructure.decompiled.cs:3874-3928`: valida via `ValidacaoRecorrenciaUseCase`, monta `PAIN012DTO` com `Aceite = resultadoValidacao.OK`, `statusRecorrencia = OK ? "PDNG" : null`, `MotivoRejeicao = codRetorno`.
- `EfetuaValidacaoCancelamentoRecorrencia` (pain.011 → pain.012) em `:3764-3830`: valida via `ValidacaoCancelamentoRecorrenciaUseCase`, `statusRecorrencia = OK ? "CCLD" : null`.
- `EfetuaValidacaoSolicitacaoPgto` (pain.013 → pain.014) em `:3930-3975`: valida via `ValidacaoSolicitacaoPgtoUseCase`, `Status = ok ? ValidadoCreditor : Rejeitado`, `Reason = enumReasonCode`.
- `EfetuaValidacaoCancelamentoSolicitaPgto` (camt.055 → camt.029) em `:3831-3872`.

Prova do reencaminhamento ao multiliquidação (RetornoLegado):
- `SPI.Core.Multiliquidacao.Application.decompiled.cs:318-330` roteia por `<MsgDefIdr>pain.012` → `InsereRetornoPAIN012` (`:471`), `pain.014` → `InsereRetornoPAIN014` (`:595`), `camt.029` → `InsereRetornoCAMT029`.
- `InsereRetornoPAIN012` POSTa `AutorizaRecorrenciaDTO` em `URL + /AutorizarRecorrencia` (`:552`); `InsereRetornoPAIN014` POSTa `ConfirmacaoSolicitacaoPgtoDTO` em `URLSEC + ConfirmarSolicitarPagamento` (`:645`).
- A validação da pain.009 chama `SolicitarAutorizacaoRecorrencia` do multi (`SPI.Core.Multiliquidacao.Application.decompiled.cs:1969` `_httpClient.PostAsync(paramMulti.URL + "SolicitarAutorizacaoRecorrencia", ...)`); pain.011 chama `CancelarAutorizacaoRecorrencia` (`:942`); pain.013 chama `SolicitarPagamento` (`:2144`).

Gate global do produto:
- `ValidacaoRecorrenciaUseCase.Validacao` (`SPI.Core.Multiliquidacao.Application.decompiled.cs:1892-1895`): se parâmetro `UsaPixAutomatico != "1"` retorna erro `AP15 - IF não utiliza PIX Automático`. Mesmo gate em `ValidacaoCancelamentoRecorrenciaUseCase` (`:880`) e `ValidacaoSolicitacaoPgtoUseCase` (`:2062`, código `AB10`).

### 1.2 Multiliquidação (CRK) — dono da regra de negócio da recorrência

Endpoints (controller): `Multiliquidacao.Web.Api.decompiled.cs:1237-1291`
- `POST SolicitarAutorizacaoRecorrencia` (pain.009 lado recebedor / persistência)
- `POST AutorizarRecorrencia` (aceite/rejeição do pagador → gera pain.012)
- `POST CancelarAutorizacaoRecorrencia` (cancelamento → gera pain.011)
- `POST SolicitarPagamento` / `ConfirmarSolicitarPagamento` (agendamento pain.013/014)
- `POST CancelarSolicitacaoPagamento` / `ConfirmarCancelamentoSolPagto` (camt.055/029)

Implementações: `Multiliquidacao.Core.Application.decompiled.cs`
- `SolicitarAutorizacaoRecorrencia` (`:7848`) valida, gera IDs, monta `SPI_PAIN009`, envia ao SPI (`_sendSPI.AddRecorrencia`) OU envia ao legado por conta (`IcIntegracaoConta`), e faz UPSERT em `TB_RECORRENCIA` com `IdSituacao = 4` (SOLICITADA), `TpJornada = 1`, `TpOrigem = 'M'`.
- `AutorizarRecorrencia` (`:7956`) valida (recorrência existe, não expirada AP07, IBGE obrigatório em aceite AP04, ISPB AP11/AP12), monta `SPI_PAIN012` com `statusRecorrencia = status ? (flCancelamento ? "CCLD" : "CFDB") : null`, `TipoJornada = "AUT" + rec.TpJornada`, e atualiza `IdSituacao` para 5 (APROVADA/CFDB) ou 3 (CANCELADA/CCLD) ou 1 (rejeitado).
- `CancelarAutorizacaoRecorrencia` (`:8106`) valida (existe AP04, ISPB AP11/AP12, já cancelada CH16), monta `SPI_PAIN011`, atualiza `IdSituacao = 3` (CANCELADA).

### 1.3 Modelo de dados do legado — TB_RECORRENCIA

`crk_multtiliquidacao.dbo.TB_RECORRENCIA` (colunas provadas por `sys.columns`):
```
ID_RECORRENCIA char(29) NOT NULL      -- <C|R><R|N> + ISPB(8) + yyyyMMddHHmm(12) + rand(7)
TP_ORIGEM char(1) NOT NULL            -- 'M' multiliquidacao
ID_PERIODICIDADE tinyint NOT NULL     -- 1 SEMANAL, 2 MENSAL, 3 TRIMESTRAL, 4 SEMESTRAL, 5 ANUAL
ID_RETENTATIVA tinyint                -- 1 NAO_PERMITE, 2 PERMITE_3R_7D (Res 505)
DT_INICIAL date NOT NULL              -- primeiro pagamento
DT_FINAL date
VL_RECORRENCIA decimal
VL_MINIMO decimal                     -- recorrencia de valor variavel
VL_MAXIMO decimal
NM_RECEBEDOR / NR_CPFCNPJRECEBEDOR / NR_SPBRECEBEDOR
NR_CPFCNPJPAGADOR / NR_SPBPAGADOR / NR_AGENCIAPAGADOR / NR_CONTAPAGADOR
NM_DEVEDOR / NR_CPFCNPJDEVEDOR        -- devedor (obligator) != pagador (debtor)
NR_CONTRATO varchar(35) NOT NULL      -- idInternoOrigem
TX_OBJETO_VINCULO varchar(35)         -- objeto de vinculo
DH_CRIACAO_RECORRENCIA / DH_CRIACAO_SOLICITACAO / DH_EXPIRACAO
ID_INICPAGTO bigint
ID_TIPOSITUACAO tinyint NOT NULL      -- 1 CRIADA,2 ALTERADA,3 CANCELADA,4 SOLICITADA,5 APROVADA,6 REJEITADA,7 EXPIRADA
DH_ULTIMA_ALTERACAO / TP_JORNADA / ID_TIPOCONTAPAGADOR / ID_TIPOCONTARECEBEDOR
```
Enums provados em `Multiliquidacao.Core.General.decompiled.cs`: periodicidade `:6121`, retentativa `:6129`, jornada `:6527`, situação `:6730`, EMV `:6759`.

Dados vivos (3 recorrências, todas expiradas), `crk_multtiliquidacao.dbo.TB_RECORRENCIA`:
```
CR0310008956858... M 4 2 7 1 12345678 vpS8mD5R 100.00   -- C=OpenFinance, R=retentativa
RN1234567820250... Q 2 1 7 NULL 12345678 NULL   60.00   -- R nao-OF, N sem retentativa
RR1626696181658... M 2 2 7 1 12345678 99999003 100.00   -- R nao-OF, R retentativa
```
Histórico de transições em `crk_multtiliquidacao.dbo.TB_HISTSITUACAORECORRENCIA` (8 linhas).

Flags derivadas do ID (prova `Multiliquidacao.Core.General.decompiled.cs:226 GetRecorrenciaId` + `SPI.Core.Multiliquidacao.Application.decompiled.cs:1931-1932`):
- 1º char do IdRecorrencia = `C` → OpenFinance, senão `R`
- 2º char = `R` → permite retentativa, senão `N`

Geradores de ID (`Multiliquidacao.Core.General.decompiled.cs:190-233`): idRecorrencia (`C|R`+`R|N`+...), idSolicitacaoRecorrencia (`SC`...), idInformacaoStatus (`IS`...), idCancelamentoRecorrencia (`IC`...), idCancelamentoAgendamento (`CA`...).

### 1.4 Códigos de rejeição do legado (com prova)

- pain.009/011 → pain.012: lista `AC01|AC04|AC06|AG12|AM05|AP01..AP15|CH16|DS27|MD01|MD20|RC09|RC10`, fallback `CH16` (`SPI.Core.Worker.TratamentoRetornoApiBacen.Infrastructure.decompiled.cs:3777-3779` e `:3898-3900`).
- pain.013 (camt.055) → camt.029: lista `AB09|AB10|AG12|CH16|CRNC|DENC|DS27|FBRD|FF08|PRJL|RC09|RC10`, fallback `AB09/AB10` (`:3841-3845`).
- pain.013 → pain.014: `enumReasonCode`, fallback `AB10` (`:3980-3983`).

### 1.5 Conversão de frequência do legado (com prova)

- `SPI` recebe do BACEN e converte (`SPI.Core.Multiliquidacao.Application.decompiled.cs:2018` `ConverteFrequencia`): `MIAN→SEMESTRAL`, `MNTH→MENSAL`, `QURT→TRIMESTRAL`, `WEEK→SEMANAL`, default→`ANUAL`.
- `Multi` gera para o SPI (`Multiliquidacao.Core.Application.decompiled.cs:7834` `ConvertFrequenciaMultiToSPI`): `SEMANAL→WEEK`, `MENSAL→MNTH`, `ANUAL→YEAR`, `TRIMESTRAL→QURT`, `SEMESTRAL→MIAN`.

Ou seja: os códigos ISO usados pelo legado são `WEEK, MNTH, QURT, MIAN, YEAR`. Não há DAIL nem FRTN (quinzenal).

---

## 2. Arquitetura do NOSSO sistema (com prova)

Nosso PIX Automático vive em `SpiService.Recurrences` (contexto), com schema, controller, 3 workers e builders/parser ISO. Registro-mestre de módulos:

- Contexto `apps/spi_service/lib/spi_service/recurrences.ex` (740 linhas)
- Schema recorrência `apps/spi_service/lib/spi_service/recurrences/recurrence.ex`
- Schema instrução `apps/spi_service/lib/spi_service/recurrences/instruction.ex`
- Worker agendador `.../recurrences/scheduler_worker.ex` (GenServer 60s)
- Worker executor `.../recurrences/execution_worker.ex` (consumer NATS `monetarie.spi.recurrence.execute`)
- Worker notificação `.../recurrences/notification_worker.ex` (pré-débito diário)
- Controller `apps/spi_service/lib/spi_service_web/controllers/recurrence_controller.ex`
- Rotas `apps/spi_service/lib/spi_service_web/router.ex:161-169`
- Builders ISO `apps/shared/lib/shared/bacen/iso20022/message_builder.ex` (build_pain009/011/012/013/014)
- Parser ISO `apps/shared/lib/shared/bacen/iso20022/message_parser.ex:99-108`
- Client BACEN `apps/shared/lib/shared/bacen/spi_client.ex:297-435`
- Inbound `apps/spi_service/lib/spi_service/workers/inbound_processor.ex:213` (só pain.014)
- Front `pix/frontend/admin/src/views/RecurrenceListView.vue` + `PixAutomaticoStatusBadge.vue`
- Workers montados no supervisor `apps/spi_service/lib/spi_service/workers/supervisor.ex:36-38`

### 2.1 Modelo (nós = ORIGINADOR / lado recebedor apenas)

Nosso fluxo trata a instituição como CRIADORA do mandato (recebedor/creditor):
- `create_recurrence` (`recurrences.ex:59`) insere e dispara `send_pain009` (`:502`).
- `update_recurrence` (`:190`) dispara `send_pain011` (`:529`).
- `cancel_recurrence` (`:340`) dispara `send_pain012` (`:549`).
- Execução da parcela: `execute_pending_instructions` (`:398`) cria `Instruction` e publica `monetarie.spi.recurrence.execute`; `ExecutionWorker.execute_payment` (`execution_worker.ex:253`) bloqueia saldo e envia `pacs.008` DIRETO (`build("pacs.008")`, `:287`), NÃO pain.013.

Schema `monetarie_spi.recurrences` (DDL `apps/shared/priv/repo/sql/mon_pix_schema_clean.sql:5927`):
```
id uuid, mandate_id varchar(35), participant_ispb, debtor_*, creditor_*,
amount numeric(18,2), currency, frequency varchar(20), start_date, end_date,
next_execution, execution_count, max_executions, status varchar(20),
description, original_message_id, last_modification_id, last_cancellation_id,
reason_code, reason_description, created_by, last_execution_at,
cancelled_at, paused_at, resumed_at
```
Status (`recurrence.ex:30`): `PENDING_APPROVAL ACTIVE PAUSED CANCELLED COMPLETED EXPIRED`.
Frequências (`recurrence.ex:31`): `DAIL WEEK FRTN MNTH QURT SEMI YEAR`.

### 2.2 Roots ISO dos builders (corretos)

`message_builder.ex`: build_pain011 usa `<MndtCxlReq>` (`:1044`), build_pain012 usa `<MndtAccptncRpt>` (`:1119`), build_pain013 usa `<CdtrPmtActvtnReq>`-family, build_pain014 usa `<CdtrPmtActvtnReqStsRpt>` (`:2002` no book v111). O parser detecta corretamente MndtInitnReq→009, MndtAccptncRpt→012, MndtCxlReq→011, CdtrPmtActvtnReqStsRpt→014, CdtrPmtActvtnReq→013 (`message_parser.ex:99-108`).

Cobertura de versão XSD (`spi_version.ex:126-130`): pain.009 1.1, pain.011 1.3, pain.012 1.3, pain.013 2.2, pain.014 2.3 (nossa cobertura é igual ou mais nova que o legado; o legado registra até pain.014 2.2 em `CRK_SPIDOMINIO.dbo.SpiCadMessageVigencia`).

---

## 3. Comparação mensagem a mensagem

### pain.009 (criação de autorização)
- Legado: recebe pain.009 do BACEN (pagador PSP) → valida no multi (`SolicitarAutorizacaoRecorrencia`) → persiste recorrência `IdSituacao=4` → responde pain.012 com `Aceite` + status `PDNG` (aprovado) ou `MotivoRejeicao` (AC01/AP04/CH16...). Também pode ORIGINAR (recebedor) via `AddRecorrencia`.
- Nós: só ORIGINAMOS (creditor). `send_pain009` monta XML e chama `SpiClient.create_recurrence`. Ao receber HTTP OK, marcamos `status = "ACTIVE"` IMEDIATAMENTE (`recurrences.ex:511-516`), sem esperar a pain.012 de aceite do pagador. NÃO recebemos pain.009 inbound. NÃO geramos pain.012 de resposta.

### pain.011 (cancelamento de autorização)
- Legado: recebe pain.011 do BACEN → valida (`CancelarAutorizacaoRecorrencia`, checa AP04/AP11/AP12/CH16-já-cancelada) → `IdSituacao=3` → responde pain.012 `statusRecorrencia=CCLD`. Também origina pain.011 quando o pagador cancela.
- Nós: `update_recurrence` (semântica "amendment") dispara `send_pain011` que monta `<MndtCxlReq>` e chama `SpiClient.update_recurrence`. Ou seja, um "update" no nosso modelo emite um pedido de CANCELAMENTO. NÃO recebemos pain.011 inbound; NÃO geramos pain.012 em resposta.

### pain.012 (retorno de aceite/rejeição)
- Legado: GERA pain.012 como resposta a 009/011 (`EfetuaValidacaoRecorrencia`/`EfetuaValidacaoCancelamentoRecorrencia`), e CONSOME pain.012 recebido (`ImportaPAIN012` + `InsereRetornoPAIN012` → `/AutorizarRecorrencia`), atualizando `IdSituacao`.
- Nós: `cancel_recurrence` dispara `send_pain012` que monta `<MndtAccptncRpt>` e chama `SpiClient.cancel_recurrence` (`spi_client.ex:322-326`). Isto usa a mensagem de RESPOSTA (acceptance report) como se fosse um comando de cancelamento OUTBOUND. NÃO consumimos pain.012 inbound para reconciliar status (nenhum handler no `inbound_processor.ex` dispatcher `:199-217`).

### pain.013 (agendamento da parcela)
- Legado: recebe pain.013 do BACEN → `ImportaPAIN013` persiste → valida no multi (`SolicitarPagamento`, monta `SolicitacaoPagtoSpiDTO` com `idRecorrencia`, `dtVencimento`, `tpFinalidade`, `DtHrAceite`, `DocFiscal`, `Tributos`) → responde pain.014.
- Nós: `build_pain013` EXISTE no builder mas NÃO é chamado por nenhum fluxo de produção (grep de callers vazio fora de builder/testes). A execução da parcela pula o protocolo pain.013/014 e envia `pacs.008` direto do lado recebedor (`execution_worker.ex:287`).

### pain.014 (retorno do agendamento)
- Legado: GERA pain.014 (`EfetuaValidacaoSolicitacaoPgto`, `Status=ValidadoCreditor|Rejeitado`, `Reason=enumReasonCode`) e CONSOME pain.014 (`ImportaPAIN014` + `InsereRetornoPAIN014` → `/ConfirmarSolicitarPagamento`).
- Nós: `build_pain014` EXISTE mas NÃO é chamado (sem callers). RECEBEMOS pain.014 inbound (`inbound_processor.ex:213 → process_payment_activation_status :3179`) porém apenas publicamos evento NATS `recurrence.activation_status` que NÃO tem consumidor (grep vazio) e NÃO atualiza a recorrência/instrução.

---

## 4. GAPS (registro-mestre)

Ver objeto estruturado. Resumo:
- Lado PAGADOR (inbound) do PIX Automático ausente por inteiro: recepção de pain.009/011/013 e geração de pain.012/014.
- Geração de pain.012/014 ausente (builders existem, mas nunca são disparados como resposta).
- Reconciliação de status por pain.012/014 inbound ausente (marcamos ACTIVE no ACK de transmissão, não no aceite).
- Semântica invertida no wiring outbound (update→pain.011 cancelamento; cancel→pain.012 acceptance report).
- Regras de negócio Res 505 ausentes na nossa borda: retentativa 3R/7D, jornadas 1..4, valor mínimo/máximo (recorrência variável), devedor != pagador, objeto de vínculo/contrato, expiração (AP07), IBGE em aceite (AP04), gate `UsaPixAutomatico`/AP15, mapa de reason codes (AC01/AP04/CH16/AB10...).
- Divergência de frequência: nós usamos `SEMI` e `DAIL`/`FRTN`; o legado/BACEN usa `MIAN` para semestral e não tem diário/quinzenal.

---

## 5. Itens COBERTOS (confiança)

- Builders ISO 5/5 presentes e com roots corretos (pain.009/011/012/013/014) e versões XSD iguais ou mais novas que o legado.
- Parser detecta os 5 roots com a semântica ISO correta (inclui nota interna de que a rotulagem anterior estava trocada).
- CRUD de mandato + ciclo pause/resume/cancel + histórico de execuções (instructions) implementados e expostos por API REST e tela Vue.
- Scheduler + executor com bloqueio de saldo (debit-then-send), retentativa com backoff exponencial por instrução, e outbox atômico (ADR-008) para os eventos.

---

## 6. Notas de método

- O legado é primariamente lado-PAGADOR (recebe 009/011/013, responde 012/014) com capacidade de originar (lado recebedor) via multiliquidação. Nós somos apenas lado-RECEBEDOR/originador e ainda assim sem o protocolo pain.013/014 (usamos pacs.008 direto).
- INCONCLUSIVO: valor exato aceito pelo XSD BACEN para `SEMI` vs `MIAN` não foi aberto nesta auditoria; a divergência é comprovada pela tabela de conversão do legado, mas a rejeição efetiva do BACEN a `SEMI` precisa de conferência no XSD `pain.*.spi` (enum de frequência).
