← ISO 20022: integration and operational fundamentals
07 / 10 · 60 MIN

Workshop: amounts, counts, and identities

Build reconciliation evidence using decimal arithmetic and complementary identity, count, and unit controls.

Lab contract

Reconciliation needs a definition of what is being compared before calculation starts. The lab uses Batch and Item in the invented namespace urn:dr:training:reconciliation:1. These are not ISO messages or a normative simplification of pain.001. Profile DR-REC-1 accepts one to one hundred rows, identities consisting of T and two digits, one currency per batch, and strictly positive amounts. Total must match the sum and count must match the items. These rules expose concrete failures without banking data. Before transferring the reasoning into real work, obtain the MDR, schema, community rules, and definitions of the fields actually used.

Represent before adding

Constructing Decimal directly from validated strings retains the decimal values of 0.10 and 0.20. Their exercise sum is exactly 0.30. Converting to float first can introduce an approximation that later conversion does not remove. The model bounds each amount to nine integer digits and two fractional digits; precision 32 is sufficient for these bounded sums. It promises no unlimited exactness. Also separate value from representation: 1.2 and 1.20 have equal value but different bytes. Comparing file hashes answers another question and does not replace comparison of business data.

Reject instead of inventing

The fictional profile permits only ASCII digits and a decimal point, with no sign or exponent. Its rule is deliberately narrower than xs:decimal. Library acceptance of 1e2 does not imply interface acceptance. An empty string, NaN, comma, negative number, or 1.005 must produce the expected rejection. This exercise authorizes no rounding. Turning absence into zero would conceal information and violate positivity. In a real interface, scale and rounding depend on the contract; do not conclude that every currency or message uses two decimals. Retain input and the failing rule in the synthetic diagnostic record.

Complementary controls

Two ten-euro rows can total twenty even when one reference was duplicated and another disappeared. Sum, count, and identity are therefore separate checks. Fixture T01,T01 rejects duplication even with a correct total. Comparison with the expected set identifies the missing reference. Mixing EUR and USD is rejected because this profile defines neither exchange conversion nor a shared monetary total. When a transformation excludes items under an authorized rule, recalculate aggregates and retain links to exclusions. Repairing only the header or renaming a duplicated reference does not demonstrate transformation fidelity.

Read XML within scope

Run the program in a temporary directory with Python 3.13 and save evidence.json. It parses trusted synthetic XML using ElementTree and checks custom rules. It loads no XSD and is not a secure gateway for untrusted input. An expanded name includes URI and local name; changing only the prefix preserves the result. In contrast, unprefixed Ccy and namespaced a:Ccy are different attributes. The default namespace does not apply to the former. The Extra-child test additionally demonstrates that reading element text alone does not check the entire permitted structure.

Closure and workplace application

Reserve fifteen minutes to predict outcomes, twenty to run and change a fixture, fifteen to examine rejections, and ten to explain conclusions. Use an incorrect total, a mismatching count, and a duplicate, one defect at a time. Compare expected and observed outcomes and identify the first failing rule. In an APS incident, this discipline separates representation, transformation, and contract errors. The thirty-four-check report also includes the states used in the next lesson. It demonstrates the executed teaching model; external conformance, real data, and production-team acceptance require their own evidence.

"""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
IN PRACTICE

Case: a count of two and a total of twenty hide duplicated T01 and missing T02. Adding identity reconciliation changes the acceptance decision.

Common pitfalls

Float before Decimal; zero for missing values; cross-currency sums without agreement; correct totals treated as proof of preserved identities.

Related topics: Messages, headers, and states · Layered validation · Mapping and data integrity

Take this idea with you

Define units and bounds, validate representation, reconcile sum, count, and identities, and state the scope actually demonstrated.

Create account

Reference: Decimal arithmetic · BigSavant ISO20022 fundamentals professional assessment2026.10

ISO 20022 is a trademark of the International Organization for Standardization (ISO). bigsavant.com is an independent preparation platform and is not affiliated with, associated with, sponsored, authorised or endorsed by ISO. Content and questions are original, are not official exam questions, and completing our tests does not award or guarantee any certification. Names are used only to identify the subject. All other trademarks belong to their respective owners.