← ISO 20022: fundamentos de integração e operação
07 / 10 · 60 MIN

Oficina: montantes, contagens e identidades

Construir evidência de reconciliação com aritmética decimal e controlos complementares de identidade, contagem e unidade.

Contrato do laboratório

Uma reconciliação precisa de definir o que compara antes de calcular. O laboratório usa Batch e Item no namespace inventado urn:dr:training:reconciliation:1. Não são mensagens ISO nem uma simplificação normativa de pain.001. O perfil DR-REC-1 aceita uma a cem linhas, identidade T seguida de dois algarismos, uma moeda por lote e montantes estritamente positivos. O total deve coincidir com a soma e a contagem com os itens. Estas regras permitem observar falhas concretas sem introduzir dados bancários. Antes de transferir o raciocínio para trabalho real, obter o MDR, schema, regras da comunidade e definição dos campos efetivamente usados.

Representar antes de somar

Criar Decimal diretamente a partir do texto validado conserva o valor decimal de 0.10 e 0.20. A soma do exercício é exatamente 0.30. Converter primeiro para float pode introduzir uma aproximação que uma conversão posterior não elimina. O modelo limita cada valor a nove algarismos inteiros e duas casas; precisão 32 é suficiente para as somas delimitadas. Não se promete exatidão ilimitada. Separar também valor e representação: 1.2 e 1.20 têm o mesmo valor, mas bytes diferentes. Uma comparação de hashes de ficheiros responde a outra pergunta e não substitui a comparação dos dados de negócio.

Rejeitar em vez de inventar

O perfil fictício permite apenas algarismos ASCII e ponto decimal, sem sinal ou expoente. A regra é deliberadamente mais restrita que xs:decimal. A biblioteca aceitar 1e2 não significa que a interface o aceite. Texto vazio, NaN, vírgula, número negativo ou 1.005 devem produzir a rejeição prevista. Não há arredondamento autorizado neste exercício. Transformar ausência em zero esconderia informação e violaria a regra de positividade. Numa interface real, a escala e o arredondamento dependem do contrato; não concluir que todas as moedas ou mensagens usam duas casas. Guardar a entrada e a regra falhada no diagnóstico sintético.

Controlos que se complementam

Duas linhas de dez euros podem somar vinte mesmo quando uma referência foi repetida e outra desapareceu. Por isso, soma, contagem e identidade são verificações diferentes. A fixture T01,T01 recusa duplicação mesmo com total correto. A comparação com o conjunto esperado identifica a referência em falta. Misturar EUR e USD é recusado porque o perfil não define câmbio nem um total monetário comum. Se uma transformação excluir itens por regra autorizada, recalcular agregados e manter a ligação às exclusões. Corrigir apenas o cabeçalho ou renomear uma referência repetida não demonstra fidelidade da transformação.

Ler o XML no âmbito certo

Executar o programa numa pasta temporária com Python 3.13 e guardar evidence.json. O programa analisa XML sintético de confiança usando ElementTree e verifica regras próprias. Não carrega XSD e não é um gateway seguro para entrada não confiável. O nome expandido inclui URI e nome local; alterar só o prefixo mantém o resultado. Já Ccy sem prefixo e a:Ccy com namespace não são o mesmo atributo. O namespace por defeito não se aplica ao primeiro. O teste de filho Extra demonstra ainda que ler o texto do elemento não verifica sozinho toda a estrutura permitida.

Fecho e aplicação no trabalho

Reservar quinze minutos para prever os resultados, vinte para executar e alterar uma fixture, quinze para analisar rejeições e dez para explicar as conclusões. Usar um total errado, uma contagem divergente e uma repetição, um defeito de cada vez. Comparar o resultado esperado com o observado e localizar a primeira regra que falhou. Num incidente APS, esta disciplina separa erro de representação, transformação e contrato. O relatório de 34 verificações inclui também os estados da próxima aula. Demonstra o modelo didático executado; conformidade externa, dados reais e aceitação da equipa de produção exigem evidência própria.

"""Original DR synthetic reconciliation lab. Python 3.13 stdlib, no network.
Run: python3 run.py --output evidence.json
Not ISO messages, XSD validation, payment execution, or bank acceptance evidence.
Only trusted in-script XML fixtures are parsed. Not a production XML gateway.
"""
import argparse
import hashlib
import json
import re
import sys
from decimal import Decimal, localcontext
from pathlib import Path
from xml.etree import ElementTree as ET

NS = 'urn:dr:training:reconciliation:1'
PROFILE = 'DR-REC-1'
checks = []

def check(name, actual, expected):
    assert actual == expected, (name, actual, expected)
    checks.append(dict(name=name, actual=actual, expected=expected, passed=True))

def error(fn):
    try:
        fn()
    except ValueError as exc:
        return str(exc)
    raise AssertionError('Expected rejection')

def amount(text):
    # Deliberately narrower than xs:decimal: ASCII, unsigned, <=9 whole
    # digits and <=2 fraction digits. A DR fixture rule, not a currency law.
    if not re.fullmatch(r'[0-9]{1,9}(?:\.[0-9]{1,2})?', text):
        raise ValueError('amount lexical profile')
    value = Decimal(text)
    if value <= 0:
        raise ValueError('amount positive profile')
    return value

def batch(rows, count=None, total='0.30', prefix=''):
    tag = lambda local: prefix + ':' + local if prefix else local
    decl = 'xmlns:' + prefix if prefix else 'xmlns'
    body = ''.join(f'<{tag("Item")} id="{i}" Ccy="{c}">{a}</{tag("Item")}>' for i,c,a in rows)
    return f'<{tag("Batch")} {decl}="{NS}" profile="{PROFILE}" count="{len(rows) if count is None else count}" total="{total}">{body}</{tag("Batch")}>'

def reconcile(xml):
    try:
        root = ET.fromstring(xml)
    except ET.ParseError:
        raise ValueError('XML syntax') from None
    if root.tag != '{'+NS+'}Batch':
        raise ValueError('root expanded name')
    if root.get('profile') != PROFILE:
        raise ValueError('profile binding')
    if set(root.attrib) != {'profile','count','total'}:
        raise ValueError('root attributes')
    if not re.fullmatch(r'[0-9]{1,3}', root.get('count','')):
        raise ValueError('count lexical profile')
    items = list(root)
    if len(items) != int(root.get('count')) or not 1 <= len(items) <= 100:
        raise ValueError('item count')
    ids = set()
    totals = {}
    with localcontext() as ctx:
        ctx.prec = 32  # Bound: <=100 values, each <=9 whole +2 fraction digits.
        for item in items:
            if item.tag != '{'+NS+'}Item' or list(item):
                raise ValueError('item structure')
            if set(item.attrib) != {'id','Ccy'}:
                raise ValueError('item attributes')
            ident = item.get('id')
            if not re.fullmatch(r'T[0-9]{2}', ident):
                raise ValueError('item identity')
            if ident in ids:
                raise ValueError('duplicate item identity')
            ids.add(ident)
            currency = item.get('Ccy')
            if currency not in {'EUR','USD'}:
                raise ValueError('fixture currency')
            totals[currency] = totals.get(currency, Decimal('0')) + amount(item.text or '')
        # This particular exercise declares one currency and one total.
        if len(totals) != 1:
            raise ValueError('mixed currency total')
        declared = amount(root.get('total'))
        if next(iter(totals.values())) != declared:
            raise ValueError('control total')
    return dict(count=len(items), ids=sorted(ids), totals={k:format(v,'f') for k,v in totals.items()})

def observe(events, expected):
    """Fictional event log; not pacs.002 codes or a universal state machine."""
    seen, latest = {}, {}
    for e in events:
        eid, target, seq, state = e
        if target not in expected:
            raise ValueError('unknown target')
        if state not in {'RECEIVED','PENDING','ACCEPTED','REJECTED'}:
            raise ValueError('unknown fixture state')
        if type(seq) is not int or seq < 1:
            raise ValueError('invalid sequence')
        if eid in seen:
            if seen[eid] != e:
                raise ValueError('conflicting event identity')
            continue
        seen[eid] = e
        old = latest.get(target)
        if old and seq == old[0] and state != old[1]:
            raise ValueError('conflicting target sequence')
        if old is None or seq > old[0]:
            latest[target] = (seq,state)
    return {t:latest[t][1] if t in latest else 'UNKNOWN' for t in sorted(expected)}

def main():
    rows = [('T01','EUR','0.10'),('T02','EUR','0.20')]
    good = batch(rows)
    check('decimal total', reconcile(good)['totals'], {'EUR':'0.30'})
    check('two distinct items', reconcile(good)['count'], 2)
    check('prefix independent result', reconcile(batch(rows,prefix='x')), reconcile(good))
    check('decimal value equivalent', amount('1.20') == amount('1.2'), True)
    check('binary float contrast', 0.1 + 0.2 == 0.3, False)
    for name, text in [('exponent','1e2'),('comma','1,20'),('nonfinite','NaN'),('negative','-1.00'),('excess scale','1.005'),('blank',''),('too many digits','1000000000'),('nonascii','١.٢')]:
        check(name+' rejected', error(lambda:amount(text)), 'amount lexical profile')
    check('zero rejected', error(lambda:amount('0.00')), 'amount positive profile')
    check('wrong namespace rejected', error(lambda:reconcile(good.replace(NS,NS+'x'))), 'root expanded name')
    check('wrong profile rejected', error(lambda:reconcile(good.replace(PROFILE,'DR-REC-2'))), 'profile binding')
    check('broken XML rejected', error(lambda:reconcile(good[:-1])), 'XML syntax')
    check('declared count mismatch', error(lambda:reconcile(batch(rows,count=3))), 'item count')
    check('duplicate item rejected', error(lambda:reconcile(batch([rows[0],rows[0]],total='0.20'))), 'duplicate item identity')
    check('mixed currency rejected', error(lambda:reconcile(batch([rows[0],('T02','USD','0.20')]))), 'mixed currency total')
    check('incorrect total rejected', error(lambda:reconcile(batch(rows,total='0.31'))), 'control total')
    check('namespaced attribute differs', error(lambda:reconcile(good.replace(' Ccy=', ' xmlns:a="'+NS+'" a:Ccy='))), 'item attributes')
    check('nested item rejected', error(lambda:reconcile(good.replace('0.10</Item>','0.10<Extra/></Item>'))), 'item structure')
    expected = {'T01','T02','T03'}
    events = [('e1','T01',1,'RECEIVED'),('e2','T01',2,'ACCEPTED'),('e3','T02',1,'REJECTED')]
    states = observe(events,expected)
    check('partial outcome retains unknown', states, {'T01':'ACCEPTED','T02':'REJECTED','T03':'UNKNOWN'})
    check('duplicate event is idempotent', observe(events+[events[1]],expected), states)
    check('late lower sequence does not regress', observe(events+[('e4','T01',1,'RECEIVED')],expected), states)
    check('same event conflicting body', error(lambda:observe(events+[('e2','T01',2,'REJECTED')],expected)), 'conflicting event identity')
    check('same sequence conflicting state', error(lambda:observe(events+[('e5','T01',2,'REJECTED')],expected)), 'conflicting target sequence')
    check('unknown target isolated', error(lambda:observe([('e6','T99',1,'ACCEPTED')],expected)), 'unknown target')
    check('unknown state isolated', error(lambda:observe([('e7','T01',1,'SETTLED')],expected)), 'unknown fixture state')
    check('received is not accepted', observe(events[:1],expected)['T01'], 'RECEIVED')
    check('observed state coverage', sum(s!='UNKNOWN' for s in states.values()), 2)
    check('accepted item count', sum(s=='ACCEPTED' for s in states.values()), 1)
    check('all missing remains unknown', observe([],expected), {t:'UNKNOWN' for t in sorted(expected)})
    output = dict(scope='Original trusted synthetic fixtures only; not XSD validation, ISO conformance, settlement evidence or production parser security.',python=sys.version.split()[0],runnerSha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),passed=len(checks),checks=checks)
    parser=argparse.ArgumentParser();parser.add_argument('--output',required=True)
    Path(parser.parse_args().output).write_text(json.dumps(output,indent=2)+'\n')
    print(json.dumps({'passed':len(checks),'scope':output['scope']}))
if __name__ == '__main__':
    main()
NA PRÁTICA

Caso: contagem dois e total vinte escondem T01 duplicado e T02 ausente. Acrescentar reconciliação de identidades muda a decisão de aceitação.

Armadilhas comuns

Float antes de Decimal; zero para valores ausentes; soma entre moedas sem acordo; totais corretos como prova de identidades preservadas.

Tópicos relacionados: Mensagens, cabeçalhos e estados · Validação por camadas · Mapeamento e integridade de dados

Leva esta ideia contigo

Definir unidade e limites, validar representação, reconciliar soma, contagem e identidades, e declarar o âmbito efetivamente demonstrado.

Criar conta

Referência: Decimal arithmetic · DR ISO20022 fundamentals professional assessment2026.10

ISO 20022 é uma marca comercial da Organização Internacional de Normalização (ISO). A dr.pt é uma plataforma de preparação independente e não está afiliada, associada, patrocinada, autorizada nem aprovada por ISO. Os conteúdos e as perguntas são originais, não são perguntas oficiais de exame, e concluir os nossos testes não atribui nem garante qualquer certificação. Os nomes são usados apenas para identificar o tema. Todas as outras marcas pertencem aos respetivos titulares.