libeufin

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

commit 3d9dffab37a4117a147748fdde402ee0a4622669
parent 194031980e20f931803f58f005255cbc9fab4a59
Author: Antoine A <>
Date:   Thu, 12 Dec 2024 13:58:30 +0100

nexus: handle casez where fee are higher than amount for maerki_baumann

Diffstat:
Mcommon/src/main/kotlin/TalerCommon.kt | 2++
Mnexus/sample/platform/maerki_baumann_camt053.xml | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt | 9+++++++++
Mnexus/src/main/kotlin/tech/libeufin/nexus/iso20022/camt.kt | 124++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mnexus/src/test/kotlin/Iso20022Test.kt | 8++++++++
Mnexus/src/test/kotlin/RegistrationTest.kt | 8++++++++
Mtestbench/src/main/kotlin/Main.kt | 4++--
7 files changed, 196 insertions(+), 50 deletions(-)

diff --git a/common/src/main/kotlin/TalerCommon.kt b/common/src/main/kotlin/TalerCommon.kt @@ -232,6 +232,8 @@ class TalerAmount { return TalerAmount(value, frac, currency) } + operator fun compareTo(other: TalerAmount) = compareValuesBy(this, other, { it.value }, { it.frac }) + operator fun plus(increment: TalerAmount): TalerAmount { require(this.currency == increment.currency) { "currency mismatch ${this.currency} != ${increment.currency}" } val value = Math.addExact(this.value, increment.value) diff --git a/nexus/sample/platform/maerki_baumann_camt053.xml b/nexus/sample/platform/maerki_baumann_camt053.xml @@ -451,6 +451,97 @@ </NtryDtls> <AddtlNtryInf>PAIN-Auftrag Grothoff Hans</AddtlNtryInf> </Ntry> + <Ntry> + <Amt Ccy="CHF">.15</Amt> + <CdtDbtInd>DBIT</CdtDbtInd> + <RvslInd>true</RvslInd> + <Sts> + <Cd>BOOK</Cd> + </Sts> + <BookgDt> + <Dt>2024-12-02</Dt> + </BookgDt> + <ValDt> + <Dt>2024-12-02</Dt> + </ValDt> + <BkTxCd> + <Domn> + <Cd>PMNT</Cd> + <Fmly> + <Cd>RCDT</Cd> + <SubFmlyCd>CAJT</SubFmlyCd> + </Fmly> + </Domn> + </BkTxCd> + <Chrgs> + <TtlChrgsAndTaxAmt Ccy="CHF">.2</TtlChrgsAndTaxAmt> + <Rcrd> + <Amt Ccy="CHF">.2</Amt> + <CdtDbtInd>DBIT</CdtDbtInd> + <ChrgInclInd>true</ChrgInclInd> + <Tp> + <Prtry> + <Id>PT inc.paym.exp</Id> + </Prtry> + </Tp> + <Br>DEBT</Br> + </Rcrd> + </Chrgs> + <NtryDtls> + <Btch> + <NbOfTxs>1</NbOfTxs> + <TtlAmt Ccy="CHF">.15</TtlAmt> + <CdtDbtInd>DBIT</CdtDbtInd> + </Btch> + <TxDtls> + <Refs> + <AcctSvcrRef>ZV20241202/778108/1</AcctSvcrRef> + <EndToEndId>NOTPROVIDED</EndToEndId> + <UETR>f203fbb4-6e13-4c78-9b2a-d852fea6374a</UETR> + <TxId>41202060702.0001</TxId> + </Refs> + <Amt Ccy="CHF">-.15</Amt> + <CdtDbtInd>DBIT</CdtDbtInd> + <AmtDtls> + <InstdAmt> + <Amt Ccy="CHF">.05</Amt> + </InstdAmt> + <TxAmt> + <Amt Ccy="CHF">.05</Amt> + </TxAmt> + </AmtDtls> + <RltdPties> + <Dbtr> + <Pty> + <Nm>Grothoff Hans</Nm> + </Pty> + </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> + <RmtInf> + <Ustrd>mini</Ustrd> + </RmtInf> + <AddtlTxInf>Bank clearing payment Grothoff Hans</AddtlTxInf> + </TxDtls> + </NtryDtls> + <AddtlNtryInf>Bank clearing payment Grothoff Hans</AddtlNtryInf> + </Ntry> </Stmt> </BkToCstmrStmt> </Document> \ No newline at end of file diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt @@ -98,6 +98,15 @@ suspend fun registerIncomingPayment( } else { var bounceAmount = payment.amount if (payment.creditFee != null) { + if (payment.creditFee > bounceAmount) { + val res = db.payment.registerIncoming(payment) + if (res.new) { + logger.info("$payment skip bounce (fee higher than amount): $msg") + } else { + logger.debug("{} already seen and skip bounce (fee higher than amount): {}", payment, msg) + } + return + } bounceAmount -= payment.creditFee } val result = db.payment.registerMalformedIncoming( diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/camt.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/camt.kt @@ -40,7 +40,7 @@ data class IncomingPayment( val debtorPayto: IbanPayto ): TxNotification { override fun toString(): String { - val fee = if (creditFee == null) "" else "$creditFee " + val fee = if (creditFee == null) "" else "-$creditFee" return "IN ${executionTime.fmtDate()} $amount$fee $bankId debitor=$debtorPayto subject=\"$subject\"" } } @@ -181,22 +181,26 @@ private fun XmlDestructor.amount() = one("Amt") { TalerAmount(concat) } -/** Parse amounts and compute fees */ -private fun XmlDestructor.amountAndFee(): Pair<TalerAmount, TalerAmount?> { - val amount = amount() - var charges = TalerAmount.zero(amount.currency) +/** Parse credit fees */ +private fun XmlDestructor.creditFee(): TalerAmount? { + var charges: TalerAmount? = null opt("Chrgs")?.each("Rcrd") { - if (one("ChrgInclInd").bool() && opt("Br")?.text() == "CRED") { - if (one("CdtDbtInd").text() == "DBIT") { - charges += amount() - } + if (one("ChrgInclInd").bool() && one("CdtDbtInd").text() == "DBIT" ) { + val amount = amount() + charges = charges?.let { it + amount } ?: amount } } - return if (charges.isZero()) { - Pair(amount, null) - } else { - Pair(amount + charges, charges) + return charges +} + +/** Parse amounts and compute fees */ +private fun XmlDestructor.amountAndFee(): Pair<TalerAmount, TalerAmount?> { + var amount = amount() + val charges = creditFee() + if (charges != null) { + amount += charges } + return Pair(amount, charges) } /** Parse bank transaction code */ @@ -492,43 +496,67 @@ fun parseTx(notifXml: InputStream, dialect: Dialect): List<AccountTransactions> if (!isBooked()) return@each val code = bankTransactionCode() if (!code.isPayment()) return@each - + val kind = one("CdtDbtInd").enum<Kind>() + val reversal = one("RvslInd").bool() val entryRef = opt("AcctSvcrRef")?.text() val bookDate = executionDate() - one("NtryDtls").one("TxDtls") { - val txRef = opt("Refs")?.opt("AcctSvcrRef")?.text() - val kind = one("CdtDbtInd").enum<Kind>() - val code = optBankTransactionCode() ?: code - val (amount, fee) = amountAndFee() - val subject = wireTransferSubject() - if (!code.isReversal()) { - when (kind) { - Kind.CRDT -> { - val bankId = opt("Refs")?.opt("UETR")?.text() - val debtorPayto = payto("Dbtr") - txsInfo.add(TxInfo.Credit( - ref = bankId ?: txRef ?: entryRef, - bookDate = bookDate, - bankId = bankId, - amount = amount, - subject = subject, - debtorPayto = debtorPayto, - code = code, - creditFee = fee - )) - } - Kind.DBIT -> { - val outgoingId = outgoingId() - val creditorPayto = payto("Cdtr") - txsInfo.add(TxInfo.Debit( - ref = outgoingId.ref() ?: txRef ?: entryRef, - bookDate = bookDate, - id = outgoingId, - amount = amount, - subject = subject, - creditorPayto = creditorPayto, - code = code - )) + if (reversal) { + // Check reversal by fee over amount + require(kind == Kind.DBIT) { "reversal credit not yet supported" } + val fee = requireNotNull(creditFee()) { "Mising fee" } + val amount = amount() + one("NtryDtls").one("TxDtls") { + val txRef = opt("Refs")?.opt("AcctSvcrRef")?.text() + val bankId = opt("Refs")?.opt("UETR")?.text() + val subject = wireTransferSubject() + val debtorPayto = payto("Dbtr") + txsInfo.add(TxInfo.Credit( + ref = bankId ?: txRef ?: entryRef, + bookDate = bookDate, + bankId = bankId, + amount = amount, + subject = subject, + debtorPayto = debtorPayto, + code = code, + creditFee = fee + )) + } + } else { + one("NtryDtls").one("TxDtls") { + val txRef = opt("Refs")?.opt("AcctSvcrRef")?.text() + val kind = one("CdtDbtInd").enum<Kind>() + val code = optBankTransactionCode() ?: code + val (amount, fee) = amountAndFee() + val subject = wireTransferSubject() + if (!code.isReversal()) { + when (kind) { + Kind.CRDT -> { + val bankId = opt("Refs")?.opt("UETR")?.text() + val debtorPayto = payto("Dbtr") + txsInfo.add(TxInfo.Credit( + ref = bankId ?: txRef ?: entryRef, + bookDate = bookDate, + bankId = bankId, + amount = amount, + subject = subject, + debtorPayto = debtorPayto, + code = code, + creditFee = fee + )) + } + Kind.DBIT -> { + val outgoingId = outgoingId() + val creditorPayto = payto("Cdtr") + txsInfo.add(TxInfo.Debit( + ref = outgoingId.ref() ?: txRef ?: entryRef, + bookDate = bookDate, + id = outgoingId, + amount = amount, + subject = subject, + creditorPayto = creditorPayto, + code = code + )) + } } } } diff --git a/nexus/src/test/kotlin/Iso20022Test.kt b/nexus/src/test/kotlin/Iso20022Test.kt @@ -405,6 +405,14 @@ class Iso20022Test { subject = "multi 1 2024-11-21T15:21:59.8859234 63Z", executionTime = dateToInstant("2024-11-27"), creditorPayto = ibanPayto("CH7389144832588726658", "Grothoff Hans") + ), + IncomingPayment( + bankId = "f203fbb4-6e13-4c78-9b2a-d852fea6374a", + amount = TalerAmount("CHF:0.15"), + creditFee = TalerAmount("CHF:0.2"), + subject = "mini", + executionTime = dateToInstant("2024-12-02"), + debtorPayto = ibanPayto("CH7389144832588726658", "Grothoff Hans") ) ) )) diff --git a/nexus/src/test/kotlin/RegistrationTest.kt b/nexus/src/test/kotlin/RegistrationTest.kt @@ -443,6 +443,14 @@ class RegistrationTest { subject = "Random subject", executionTime = dateToInstant("2024-11-04"), debtorPayto = ibanPayto("CH7389144832588726658", "Mr Test") + ), + IncomingPayment( + bankId = "f203fbb4-6e13-4c78-9b2a-d852fea6374a", + amount = TalerAmount("CHF:0.15"), + creditFee = TalerAmount("CHF:0.2"), + subject = "mini", + executionTime = dateToInstant("2024-12-02"), + debtorPayto = ibanPayto("CH7389144832588726658", "Grothoff Hans") ) ), outgoing = listOf( diff --git a/testbench/src/main/kotlin/Main.kt b/testbench/src/main/kotlin/Main.kt @@ -132,8 +132,8 @@ class Cli : CliktCommand() { val currency = cfg.currency val dummyPaytos = mapOf( - "CHF" to "payto://iban/CH4189144589712575493?receiver-name=John%20Smith", - "EUR" to "payto://iban/DE18500105173385245162?receiver-name=John%20Smith" + "CHF" to "payto://iban/GENODED1SPW/DE48330605920000686018?receiver-name=Christian%20Grothoff", + "EUR" to "payto://iban/GENODED1SPW/DE48330605920000686018?receiver-name=Christian%20Grothoff" ) val dummyPayto = requireNotNull(dummyPaytos[currency]) { "Missing dummy payto for $currency"