libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit 0b1b35f95221908e3b8d49917a184fc30cecc55d
parent fc3f9450ad4c21964684f3e6a0a4b3eacbb98a8e
Author: Antoine A <>
Date:   Mon, 26 May 2025 17:08:15 +0200

nexus: bounce transactions without subject

Diffstat:
Mcommon/src/main/kotlin/Subject.kt | 6+++++-
Mnexus/sample/platform/maerki_baumann_camt053.xml | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt | 2+-
Mnexus/src/test/kotlin/Iso20022Test.kt | 8++++++++
Mnexus/src/test/kotlin/RegistrationTest.kt | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
5 files changed, 153 insertions(+), 6 deletions(-)

diff --git a/common/src/main/kotlin/Subject.kt b/common/src/main/kotlin/Subject.kt @@ -86,7 +86,11 @@ private val ALPHA_NUMBERIC_PATTERN = Regex("[0-9a-zA-Z]*") * To parse them while ignoring user errors, we reconstruct valid keys from key * parts, resolving ambiguities where possible. **/ -fun parseIncomingSubject(subject: String): IncomingSubject { +fun parseIncomingSubject(subject: String?): IncomingSubject { + if (subject == null || subject.isEmpty()) { + throw Exception("Missing subject") + } + /** Parse an incoming subject */ fun parseSingle(str: String): Candidate? { // Check key type diff --git a/nexus/sample/platform/maerki_baumann_camt053.xml b/nexus/sample/platform/maerki_baumann_camt053.xml @@ -206,6 +206,89 @@ <AddtlNtryInf>Bank clearing payment Grothoff Hans</AddtlNtryInf> </Ntry> <Ntry> + <Amt Ccy="CHF">.3</Amt> + <CdtDbtInd>CRDT</CdtDbtInd> + <RvslInd>false</RvslInd> + <Sts>BOOK</Sts> + <BookgDt> + <Dt>2025-05-23</Dt> + </BookgDt> + <ValDt> + <Dt>2025-05-23</Dt> + </ValDt> + <AcctSvcrRef>ZV20250523/851716</AcctSvcrRef> + <BkTxCd> + <Domn> + <Cd>PMNT</Cd> + <Fmly> + <Cd>RCDT</Cd> + <SubFmlyCd>OTHR</SubFmlyCd> + </Fmly> + </Domn> + </BkTxCd> + <NtryDtls> + <Btch> + <NbOfTxs>1</NbOfTxs> + <TtlAmt Ccy="CHF">.3</TtlAmt> + <CdtDbtInd>CRDT</CdtDbtInd> + </Btch> + <TxDtls> + <Refs> + <AcctSvcrRef>ZV20250523/851716/1</AcctSvcrRef> + <EndToEndId>NOTPROVIDED</EndToEndId> + <TxId>50523424675.0001</TxId> + </Refs> + <Amt Ccy="CHF">.3</Amt> + <CdtDbtInd>CRDT</CdtDbtInd> + <AmtDtls> + <InstdAmt> + <Amt Ccy="CHF">.5</Amt> + </InstdAmt> + <TxAmt> + <Amt Ccy="CHF">.5</Amt> + </TxAmt> + </AmtDtls> + <Chrgs> + <Rcrd> + <Amt Ccy="CHF">.2</Amt> + <CdtDbtInd>DBIT</CdtDbtInd> + <ChrgInclInd>true</ChrgInclInd> + <Tp> + <Prtry> + <Id>PT inc.paym.exp</Id> + </Prtry> + </Tp> + <Br>CRED</Br> + </Rcrd> + </Chrgs> + <RltdPties> + <Dbtr> + <Nm>Grothoff Hans</Nm> + </Dbtr> + <DbtrAcct> + <Id> + <IBAN>CH7389144832588726658</IBAN> + </Id> + </DbtrAcct> + </RltdPties> + <RltdAgts> + <DbtrAgt> + <FinInstnId> + <ClrSysMmbId> + <ClrSysId> + <Cd>CHSIC</Cd> + </ClrSysId> + <MmbId>087042</MmbId> + </ClrSysMmbId> + </FinInstnId> + </DbtrAgt> + </RltdAgts> + <AddtlTxInf>Bank clearing payment Grothoff Hans</AddtlTxInf> + </TxDtls> + </NtryDtls> + <AddtlNtryInf>Bank clearing payment Grothoff Hans</AddtlNtryInf> + </Ntry> + <Ntry> <Amt Ccy="CHF">.1</Amt> <CdtDbtInd>DBIT</CdtDbtInd> <RvslInd>false</RvslInd> diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt @@ -140,7 +140,7 @@ suspend fun registerIncomingPayment( } } // Check we have enough info to handle this transaction - if (payment.subject == null || payment.debtor == null) { + if (payment.debtor == null) { val res = db.payment.registerIncoming(payment) logRes(res, kind = "incomplete") return diff --git a/nexus/src/test/kotlin/Iso20022Test.kt b/nexus/src/test/kotlin/Iso20022Test.kt @@ -376,6 +376,14 @@ class Iso20022Test { executionTime = dateToInstant("2024-11-04"), debtor = ibanPayto("CH7389144832588726658", "Mr Test") ), + IncomingPayment( + id = IncomingId(null, "50523424675.0001", "ZV20250523/851716/1"), + amount = TalerAmount("CHF:0.5"), + creditFee = TalerAmount("CHF:0.2"), + subject = null, + executionTime = dateToInstant("2025-05-23"), + debtor = ibanPayto("CH7389144832588726658", "Grothoff Hans") + ), OutgoingPayment( id = OutgoingId("BATCH_SINGLE_REPORTING", "5IBJZOWESQGPCSOXSNNBBY49ZURI5W7Q4H", "ZV20241121/773541/1"), amount = TalerAmount("CHF:0.1"), diff --git a/nexus/src/test/kotlin/RegistrationTest.kt b/nexus/src/test/kotlin/RegistrationTest.kt @@ -62,7 +62,8 @@ class RegistrationTest { suspend fun Database.check( status: Map<String, Pair<SubmissionState, Map<String, SubmissionState>>>, incoming: List<IncomingPayment>, - outgoing: List<OutgoingPayment> + outgoing: List<OutgoingPayment>, + bounced: List<OutgoingPayment> ) { // Check batch status val batch_status = this.serializable( @@ -161,6 +162,30 @@ class RegistrationTest { } } assertContentEquals(outgoing, outgoing_tx) + + // Check outgoing transactions + val bounced_tx = this.serializable( + """ + SELECT end_to_end_id + ,(amount).val as amount_val + ,(amount).frac as amount_frac + ,subject + ,credit_payto + FROM initiated_outgoing_transactions + JOIN bounced_transactions USING (initiated_outgoing_transaction_id) + """ + ) { + all { + OutgoingPayment( + id = OutgoingId(null, null, null), + amount = it.getAmount("amount", this@check.currency), + subject = it.getString("subject"), + executionTime = Instant.EPOCH, + creditor = it.getOptIbanPayto("credit_payto"), + ) + } + } + assertContentEquals(bounced, bounced_tx) } @Test @@ -224,7 +249,8 @@ class RegistrationTest { )) ), incoming = emptyList(), - outgoing = emptyList() + outgoing = emptyList(), + bounced = emptyList() ) } @@ -254,7 +280,8 @@ class RegistrationTest { )), ), incoming = emptyList(), - outgoing = emptyList() + outgoing = emptyList(), + bounced = emptyList() ) } @@ -408,7 +435,8 @@ class RegistrationTest { executionTime = dateToInstant("2024-09-04"), creditor = ibanPayto("CH4189144589712575493", "Test") ), - ) + ), + bounced = emptyList() ) } @@ -455,6 +483,14 @@ class RegistrationTest { debtor = ibanPayto("CH7389144832588726658", "Mr Test") ), IncomingPayment( + id = IncomingId(null, "50523424675.0001", "ZV20250523/851716/1"), + amount = TalerAmount("CHF:0.5"), + creditFee = TalerAmount("CHF:0.2"), + subject = null, + executionTime = dateToInstant("2025-05-23"), + debtor = ibanPayto("CH7389144832588726658", "Grothoff Hans") + ), + IncomingPayment( id = IncomingId("f203fbb4-6e13-4c78-9b2a-d852fea6374a", "41202060702.0001", "ZV20241202/778108/1"), amount = TalerAmount("CHF:0.15"), creditFee = TalerAmount("CHF:0.2"), @@ -514,6 +550,22 @@ class RegistrationTest { executionTime = dateToInstant("2024-12-20"), creditor = null ), + ), + bounced = listOf( + OutgoingPayment( + id = OutgoingId(null, null, null), + amount = TalerAmount("CHF:0.8"), + subject = "bounce: 7371795e-62fa-42dd-93b7-da89cc120faa", + executionTime = Instant.EPOCH, + creditor = ibanPayto("CH7389144832588726658", "Mr Test") + ), + OutgoingPayment( + id = OutgoingId(null, null, null), + amount = TalerAmount("CHF:0.3"), + subject = "bounce: 50523424675.0001", + executionTime = Instant.EPOCH, + creditor = ibanPayto("CH7389144832588726658", "Grothoff Hans") + ) ) ) }