# Construir Mensagem: tabela-verdade de envio dos 27 tipos (2026-07-07)

## Contexto

O envio do Construir Mensagem (`POST /api/v1/messages/send`, PIX admin) despacha por `dispatch_message/3` em `pix/backend/apps/settlement_service/lib/settlement_service_web/controllers/message_controller.ex`. Auditoria completa das cláusulas dedicadas provou contaminação sistemática: 8 cláusulas delegavam a funções do `SpiClient` que postavam paths FICTÍCIOS `/pix/v2/<tipo>` (inexistentes no ICOM), SEM assinatura XMLDSig, no canal do auto-route por path (errado). Era a mesma causa raiz do incidente recorrente do camt.060 (corrigido no fallback genérico em `681cf9df`).

## Caminho da verdade comprovada

O único caminho de envio com HTTP 201 no acervo vivo (`monetarie_audit.xml_audit_logs`) é:

1. assinar XMLDSig SPI (3 References, `XmlSigner.sign_spi/2`);
2. `POST /api/v1/in/{ispb}/msgs` (endpoint genérico REAL do ICOM);
3. canal resolvido pelo `ChannelRouter` a partir do `message_type` (financeiras no primário; camt.060 FORÇADA ao primário porque no secundário o BACEN rejeita async via admi.002 "canal incorreto").

É o mesmo caminho do fluxo real de pagamento: `SpiService.Workers.OutboundSender` assina (`sign_outbound_xml`, outbound_sender.ex:271) e posta em `message_path/1` = `"/api/v1/in/#{Client.ispb()}/msgs"` (outbound_sender.ex:455). No `SpiClient`, esse funil é `send_signed_message/3` (spi_client.ex:230), com `force_proven_channel/2` (spi_client.ex:249) para o camt.060.

## O que foi corrigido nesta frente

1. `SpiClient.send_payment` (pacs.008), `request_return` (pacs.004), `create_recurrence` (pain.009), `update_recurrence` (pain.011), `cancel_recurrence` (pain.012), `confirm_return` (pain.013) e `send_investigation_resolution` (camt.029) deixaram de postar `/pix/v2/*` sem assinatura e passaram a delegar a `send_signed_message/3`. Isso corrige também o caller vivo `SpiService.Recurrences` (pain.009/011/012).
2. `SpiClient.acknowledge_system_event` e `build_admi004_request` foram DELETADOS: caminho inventado (path fictício `/pix/v2/admi004`, sem assinatura, estrutura `<SysEvtAck>` que nem existe no XSD `admi.004.spi.1.2`). A cláusula dedicada de admi.004 no dispatch foi removida.
3. O catálogo (`MessageFormCatalog`) ganhou o campo `sendable` (devolvido em `GET /api/v1/messages/types` para o front esconder ou desabilitar o botão). O controller recusa `send` de tipo não enviável com 422 e mensagem clara (message_controller.ex:141), ANTES de montar, assinar ou tocar o BACEN.
4. `SpiClient.echo/1` (pibr.001) passou a assinar pelo mesmo seam de teste (`:shared, :spi_sign_fun`) usado por `send_signed_message/3`, permitindo prova automatizada do corpo assinado no fio.

## Critério de enviabilidade (sem inferência)

Um tipo é enviável se o participante o emite em algum fluxo do SPI. Dupla evidência usada, com fonte autorizada:

- Exemplos oficiais do catálogo v5.12.1 (`mwbank/md/v5.12.1/exemplos/<tipo>/*.xml`): `AppHdr/Fr` = 00038166 (BACEN) em TODOS os exemplos indica emissão exclusiva do SPI; `Fr` = ISPB de participante (99999010, 11111111, 22222222, 66666666, 99999009) prova emissão pelo participante.
- LegadoPIX (`mwbank/LegadoPIX/SPI/src`): existência de montador de envio em `SPI.Core.Mensageria.Application.Book.v111.Rules.MontaMsg.Impl.<TIPO>` prova que o participante emite; tipo presente APENAS em `SPI.Core.Application.UseCases.SPI.RetornoLegado.Handlers.<TIPO>` é tratado somente como entrada.
- Reforço pontual do xlsx oficial (`mwbank/md/v5.12.1/xlsx/<TIPO>.xlsx`): a descrição do `CreDtTm` de camt.014/025/052/053/054 diz apenas "emissão ao participante" (unidirecional SPI para PSP); no ADMI004, `EvtCd` e `EvtDesc` são "Campo de uso exclusivo do SPI".

## Tabela-verdade (27 tipos)

Caminho enviável = `MessageBuilder.build` + assinatura XMLDSig (seam `spi_sign_fun` em teste) + `POST /api/v1/in/{ispb}/msgs`. Canal: "primário (provado)" = regra com evidência; "roteador" = `ChannelRouter.route(tipo)` (financeira: primário; não financeira: secundário), a validar vivo. Teste = caso correspondente em `message_controller_send_truth_table_test.exs` (mc = message_controller.ex, sc = spi_client.ex).

| Tipo | Caminho de envio | Canal | Evidência | Enviável? | Teste |
|---|---|---|---|---|---|
| pacs.008 | mc:944 `SpiClient.send_payment` (sc:107) -> `send_signed_message` | primário (financeira, roteador) | fluxo real: OutboundSender (message_path outbound_sender.ex:455); exemplos com Fr participante; MontaMsg.Impl.PACS008 | sim | guard 201 assinado, host primário |
| pacs.002 | fallback mc:992 -> `send_signed_message` | primário (financeira, roteador) | exemplos `pacs.002_PSP_*` Fr=99999010 (resposta do PSP recebedor); MontaMsg.Impl.PACS002; OutboundSender também emite pacs.002 no fluxo real | sim (apesar de direction INBOUND no form) | guard 201 assinado, host primário |
| pacs.004 | mc:948 `SpiClient.request_return` (sc:142) -> `send_signed_message` | primário (financeira, roteador) | exemplos Fr=99999009 (participante); MontaMsg.Impl.PACS004 | sim | guard 201 assinado, host primário |
| camt.014 | recusado (sendable false) | n/a | exemplo oficial Fr=00038166; CreDtTm "emissão ao participante"; LegadoPIX só handler de entrada CAMT014 | não | guard 422 sem request |
| camt.025 | recusado (sendable false) | n/a | 2 exemplos oficiais Fr=00038166; CreDtTm APENAS "emissão ao participante"; sem montador no LegadoPIX | não (a dúvida do mandato foi resolvida com evidência) | guard 422 sem request |
| camt.029 | mc:971 `SpiClient.send_investigation_resolution` (sc:455) -> `send_signed_message` | roteador (secundário; a validar vivo) | exemplos ACEITA/REJEITA com Fr=11111111 (participante); MontaMsg.Impl.CAMT029 | sim | guard 201 assinado, host do roteador |
| camt.052 | recusado (sendable false) | n/a | exemplo Fr=00038166; CreDtTm "pelo sistema de liquidação ... ao participante"; LegadoPIX só handler CAMT052. Solicitação correta = camt.060 com ReqdMsgNmId=camt.052 | não | guard 422 sem request |
| camt.053 | recusado (sendable false) | n/a | 4 exemplos, todos Fr=00038166; LegadoPIX só handler CAMT053. Solicitação correta = camt.060 (funil do get_balance, sc:162) | não | guard 422 sem request |
| camt.054 | recusado (sendable false) | n/a | 3 exemplos Fr=00038166; CreDtTm unidirecional; sem montador no LegadoPIX. Detalhe de lançamento se pede via camt.060 ReqdMsgNmId=camt.054 | não | guard 422 sem request |
| camt.055 | fallback mc:992 -> `send_signed_message` | roteador (secundário; a validar vivo) | exemplos SOLIC_PAGADOR/SOLIC_RECEBEDOR com Fr=11111111/22222222; MontaMsg.Impl.CAMT055 | sim | guard 201 assinado, host do roteador |
| camt.060 | fallback mc:992 -> `send_signed_message` (força primário, sc:249) | primário (PROVADO) | tela Consulta Saldo/get_balance com 201 no acervo; no secundário o BACEN rejeita async via admi.002 "canal incorreto"; 7 exemplos com Fr=99999010; MontaMsg.Impl.CAMT060 | sim | guard + dispatch_real_endpoint (host primário) |
| pain.009 | mc:955 `SpiClient.create_recurrence` (sc:300) -> `send_signed_message` | roteador (secundário; a validar vivo) | CreDtTm bidirecional no xlsx PAIN009; MontaMsg.Impl.PAIN009; caller vivo SpiService.Recurrences | sim | guard 201 assinado, host do roteador |
| pain.011 | mc:959 `SpiClient.update_recurrence` (sc:308) -> `send_signed_message` | roteador (secundário; a validar vivo) | exemplos Fr=11111111/22222222; MontaMsg.Impl.PAIN011 | sim | guard 201 assinado, host do roteador |
| pain.012 | mc:963 `SpiClient.cancel_recurrence` (sc:316) -> `send_signed_message` | roteador (secundário; a validar vivo) | 11 exemplos com Fr=11111111/22222222 (participantes); MontaMsg.Impl.PAIN012 | sim | guard 201 assinado, host do roteador |
| pain.013 | mc:967 `SpiClient.confirm_return` (sc:425) -> `send_signed_message` | roteador (secundário; a validar vivo) | 3 exemplos Fr=22222222 (PSP recebedor agenda cobrança); MontaMsg.Impl.PAIN013 | sim | guard 201 assinado, host do roteador |
| pain.014 | fallback mc:992 -> `send_signed_message` | roteador (secundário; a validar vivo) | exemplos Fr=11111111 (resposta do PSP pagador no Pix Automático); MontaMsg.Impl.PAIN014 | sim (apesar de direction INBOUND no form) | guard 201 assinado, host do roteador |
| pibr.001 | mc:980 `SpiClient.echo/1` (sc:336): constrói/assina a própria pibr.001; echo_data do form via opts | roteador (secundário; eco PROVADO vivo com 201 no acervo) | echo 201 no acervo (marco 2026-06-23); exemplo Fr=99999010; MontaMsg.Impl.PIBR001 | sim | guard 201 assinado, host do roteador |
| pibr.002 | fallback mc:992 -> `send_signed_message` | roteador (secundário; a validar vivo) | MontaMsg.Impl.PIBR002 (resposta de eco a pibr.001 recebida do BACEN); CreDtTm bidirecional | sim (apesar de direction INBOUND no form) | guard 201 assinado, host do roteador |
| admi.002 | fallback mc:992 -> `send_signed_message` | roteador (secundário; a validar vivo) | MontaMsg.Impl.ADMI002 (participante emite recusa de mensagem recebida); xlsx: "Reason of the rejection provided by the rejecting party" | sim (apesar de direction INBOUND no form) | guard 201 assinado, host do roteador + sonda do fallback no dispatch_real_endpoint |
| admi.004 | recusado (sendable false); cláusula dedicada e sender inventado DELETADOS | n/a | xlsx ADMI004: EvtCd/EvtDesc "Campo de uso exclusivo do SPI"; exemplo Fr=00038166; LegadoPIX só handler de entrada ADMI004; `<SysEvtAck>` do sender antigo nem existe no XSD | não | guard 422 sem request + guards de ausência em spi_client_namespace_test/spi_client_canonical_namespace_test |
| reda.014 | fallback mc:992 -> `send_signed_message` | roteador (secundário; reda.014 ACEITA 201 vivo em 2026-06-29) | exemplo Fr=99999010; MontaMsg.Impl.REDA014; aceite vivo registrado no handoff de 2026-06-29 | sim | guard 201 assinado, host do roteador |
| reda.016 | recusado (sendable false) | n/a | 3 exemplos (sucesso/queued/erro), todos Fr=00038166; sem montador no LegadoPIX | não | guard 422 sem request |
| reda.017 | recusado (sendable false) | n/a | exemplo Fr=00038166; LegadoPIX só handler de entrada REDA017 | não | guard 422 sem request |
| reda.022 | fallback mc:992 -> `send_signed_message` | roteador (secundário; a validar vivo) | exemplo Fr=99999010; MontaMsg.Impl.REDA022 | sim | guard 201 assinado, host do roteador |
| reda.031 | fallback mc:992 -> `send_signed_message` | roteador (secundário; a validar vivo) | exemplo Fr=99999010; MontaMsg.Impl.REDA031 | sim | guard 201 assinado, host do roteador |
| reda.041 | recusado (sendable false) | n/a | exemplo Fr=00038166; LegadoPIX só handler de entrada REDA041 | não | guard 422 sem request |
| trck.002 | fallback mc:992 -> `send_signed_message` | roteador (secundário; a validar vivo, destino GRAF) | exemplo `trck.002_PSP_msg.xml` Fr=66666666 (participante); xlsx: CreDtTm "pelo participante durante o processo de emissão ao GRAF" | sim | guard 201 assinado, host do roteador |

Observação sobre "canal a validar vivo": para os tipos sem prova viva de canal, o envio segue o `ChannelRouter` pelo tipo, exatamente como o mandato determina (sem default cego). O secundário (icom-sec) esteve em DROP na RTM em períodos anteriores; qualquer aceite vivo novo deve ser registrado aqui.

## Regras de canal aplicadas

- Financeiras (`MessageSchemas.financial?`: pacs.008, pacs.002, pacs.004): roteador resolve PRIMÁRIO. Coerente com o fluxo real do OutboundSender.
- camt.060: PRIMÁRIO forçado em `force_proven_channel/2` (prova: admi.002 "canal incorreto" no secundário, mesma regra do `get_balance/3`).
- Demais: `ChannelRouter.route(tipo)` com failover por circuito. Nenhuma contradição entre `is_financial` e evidência de aceite foi encontrada no acervo durante esta auditoria.

## Guard test

`pix/backend/apps/settlement_service/test/settlement_service_web/controllers/message_controller_send_truth_table_test.exs`: 29 testes (18 tipos enviáveis com captura da `Finch.Request` real no fio via seam `:bacen_finch_request_fun`, 9 recusas 422 sem request, 1 teste de cobertura tabela x catálogo, 1 teste de paridade do campo `sendable`). Tipo novo sem linha na tabela quebra o teste de cobertura; regressão para path fictício quebra a asserção de path; XML cru sem assinatura quebra a asserção do `<Sgntr>`.

## Pendências honestas (fora do escopo desta frente)

- Os helpers de LEITURA do `SpiClient` com paths `/pix/v2/*` (get_payment_status, get_intraday_report, get_statement, get_transfer_status, get_return_confirmation, get_investigation_resolution, get_system_events, get_agenda, get_ledger_notifications) continuam no código, SEM nenhum chamador vivo (grep em 2026-07-07). Não fazem parte do envio do Construtor. Recomendação: deletar em limpeza futura para eliminar risco de recontaminação.
- Validação viva do aceite BACEN por tipo/canal para os tipos marcados "a validar vivo" depende do HSM RTM voltar (fora desde 2026-07-03) e de tráfego no BACEN homolog.
