Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ for transaction in transactions:
print(transaction)
```

**Note**: Each transaction includes remittance information in two fields:
- `RemittanceInformation`: Contains only the first unstructured remittance line
- `RemittanceInformationFull`: Contains all unstructured remittance lines joined with spaces

This is particularly useful for international transactions that may contain multiple lines of remittance information.

### Extracting Statement Information

To get basic statement information like IBAN and opening/closing balance:
Expand Down
8 changes: 8 additions & 0 deletions pycamt/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,9 @@ def _extract_transaction_details(self, tx_detail):
-------
dict
Detailed information extracted from the transaction detail element.
Includes RemittanceInformation (first Ustrd) and RemittanceInformationFull
(all Ustrd elements joined with spaces) for backward compatibility and
comprehensive remittance data capture.
"""

data = {
Expand Down Expand Up @@ -306,6 +309,11 @@ def _extract_transaction_details(self, tx_detail):
if tx_detail.find(".//RmtInf//Ustrd", self.namespaces) is not None
else None
),
"RemittanceInformationFull": (
" ".join(rinfo.text.strip() for rinfo in tx_detail.findall(".//RmtInf//Ustrd", self.namespaces) if rinfo.text)
if tx_detail.findall(".//RmtInf//Ustrd", self.namespaces)
else None
),
}

structured_remittance_elem = tx_detail.find(".//RmtInf//Strd", self.namespaces)
Expand Down
164 changes: 164 additions & 0 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,167 @@ def test_get_statement_info(self, parser):
"ClosingBalanceDate": None
}]
assert parser.get_statement_info() == expected

def test_multiple_remittance_information(self):
"""Test that multiple Ustrd elements in RemittanceInformation are captured"""
xml_data = """
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
<GrpHdr>
<MsgId>ABC123</MsgId>
<CreDtTm>2020-06-23T18:56:25.64Z</CreDtTm>
</GrpHdr>
<Stmt>
<Acct>
<Id>
<IBAN>GB33BUKB20201555555555</IBAN>
</Id>
</Acct>
<Ntry>
<Amt Ccy="EUR">69.06</Amt>
<CdtDbtInd>CRDT</CdtDbtInd>
<BookgDt>
<Dt>2020-07-07</Dt>
</BookgDt>
<ValDt>
<Dt>2020-07-07</Dt>
</ValDt>
<AcctSvcrRef>123</AcctSvcrRef>
<NtryDtls>
<TxDtls>
<Refs>
<EndToEndId>ENDTOENDID123</EndToEndId>
</Refs>
<RmtInf>
<Ustrd>Ref.. 1234567890123456</Ustrd>
<Ustrd>Betrag EUR 69,06</Ustrd>
<Ustrd>/ADRS CH/Basel,1234</Ustrd>
<Ustrd> A12345/ Best 00123456</Ustrd>
<Ustrd>7185) Peter Pan</Ustrd>
<Ustrd>Urspr. EUR 69,06</Ustrd>
<Ustrd>ADRS Re xxxxx,4 CH/Basel</Ustrd>
<Ustrd>Land,1234/</Ustrd>
<Ustrd>ERST RAIFCH12345</Ustrd>
</RmtInf>
</TxDtls>
</NtryDtls>
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>
"""
parser = Camt053Parser(xml_data)
transactions = parser.get_transactions()

assert len(transactions) == 1
transaction = transactions[0]

# Current behavior: only first Ustrd element
assert transaction["RemittanceInformation"] == "Ref.. 1234567890123456"

# New behavior: all Ustrd elements joined
expected_full = (
"Ref.. 1234567890123456 Betrag EUR 69,06 "
"/ADRS CH/Basel,1234 A12345/ Best 00123456 7185) Peter Pan "
"Urspr. EUR 69,06 ADRS Re xxxxx,4 CH/Basel "
"Land,1234/ ERST RAIFCH12345"
)
assert "RemittanceInformationFull" in transaction
assert transaction["RemittanceInformationFull"] == expected_full

def test_single_remittance_information_backward_compatibility(self):
"""Test that single Ustrd element works for both fields"""
xml_data = """
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
<GrpHdr>
<MsgId>ABC123</MsgId>
<CreDtTm>2020-06-23T18:56:25.64Z</CreDtTm>
</GrpHdr>
<Stmt>
<Acct>
<Id>
<IBAN>GB33BUKB20201555555555</IBAN>
</Id>
</Acct>
<Ntry>
<Amt Ccy="EUR">500.00</Amt>
<CdtDbtInd>CRDT</CdtDbtInd>
<BookgDt>
<Dt>2020-06-23</Dt>
</BookgDt>
<ValDt>
<Dt>2020-06-23</Dt>
</ValDt>
<AcctSvcrRef>123</AcctSvcrRef>
<NtryDtls>
<TxDtls>
<Refs>
<EndToEndId>ENDTOENDID123</EndToEndId>
</Refs>
<RmtInf>
<Ustrd>Single remittance info</Ustrd>
</RmtInf>
</TxDtls>
</NtryDtls>
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>
"""
parser = Camt053Parser(xml_data)
transactions = parser.get_transactions()

assert len(transactions) == 1
transaction = transactions[0]

# Both fields should have the same value for single Ustrd
assert transaction["RemittanceInformation"] == "Single remittance info"
assert transaction["RemittanceInformationFull"] == "Single remittance info"

def test_no_remittance_information(self):
"""Test that transactions without RemittanceInformation still work"""
xml_data = """
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
<GrpHdr>
<MsgId>ABC123</MsgId>
<CreDtTm>2020-06-23T18:56:25.64Z</CreDtTm>
</GrpHdr>
<Stmt>
<Acct>
<Id>
<IBAN>GB33BUKB20201555555555</IBAN>
</Id>
</Acct>
<Ntry>
<Amt Ccy="EUR">500.00</Amt>
<CdtDbtInd>CRDT</CdtDbtInd>
<BookgDt>
<Dt>2020-06-23</Dt>
</BookgDt>
<ValDt>
<Dt>2020-06-23</Dt>
</ValDt>
<AcctSvcrRef>123</AcctSvcrRef>
<NtryDtls>
<TxDtls>
<Refs>
<EndToEndId>ENDTOENDID123</EndToEndId>
</Refs>
</TxDtls>
</NtryDtls>
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>
"""
parser = Camt053Parser(xml_data)
transactions = parser.get_transactions()

assert len(transactions) == 1
transaction = transactions[0]

# Both fields should be None when no remittance info exists
assert "RemittanceInformation" not in transaction # Filtered out by return statement
assert "RemittanceInformationFull" not in transaction # Filtered out by return statement
Loading