# Contrato da consulta de operacao na cabine PIX (MGMT Query re-fonteada)

Data: 2026-07-07
Escopo: metade CABINE da tarefa 1.10 (`pix/backend`). A metade CORE
(re-apontar `POST /pix/mgmt/query` do `core/backend` e o `useMgmtQuery` do front)
consome este endpoint e faz o merge com as pacs locais. Este documento e o
contrato que desbloqueia a metade Core.

Base de decisao: `docs/reports/2026-07-07-mapa-onz-avivpay.md`, secao 3 (mapa
campo a campo 3.4). Decisao do dono: manter 100% o contrato de resposta que a
tela ja consome e trocar a fonte de dados de OnZ para camt.060 -> camt.054.

## 1. Endpoint

Na cabine (spi_service):

    POST /api/v1/operations/query
    Content-Type: application/json
    { "end_to_end_id": "<E2E>" }

Via gateway (settlement_service, o host `pixapi-h` que o Core alcanca):

    POST /api/v1/operations/query   -> proxied 1:1 ao spi_service

Aceita `end_to_end_id` (canonico), com apelidos `e2e_id` e `e2e`. O identificador
tem de ter forma de E2E (`[EDL]` + ISPB(8) + AAAAMMDDHHMM + 11) ou de STR
(`[A-Z]{3}` + AAAAMMDD + 9) — mesma validacao de `RptgReq/Id` (IdType do XSD
`camt.060.spi.1.9`) usada pela consulta de operacao do Construtor de Mensagens.

## 2. Fluxo (o que o endpoint faz)

1. Valida a forma do E2E/STR. Malformado -> `invalid_e2e` (HTTP 400), sem
   disparar nada.
2. Dispara a camt.060 de CONSULTA DE OPERACAO (detalha-lancto,
   `ReqdMsgNmId=camt.054`, `RptgReq/Id=<E2E>`, canal primario, assinada no HSM)
   pelo funil provado `SpiService.Camt060.query("detalha-lancto", ...)`
   (reuso de `SpiService.Med.InfractionEnricher.request_operation_detail/1`).
   O `GrpHdr/MsgId` da camt.060 e persistido em `monetarie_spi.camt060_requests`
   (status `sent`) ANTES do envio.
3. Aguarda (poll do registro `camt060_requests`) a camt.054 correlacionada. A
   correlacao e o mecanismo EXISTENTE: o BACEN ecoa o `MsgId` no `Ntfctn/Id` da
   camt.054, e o `InboundProcessor` fecha a requisicao
   (`Camt060Requests.correlate_response/3`, status -> `responded`,
   `response_msg_id` = id da camt.054) e persiste o XML em
   `monetarie_spi.bacen_inbound` (`resource_id` = esse id).
4. Ao fechar, le o corpo em `bacen_inbound` e parseia com
   `Shared.Spi.Camt054Parser.parse/1`.

Padrao sincrono com timeout (mesma natureza do round-trip assincrono BACEN, so
que a chamada HTTP admin espera a resposta): timeout e poll configuraveis por
`config :spi_service, :operation_query_timeout_ms` (default 15000) e
`:operation_query_poll_ms` (default 250). O proxy do gateway usa timeout de rede
de 20s (cobre a espera com folga). Fail-closed: **timeout e um status explicito,
nunca resultado fabricado**.

## 3. Resposta

Sempre carrega `status` discriminador. Codigos HTTP: `invalid_e2e` -> 400; todos
os demais (`ok`/`timeout`/`error`) -> 200 (sao desfechos validos da consulta que
o Core renderiza).

### 3.1 `status: "ok"`

```json
{
  "status": "ok",
  "raw_message_id": "<id da camt.054 recebida (bacen_inbound.resource_id)>",
  "request_msg_id": "<MsgId da camt.060 emitida>",
  "camt054": { ...ver 3.3... }
}
```

### 3.2 Demais status

```json
{ "status": "timeout", "request_msg_id": "...", "detail": "camt.054 correlacionada nao chegou dentro do tempo limite" }
{ "status": "timeout", "request_msg_id": "...", "raw_message_id": "...", "detail": "camt.054 correlacionada, mas o corpo nao materializou em bacen_inbound no tempo limite" }
{ "status": "error",   "detail": "camt.060 de consulta de operacao nao foi enviada ao BACEN", "reason": "..." }
{ "status": "error",   "request_msg_id": "...", "raw_message_id": "...", "detail": "camt.054 correlacionada sem lancamento parseavel" }
{ "status": "error",   "request_msg_id": "...", "raw_message_id": "...", "detail": "camt.054 correlacionada e de outro E2E (<got>)" }
{ "status": "invalid_e2e", "detail": "..." }
```

- `timeout`: a camt.060 SAIU, mas no prazo (a) a camt.054 nao correlacionou, ou
  (b) correlacionou mas o corpo nao materializou em `bacen_inbound`. Importante:
  o `InboundProcessor` NAO e transacional e marca `responded` ANTES de inserir o
  corpo; por isso o fluxo, ao ver `responded` sem corpo, CONTINUA polando ate o
  deadline (o corpo chega em ms) — nunca devolve erro/ok fabricado nessa janela.
  O Core pode reconsultar; o registro em `camt060_requests` segue `sent`/`responded`
  (o watchdog `StuckOutboundChecker` marca `timeout` apos 30 min).
- `error`: a camt.060 nao saiu (BACEN desabilitado/HSM fora/falha de envio), OU o
  corpo materializado nao e parseavel, OU o E2E da camt.054 correlacionada diverge
  do consultado (guarda money-adjacent: nunca devolve dado de outra operacao).

## 3.3 `camt054` — campos (mapa 3.4)

Todos os campos abaixo saem do `Ntfctn/Ntry` + `NtryDtls/TxDtls` do camt.054,
na semantica do XSD SPI `camt.054.spi.1.15.xsd`. Valor monetario: `amount` vem
como string decimal (ex.: `"1234.56"`) na borda de serializacao e
`amount_cents` como inteiro de centavos deterministico (`123456`).

| Campo `camt054`          | `MgmtFormatted` (Core)     | Origem no camt.054                                   |
|--------------------------|----------------------------|------------------------------------------------------|
| `end_to_end_id`          | `e2e`                      | `Ntry/NtryDtls/TxDtls/Refs/EndToEndId`               |
| `ntfctn_id`              | (correlacao)               | `Ntfctn/Id` (echo do MsgId da camt.060)              |
| `group_msg_id`           | `msgId`                    | `GrpHdr/MsgId`                                        |
| `amount` (string)        | `valor`                    | `Ntry/Amt`                                            |
| `amount_cents` (int)     | `valor` (centavos)         | `Ntry/Amt` -> centavos                               |
| `currency`               | —                          | `Ntry/Amt/@Ccy` (BRL)                                |
| `status`                 | `status`                   | `Ntry/Sts/Cd` (BOOK/INFO)                            |
| `status_normalized`      | `statusNormalized`         | BOOK->CONCLUIDA, PDNG->PDNG, demais cru              |
| `credit_debit_indicator` | `tipoMovimento`            | `Ntry/CdtDbtInd` (CRDT=Recebido / DBIT=Enviado)      |
| `local_instrument`       | `formaDeIniciacao`         | `TxDtls/LclInstrm/Prtry` (MANU/DICT/QRDN/QRES/...)   |
| `purpose`                | `finalidade`/`tipoTransacao`| `TxDtls/Purp/Cd`                                    |
| `debtor_name`            | `pagador`                  | `TxDtls/RltdPties/Dbtr/Pty/Nm`                       |
| `debtor_document`        | `documentoPagador`         | `RltdPties/Dbtr/Pty/Id//Othr/Id` (CPF/CNPJ)         |
| `debtor_account`         | `contaPagador`             | `RltdPties/DbtrAcct/Id/Othr/Id`                      |
| `debtor_account_issuer`  | `agenciaPagador`           | `RltdPties/DbtrAcct/Id/Othr/Issr`                    |
| `debtor_account_type`    | `tipoContaPagador`         | `RltdPties/DbtrAcct/Tp/Cd` (CACC/SVGS/TRAN/SLRY)     |
| `debtor_ispb`            | `ispbPagador`              | `RltdAgts/DbtrAgt/FinInstnId/ClrSysMmbId/MmbId`      |
| `creditor_name`          | `recebedor`                | `RltdPties/Cdtr/Pty/Nm` (ver nota B — geralmente nil)|
| `creditor_document`      | `documentoRecebedor`       | `RltdPties/Cdtr/Pty/Id//Othr/Id`                     |
| `creditor_account`       | `contaRecebedor`           | `RltdPties/CdtrAcct/Id/Othr/Id`                      |
| `creditor_account_issuer`| `agenciaRecebedor`         | `RltdPties/CdtrAcct/Id/Othr/Issr`                    |
| `creditor_account_type`  | `tipoContaRecebedor`       | `RltdPties/CdtrAcct/Tp/Cd`                           |
| `creditor_ispb`          | `ispbRecebedor`            | `RltdAgts/CdtrAgt/FinInstnId/ClrSysMmbId/MmbId`      |
| `proxy_key`              | `idContaTransacional`      | `RltdPties/CdtrAcct/Prxy/Id` (chave PIX)             |
| `instruction_id`         | —                          | `TxDtls/Refs/InstrId`                                |
| `tx_id`                  | —                          | `TxDtls/Refs/TxId`                                   |
| `clearing_system_ref`    | —                          | `TxDtls/Refs/ClrSysRef` (NumCtrl STR)               |
| `proprietary_ref`        | `idConciliacao`            | `TxDtls/Refs/Prtry/Ref` (ver nota C)                |
| `remittance_info`        | `infoEntreUsuarios`        | `TxDtls/RmtInf/Ustrd`                                |
| `booking_date`           | `criacao`                  | `Ntry/BookgDt/Dt`                                    |
| `value_datetime`         | `dataLiquidacao`           | `Ntry/ValDt/DtTm`                                    |
| `acceptance_datetime`    | (auxiliar)                 | `TxDtls/RltdDts/AccptncDtTm`                         |
| `is_return` (bool)       | (deriva devolucao)         | presenca de `TxDtls/RtrInf`                          |
| `return_reason_code`     | `motivoDevolucao`          | `TxDtls/RtrInf/Rsn/Cd` (BE08/FR01/MD06/SL02)        |
| `return_additional_info` | `motivoDevolucao` (texto)  | `TxDtls/RtrInf/AddtlInf`                             |
| `original_end_to_end_id` | `e2eOriginal`              | `TxDtls/Refs/OrgnlEndToEndId` (ver nota D)          |
| `priority`               | `prioridade`               | **nil** — nao vem no camt.054                        |
| `error_code`             | `codigoErro`               | **nil** — merge do Core (pacs.002 RJCT)             |
| `error_reason`           | `motivoErro`/`descricaoErro`| **nil** — merge do Core                             |
| `cancellation_reason`    | `motivoCancelamento`       | **nil** — merge do Core (camt.056/pacs.004)         |
| `debtor_bank_name`       | `nomeBancoPagador`         | **nil** — ver nota A (resolver ISPB no Core)        |
| `creditor_bank_name`     | `nomeBancoRecebedor`       | **nil** — ver nota A                                 |

## 4. O que o Core tem de fundir (nao vem do camt.054)

O camt.054 de credito **nao carrega** rejeicao, cancelamento nem prioridade com
o detalhe que o MGMT OnZ agregava. O readequar honesto = **merge**: `mgmt.formatted`
vem deste `camt054`, e o Core preenche a partir das pacs locais que a cabine ja
persiste:

- `codigoErro`/`motivoErro`/`descricaoErro`: da pacs.002 RJCT (`localPacs`,
  `reason_code`/`reason_description`) — o Core ja usa isso para `rejectionCode`.
- `motivoCancelamento`/`solicitanteCancelamento`/`dataCancelamento`: do fluxo
  camt.056/pacs.004.
- `e2eDevolucao`/`valorDevolucao`/`motivoDevolucao`/`dataDevolucao`: da pacs.004
  (`RtrId`, `RtrdIntrBkSttlmAmt`, `RtrRsnInf/Rsn/Cd`); no camt.054, a entry de
  devolucao aparece com `is_return=true` + `return_reason_code`.
- `prioridade`: da pacs.008 (`InstrPrty`).
- `messages[]`: o Core sintetiza a partir de `camt054` (tipo inferido:
  pacs.008 credito, pacs.004 devolucao) e/ou das `localPacs`.

## 5. Notas do XSD (por que alguns campos vem nil)

- **A. Nome do banco (pagador/recebedor):** o `FinancialInstitutionIdentification18`
  do XSD SPI 1.15 so tem `ClrSysMmbId/MmbId` (ISPB). Nao ha `<Nm>` de instituicao
  no camt.054. O Core resolve ISPB -> nome via `institution_directory` (437
  instituicoes semeadas do SPB), como ja faz no comprovante.
- **B. Nome do recebedor:** no XSD, apenas `PartyIdentification135Dbtr` tem `<Nm>`;
  o `PartyIdentification135Cdtr` so tem `Id` (documento). Logo `creditor_name`
  quase sempre vem nil no camt.054 — o Core deve preferir o nome do recebedor da
  pacs.008 local quando existir.
- **C. idConciliacao:** o `TransactionReferences6` do XSD 1.15 nao tem
  `AcctSvcrRef`; o mais proximo e `Refs/Prtry/Ref` (ProprietaryReference1, dominio
  PAGAGD/PAGFRD/PAGPRI). `tx_id` e `clearing_system_ref` acompanham para o Core
  escolher a chave de conciliacao adequada.
- **D. e2eOriginal:** nao existe `OrgnlEndToEndId` no `TransactionReferences6` do
  perfil SPI 1.15. O campo e extraido por robustez (outros perfis) mas vem nil
  aqui; para devolucao, o `e2eOriginal` deve vir da pacs.004 local do Core.

## 6. Implementacao (cabine)

- Parser: `Shared.Spi.Camt054Parser.parse/1`
  (`pix/backend/apps/shared/lib/shared/spi/camt054_parser.ex`). Reusa
  `Shared.Med.TransactionEnrichment.extract_camt054_entry/1` para o nucleo
  (E2E/valor/ISPBs) e o reparo Latin-1->UTF-8 (`scrub_to_utf8/1`, prova viva
  2026-07-07: BACEN envia byte 0xE7 cru apesar de declarar UTF-8). Centavos em
  Decimal (sem float).
- Fluxo: `SpiService.OperationQuery.query/2`
  (`pix/backend/apps/spi_service/lib/spi_service/operation_query.ex`).
- Controller: `SpiServiceWeb.OperationsQueryController`
  (`.../spi_service_web/controllers/operations_query_controller.ex`), rota
  `POST /api/v1/operations/query`.
- Proxy: `SpiProxyController.operation_query`
  (settlement_service), rota `POST /api/v1/operations/query`. RBAC: exige
  permissao SPI `:create` (`Shared.Plugs.RequirePermission`), mesma classe do
  `:echo` — a acao dispara uma camt.060 assinada no HSM ao BACEN (efeito
  colateral com custo/rate).

## 7. Pendencia de validacao viva

Validar contra um E2E real em HML (camt.060 detalha-lancto assinada no HSM ->
camt.054 correlacionada -> parse) depende do HSM RTM e de trafego BACEN homolog.
Ate la, o contrato acima esta provado por testes (parser 12/12, fluxo 5/5,
controller 4/4) com camt.054 real-shaped 1:1 com o XSD.
