# Dossiê de paridade — Participante indireto (IspbIF vs Debtor/Creditor)

Auditoria read-only. Zero inferência: toda afirmação abaixo cita prova (arquivo:linha ou
tabela.coluna). Onde não há prova, o veredito é INCONCLUSIVO. Escrita pt-br.

Escopo: como o legado (.NET CRK/Corner) modela o participante DIRETO liquidante (IspbIF)
separado do pagador/recebedor reais (IspbDebtor/IspbCreditor) em TODO o money-path
(long-poll ICOM, envio, camt.054, Conta PI, geração de ids, cadastro REDA) e o que a NOSSA
cabine (Elixir umbrella `pix/backend`) faz. Pergunta-guia do mandato: o nosso money-path
modela liquidante para indireto ou assume ISPB único?

---

## 0. Resposta curta

O legado é **multi-ISPB por construção**: cada mensagem carrega três ISPBs distintos
(`IspbIF` = participante DIRETO liquidante, `IspbDebtor`/`IspbCreditor` = pagador/recebedor
reais, que podem ser INDIRETOS), e o pipeline inteiro (envio, long-poll, delete, camt.054,
`MessageId`/`E2E`) é chaveado por `IspbIF`. A conta de reserva (Conta PI) é movimentada
distinguindo Propria/Liquidante/**SME** (indireto) via LPI0001-0004.

A NOSSA cabine **modela o conceito** (schema `pix_participants` com `role: direct/indirect`
+ `liquidante_ispb`; `resolve_sender_ispb`/`classify_sender_ispb`; cadastro REDA
reda.014/022/031), mas o money-path efetivo **assume ISPB único** em três pontos provados:
(1) a linha da transação e a pacs.008 **colapsam** `debtor_ispb`/AppHdr `Fr`/DbtrAgt no
ISPB do liquidante RESOLVIDO, descartando o ISPB do indireto original (só logado); (2) o
long-poll ICOM sobe **um único** coordenador CPM+CSM para a ISPB da própria instituição, sem
iterar o conjunto de liquidantes; (3) o POST de envio é fixo em `Client.ispb()`. Além disso,
o registro REDA (`indirect_participants`) está **desconectado** do roteamento de money-path
(`pix_participants`), e o ramo `role: "indirect"` está **dormente** (a seed só popula o
participante direto). A Conta PI não distingue SME (LPI0001-0004 ausente no backend PIX).

---

## 1. LEGADO — comportamento provado

### 1.1 Modelo de dados: três ISPBs por mensagem

`SPI.Core.Domain.decompiled.cs` — a entidade `Message` (e `MessageForStatus`, `OperMessage`,
`LoteMessageFull`) tem TRÊS colunas de ISPB distintas:

- `SPI.Core.Domain.decompiled.cs:287-291` (`Message`): `IspbIF` / `IspbDebtor` / `IspbCreditor`.
- `SPI.Core.Domain.decompiled.cs:363-366` (`MessageForStatus`): idem.
- `SPI.Core.Domain.decompiled.cs:1285-1289`, `:1756`, `:1793-1795`, `:1813` (OperMessage/LoteMessageFull).
- `SPI.Core.Mensageria.Infrastructure.decompiled.cs:209`: `eb.Property(b => b.IspbIF).HasColumnName("IspbIF").HasColumnType("char(8)")` — coluna persistida.

Prova no banco vivo (SQL Server, `crk_spi.dbo.SpiMessage`): a tabela tem as colunas
`IspbDebtor`, `IspbCreditor`, `IspbIF` (query `sys.columns`). No backup atual há 44 linhas com
`IspbIF` único `12345678` (ambiente de teste), então a divergência não aparece nesse acervo
de mensagens; mas o modelo carrega os três campos.

`InstituicaoFinanceira` (o "dono" do IspbIF): `SPI.Core.Domain.decompiled.cs:1748-1759` —
campos `IdInstFinanc`, `IspbIF`, `TpEmpresa`. É a fonte do participante DIRETO.

### 1.2 IspbIF derivado da InstFinanc, distinto do debtor/creditor

Padrão repetido em toda validação de mensagem (`SPI.Core.Application.decompiled.cs`):

```
2718  if (string.IsNullOrEmpty(dados.IspbIF))
2720      dados.IspbIF = instFinanc.IspbIF;          // participante DIRETO (config)
...
2733  if (string.IsNullOrEmpty(dados.IspbIF))
2735      dados.IspbIF = (ehBacen ? dados.IspbCreditor : dados.IspbDebtor);  // fallback
```

Igual em `:3659-3677` (PAIN009), `:3772-3788`, `:3884-3912`. O "complemento" de retorno
sempre carimba `ISPBEmpresa = instFinanc.IspbIF`:
`SPI.Core.Application.decompiled.cs:2665`, `:2838`, `:3046`, `:4356`. Ou seja, o IspbIF é o
participante DIRETO que a InstFinanc representa, e é semanticamente distinto do pagador
(`IspbDebtor`) e do recebedor (`IspbCreditor`), que podem ser INDIRETOS.

Validação explícita de que debtor e creditor existem como participantes
(`SPI.Core.Application.decompiled.cs:2737-2743`, `:3679-3685`): rejeita
`ParticipanteDebtorInvalido` / `ParticipanteCreditorInvalido`.

### 1.3 MessageId e E2E gerados COM o IspbIF

`SPI.Core.Application.decompiled.cs:7012`:
`messageFull2.MessageId = _seedRepository.GeraMsgId(messageFull.IspbIF, ...)`. O gerador
(`GeradorMsgId`, `General.decompiled.cs:288`) monta `prefixo + ISPB(8) + yyyyMMddHHmm + rnd`.
Então o `MessageId`/`E2E` que o BACEN deduplica é ancorado no ISPB do DIRETO liquidante.

### 1.4 Envio (POST) por IspbIF

`SPI.Core.General.decompiled.cs:6192`: `ISendService.Send(string ispbIF, string msg)`.
URL do POST, `SPI.Core.Mensageria.Geral.Infrastructure.decompiled.cs:742` e `:848`:
```
_httpClient.PostAsync(new Uri($"{_url}/{_config.MetodoEnvio}/{ispbIF}/msgs"), ...)
```
O slot de envio ao SPI é POR `ispbIF`.

### 1.5 Long-poll ICOM por IspbIF (multi-ISPB)

`SPI.Core.Worker.RetornoApiBacen.decompiled.cs:209-223`:
```
IEnumerable<string> enumerable =
   (await _repoIF.List(_config.ISPBSIFS)).Select(d => d.IspbIF).Distinct();
for (num ...) foreach (item in enumerable) { thread.Start(new DadosThread{ Ispb = item }); }
```
Uma thread de leitura POR IspbIF distinto (lista `ISPBSIFS` na config;
`EnvioApiBacen.Application:95` / `RetornoApiBacen.Application:108`).
`RetornoApiBacen.Infrastructure.decompiled.cs:1337` `Processamento(string ispbIF, ...)`,
`:1355` `_receiveService.Receive(ispbIF, cursor)`, `:1395` `_receiveService.Delete(ispbIF, resourceId)`.
URL do receive, `Mensageria.Geral.Infrastructure.decompiled.cs:446`/`:601`:
`GetAsync($"{_url}/{_config.MetodoRetorno}/{ispbIF}/stream/start")`. Cursor `PIResourceIdNext`,
ack pós-processamento, rollback (não avança) em erro, 204=fim, 410 tolerado. At-least-once
POR IspbIF.

### 1.6 camt.054 casa por IspbIF

O complemento de retorno grava `ISPBEmpresa = instFinanc.IspbIF`
(`SPI.Core.Application.decompiled.cs:2665`, `:3046`, `:4356`), e a operação recebe
`ret.IspbIF = retorno.Complemento.ISPBEmpresa` (`:3246`, `:7905`, `:8085`, `:8348`, `:8418`),
ou `ret.IspbIF = operacaoOrig.IspbIF` na devolução (`:8189`). A conciliação de liquidação é
por IspbIF.

### 1.7 Conta PI (reserva SPB) distingue Propria/Liquidante/SME

`SPI.Core.SPB.Application.decompiled.cs:49-85` — mensagens LPI de movimentação da Conta PI:
- `LPI0001` = crédito Propria/Liquidante (`<ISPBIF>` + `<ISPBPSPI>`), `:49-51`.
- `LPI0002` = crédito **SME** (indireto) (`<ISPBIEME>` + `<NumCtrlIEME>`), `:53-55`.
- `LPI0003` = débito Propria/Liquidante (`<ISPBPSPI>` + `<ISPBIFCredtd>` + `<FinlddLPI>`), `:57-59`.
- `LPI0004` = débito **SME** (indireto), `:61-63`.

O tipo é escolhido por `enumTipoMsgSaldo` {Propria, Liquidante, SME}. O saldo do INDIRETO na
Conta PI é fundeado com mensagens SME próprias.

### 1.8 Tabelas de participante indireto (cadastro/roteamento)

- `crk_multtiliquidacao.dbo.TB_PARTICIPANTECONTROLADO` (5 linhas): colunas `NR_SPB` (ISPB
  indireto), `NR_SPBDIRETO` (ISPB do direto liquidante), `ID_TIPOCONTA`, `NR_AGENCIA`,
  `NR_CONTA`, `IC_INTEGRACAOCONTA`, `TP_SISTEMACONTA` (provedor de CC). Dados vivos provam o
  mapeamento indireto→direto:
  - `10664513` → direto `12345678` (Topazio), `39339421` → `12345678` (Topazio),
    `60779196` → `12345678` (Crefisa), `61033106` → `12345678` (Crefisa),
    `12345678` = direto (NR_SPBDIRETO NULL, BNP).
- `crk_multtiliquidacao.dbo.TB_OPERACAO`: coluna `NR_SPBCONTROLADOR` (ISPB do controlador que
  liquida) distinta de `NR_SPBPAGADOR`/`NR_SPBRECEBEDOR`. Prova viva: existem operações com
  `NR_SPBCONTROLADOR=12345678` e `NR_SPBPAGADOR=04866275` (controlador ≠ pagador).
- `CRK_SPIDOMINIO.dbo.SpiPartIndireto`: `IdInstFinanc`, `DsCnpjParticipante`, `NmParticipante`,
  `IcAtivo` — registro do indireto por InstFinanc.
- `CRK_SPIDOMINIO.dbo.SpiPendDesvPartIndireto`: `IdInstFinanc`, `IspbParticipante`,
  `DtHrEntrada`, `DtHrLimite`, `Issuer`, `IcPendente` — prazo de desvinculação do indireto.
- Domain: `ParticipanteIndireto` (`SPI.Core.Domain.decompiled.cs:921-930`),
  `PrazoDesvinculacaoParticipanteIndireto` (`:995-1008`).

---

## 2. NOSSO — comportamento provado

### 2.1 Existe o modelo direto/indireto (schema + contexto)

`apps/shared/lib/shared/participants/participant.ex` — tabela `pix_participants`:
`ispb`, `name`, `role` ("direct"|"indirect", linha 18), `liquidante_ispb` (linha 19),
`settlement_account_id` (linha 21, conta TigerBeetle), `entity_type`. Changeset valida que
indireto exige `liquidante_ispb` (`:49-58`).

`apps/shared/lib/shared/participants/participants.ex`:
- `get_liquidante_for/1` (`:69-74`): direto retorna a si, indireto retorna o liquidante.
- `list_indirect_for_liquidante/1` (`:91-99`).
- `resolve_spi_ispb/1` (`:109-122`): direto→própria ISPB, indireto→`liquidante_ispb`,
  desconhecido→as-is.

Este modelo espelha `TB_PARTICIPANTECONTROLADO` (NR_SPB→NR_SPBDIRETO).

### 2.2 Resolução do remetente (fail-closed) — usa liquidante para indireto

`apps/settlement_service/lib/settlement_service/workers/core_event_processor.ex:1630-1674`
(`resolve_sender_ispb` → `classify_sender_ispb`):
- `%{role: "indirect", liquidante_ispb: liq}` → `{:ok, liq}` (`:1648-1649`).
- `%{role: "direct", ispb}` → `{:ok, ispb}` (`:1651-1652`).
- desconhecido → fallback participante direto (nossa própria ISPB no SCD) → `{:ok, direct.ispb}`
  (`:1661-1663`); senão `{:error, :unresolvable_sender_ispb}` (`:1669-1671`).

É fail-closed (nunca envia pacs.008 com ISPB chutado). Para o participante DIRETO/liquidante
(AppHdr `Fr` + certificado de assinatura), isto está correto e espelha `IspbIF`.

### 2.3 DEFEITO: money-path colapsa o ISPB do debtor no liquidante

`core_event_processor.ex` `do_payment_request` (`:530-707`):
- `raw_debtor_ispb = message["debtor_ispb"] || message["entity_ispb"]` (`:238`) — o ISPB do
  pagador (que, para um indireto, seria o ISPB do indireto).
- `sender_ispb = resolve_sender_ispb(raw_debtor_ispb)` (`:249`,`:264`) → liquidante.
- A linha da transação grava **`debtor_ispb: sender_ispb`** (`:558`) — o liquidante, NÃO o
  indireto. `raw_debtor_ispb` só aparece no log (`:608`), nunca é persistido como campo.
- A pacs.008 é montada com **`debtor_ispb: tx.debtor_ispb`** (DbtrAgt do Document, `:686`) e
  **`from_ispb: tx.debtor_ispb`** (AppHdr `Fr`, `:704`) — ambos = liquidante resolvido.

`apps/shared/lib/shared/bacen/iso20022/message_builder.ex`:
- `build/2` `:60`: `from_ispb = params[:from_ispb] || Shared.Bacen.Client.ispb()` (default
  ISPB único).
- AppHdr `:146-147`: `<Fr>...<Id>#{from_ispb}</Id>` / `<To>...#{to_ispb}</To>`.
- pacs.008 `render_cdt_trf_tx_inf` `:237`: `<DbtrAgt>...<MmbId>#{v[:debtor_ispb]}</MmbId>`.

Resultado: para um indireto genuíno (pagador), o DbtrAgt e o `debtor_ispb` persistido saem
com o ISPB do liquidante, não do indireto. O legado guarda e propaga `IspbIF` (liquidante) e
`IspbDebtor` (indireto) SEPARADOS por todo o pipeline; nós descartamos o segundo.

### 2.4 DEFEITO: long-poll ICOM assume ISPB único

`apps/spi_service/lib/spi_service/icom/application_supervisor.ex:75-105`:
```
ispb = Keyword.get(opts, :ispb) || Application.get_env(:shared, :institution_ispb)   # :75-77
children = build_children(inbound_source, ispb)                                       # :79
# build_children(:icom_http, ispb): UM CpmCoordinator + UM CsmCoordinator para essa ispb  :89-100
```
Sobe exatamente um coordenador CPM e um CSM para a ISPB da instituição. O Coordinator é "um
por (ispb, canal)" (`icom/csm/coordinator.ex:5`, `:149-150` lock por ISPB) — a arquitetura
SUPORTA multi-ISPB, mas o supervisor não itera o conjunto de liquidantes/diretos. Contraste:
o legado sobe uma thread por `IspbIF` distinto de `ISPBSIFS` (`RetornoApiBacen:209-223`).

O cliente HTTP em si é parametrizado por ISPB
(`apps/shared/lib/shared/bacen/client.ex:355-366`: `/api/v1/out/#{ispb}/stream/start`), então
o gap é de ORQUESTRAÇÃO (quem sobe quantas sessões), não de transporte.

### 2.5 DEFEITO: envio (POST) fixo em Client.ispb()

`apps/spi_service/lib/spi_service/workers/outbound_sender.ex:1456`:
```
defp message_path(_type), do: "/api/v1/in/#{Client.ispb()}/msgs"
```
O slot de envio é a ISPB única da cabine, não o `ispbIF` da mensagem. A assinatura escolhe o
cert por `message["from_ispb"] || message["debtor_ispb"] || Client.ispb()` (`:883`) — para
indireto isso é o liquidante (correto), mas o PATH do POST ignora essa resolução. Contraste:
legado `{_url}/{MetodoEnvio}/{ispbIF}/msgs`.

Nota: com um único participante direto (Monetarie SCD, ISPB 46026562 = `Client.ispb()`), os
2.4/2.5 coincidem na prática. O risco é estrutural/latente, não live hoje.

### 2.6 AUSENTE: Conta PI não distingue Propria/Liquidante/SME (LPI0001-0004)

`check_and_block_balance(sender_ispb, ...)` → `create_spi_balance_block(ispb=sender_ispb, ...)`
(`core_event_processor.ex:299`, `:1752`, `:1803`): bloqueia UM saldo de Conta PI chaveado pela
ISPB resolvida (liquidante). Não há LPI0001-0004 nem tipo SME no backend PIX — grep de
`LPI0001..0004|SME|IEME` só acha comentário em
`apps/spi_service/lib/spi_service/statements/statement_file.ex:128` (parser de nome de arquivo)
e `VSME` no `camt053_parser.ex:83` (extração de saldo). A movimentação da reserva por LPI é
domínio da cabine SPB (fora deste repo), mas o CONCEITO de fundear o saldo do INDIRETO (SME)
separado do próprio não está modelado no money-path PIX.

### 2.7 PARCIAL: cadastro REDA existe, mas desconectado do roteamento

Existe um modelo REDA COMPLETO e correto para o cadastro do indireto no BACEN:
- Schema `apps/shared/lib/shared/schemas/reda/indirect_participant.ex` (tabela
  `monetarie_settlement.indirect_participants`): `indirect_ispb`, `indirect_cnpj`, `name`,
  `status`, `confirmation_deadline` (PRAZOCONFI da reda.017, `:35`), responsáveis, etc.
- Contexto `apps/shared/lib/shared/reda.ex`: `create/1`, `mark_status/3`,
  `apply_party_report/1` (reda.017, `:144`), `apply_activity_advice/1` (reda.041, `:171`).
- `apps/shared/lib/shared/reda/sender.ex`: envia reda.014 (registro), reda.022 (responsáveis),
  reda.031 (desligamento) assinadas ao BACEN. **Mantém o ISPB do indireto DISTINTO**: reda.022
  `party_ispb = row.indirect_ispb` (SysPtyId) vs `from_ispb = Client.ispb()` (`:103-111`),
  citando o próprio legado (`IspbDebtor` ≠ `ISPBEmpresa/IspbIF`). reda.014 carrega
  `indirect_ispb`/`indirect_cnpj` (`:99-101`).
- Controller `apps/settlement_service/lib/settlement_service_web/controllers/admin/reda_controller.ex`.

MAS: a tabela REDA (`indirect_participants`) e a tabela de roteamento money-path
(`pix_participants`) são DISJUNTAS — sem FK, sem sincronização. Grep de
`pix_participants|Shared.Participants` em `reda.ex`/`reda/`/`reda_controller.ex` = vazio.
Registrar um indireto via REDA NÃO o insere em `pix_participants`, então o money-path não
passa a roteá-lo como indireto (e vice-versa). São dois mundos paralelos com o mesmo nome.

### 2.8 PARCIAL/DORMENTE: ramo indireto do money-path sem dados

Seed `apps/shared/priv/repo/seeds/pix_participants_seed.exs:19-22`: popula `pix_participants`
com EXATAMENTE UM participante — MONETARIE, `role: "direct"`, `entity_type: "SCD"`, ISPB
46026562. Nenhum indireto é semeado (grep `role:.*indirect` na seed = vazio). A seed principal
`apps/shared/priv/repo/seeds.exs` mexe em `Shared.Schemas.Spi.Participant` (tabela
`monetarie_settlement.participants`), OUTRA tabela, todos `is_direct: true`.

Consequência provada: em `classify_sender_ispb`, sem linha `role: "indirect"` em
`pix_participants`, todo ISPB não-próprio cai no fallback do participante direto
(`:1661-1663`) e resolve para 46026562. O ramo indireto (`:1648-1649`) existe mas nunca é
exercitado com dados reais. É latente, não um bug live.

---

## 3. Matriz de GAPS

| id | título | tipo | risco | legado | nosso |
|----|--------|------|-------|--------|-------|
| g1 | Money-path colapsa debtor no liquidante (perde ISPB do indireto) | divergencia | alto | IspbIF≠IspbDebtor persistidos e propagados | `debtor_ispb`=liquidante; AppHdr Fr+DbtrAgt=liquidante; raw só logado |
| g2 | Long-poll ICOM assume ISPB único | divergencia | medio | 1 thread por IspbIF (`ISPBSIFS`) | 1 CPM+1 CSM p/ `institution_ispb` |
| g3 | POST de envio fixo em Client.ispb() | divergencia | medio | `.../{ispbIF}/msgs` | `.../#{Client.ispb()}/msgs` |
| g4 | Conta PI não distingue Propria/Liquidante/SME | ausente | medio | LPI0001-0004 (SME p/ indireto) | 1 saldo por ISPB resolvida; sem LPI/SME |
| g5 | Registro REDA desconectado do roteamento money-path | parcial | medio | InstFinanc/ParticipanteControlado é a mesma autoridade | `indirect_participants` (REDA) ≠ `pix_participants` (rota); sem FK/sync |
| g6 | Ramo indireto do money-path dormente (sem seed) | parcial | baixo | TB_PARTICIPANTECONTROLADO populado | seed só o direto; ramo `role:indirect` nunca exercitado |
| g7 | Prazo de desvinculação do indireto | parcial | baixo | SpiPendDesvPartIndireto / PrazoDesvinculacao | só `confirmation_deadline` (reda.017); sem tabela de desvinculação |
| c1 | Modelo direct/indirect + liquidante | coberto | info | ParticipanteControlado | `pix_participants` role+liquidante_ispb |
| c2 | Resolução do liquidante para AppHdr Fr / assinatura (fail-closed) | coberto | info | IspbIF = InstFinanc | `classify_sender_ispb` → liquidante |
| c3 | MessageId/E2E ancorados na ISPB liquidante | coberto | info | GeraMsgId(IspbIF) | `generate_msg_id(sender_ispb)`/`generate_e2e_id(sender_ispb)` |
| c4 | Ciclo REDA reda.014/022/031 + inbound 016/017/041, indireto distinto | coberto | info | REDA + SpiPartIndireto | `Shared.Reda` + `Reda.Sender` (party_ispb distinto) |

---

## 4. Detalhe dos gaps

### g1 — colapso do debtor (divergencia, alto)
- Legado: `Message.IspbIF`, `IspbDebtor`, `IspbCreditor` são três colunas; `IspbIF` vem da
  InstFinanc (`Application:2720`), debtor/creditor vêm do payload. Persistidos e usados no
  camt.054/status por toda a vida da operação.
- Nosso: `do_payment_request` grava `debtor_ispb: sender_ispb` (liquidante,
  `core_event_processor.ex:558`) e monta a pacs.008 com `debtor_ispb`+`from_ispb` = liquidante
  (`:686`,`:704`); `raw_debtor_ispb` (indireto) só no log (`:608`).
- Risco: para um indireto real, o DbtrAgt do Document e o registro persistido saem com o ISPB
  do liquidante, apagando a identidade do indireto na mensagem ao BACEN e no extrato/reconc.
  Latente hoje (sem indireto vivo — ver g6), mas é o defeito central do escopo: vira "alto"
  assim que um indireto for onboardado.

### g2 — ICOM single-ISPB (divergencia, medio)
- Legado: `RetornoApiBacen.decompiled.cs:209-223` uma thread por IspbIF distinto.
- Nosso: `icom/application_supervisor.ex:75-100` sobe 1 CPM + 1 CSM para `institution_ispb`.
- Risco: se a Monetarie liquidar para um segundo participante direto (ou um liquidante com
  ISPB ≠ da instituição), o inbound desse ISPB nunca seria pollado. Baixo-médio no SCD atual
  (participante direto único), mas estrutural.

### g3 — envio single-ISPB (divergencia, medio)
- Legado: `Mensageria.Geral.Infrastructure:742/848` `.../{ispbIF}/msgs`.
- Nosso: `outbound_sender.ex:1456` `.../#{Client.ispb()}/msgs`.
- Risco: mensagem cujo liquidante ≠ `Client.ispb()` seria postada no slot errado. Coincide na
  prática hoje (único direto = Client.ispb()).

### g4 — Conta PI sem SME (ausente, medio)
- Legado: LPI0001-0004 (`SPB.Application:49-85`) fundeia o saldo do INDIRETO (SME) separado.
- Nosso: `create_spi_balance_block(ispb=sender_ispb)` — saldo único por ISPB resolvida; sem
  LPI/SME (grep confirma ausência no backend PIX; VSME/LPI só aparecem em parsers).
- Risco: o saldo/reserva do indireto não é modelado à parte no PIX. Parte disso é da cabine
  SPB (fora do repo); a lacuna é a ausência do conceito no money-path PIX.

### g5 — REDA desconectado do roteamento (parcial, medio)
- Nosso: `indirect_participants` (REDA/BACEN, `reda.ex`) e `pix_participants` (rota money-path,
  `participants.ex`) são tabelas distintas sem FK/sync (grep cruzado vazio).
- Risco: um indireto pode estar ACTIVE no BACEN via REDA mas não roteado no money-path (ou o
  inverso). Inconsistência operacional; o dado que o operador cadastra não alimenta a decisão
  de liquidação.

### g6 — ramo indireto dormente (parcial, baixo)
- Nosso: seed só o participante direto (`pix_participants_seed.exs:19-22`); `classify_sender_ispb`
  fallback para direto resolve tudo para 46026562. Ramo `role:indirect` nunca exercitado.
- Risco: baixo/informativo — o código existe, falta dado e teste vivo. É a razão de g1/g2/g3
  não estourarem hoje.

### g7 — desvinculação (parcial, baixo)
- Legado: `SpiPendDesvPartIndireto` + `PrazoDesvinculacaoParticipanteIndireto` (prazo/limite,
  `Issuer`, `IcPendente`).
- Nosso: só `confirmation_deadline` (PRAZOCONFI da reda.017, `indirect_participant.ex:35`).
  Não há tabela/fluxo dedicado de desvinculação com prazo-limite.

---

## 5. Itens COBERTOS (confiança)

- c1: modelo direct/indirect com liquidante existe e valida
  (`participant.ex:18-19,49-58`; `participants.ex`).
- c2: resolução do liquidante para o participante DIRETO (AppHdr Fr + assinatura), fail-closed
  (`core_event_processor.ex:1646-1674`). Espelha `IspbIF = InstFinanc.IspbIF`.
- c3: `MessageId`/`E2E` gerados com a ISPB resolvida (liquidante)
  (`core_event_processor.ex:446,550`), espelhando `GeraMsgId(IspbIF)` do legado.
- c4: ciclo REDA completo (reda.014/022/031 outbound + reda.016/017/041 inbound) mantendo o
  ISPB do indireto DISTINTO no SysPtyId (`reda/sender.ex:103-115`; `reda.ex:144,171`).

---

## 6. Veredito

O escopo "modela liquidante para indireto ou assume ISPB único?" resolve como **HÍBRIDO com
regressão no money-path**: o conceito e o cadastro (REDA + `pix_participants`) existem e a
resolução do liquidante para AppHdr Fr/assinatura/ids está correta (c1-c4), mas o money-path
efetivo **colapsa a identidade do pagador/recebedor indireto no liquidante** (g1) e **assume
ISPB único** no long-poll e no envio (g2, g3), sem a distinção de Conta PI SME (g4). O registro
REDA está desacoplado do roteamento (g5) e o ramo indireto está dormente por falta de dados
(g6). Para operar um participante indireto real com a fidelidade do legado, g1/g5 são os
bloqueantes; g2/g3/g4 tornam-se relevantes se houver mais de um participante direto/liquidante.
