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:
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"