# De → Para 100% auditado: histórico legado AutBank (PIX e SPB) → produção Monetarie

Data: 2026-07-08
Estado: mapa completo e executável, validado empiricamente contra o banco legado restaurado e contra o código/schema de produção das cabines (4 auditorias de código + auditoria de dados). Objetivo: migrar 100% do histórico obrigatório sem nenhuma falha de inserção.

## Legenda

- OK: mapeamento direto
- TR: com transformação (regra ao lado)
- GATE: só com pré-condição/aprovação (saldo, COA)
- EXCLUI: não migrar (ruído, cache, catálogo, config, segredo)
- DECISAO: escolha pequena e localizada do time (não é bloqueio)

---

## 0. O que a auditoria mudou (leia primeiro)

1. As três tabelas "gigantes" (`MSG_ENDERECAMENTO`, `MSG_SOLICITACAO_DEVOLUCAO`, `MSG_RELATO_DE_INFRACAO`, ~3,0 milhões de linhas) NÃO são arquivo de pagamentos. São **log de polling do DICT** (`ListClaimRequest` 602.643, `ListRefundsRequest` 602.674, `ListInfractionReportsRequest` 301.313, por lado). Isso é ruído operacional. O arquivo regulatório real de mensagens de pagamento são ~30 mil linhas em `MENSAGEM_BACEN_PSP` + `MENSAGEM_RESPOSTA_BACEN_PSP`.
2. Volume real migrável: de 3.484.249 linhas do PIX, ~3,19 milhões são ruído (polling DICT + `TESTE_CONECTIVIDADE` 122.245 + `THUMBPRINT_SITE_HISTORICO` 53.183 + registros de envio + caches). **O histórico real é ~296 mil linhas.**
3. Identidade não precisa de de-para de conta: no PIX, `institution_id`/`branch_id`/`system_id` são inteiros constantes `1` (não há FK; as partes são texto livre). No SPB, `spb_operations.institution_id` fica NULL e `bacen_messages.institution_id` resolve por ISPB (basta a linha de `institutions` do ISPB 46026562 existir).
4. Valor: as cabines guardam reais (ou centavos inteiros), nunca subcentavo. Mirando as tabelas canônicas (`messages`+`payments`, `returns`, `statements`, `balances`, `refund_requests`, `infractions`, e todo o SPB), o valor entra **em reais como está**. O bug 100x era só na fronteira Core/TigerBeetle, fora deste escopo.
5. Enriquecimento por E2E via CAMT.060 NÃO deve ser usado para a migração (ver seção 8). O banco legado é a fonte de verdade da retenção.
6. Janela: PIX 2025-11-27 a 2026-06-30 (backup atual); SPB 2025-12-02 a 2026-06-30. Um NOVO backup virá até a data de corte de 08/07, cobrindo a janela final 30/06 a 08/07 (cutover). O mapa de→para NÃO muda para o novo backup (mesmas tabelas/colunas); muda só o volume (mais ~8 dias) e a reconciliação de fronteira da seção 10. Produção começou em 08/07 com `messages`=0, então a janela 30/06 a 08/07 pertence 100% ao legado (sem transação nativa concorrente, sem duplicidade).

---

## 1. Regras globais de carga (a parte que garante "sem falha")

Estas regras valem para TODAS as projeções abaixo. São o que impede rejeição de INSERT.

### 1.1 Identidade (PIX)
- `messages.institution_id = 1`, `messages.branch_id = 1`, `messages.system_id = 1` (inteiros constantes, NOT NULL, sem FK).
- `returns.institution_id = 1`, `returns.system_id = 1`.
- `accounts.institution_id = 1`, `branch_id = 1` (se precisar).
- Partes (pagador/recebedor) = colunas de texto livre (`debtor_ispb`/`creditor_ispb` char(8), `*_account`, `*_name`, `*_document`). Nenhum FK.

### 1.2 Identidade (SPB)
- `spb_operations.institution_id = NULL` (coluna nullable, sem FK). Partes em `sender_ispb`/`receiver_ispb` (texto, NOT NULL).
- `bacen_messages.institution_id = (SELECT id FROM public.institutions WHERE ispb='46026562')`. Pré-condição: rodar o seed de `institutions` (production_seed) antes.

### 1.3 Valor monetário (conversão por coluna)
Origem legada = reais `numeric(17,2)` (`MOVIMENTOS_CONTA_PI.VALOR_LANCAMENTO` = `numeric(17,4)`).

| Coluna destino | Unidade | Conversão |
|---|---|---|
| `payments.amount numeric(18,5)` | reais | as-is |
| `returns.amount` / `original_amount numeric(18,2)` | reais | as-is |
| `statement_entries.amount`, `balances.*`, `balance_history.*`, `balance_blocks.amount numeric(18,2)` | reais | as-is |
| `refund_requests.amount`, `infractions.transaction_amount numeric(18,2)` | reais | as-is |
| SPB `spb_operations.amount`, `bacen_messages.amount`, `balance_movements.amount` | reais | as-is |
| (evitar) `transactions.amount`, `transaction_returns.amount`, `med_claims.amount_cents` (bigint) | centavos | round(reais×100) |

Regra: mirar as tabelas canônicas em reais e NÃO aplicar ×100 nelas. `messages` não tem coluna de valor (o valor vive em `payments`). `MOVIMENTOS_CONTA_PI` (4 casas) só preserva precisão em `payments.amount (18,5)`; ao ir para `balances (18,2)`, arredondar explícito e reconciliar resíduo.

### 1.4 Normalização obrigatória de ISPB/CPF/CNPJ (maior risco de rejeição silenciosa)
Há CHECK `NOT VALID` em toda coluna ISPB/CPF/CNPJ, enforçado no INSERT. Antes de inserir, normalizar cada valor para: MAIÚSCULO, sem máscara (`.`/`-`/`/`), tamanho exato (ISPB 8, CPF/CNPJ 11/14) ou NULL. Regex: ISPB `^[0-9A-Z]{8}$`; CPF/CNPJ `^[0-9]{11}$|^[0-9A-Z]{12}[0-9]{2}$`. Valor fora do padrão e não-nulo é rejeitado.

### 1.5 Timestamps e partições
- Converter horário Brasil do legado para `timestamptz` UTC (as partições de `messages` são por `operation_time` UTC).
- `messages` é particionada e só existem partições 2026-03..08. **Pré-criar, antes da carga, as partições `messages_2025_11`, `messages_2025_12`, `messages_2026_01`, `messages_2026_02`** (senão as linhas caem em `messages_default` e o Postgres passa a recusar ATTACH dessas faixas depois). O `PartitionManager` só cria mês corrente/futuro, nunca passado.

### 1.6 Proveniência e idempotência
- Prefixo `LEGACY-` em toda chave natural (message_id, resource_id, return_id, refund_id, infraction_id, claim_id, nuop, control_number, statement_id, notification_id, fraud_marker_id).
- Proveniência: `bacen_messages.source='legacy_ab_spb'`, `spb_operations.created_by='legacy_ab_spb'`, `statements.source='legacy_ab_pix'`; no PIX sem coluna `source`, usar `resource_id`/`str_control`/`json_input->>'source'='legacy_ab_pix'`.
- Chaves de idempotência (`ON CONFLICT DO NOTHING`): messages `(unique_id,operation_time)` e `(end_to_end_id,operation_time)`; payments dedup manual por `message_id`; keys `(key_type,key_value)`; claims `claim_id`; refund_requests `refund_id`; infractions `infraction_id`; statements `statement_id`; returns `return_id`; fraud_markers `fraud_marker_id`; bacen_inbound `(ispb,resource_id)` (confirmar no banco vivo); bacen_outbound `(message_id,send_time)`; xml_messages `(message_id,message_time)`; SPB bacen_messages `message_id`; spb_operations `nuop` (+ índice parcial inbound STR `(message_type,control_number_clearing) WHERE direction='inbound'`); institution_certificates `(ispb,dom_spb)`.

### 1.7 Ordem de carga por dependência (FK)
- SPB: `spb_operations` ANTES de `operation_events` (FK NOT NULL, CASCADE) e antes de `bacen_messages.operation_id` (FK nullable).
- PIX: `infraction_types` (seed) antes de `infractions` se `infraction_type_id` for preenchido. Demais tabelas PIX não têm FK entre si (independentes no banco).

### 1.8 Enums (valores permitidos)
- `messages.direction` / `message_history.direction` / `xml_messages.direction`: `OUTBOUND` | `INBOUND`.
- `messages.debit_credit`: `DEBIT` | `CREDIT`.
- `infractions.key_type`: `CPF` | `CNPJ` | `PHONE` | `EMAIL` | `EVP`.
- `messages.status_id` numérico (1=PDNG ... 9=settled ... 10=RTRN); usar o mapa da seção 6.
- SPB: nenhum enum (status/direction são varchar).

---

## 2. PIX Fase 1 — arquivo bruto de mensagens (CORRIGIDO)

### 2.1 `MENSAGEM_BACEN_PSP` (21.808, tudo INBOUND) + `MENSAGEM_BACEN_PSP_MENSAGEM` (corpo gzip, 1:1 por ID) → `monetarie_spi.bacen_inbound` + `monetarie_spi_msg.xml_messages`

Tipos: PACS008 8.237 (créditos recebidos), PACS002 13.090 (confirmações), PACS004 466, ADMI002 12, CAMT054 3.

| Origem | Destino `bacen_inbound` | Nota |
|---|---|---|
| `ID` | `resource_id`=`'LEGACY-ABPI-'||ID` | OK (NOT NULL) |
| `COD_MSG` | `message_type` | OK |
| `ISPB_DESTINO`(=46026562) | `ispb` | TR normalizar; NOT NULL |
| `_MENSAGEM.MENSAGEM` (gzip) | `xml_content` | TR gunzip; e também → `xml_messages` |
| `DATA_HORA_REGISTRO` | `receive_time`,`receive_date` | OK NOT NULL |
| fixo `0` | `return_code` | TR NOT NULL, sem valor legado; usar 0 |
| `ISPB_ORIGEM`,`COD_USUARIO`,`DATA_HORA_RETIRADA_STREAM_BACEN` | metadado | guardar |

`xml_messages`: `message_id`=id sintético, `message_time`=DATA, `xml_content`=gunzip, `direction`=`INBOUND`, `message_code`=COD_MSG.

### 2.2 `MENSAGEM_RESPOSTA_BACEN_PSP` (8.761, nossas respostas pacs.002) + `_MENSAGEM` → `monetarie_spi.bacen_outbound` + `xml_messages`

| Origem | Destino `bacen_outbound` |
|---|---|
| `ID` | `message_id`=`'LEGACY-ABPI-RESP-'||ID` (NOT NULL) |
| `DATA_HORA_REGISTRO` | `send_time`,`send_date` (NOT NULL) |
| `ISPB_ORIGEM`(=46026562) | `ispb` (TR normalizar, NOT NULL) |
| fixo `0` | `return_code` (NOT NULL) |
| `_MENSAGEM.MENSAGEM` (gzip) | `xml_content` (gunzip) → `xml_messages` direction=OUTBOUND |
| `ID_BACEN_PSP` | correlação com a mensagem recebida (metadado) |

### 2.3 `ORDEM_PAGAMENTO_MENSAGEM` (4.373, corpo dos nossos pacs.008 de saída, texto) → `bacen_outbound` + `xml_messages`

`ID_MSG`→`message_id`=`'LEGACY-OPM-'||ID`; `MENSAGEM`(texto XML)→`xml_content`; `DATA_HORA_REGISTRO`→tempo; direction=OUTBOUND. (`ORDEM_PAGAMENTO.MENSAGEM` é 100% vazio; o corpo está aqui.)

### 2.4 `MENSAGENS_LPI` (1.106) / `MENSAGENS_LPI_RETORNO` (1.103) → `bacen_outbound` / `bacen_inbound`

`CODMSG`→`message_type`; `NUMCTRL`→`resource_id`/`message_id`=`'LEGACY-LPI-'||NUMCTRL`; `MENSAGEM`(texto)→`xml_content`; `DATA_MOVIMENTO`→tempo. (Conta PI / LPI0006.)

### 2.5 DICT wire ops significativas (opcional) → `monetarie_dict.operations`

Das tabelas `MSG_ENDERECAMENTO`/`MSG_SOLICITACAO_DEVOLUCAO`/`MSG_RELATO_DE_INFRACAO`, importar SOMENTE as linhas cujo tipo NÃO começa com `List` (GetEntry 4.334, CreateEntry 99, DeleteEntry 41, Create/Confirm/Cancel Claim, CreateRefund 198, CloseRefund 8, CreateInfractionReport 384, etc.; ~10 mil linhas). Descompactar o XML e mapear `operation_type`,`key_type`,`key_value`,`status`,`request_data`,`response_data`,`requested_at`. As linhas `List*` (~3,0 milhões) são polling: EXCLUI. O estado resultante já está nas tabelas de negócio (2.x da Fase 2).

---

## 3. PIX Fase 2 — entidades de negócio

### 3.1 Saída: `ORDEM_PAGAMENTO` (4.445) + `BLOQUEIO_ORDEM_PAGAMENTO` (junta 99% por `ID_ORDEM_PAGAMENTO`/E2E) + `STATUS_ORDEM_PAGAMENTO` → `messages` + `payments` + `message_history` + `balance_blocks`

`messages` (direction=OUTBOUND, identity=1):
| Origem | Destino | Nota |
|---|---|---|
| `ORDEM.END_TO_END` | `end_to_end_id` char(32) | TR truncar 33→32 se preciso; idempotência |
| `ORDEM.COD_MSG` | `message_code` | OK NOT NULL |
| `ORDEM.DATA_HORA_REGISTRO` | `operation_time`(UTC),`movement_date` | TR NOT NULL, define partição |
| `STATUS_ORDEM_PAGAMENTO` terminal | `status_id` | TR mapa seção 6 (default 1) |
| `ORDEM.ID` | `message_id`=`'LEGACY-OP-'||ID`, `unique_id` | OK NOT NULL |
| fixo | `institution_id=branch_id=system_id=1`, `direction=OUTBOUND`, `debit_credit=DEBIT` | OK |
| `46026562` | `debtor_ispb` (TR normalizar) | NOT NULL |
| `BLOQUEIO.COD_ISPB_CONTRA_PARTE` | `creditor_ispb` (TR normalizar) | NOT NULL |

`payments` (NOT NULL: message_id, operation_time, amount, currency, defaults nos demais):
| Origem `BLOQUEIO` | Destino | Nota |
|---|---|---|
| `VALOR` | `amount` (reais as-is) | |
| `NOME`/`CPF_CNPJ`/`NRO_CONTA`/`TIPO_CONTA`/`COD_AGENCIA` | `debtor_name`/`debtor_cpf_cnpj`(norm)/`debtor_account`/`debtor_account_type`/... | texto |
| `NOME_CONTRA_PARTE`/`CPF_CNPJ_CONTRA_PARTE`/`NRO_CONTA_CONTRA_PARTE` | `creditor_*` (norm CPF/CNPJ) | texto |
| `CHAVE_ENDERECAMENTO` | `creditor_proxy` | |
| `CPF_CNPJ_INICIADOR` | `initiating_party` | |
| saque/troco/finalidade/MODELO_CTB/NIVEL_SERVICO | `json_input` | DECISAO: json |

`STATUS_ORDEM_PAGAMENTO` (todas as transições) → `message_history` (`message_id`,`history_time`,`status_id`,`status_origin`='legacy'). `BLOQUEIO`+`STATUS_BLOQUEIO` → `balance_blocks` (`ispb`,`amount`,`status`,`reference_id`=`'LEGACY-...'`, `MOTIVO_MED`→`reason`).

### 3.2 Entrada: `ANOTACAO_CREDITO` (8.761, E2E único) + `STATUS_ANOTACAO_CREDITO` → `messages` + `payments` (direction=INBOUND) + `message_history`

`credit_notifications` está INATIVA no código (sem writer); usar `messages`+`payments` inbound como alvo canônico.
| Origem | Destino | Nota |
|---|---|---|
| `END_TO_END` | `messages.end_to_end_id` | idempotência |
| `VALOR` | `payments.amount` (reais as-is) | |
| `COD_ISPB`(nós) | `messages.creditor_ispb` (norm) | |
| `COD_ISPB_CONTRA_PARTE` | `messages.debtor_ispb` (norm) | |
| `NOME`/`CPF_CNPJ`/`NRO_CONTA` | `payments.creditor_*` | texto |
| `NOME_CONTRA_PARTE`/`CPF_CNPJ_CONTRA_PARTE`/`NRO_CONTA_CONTRA_PARTE` | `payments.debtor_*` | texto |
| `DATA_HORA_REGISTRO` | `operation_time`,`movement_date` | partição |
| `STATUS_ANOTACAO_CREDITO` (EFETIVADO/CANCELADO) | `status_id` | mapa seção 6 |
| `ID_ANOTACAO_CREDITO` | `message_id`=`'LEGACY-AC-'||ID`,`unique_id` | |
| fixo | identity=1, direction=INBOUND, debit_credit=CREDIT | |
| `COD_DEVOLUCAO_PACS004`/`INFO_DEVOLUCAO_PACS004`/`INFO_ERROS`; saque/troco | `json_input` | |

### 3.3 Devolução: `ANOTACAO_DEVOLUCAO` (19) → `monetarie_spi.returns`; `SOLICITACAO_DEVOLUCAO` (205), `DEVOLUCAO_ESPECIAL` (373) → `monetarie_dict.refund_requests`

`returns` tem muitos NOT NULL: `message_id`,`original_payment_id`,`original_end_to_end_id`,`original_amount`,`amount`,`reason_code`,`requestor_ispb/account/document`,`counterparty_ispb/account/document`,`status`,`institution_id`,`system_id`. Mapear de `ANOTACAO_DEVOLUCAO` (`END_TO_END_PACS008`→`original_end_to_end_id`, `END_TO_END_PACS004`→`return_end_to_end_id`, `VALOR`→`amount`/`original_amount`) complementando com o `ORDEM_PAGAMENTO`/`ANOTACAO_CREDITO` original para preencher os NOT NULL. Onde faltar dado do original, cruzar por E2E.
`SOLICITACAO_DEVOLUCAO`→`refund_requests` (`ID`→`refund_id`, `VALOR_DEVOLUCAO`→`amount`, `PARTICIPANTE_SOLICITANTE/CONTESTADO`→creditor/debtor_ispb, `ESTADO`→`status`, `DATA_CRIACAO`/`ULTIMA_MODIFICACAO`→`bacen_creation_time`/`bacen_last_modified`, `ID_RELATO_INFRACAO` liga infração).

### 3.4 Infração/MED: `RELATO_DE_INFRACAO` (391) + `_OPERACAO_RELATOR` (379) + `_OPERACAO_CONTESTADO` (11) + `MARCACAO_FRAUDE` (8) → `monetarie_dict.infractions` (+ `fraud_markers`)

`infractions` NOT NULL: `reporter_ispb`,`reported_ispb` (o resto nullable; FK opcional a `infraction_types`). Mapear `ID`→`infraction_id`, `ISPB_RELATOR`→`reporter_ispb`, `ISPB_CONTRAPARTE`→`reported_ispb`, `ID_TRANSACAO`→`end_to_end_id`, `TIPO_INFRACAO`→`infraction_type_id` (TR mapa; só setar se semear `infraction_types`), `ESTADO`→`status_id`, `DATA_HORA_CRIACAO`→`creation_date`. Valores de `_OPERACAO_RELATOR` (`VALOR_OPERACAO`→`transaction_amount` reais) complementam. `MARCACAO_FRAUDE`→`fraud_markers` (`ID`→`fraud_marker_id`, `TIPO_FRAUDE`,`STATUS_MARCACAO_FRAUDE`→`status`, `DATA_HORA_REGISTRO`→`bacen_creation_time`).

### 3.5 Chaves/claims: `CLAIM_PORTABILITY` (12) → `monetarie_dict.claims`; `ENDERECAMENTO` (303) + `CONSULTA_DICT_RESPOSTA` (34) → `monetarie_dict.keys`

`claims` NOT NULL: `claim_id`,`key_type`,`key_value`,`claim_type`,`claimer_ispb`. `keys` NOT NULL: `key_type`,`key_value`,`owner_cpf_cnpj`,`owner_name`,`owner_type`,`ispb`,`account_number`,`account_type`,`creation_date`,`last_updated`; idempotência `(key_type,key_value)`. Mapear `ENDERECAMENTO` (`CHAVE`,`TIPO`,`CPF_CNPJ`,`NOME`,`INSTITUICAO`,`CONTA`,`TIPO_CONTA`,`DATA_CRIACAO`,`DATA_POSSE`). DECISAO: confirmar se `ENDERECAMENTO`/`CONSULTA_DICT_RESPOSTA` são chaves próprias (importar) ou consulta de terceiros (arquivo).

### 3.6 Extrato: `CAMT054` (3), `CAMT053` (834), `CAMT052` (16), `CAMT052_ARQUIVO_LANCAMENTOS` (10) → `monetarie_spi.statements` + `statement_entries`

`statement_entries` NOT NULL: `statement_id`,`entry_index`,`amount`,`credit_debit`,`booking_date`,`value_date`. `CAMT054` (rico) → entries (`END_TO_END`→`end_to_end_id`, `VALOR`→`amount` reais, `IND_CREDT_DEBT`→`credit_debit`, `DATA_CONTABIL`→`booking_date`, contrapartes→`counterparty_*`). `CAMT053`/`CAMT052` → cabeçalho `statements` (`statement_id`=`'LEGACY-...'`, `ispb`,`from_date`,`to_date`, `source='legacy_ab_pix'`).

### 3.7 camt.060 consulta: `CAMT060` (714) + `CAMT060_REGISTRO_ENVIO` (714) + `MENSAGEM_CONSULTA_CONTA_SPI_ENVIO` (714) / `_RECEBIMENTO` (853) → `monetarie_spi.camt060_requests` + `balance_queries`

Histórico de consultas de saldo/lançamento. `MSG_ID`/`RESOURCE_ID`→`msg_id`, `CHAVE`→correlação, `DATA_HORA_*`→`sent_at`/`responded_at`. (Corpo bruto também pode ir a bacen_outbound/inbound.)

### 3.8 QR Code: `QR_CODE_PAYLOAD` (861), `QR_CODE_DINAMICO_VALOR_PROCESSADO` (688), `QR_CODE_ESTATICO_INFO` (3) → `monetarie_settlement.qr_codes`

`tx_id`,`qr_type`,`amount` reais,`receiver_*`,`emv_payload`,`expiration_time`.

### 3.9 Saldos: `BALANCO_SALDO_SPI` (1.382), `SALDO_CONTA_PI` (215) → `monetarie_spi.balances`/`balance_history` — GATE

Só como histórico (`balance_history`), com snapshot, sem tocar `balances` corrente. `TIPO`/`VALOR`/`IND_CREDT_DEBT`/`DATA_HORA`.

### 3.10 Contábil: `MOVIMENTOS_CONTABEIS_*` (8 tabelas, ~13k), `MOVIMENTOS_CONTA_PI` (1.112), `MOVIMENTOS_INTERNOS` (49) → `monetarie_settlement.journal_entries` — GATE COA

Só depois de semear `chart_of_accounts`. `DATA_LANCAMENTO`/`DATA_CONTABIL`/`VALOR`/`MODELO`/`END_TO_END`/partes. `MOVIMENTOS_CONTA_PI.VALOR_LANCAMENTO` é 4 casas (arredondar).

### 3.11 Remuneração: `REMUNERACAO_SPI` (143), `PROCESSA_REMUNERACAO_CAMT053` (143) → DECISAO

Sem tabela viva equivalente. Alvo candidato: extrato/contábil. Decidir com o time.

---

## 4. SPB — de → para

### 4.1 `CORPO_TRANS_INTERF_PASS` (10.461) + `CORPO_TRANS_INTERF_USMSG_PASS` (3.640) → `public.bacen_messages`

NOT NULL a preencher: `message_id`(`'LEGACY-SPB-'||pk`), `message_type`(regex `<CodMsg>`), `message_category`, `state`(mapa seção 6 — SEM default, obrigatório), `direction`(`CODSISTEMA_O`: SPB=inbound; EB/GI/SGR=outbound), `institution_id`(lookup ISPB), `content`(jsonb do XML). `XML`→`xml_content`; `DTHRINC`→`sent_at`/`received_at`; `source='legacy_ab_spb'`. Idempotência `message_id`.

### 4.2 `TRANS_INTERF_PASS` (10.461) + `TRANS_SISTEMAS` (130) → `public.spb_operations` → `public.operation_events`

NOT NULL: `state`(mapa composto seção 6 — SEM default), `sender_ispb`,`receiver_ispb` (norm). `CODINST`/`CODINSTLIQ`→sender/receiver; `NUMCTRLIF`/`NUMCTRLSPB`/`NUMCTRLSTR`→`control_number`/`control_number_clearing` (inbound com `LEGACY-`); `VALORMOVTO`(de TRANS_SISTEMAS ou tag do catálogo)→`amount` reais; `CODEVENTO`/`TIPO_M`→`message_type`; `nuop`=`'LEGACY-SPB-'||pk` (idempotência); `institution_id=NULL`; `created_by='legacy_ab_spb'`. Depois: `operation_events` (`operation_id` FK NOT NULL → carregar operações primeiro; `event_type='legacy_import'`).

### 4.3 `CERTIFICADOS` (76) → `public.institution_certificates` (metadado público)

`NUMSERIE`→`serial`,`ISPB`→`ispb`(norm),`CN`→`subject_cn`,`DTINI`/`DTFIM`→validade,`CA`/`STATUS_CER`→situação; `pem` (montar do público) NOT NULL. Idempotência `(ispb,dom_spb)`. Sem `CONTAINER`/material sensível. Opcional.

---

## 5. Classificação 100% das tabelas não-vazias (178 PIX + 37 SPB)

### PIX — IMPORTAR (entidade/arquivo)
Negócio: `ANOTACAO_CREDITO`,`STATUS_ANOTACAO_CREDITO`,`ORDEM_PAGAMENTO`,`STATUS_ORDEM_PAGAMENTO`,`ORDEM_PAGAMENTO_MENSAGEM`,`BLOQUEIO_ORDEM_PAGAMENTO`,`STATUS_BLOQUEIO_ORDEM_PAGAMENTO`,`ANOTACAO_DEVOLUCAO`,`SOLICITACAO_DEVOLUCAO`,`DEVOLUCAO_ESPECIAL`,`RELATO_DE_INFRACAO`,`RELATO_DE_INFRACAO_OPERACAO_RELATOR`,`RELATO_DE_INFRACAO_OPERACAO_CONTESTADO`,`MARCACAO_FRAUDE`,`CLAIM_PORTABILITY`,`ENDERECAMENTO`,`CONSULTA_DICT_RESPOSTA`,`CAMT052`,`CAMT053`,`CAMT054`,`CAMT052_ARQUIVO_LANCAMENTOS`,`CAMT060`,`MENSAGEM_CONSULTA_CONTA_SPI_ENVIO`,`MENSAGEM_CONSULTA_CONTA_SPI_RECEBIMENTO`,`QR_CODE_PAYLOAD`,`QR_CODE_DINAMICO_VALOR_PROCESSADO`,`QR_CODE_ESTATICO_INFO`,`MENSAGENS_LPI`,`MENSAGENS_LPI_RETORNO`.
Arquivo: `MENSAGEM_BACEN_PSP`,`MENSAGEM_BACEN_PSP_MENSAGEM`,`MENSAGEM_RESPOSTA_BACEN_PSP`,`MENSAGEM_RESPOSTA_BACEN_PSP_MENSAGEM`.
DICT wire (só linhas não-`List*`): `MSG_ENDERECAMENTO_ENVIADA/RECEBIDA`,`MSG_SOLICITACAO_DEVOLUCAO_ENVIADA/RECEBIDA`,`MSG_RELATO_DE_INFRACAO_ENVIADA/RECEBIDA`,`MSG_MARCACAO_FRAUDE_ENVIADA/RECEBIDA`,`MSG_RECONCILIACAO_ENVIADA/RECEBIDA`.

### PIX — IMPORTAR com GATE
Saldo: `BALANCO_SALDO_SPI`,`SALDO_CONTA_PI`. Contábil (COA): `MOVIMENTOS_CONTABEIS_CLIENTE_CREDITO_EXTERNO`,`_CREDITO_INTERNO`,`_DEBITO_EXTERNO`,`_DEBITO_INTERNO`,`MOVIMENTOS_CONTABEIS_CONTAPI_CREDITO`,`_DEBITO`,`MOVIMENTOS_CONTABEIS_DEVOLUCAO_CREDITO_EXTERNO`,`_DEBITO_EXTERNO`,`MOVIMENTOS_CONTA_PI`,`MOVIMENTOS_INTERNOS`,`MOVIMENTOS_INTERNOS_RECUSADOS`,`MOVIMENTO_CONTABIL_NAO_REGISTRADO`,`OPERACOES_CONTABEIS`.

### PIX — DECISAO
`REMUNERACAO_SPI`,`PROCESSA_REMUNERACAO_CAMT053`,`AVISOS_SPI`,`AVISOS_SPI_XML`,`ENDERECAMENTO_HISTORICO`,`ENDERECAMENTO_EVENTOS`,`MED_PARCIAL_RESULTADO`,`PROCESSA_MED`,`CONCILIACAO_MOVIMENTOS_DIVERGENTES`,`CONCILIACAO_SALDOS`.

### PIX — EXCLUIR (ruído/cache/log/config/processing/segredo)
Polling DICT (linhas `List*`, ~3,0 mi): as 6 tabelas `MSG_*` acima (só as linhas List). Heartbeat: `TESTE_CONECTIVIDADE`,`TESTE_CONECTIVIDADE_AUTOMATICO`. Certificados/thumbprints: `THUMBPRINT_SITE_HISTORICO`,`THUMBPRINT_SITE`,`THUMBPRINT_CERTIFICADO`,`DOWNLOAD_CERTIFICADOS_SITE`. Cache dashboard: `RESULTADO_GRAFICO`,`RESULTADO_GRAFICO_DETALHE`,`RESULTADO_GRAFICO_INDIRETO`,`RESULTADO_GRAFICO_INDIRETO_DETALHE`. Registros de envio (correlação, regenerável): `PIBR001_REGISTRO_ENVIO`,`PACS002_REGISTRO_ENVIO`,`PACS008_REGISTRO_ENVIO`,`PACS004_REGISTRO_ENVIO`,`CAMT060_REGISTRO_ENVIO`,`PAIN012_REGISTRO_ENVIO`. Processing/idempotência: `PROCESSA_ANOTACAO_CREDITO`,`PROCESSA_CREDITO`,`PROCESSA_DEBITO`,`PROCESSA_BLOQUEIO`,`PROCESSA_PACS008`,`PROCESSA_PACS004`,`PROCESSA_ANOTACAO_DEVOLUCAO`,`PROCESSA_BLOQUEIO_DEVOLUCAO_ESPECIAL`,`PROCESSA_ORDEM_PAGAMENTO_ID_IDEMPOTENTE`,`PROCESSA_RECEBER_RELATO_INFRACAO`,`PROCESSA_MOVIMENTO_CONTABIL_CREDITO`,`_DEBITO`,`PROCESSA_TRANSFERENCIA_INTERNA`,`PROCESSA_AGENDAMENTO`. Erros/inválidas (opcional-auditoria): `ERROS_ANOTACAO_CREDITO`,`ERROS_ORDEM_PAGAMENTO`,`MENSAGEM_DICT_INVALIDA`,`MENSAGEM_ICOM_INVALIDA`,`MENSAGEM_ERRO`,`FALHA_CANCELAMENTO_DEBITO`,`FALHA_MOVIMENTO_DEBITO`. Alertas/relatórios: `HISTORICO_ALERTAS`,`ALERTAS`,`ENVIO_ALERTAS`,`LOG_ENVIO_ALERTAS`,`RELATORIO_MENSAL`,`LOG_RELATORIO_MENSAL`. Sincronismo: `SINCRONISMO_LOG`,`VERIFICA_SINCRONISMO`,`VERIFICA_SINCRONISMO_STATUS`,`VERIFICA_SINCRONISMO_CID`,`UPLOAD_SINCRONISMO`,`VSYNCS`. Controle/polling: todas `CONTROLE_*` (13),`ANOTACAO_DEVOLUCAO_POLLING`,`SCHEDULER`,`LOG_EXECUCAO_PAGAMENTOS_AGENDADOS`,`LOG_ORDEM_PAGAMENTO`,`ORDEM_PAGAMENTO_AGENDADO`,`GESTAO_PAIN012_ENVIO_MENSAGEM`,`ICOM_PAIN009_POLL_MENSAGEM`. Config/ref: todas `PARAMETROS_*` (16),`MODELOS_CONTABEIS`,`CONDICOES_CONTABEIS`,`DE_PARA_SGR`,`ESCOPOS`,`FERIADOS`,`DICT_GRADE_HORARIA`,`DICT_SERVICO_GRADE_HORARIA`,`LOG_DICT_GRADE_HORARIA`,`LOG_DICT_SERVICO_GRADE_HORARIA`,`LOG_PARAMETROS_*` (6),`VERSAO_DEPLOY`,`VERSAO_MANUAL`,`CONTROLE_BAIXA_SCRIPT`,`CONTROLE_BAIXA_VERSAO`,`RESPOSTA_INTEGRACAO`,`RESULTADO_SITUACAO_CONTA_PI`,`RESTRICAO_INFRACAO_*` (6),`PARTICIPANTE_PIX` (usar diretório vivo),`MENSAGEM_CONSULTA...` (já em IMPORTAR),`CONTROLE_MENSAGENS_PIX`.

### SPB (AB_MGI, 37 não-vazias) — IMPORTAR
`TRANS_INTERF_PASS`,`TRANS_SISTEMAS`,`CORPO_TRANS_INTERF_PASS`,`CORPO_TRANS_INTERF_USMSG_PASS`,`CERTIFICADOS`(metadado),`TRANS_INTF_ENV`(71),`TRANS_INTF_REC`(33).
### SPB — EXCLUIR (catálogo de mensagens da cabine já existe)
`MENSAGENS_COL`,`DOMINIOS`,`CONFIG_TAGS`,`COLUNAS`,`TIPODADO`,`MENSAGENS`,`DESC_EVENTOS`,`EVENTOS`,`EVENTOS_REGRAS`,`SERVICOS`,`REGRAS`,`INSTITUICOES`,`TRANS_SISTEMAS`(catálogo? não — é operação),`GRADE_HORARIO`,`GRUPOSERVICO`,`VERSAO_*`,`FILASMSG`,`PERSONALIZATION`,`SISTEMASINT`,`INST_EXTERNAS`,`ASSN_TRANS_INTERF*`(assinaturas),`__RefactorLog`,`PARAMETROS*`,`MENSAGENS_ERRO`,`LOG_INT_WS`,`ENVIAPSTI`,`ARQ_XML_CONSULTA`,`CORPO_TRANS_INTERF_USMSG_PASS`(já em importar),`CONTROLE_INTEGRA`.

Nenhuma tabela não-vazia fica sem classificação.

---

## 6. Mapa de status

PIX ordem `STATUS_ORDEM_PAGAMENTO`→`messages.status_id`: EFETIVADA→settled(9); RECUSADA→rejected; ERRO→cancelled; ICOM→accepted; AGENDADA→pending; senão pending.
PIX crédito `STATUS_ANOTACAO_CREDITO`: EFETIVADO→settled; CANCELADO→rejected/cancelled.
PIX bloqueio `STATUS_BLOQUEIO`: APROVADO→confirmed; RECUSADO→released; ERRO→cancelled.
PIX extrato `STATUS`: SUCESSO→booked; ERRO→legacy_erro. Infração/refund: enum DICT como está.
SPB composto (top-down): SITUACAO='C'→cancelled; STATUSSGR='9'→r1_rejected; SGR='5'+STR='02'+CODSISTEMA_O='SPB'→confirmed; SGR='5'+STR='02'→r1_confirmed; SGR='5'+STR in('04','05')→r1_rejected; SGR='5'+STR='06'→processing; SGR='3'+STR=''→created; SGR in('1','2')+STR=''→created; SGR=''+STR=''→confirmed; senão processing. Não mapeados (→processing+reportar): STATUSSTR 03/07, TIPO_M R3, STATUSSPB completo, TIPO_E 'N'.

---

## 7. Ordem sequencial de execução

Fase 0: snapshot Aurora prod; criar partições `messages_2025_11..2026_02`; seed `institutions` (SPB) e, se contábil, `chart_of_accounts` (PIX); fixar conversões de valor e normalização ISPB/CPF.
Fase 1 (arquivo): PIX `bacen_inbound`/`bacen_outbound`/`xml_messages` (MENSAGEM_BACEN_PSP + RESPOSTA + ORDEM_PAGAMENTO_MENSAGEM + LPI); SPB `bacen_messages`. Reconciliar contagem.
Fase 2 (entidades):
1. PIX `messages`+`payments` (saída ORDEM + entrada ANOTACAO) → `message_history` → `balance_blocks`.
2. PIX `returns`/`refund_requests` → `infractions`/`fraud_markers` → `claims`/`keys`.
3. PIX `statements`/`statement_entries` → `camt060_requests`/`balance_queries` → `qr_codes`.
4. PIX saldo (GATE) e contábil (GATE COA).
5. SPB `spb_operations` → `operation_events` → ligar `bacen_messages.operation_id` → `institution_certificates`.
Após cada fase: `verify` (contagem + reconciliação financeira ao centavo) e prova de idempotência (2ª execução = delta zero).

---

## 8. Enriquecimento por E2E / CAMT.060 — decisão

Não usar na migração histórica. O camt.060 `detalha-lancto` (por E2E → camt.054) existe, mas: (a) só retorna lançamento que liquidou (rejeitado/ERRO não tem lançamento, é onde mais se quer completar e vem vazio); (b) devolve subconjunto do que o legado já tem (E2E, valor, status, partes, XML bruto); (c) o próprio legado nunca consultava o BACEN para isso, lia o banco histórico dele; (d) não há janela de retenção documentada para lançamento de meses atrás (afirmar seria inferência), além do custo de milhares de consultas assinadas no HSM. O banco legado é a fonte de verdade da retenção. Manter camt.060 detalha-lancto só para consulta pontual sob demanda (MED/auditoria).

---

## 9. Concatenação com o estado atual do banco PIX (medido em 2026-07-08, leitura via rpc do pix-api)

O alvo da migração é o `mon_pix` de produção, que entrou em operação em 08/07. O razão de transações está zerado: a importação do legado é, na prática, quase todo o histórico PIX que existirá para o período Nov/2025 a 30/06/2026. Não há sobreposição temporal com o nativo (que começa em julho/2026).

| Tabela destino | PROD hoje | HML hoje (ref.) | Legado a importar (origem) | Observação |
|---|---:|---:|---:|---|
| `monetarie_spi.messages` | 0 | 48 | ~13.206 (ORDEM 4.445 + ANOTACAO_CREDITO 8.761) | append limpo; PROD sem histórico |
| `monetarie_spi.payments` | 0 | 48 | ~13.206 | 1:1 com messages |
| `monetarie_spi.message_history` | 0 | 139 | ~30.784 (STATUS_ORDEM 13.262 + STATUS_ANOTACAO 17.522) | append limpo |
| `monetarie_spi.bacen_inbound` | 3.299 | 51.861 | 21.808 (MENSAGEM_BACEN_PSP) | append com prefixo LEGACY-; sem colisão |
| `monetarie_spi.bacen_outbound` | 618 | 1.592 | ~14.240 (RESPOSTA 8.761 + ORDEM_PAG_MSG 4.373 + LPI 1.106) | append com prefixo LEGACY- |
| `monetarie_spi_msg.xml_messages` | 0 | 0 | ~30.000 (corpos gzip) | append limpo |
| `monetarie_spi.returns` | 0 | 0 | ~19 (ANOTACAO_DEVOLUCAO) | append limpo |
| `monetarie_spi.statements` | 0 | 1 | ~850 (CAMT053 834 + CAMT052 16) | append |
| `monetarie_spi.statement_entries` | 0 | 0 | ~13.250 (EXTRATO 13.257 + CAMT054) | append |
| `monetarie_spi.balance_blocks` | 0 | 53 | 4.392 (BLOQUEIO) | append |
| `monetarie_dict.keys` | 303 | 1.170 | ~337 (ENDERECAMENTO 303 + CONSULTA_DICT 34) | **DEDUP** por `(key_type,key_value)` vs sincronizado |
| `monetarie_dict.claims` | 79 | 446 | 12 (CLAIM_PORTABILITY) | **DEDUP** por `claim_id` |
| `monetarie_dict.refund_requests` | 18 | 67 | ~578 (SOLICITACAO 205 + DEVOL_ESPECIAL 373) | **DEDUP** por `refund_id` |
| `monetarie_dict.infractions` | 0 | 0 | 391 (RELATO_DE_INFRACAO) | append |
| `monetarie_dict.fraud_markers` | 0 | 0 | 8 (MARCACAO_FRAUDE) | append |
| `monetarie_dict.operations` | 0 | 0 | ~10.000 (DICT wire não-List) | append (opcional) |
| `monetarie_settlement.qr_codes` | 0 | 0 | ~1.549 (QR_CODE_PAYLOAD+DINAMICO) | append |

Leitura empírica: PROD `messages`/`payments`/`message_history`/`returns`/`statements` = 0 (só o diretório DICT e o ICOM de hoje). HML: 48 messages (teste), datas 2026-06-25 a 2026-07-07.

Duas naturezas de concatenação:
- Domínio de transação/mensagem (messages, payments, history, returns, statements, balance_blocks, archive): PROD tem ~zero histórico, então é **append puro** do legado (com prefixo `LEGACY-` nas chaves, sem colisão com o nativo de julho).
- Domínio DICT diretório (keys, claims, refund_requests): PROD **já tem estado vivo sincronizado do BACEN** (303/79/18). O legado traz os mesmos objetos em estado histórico, então aqui NÃO é append cego: **deduplicar pela chave natural** (`(key_type,key_value)`, `claim_id`, `refund_id`) com `ON CONFLICT DO NOTHING`, para não sobrescrever o estado corrente sincronizado. Só entram os históricos que não existem hoje.

Isso confirma as regras da seção 1: como PROD `messages` está vazio e não há partição para 2025-11..2026-02, é obrigatório pré-criar essas partições antes de carregar; a identidade constante `1` e o prefixo `LEGACY-` garantem inserção sem colisão; o valor entra em reais nas canônicas.

## 10. Janela de corte 30/06 → 08/07 e reconciliação com o novo backup

O backup atual termina em 30/06; o novo virá até 08/07. Produção começou 08/07 com o razão zerado (`messages`=0), logo a janela 30/06→08/07 é 100% do legado, sem transação nativa concorrente. Aqui a reconciliação contra o BACEN É viável (dias recentes, extrato ainda disponível), ao contrário do histórico de meses atrás (seção 8).

### 10.1 Régua (ponto de retomada = maior timestamp no backup atual, medido)

| Tabela | Último registro no backup de 30/06 |
|---|---|
| `ANOTACAO_CREDITO` (entrada) | 2026-06-30 15:44:54 |
| `MENSAGEM_BACEN_PSP` (arquivo) | 2026-06-30 15:45:33 |
| `CAMT053` (extrato) | 2026-06-30 17:02:56 |
| `ORDEM_PAGAMENTO` / `BLOQUEIO` (saída) | 2026-06-26 15:38 (saída parou em 26/06) |
| `MOVIMENTOS_CONTABEIS_CLIENTE_CREDITO_EXTERNO` | 2026-06-26 13:12:55 |

Perfil da cauda (volume baixo): crédito de entrada por dia 20–30/06 = 4/1/8/3/4/2/4/6 lançamentos (valores até R$ 1.193.744,00 em 30/06; R$ 608.985,89 em 25/06). Saída até 26/06. Portanto o delta 30/06→08/07 deve ser dezenas de lançamentos, reconciliáveis um a um.

### 10.2 Método para bater o delta

1. Delta por idempotência: carregar o novo backup com o MESMO de→para (seções 1–6). A chave natural (`end_to_end_id`/`message_id` com prefixo `LEGACY-`) faz a sobreposição (≤30/06) não recarregar; só entra o novo (>30/06). Delta = timestamp > régua 10.1 OU chave natural inédita.
2. Referência autoritativa BACEN (viável para 30/06–08/07): baixar o extrato oficial da Conta PI por dia via o pipeline de extrato (camt.052 anuncia `RELACAO_LANCAMENTOS`, GET mTLS `arq:1130`, unzip, verify sha256, parse 17 colunas → `statements`/`statement_entries`) e/ou camt.053 EOD. Isso dá a lista definitiva de lançamentos liquidados por dia.
3. Batimento por E2E, por dia: comparar {conjunto de E2E, quantidade, soma de valor} do delta legado × extrato BACEN. Deve fechar (mesmo conjunto de E2E, mesma quantidade, mesmo valor ao centavo). Divergências: E2E no extrato e ausente no legado = registro perdido no legado (investigar); E2E no legado e ausente no extrato = não liquidou (rejeitado/pendente, conferir status via pacs.002/status).
4. Cross-check com produção: `messages`=0 em prod confirma que nenhuma transação da janela foi materializada na cabine nova. `bacen_inbound` de prod (3.299 desde 08/07) é tráfego DICT/ICOM de go-live, não liquidação. Confirmar que os E2E do delta não aparecem como pagamento em prod (não aparecem, pois messages=0) → sem duplicidade.
5. Saída: relatório de reconciliação por dia (delta legado × extrato BACEN × prod) com delta zero ao centavo, antes de declarar o cutover completo.

### 10.3 Impacto no mapa
Nenhuma mudança de tabela/coluna. A importação vira uma carga incremental idempotente (roda de novo com o backup novo, insere só o delta) mais o batimento 10.2. O extrato baixado para a reconciliação já entra em `statements`/`statement_entries` (seção 3.6) com `source='bacen_file'`, servindo de prova documental do fechamento.

## 11. Ensaio executado (validação empírica do mapa contra o schema REAL)

Executado em 2026-07-08 localmente contra o schema de produção REAL, para de-riscar antes de tocar HML/prod. Ambiente: Postgres 16 (igual ao Aurora) carregado com o DDL real `mon_pix_schema_clean.sql` (438 KB) + deltas pós-clean aplicados: 4 partições históricas criadas (`messages_2025_11`/`2025_12`/`2026_01`/`2026_02`), PK de `bacen_inbound` corrigida para `(ispb,resource_id)`, e 29 CHECK de ISPB/CPF (NOT VALID) ativos. Origem = backup legado no SQL Server.

Resultados (carga + reconciliação + idempotência), todos verdes:

| Caminho | Origem | Destino | Reconciliação | Idempotência |
|---|---:|---:|---|---|
| Crédito de entrada (`ANOTACAO_CREDITO`→`messages`+`payments` INBOUND) | 8.761 | 8.761 + 8.761 | R$ 159.722.913,42 = ao centavo | 0 |
| Pagamento de saída (`ORDEM`+`BLOQUEIO`→`messages`+`payments` OUTBOUND) | 4.392 | 4.392 + 4.392 | R$ 168.935.505,42 = ao centavo (status 4.255 liquidadas) | 0 |
| Histórico de status (`STATUS_ORDEM`+`STATUS_ANOTACAO`→`message_history`) | 30.784 | 30.678 | 106 órfãos = ordens sem BLOQUEIO (esperado) | 0 |
| Bloqueios (`BLOQUEIO`+status→`balance_blocks`) | 4.392 | 4.392 | R$ 168.935.505,42 = ao centavo | 0 |
| Arquivo IN (`MENSAGEM_BACEN_PSP`→`bacen_inbound`) | 21.808 | 21.808 | por tipo idêntico (PACS002 13.090/PACS008 8.237/PACS004 466/ADMI002 12/CAMT054 3) | 0 |
| Arquivo OUT/respostas (`MENSAGEM_RESPOSTA_BACEN_PSP`→`bacen_outbound`) | 8.761 | 8.761 | igual | 0 |
| Devoluções (`ANOTACAO_DEVOLUCAO`→`returns`) | 19 | 19 | todas com pagamento original vinculado | 0 |
| MED refund (`SOLICITACAO_DEVOLUCAO`→`refund_requests`) | 205 | 205 | igual | 0 |
| Infrações (`RELATO_DE_INFRACAO`→`infractions`) | 391 | 391 | igual | 0 |
| Marcação fraude (`MARCACAO_FRAUDE`→`fraud_markers`) | 8 | 8 | igual | 0 |
| Claims (`CLAIM_PORTABILITY`→`claims`) | 12 | 12 | igual | 0 |
| Chaves (`ENDERECAMENTO`→`keys`) | 303 | 303 | igual | 0 |
| Extrato (`EXTRATO_MOVIMENTO_GI`→`statements`+`statement_entries`) | 13.250 | 7 statements + 13.250 entries | C R$ 162.120.983,11 / D R$ 172.527.292,60 = ao centavo | 0 |
| Corpo XML de saída (`ORDEM_PAGAMENTO_MENSAGEM`→`xml_messages`) | 4.373 | 4.373 | pacs.008 4.355 / pacs.004 18 (namespace conferido) | 0 |
| SPB operações (`TRANS_INTERF_PASS`→`spb_operations`+`operation_events`) | 10.461 | 10.461 + 10.461 | status composto (confirmed 10.307/created 103/cancelled 23/r1_rejected 23/processing 5); carga ordenada por FK OK | 0 |
| SPB arquivo (`CORPO_TRANS_INTERF_PASS`→`bacen_messages`) | 10.461 | 10.461 | message_type extraído do XML (STR0008 8.098/LPI/STR/SME); `operation_id` 100% vinculado | 0 |

Total carregado no ensaio: ~127 mil linhas PIX + ~21 mil SPB, zero rejeição de INSERT, todo o dinheiro reconciliado ao centavo, tudo idempotente.

Técnica para colunas XML largas: `FOR JSON` do SQL Server quebra resultados acima de 2033 chars em várias linhas (corrompe o objeto). Para XML (~4 KB) usei extração direta delimitada por `CHAR(31)` (sem FOR JSON) → COPY CSV com `DELIMITER E'\x1f'`; funcionou para `xml_messages` e `bacen_messages`.

Provas obtidas: (a) todos os INSERTs passam nas restrições reais (NOT NULL, enums OUTBOUND/INBOUND e DEBIT/CREDIT, CHECK de ISPB/CPF após normalização, chaves de idempotência); (b) roteamento de partição correto: os créditos caíram em `messages_2025_12` (5.254), `2026_01` (2.403), `2026_02` (374), `2026_03..06` (o resto), ZERO em `messages_default` (sem as 4 partições pré-criadas, 8.031 linhas ficariam presas na default e sem ATTACH depois); (c) status de saída correto: 4.255 liquidadas, 117 rejeitadas, 19 erro, 1 icom.

Dois defeitos de mapeamento encontrados e corrigidos no ensaio (importantes para o de→para):
1. Status terminal da ordem vem da TABELA `STATUS_ORDEM_PAGAMENTO` (última transição por ordem), NÃO da coluna `ORDEM_PAGAMENTO.STATUS_ORDEM_PAGAMENTO` (que é sempre `ENVIADA`). Usar a coluna daria status errado em 100% das ordens.
2. Poucas ordens (1 em 4.392) têm `COD_ISPB_CONTRA_PARTE` vazio (transferência interna/mesmo ISPB, E2E começa com o nosso ISPB). Regra: `creditor_ispb = coalesce(normalize(contraparte), '46026562')` (fallback interno).

Restam só itens menores/gated (o mecanismo de cada um já está provado): (a) `bacen_inbound.xml_content` dos corpos gzip de entrada (o gunzip foi provado numa mensagem; a carga em massa é passo do loader Python que lê o blob direto, sem o limite de largura do sqlcmd); (b) `qr_codes` (exige parsear `JSON_QR_CODE_INFO`, que carrega tx_id/recebedor); (c) `camt060_requests` e `balances`/contábil (GATE: saldo com snapshot, contábil precisa semear a COA); (d) `spb_operations.amount` fica em 0 até extrair o valor do XML/catálogo (`TRANS_INTERF_PASS` não carrega valor). A carga real dentro do Aurora de HML é um passo de execução dentro da VPC (o Aurora não é alcançável direto; canais: runner dentro da VPC com psql, ou o loader retargetado rodando de um host com rota ao Aurora).

## 12. Reuso do loader

Base: `etl/legacy_pix_spb/load_legacy_pix_spb.py` (staging JSONB + projeção idempotente + verify + mapas de status, provado ao centavo). Ajustes para produção: retarget `cecresa_*`→schemas reais (`monetarie_spi.messages`+`payments`, `monetarie_dict.*`, `monetarie_settlement.*`, `mon_spb.public`); conexão revisada ao Aurora; aplicar as regras globais da seção 1 (identidade constante, conversão de valor por coluna, normalização ISPB/CPF, partições, prefixo LEGACY, ordem de FK); gunzip das mensagens; filtro `NOT LIKE 'List%'` nas tabelas DICT; gates de saldo/COA.
