Formato baseado em Keep a Changelog; este projeto segue versionamento semântico.
0.1.0 - 2026-09-13
Primeira versão pública.
Adicionado
- Suporte real a lote (
encode/3aceita 1 mensagem ou uma lista;decode/1devolve 1 struct ou uma lista, dependendo de quantos itens o XML traz) nas 11 mensagens cujo elemento de transação émax: ilimitadono XSD:Pacs002,Pacs004,Pacs008,Camt052,Camt053,Camt054,Pain009,Pain012,Pain013,Pain014,Trck002. Antes, mais de 1 transação virava{:error, {:unsupported_batch, n}}(issue #47) — agora é suportado de verdade, dos dois lados (enviar e receber), sem API paralela: a mesmaencode/3/decode/1, só que polimórfica. Isox.encode/2eIsox.decode/1: API genérica porIsox.Envelope(cabeçalho + modelo da mensagem), que despacha pelo tipo do modelo (encode/2) ou pelo namespace do XML (decode/1) — não precisa mais saber de antemão qual mensagem está sendo codificada ou decodificada.- Codec
encode/3/decode/1tipado por mensagem, de baixo nível, para as mensagens do catálogo do SPI:Admi002,Admi004,Camt014,Camt025,Camt029,Camt052,Camt053,Camt054,Camt055,Camt060,Pacs002,Pacs004,Pacs008,Pain009,Pain011,Pain012,Pain013,Pain014,Pibr001,Pibr002,Reda014,Reda016,Reda017,Reda022,Reda031,Reda041,Trck002. Isox.Registrypara descobrir o módulo certo a partir do namespace de um XML de entrada, quando o tipo da mensagem não é conhecido de antemão (usado porIsox.decode/1por baixo).mix catalog.gen: gera o codec de cada mensagem a partir dos XSDs publicados pelo Banco Central (não redistribuídos neste pacote).Isox.Xmldsig: canonicalização XML exclusiva (xml-exc-c14n#), assinatura e verificação RSA-SHA256 no perfil do Manual de Segurança do SFN Vol. II (três<ds:Reference>—KeyInfo,AppHdr,Document), comKeyInfoporX509IssuerSerial.Isox.Xmldsig.TestCApara gerar certificados de teste em memória.mix xmldsig.spike: mede o throughput local de assinar/verificar.
Corrigido
Trck002: nada validava 3 regras da planilha do catálogo (BCB) que o XSD sozinho não expressa (o mesmo enum de 5 opções é reusado pra conta devedora e credora, ePrxy/InstrIdsão apenas opcionais na estrutura, sem condição nenhuma):cdtr_acct_proxy(chave Pix) — obrigatório quandolcl_instrmé"DICT"/"QRDN"/"QRES"/"APDN"/"APES"/"INIC", proibido quando é"MANU"/"AUTO". Dava pra montar uma transaçãoQRDNsem chave nenhuma, ou umaMANUcarregando uma chave Pix que não devia existir, sem erro algum. O próprio teste do módulo tinha essa segunda combinação inconsistente (InstrId+cdtr_acct_proxyjunto comlcl_instrm = "MANU") sem ninguém notar — impossível na prática, já que devolução (que exigeMANU) e chave Pix (proibida emMANU) se excluem mutuamente.cdtr_acct_typenunca pode ser"SLRY"(Conta-Salário não recebe pagamentos) — o XSD permite porque reusa o mesmo enum de tipo de conta pros dois lados.instr_idpresente (transação de devolução, análoga a umaPacs004) exigelcl_instrm == "MANU".
encode/3agora valida as 3, confirmado pelo único exemplo oficial do BCB (idêntico em ambas as versões do catálogo). Achado no deep dive de validação do catálogo (issue #63, trck.002) — última das 27 mensagens do catálogo revisadas.Reda041: a moduledoc dizia queRcrd.Othreramax: ilimitado, mas o schema real da BCB limita a[1..3](maxOccurs="3", semminOccursdeclarado —1implícito), consistente com só existirem 3 códigos possíveis de campo alterado (FldNm:MODP/NOME/NOMR) na tabela de domínios. O comportamento em si já estava certo — o motor (fix da issue #50) já rejeita 0 ou mais de 3 alterações viaconfirm/2— só a documentação estava errada. Reforçada cobertura de teste pros dois limites. Achado no deep dive de validação do catálogo (issue #62, reda.041).Reda022: a moduledoc dizia queModeramax: ilimitado, mas o schema real da BCB exige exatamente 4 (minOccurs="4" maxOccurs="4") — sempre as 4 modificações juntas (contato, diretor, endereço técnico, CPF do diretor), nunca um subconjunto, confirmado pelo único exemplo oficial do catálogo.encode/3agora valida essa composição exata (4 itens, um de cada tipo) com mensagem clara, em vez de deixar oconfirm/2(round-trip) rejeitar com um erro de XML de baixo nível. Também corrigido:Rspnsbltyé fixo por tipo ("CONTATOPSP"em:contact,"DIRETORPSP"em:director, conforme a planilha do catálogo), mas nada garantia que o valor certo fosse usado no ramo certo — o schema só valida que é um dos 2 valores do enum, não a correspondência com o tipo; dava pra montar, por exemplo, um:contactcom"DIRETORPSP"sem erro nenhum. Agoraencode/3valida essa correspondência também. Achado no deep dive de validação do catálogo (issue #60, reda.022).Reda016: nada validava a regra cruzada entrests,rsn_prtryesys_pty_ispb— dava pra montar um"COMP"(sucesso) semSysPtyId(mesmo a planilha do catálogo exigindo explicitamente: "devem ser preenchidos caso Status seja 'COMP'") ou carregando um motivo de erro sobrando, ou um"QUED"/"REJT"sem motivo, sem erro nenhum — o próprio teste do módulo tinha essa combinação inconsistente ("COMP"semSysPtyId) sem ninguém notar. A moduledoc também só mencionava 2 dos 3 status reais (Status6CodetemCOMP/QUED/REJT—QUED, fila/pendência, usa motivo igualREJT, confirmado pelo exemplo oficial).encode/3agora valida tudo isso, confirmado pelos 3 exemplos oficiais do BCB (2 por versão do catálogo, XSD idêntico). Também corrigido:rspnsbl_pty_ispbpreenchido semsys_pty_ispbera descartado silenciosamente no encode (o elemento contêinerSysPtyIdsó é emitido quandosys_pty_ispbexiste) — agora é rejeitado explicitamente em vez de perder o dado calado. Achado no deep dive de validação do catálogo (issue #58, reda.016).Pain014: nada validava a regra cruzada da planilha do catálogo entretx_stsersn_prtry— dava pra montar umACSP(aceite) com motivo sobrando, ou umRJCT(rejeição) sem motivo nenhum, sem erro algum.encode/3agora valida:ACSPexigersn_prtryausente,RJCTexigersn_prtrypresente — confirmado pelos 4 exemplos oficiais do BCB (2 por versão do catálogo). Achado no deep dive de validação do catálogo (issue #54, pain.014).Pain013: o blocoTax(divisão de tributos IBS/CBS — Split Payment da reforma tributária) era completamente ignorado —decode/1descartava silenciosamente os dados de tributo de qualquer XML real que os trouxesse (sem erro nenhum), eencode/3não tinha como montá-los. Confirmado com o exemplo oficial novo do BCB introduzido no catálogo v5.13.1 (pain.013_SplitPayment.xml), que só decodificava com perda de dado. Agoratax_ref_nb/tax_recordsfazem parte da struct, com as regras da planilha do catálogo validadas emencode/3: o bloco só é permitido quandodbtr_cpf_cnpjé CNPJ (14 caracteres); cada tipo de tributo presente precisa de umRecordctgy: "INF"; e a soma dos valores efetivos (CORtem prioridade sobreINFquando ambos existem) não pode excedervalue. Também nada validava a regra cruzada entreReqdExctnDtePurp/Prtry— obrigatório quando"AGND", proibido quando"NTAG"/"RIFL"— confirmada nos 4 exemplos oficiais do BCB para esta mensagem, sem exceção (o próprio teste do módulo tinha essa combinação inconsistente sem ninguém notar). Achado no deep dive de validação do catálogo (issue #53, pain.013).Pain012: nada validava a regra cruzada da planilha do catálogo entreaccptd,rjct_rsn_prtry,mndt_stsemndt_prcg_dtls— dava pra montar uma resposta de aceite semmndt_sts(obrigatório pela planilha quandoaccptd = "true"), ou uma rejeição carregandomndt_sts/mndt_prcg_dtls(que a planilha diz que não devem ser preenchidos quandoaccptd = "false"), sem erro nenhum — o próprio teste do módulo tinha essa combinação inconsistente (aceite sem nenhum dado deSplmtryData) sem ninguém notar.encode/3agora valida isso explicitamente, confirmado pelos 12 exemplos oficiais do BCB pra esta mensagem, sem exceção. Achado no deep dive de validação do catálogo (issue #52, pain.012).xs:boolean(TrckgInd/DtAdjstmntRuleInd, usados emPain009,Pain011,Pain012) não tinha validação nenhuma — mesma causa raiz doxs:date(issue #47): o XSD desses campos não declarapatternnenhum, confiando na validação léxica embutida do tipo, que o motor não implementava. Qualquer string passava como boolean válido, tanto no parse quanto no build (XML inválido saía sem erro nenhum). O motor (Isox.Xml.Codec, módulo interno) agora valida contra as 4 representações léxicas válidas dexs:boolean(true/false/1/0). Achado no deep dive de validação do catálogo (issue #50, pain.009).Elemento repetido com
maxOccursnumérico maior que 1 (ex.:MndtPrcgDtlsdoPain009,minOccurs="3" maxOccurs="3") só tinha o mínimo validado — o máximo nunca era checado, então mais itens do que o schema permite passava reto pelo parse (e pelo round-trip deconfirm/2, que usa o mesmo parse). O motor (Isox.Xml.Codec) agora rejeita contagem acima do máximo declarado. Achado no mesmo deep dive (issue #50, pain.009) — também afetaPain011(MndtPrcgDtls,maxOccurs="2").Camt060:RptgPrd(período do relatório) sempre incluíaFrToTm(horário) mesmo semrptg_prd_fr_tm/rptg_prd_to_tminformados, produzindo um<FrToTm></FrToTm>vazio que violava o schema — masFrToTmé independentemente opcional (minOccurs="0", separado deFrToDt), usado só em consulta de relação de lançamentos. Consultas de saldo de dia anterior, de remuneração da Conta PI, ou de arquivoTRD/TRTusam só data, sem horário — 3 dos 7 exemplos oficiais do BCB para esta mensagem são exatamente esse caso, e ficavam impossíveis de montar (encode/3errava com "elemento obrigatório ausente: FrTm").FrToTmagora só entra quando os campos de horário são realmente informados (e os dois precisam vir juntos). Achado no deep dive de validação do catálogo (issue #46, camt.060).Camt029: nada validava a regra cruzada da planilha do catálogo entrepmt_inf_cxl_sts,rsn_prtryecxl_prcg_tp— dava pra montar umRJCR(rejeição) sem motivo nenhum, ou uma combinaçãoACCR(aceite) com motivo sobrando, oucxl_prcg_tpinconsistente com o status, tudo sem nenhum erro (o próprio teste do módulo tinha essa combinação inconsistente sem ninguém notar).encode/3agora valida explicitamente:ACCRexigersn_prtryausente ecxl_prcg_tp == "DHAC";RJCRexigersn_prtrypresente ecxl_prcg_tp == "DHRC"— confirmado pelos 4 exemplos oficiais do BCB (2 por versão do catálogo). Achado no deep dive de validação do catálogo (issue #41, camt.029).Camt025:StsRsn.AddtlInf(opcional) estava sendo tratado como incondicional eStsRsn.Rsn(obrigatório no schema sempre queStsRsnaparece) como condicional — o inverso do XSD real. Na prática só se manifestava comaddtl_infpresente ersn_prtryausente: virava um erro confuso de round-trip ("elemento obrigatório ausente: Rsn") em vez de uma mensagem clara.encode/3agora valida isso explicitamente antes de montar o XML. Achado no deep dive de validação do catálogo (issue #40, camt.025).Validação de valor decimal (
fractionDigits/totalDigits/minInclusive/maxInclusivedo XSD, ex.:ActiveCurrencyAndAmount_SimpleType): não existia — um valor monetário negativo, com casas decimais a mais, com mais dígitos que o permitido, ou nem sequer numérico ("abc") passava reto peloencode/3de qualquer mensagem com campo de valor (Pacs008,Pacs004,Camt053,Camt054,Pain009/011/012/013,Trck002, entre outras). Achado no deep dive de validação do catálogo (issue #49).xs:datesempatternno XSD (ex.:OrgnlTxRef/IntrBkSttlmDtdoPacs002): um valor não-data crashava (ArgumentError) em vez de devolver erro — o motor não validavaxs:datede jeito nenhum quando o XSD não declarava pattern, e o crash acontecia fora da zona protegida doparse/2, direto em quem chamadecode/1.Elemento repetido (
max: ilimitado) que o modelo assume como exatamente 1 (TxInfAndStsdoPacs002,TxInfdoPacs004,CdtTrfTxInfdoPacs008, e outros 8:Camt052/053/054,Pain009/012/013/014,Trck002): uma mensagem com mais de um item crashava (MatchError) em vez de devolver erro — confirmado com exemplos oficiais do BCB que vêm em lote (pacs.002_SPI_10_msg.xml,pacs.004_SPI_10_msg.xml,pacs.008_CONTA_10_msg.xml, entre outros).decode/1agora devolve{:error, {:unsupported_batch, contagem}}nesses 11 módulos — não passou a suportar lote, só parou de crashar por causa dele. Achado no deep dive de validação do catálogo (issue #47, pacs.002).