commit 0b1b35f95221908e3b8d49917a184fc30cecc55d
parent fc3f9450ad4c21964684f3e6a0a4b3eacbb98a8e
Author: Antoine A <>
Date: Mon, 26 May 2025 17:08:15 +0200
nexus: bounce transactions without subject
Diffstat:
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")
+ )
)
)
}