libeufin

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

commit 3cf09bce45f834c53ff1aa0222e0a9eefa45b23b
parent d8aab9cdfef774b1f01a039fb404859adf17e22f
Author: Antoine A <>
Date:   Thu, 14 Nov 2024 19:24:33 +0100

nexus: incoming txs credit_fee

Diffstat:
Mcommon/src/main/kotlin/Constants.kt | 4++--
Mcommon/src/main/kotlin/TalerCommon.kt | 23+++++++++++++++++++++++
Mcommon/src/main/kotlin/TalerMessage.kt | 5+++++
Mdatabase-versioning/libeufin-bank-procedures.sql | 28++++++++++++++--------------
Adatabase-versioning/libeufin-nexus-0009.sql | 23+++++++++++++++++++++++
Mdatabase-versioning/libeufin-nexus-procedures.sql | 13+++++++++++--
Mnexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt | 6+++++-
Mnexus/src/main/kotlin/tech/libeufin/nexus/db/ExchangeDAO.kt | 4++++
Mnexus/src/main/kotlin/tech/libeufin/nexus/db/ListDAO.kt | 4++++
Mnexus/src/main/kotlin/tech/libeufin/nexus/db/PaymentDAO.kt | 57+++++++++++++++++++++++++++++++++------------------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/iso20022/camt.kt | 43++++++++++++++++++++++++++++++++++---------
Mnexus/src/test/kotlin/Iso20022Test.kt | 6++++--
Mnexus/src/test/kotlin/RegistrationTest.kt | 9+++++++--
13 files changed, 169 insertions(+), 56 deletions(-)

diff --git a/common/src/main/kotlin/Constants.kt b/common/src/main/kotlin/Constants.kt @@ -26,8 +26,8 @@ const val SERIALIZATION_RETRY: Int = 10 const val MAX_BODY_LENGTH: Int = 4 * 1024 // 4kB // API version -const val WIRE_GATEWAY_API_VERSION: String = "3:0:2" -const val REVENUE_API_VERSION: String = "1:0:1" +const val WIRE_GATEWAY_API_VERSION: String = "3:1:2" +const val REVENUE_API_VERSION: String = "1:1:1" // HTTP headers const val X_CHALLENGE_ID: String = "X-Challenge-Id" diff --git a/common/src/main/kotlin/TalerCommon.kt b/common/src/main/kotlin/TalerCommon.kt @@ -201,6 +201,11 @@ class TalerAmount { fun number(): DecimalNumber = DecimalNumber(value, frac) + /* Check if zero */ + fun isZero(): Boolean = value == 0L && frac == 0 + + fun notZeroOrNull(): TalerAmount? = if (isZero()) null else this + /* Check is amount has fractional amount < 0.01 */ fun isSubCent(): Boolean = (frac % CENT_FRACTION) > 0 @@ -229,11 +234,29 @@ class TalerAmount { } 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) val frac = Math.addExact(this.frac, increment.frac) return TalerAmount(value, frac, currency).normalize() } + operator fun minus(decrement: TalerAmount): TalerAmount { + require(this.currency == decrement.currency) { "currency mismatch ${this.currency} != ${decrement.currency}" } + var frac = this.frac + var value = this.value + if (frac < decrement.frac) { + if (value <= 0) { + throw ArithmeticException("negative result") + } + frac += FRACTION_BASE + value -= 1 + } + if (value < decrement.value) { + throw ArithmeticException("negative result") + } + return TalerAmount(value - decrement.value, frac - decrement.frac, currency).normalize() + } + internal object Serializer : KSerializer<TalerAmount> { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("TalerAmount", PrimitiveKind.STRING) diff --git a/common/src/main/kotlin/TalerMessage.kt b/common/src/main/kotlin/TalerMessage.kt @@ -124,6 +124,7 @@ sealed interface IncomingBankTransaction { val date: TalerProtocolTimestamp val amount: TalerAmount val debit_account: String + val credit_fee: TalerAmount? } @Serializable @@ -132,6 +133,7 @@ data class IncomingKycAuthTransaction( override val row_id: Long, override val date: TalerProtocolTimestamp, override val amount: TalerAmount, + override val credit_fee: TalerAmount? = null, override val debit_account: String, val account_pub: EddsaPublicKey ): IncomingBankTransaction @@ -141,6 +143,7 @@ data class IncomingReserveTransaction( override val row_id: Long, override val date: TalerProtocolTimestamp, override val amount: TalerAmount, + override val credit_fee: TalerAmount? = null, override val debit_account: String, val reserve_pub: EddsaPublicKey ): IncomingBankTransaction @@ -150,6 +153,7 @@ data class IncomingWadTransaction( override val row_id: Long, override val date: TalerProtocolTimestamp, override val amount: TalerAmount, + override val credit_fee: TalerAmount? = null, override val debit_account: String, val origin_exchange_url: String, val wad_id: String // TODO 24 bytes Base32 @@ -195,6 +199,7 @@ data class RevenueIncomingBankTransaction( val row_id: Long, val date: TalerProtocolTimestamp, val amount: TalerAmount, + val credit_fee: TalerAmount? = null, val debit_account: String, val subject: String ) \ No newline at end of file diff --git a/database-versioning/libeufin-bank-procedures.sql b/database-versioning/libeufin-bank-procedures.sql @@ -62,24 +62,24 @@ CREATE FUNCTION amount_left_minus_right( ) LANGUAGE plpgsql IMMUTABLE AS $$ BEGIN -IF l.val > r.val THEN - ok = TRUE; - IF l.frac >= r.frac THEN - diff.val = l.val - r.val; - diff.frac = l.frac - r.frac; - ELSE - diff.val = l.val - r.val - 1; - diff.frac = l.frac + 100000000 - r.frac; - END IF; -ELSE IF l.val = r.val AND l.frac >= r.frac THEN - diff.val = 0; - diff.frac = l.frac - r.frac; - ok = TRUE; - ELSE +diff = l; +IF diff.frac < r.frac THEN + IF diff.val <= 0 THEN diff = (-1, -1); ok = FALSE; + RETURN; END IF; + diff.frac = diff.frac + 100000000; + diff.val = diff.val - 1; +END IF; +IF diff.val < r.val THEN + diff = (-1, -1); + ok = FALSE; + RETURN; END IF; +diff.val = diff.val - r.val; +diff.frac = diff.frac - r.frac; +ok = TRUE; END $$; COMMENT ON FUNCTION amount_left_minus_right IS 'Subtracts the right amount from the left and returns the difference and TRUE, if the left amount is larger than the right, or an invalid amount and FALSE otherwise.'; diff --git a/database-versioning/libeufin-nexus-0009.sql b/database-versioning/libeufin-nexus-0009.sql @@ -0,0 +1,23 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2024 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER is distributed in the hope that it will be useful, but WITHOUT ANY +-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +-- A PARTICULAR PURPOSE. See the GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + +BEGIN; + +SELECT _v.register_patch('libeufin-nexus-0009', NULL, NULL); + +SET search_path TO libeufin_nexus; + +ALTER TABLE incoming_transactions ADD COLUMN credit_fee taler_amount; +COMMIT; diff --git a/database-versioning/libeufin-nexus-procedures.sql b/database-versioning/libeufin-nexus-procedures.sql @@ -193,6 +193,7 @@ COMMENT ON FUNCTION register_outgoing CREATE FUNCTION register_incoming( IN in_amount taler_amount + ,IN in_credit_fee taler_amount ,IN in_subject TEXT ,IN in_execution_time INT8 ,IN in_debit_payto TEXT @@ -213,6 +214,10 @@ local_amount taler_amount; local_subject TEXT; local_debit_payto TEXT; BEGIN +IF in_credit_fee = (0, 0)::taler_amount THEN + in_credit_fee = NULL; +END IF; + IF in_type IS NULL THEN -- No talerable logic ELSIF in_type = 'reserve' THEN @@ -220,7 +225,7 @@ ELSIF in_type = 'reserve' THEN -- Reconcile missing bank_id if metadata match -- Check for reserve_pub reuse SELECT incoming_transaction_id, bank_id IS DISTINCT FROM in_bank_id, - bank_id IS NULL AND amount = in_amount + bank_id IS NULL AND amount = in_amount AND debit_payto = in_debit_payto AND subject = in_subject INTO out_tx_id, out_reserve_pub_reuse, need_reconcile @@ -285,12 +290,14 @@ ELSE -- Store the transaction in the database INSERT INTO incoming_transactions ( amount + ,credit_fee ,subject ,execution_time ,debit_payto ,bank_id ) VALUES ( in_amount + ,in_credit_fee ,in_subject ,in_execution_time ,in_debit_payto @@ -319,6 +326,7 @@ END $$; CREATE FUNCTION register_and_bounce_incoming( IN in_amount taler_amount + ,IN in_credit_fee taler_amount ,IN in_subject TEXT ,IN in_execution_time INT8 ,IN in_debit_payto TEXT @@ -333,10 +341,11 @@ CREATE FUNCTION register_and_bounce_incoming( LANGUAGE plpgsql AS $$ DECLARE init_id INT8; +bounce_amount taler_amount; BEGIN -- Register incoming transaction SELECT reg.out_found, reg.out_tx_id - FROM register_incoming(in_amount, in_subject, in_execution_time, in_debit_payto, in_bank_id, NULL, NULL, NULL) as reg + FROM register_incoming(in_amount, in_credit_fee, in_subject, in_execution_time, in_debit_payto, in_bank_id, NULL, NULL, NULL) as reg INTO out_found, out_tx_id; -- Bounce incoming transaction diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt @@ -96,9 +96,13 @@ suspend fun registerIncomingPayment( logger.debug("{} already seen and ignored bounce: {}", payment, msg) } } else { + var bounceAmount = payment.amount + if (payment.creditFee != null) { + bounceAmount -= payment.creditFee + } val result = db.payment.registerMalformedIncoming( payment, - payment.amount, + bounceAmount, randEbicsId(), Instant.now() ) diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/db/ExchangeDAO.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/db/ExchangeDAO.kt @@ -35,6 +35,8 @@ class ExchangeDAO(private val db: Database) { ,execution_time ,(amount).val AS amount_val ,(amount).frac AS amount_frac + ,(credit_fee).val AS credit_fee_val + ,(credit_fee).frac AS credit_fee_frac ,debit_payto ,type ,reserve_public_key @@ -49,6 +51,7 @@ class ExchangeDAO(private val db: Database) { row_id = it.getLong("incoming_transaction_id"), date = it.getTalerTimestamp("execution_time"), amount = it.getAmount("amount", db.bankCurrency), + credit_fee = it.getAmount("credit_fee", db.bankCurrency).notZeroOrNull(), debit_account = it.getString("debit_payto"), reserve_pub = EddsaPublicKey(it.getBytes("reserve_public_key")), ) @@ -56,6 +59,7 @@ class ExchangeDAO(private val db: Database) { row_id = it.getLong("incoming_transaction_id"), date = it.getTalerTimestamp("execution_time"), amount = it.getAmount("amount", db.bankCurrency), + credit_fee = it.getAmount("credit_fee", db.bankCurrency).notZeroOrNull(), debit_account = it.getString("debit_payto"), account_pub = EddsaPublicKey(it.getBytes("account_pub")), ) diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/db/ListDAO.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/db/ListDAO.kt @@ -34,6 +34,8 @@ class ListDAO(private val db: Database) { SELECT (amount).val AS amount_val ,(amount).frac AS amount_frac + ,(credit_fee).val AS credit_fee_val + ,(credit_fee).frac AS credit_fee_frac ,subject ,(bounced_transactions.initiated_outgoing_transaction_id IS NOT NULL) AS bounced ,execution_time @@ -53,6 +55,7 @@ class ListDAO(private val db: Database) { IncomingTxMetadata( date = it.getLong("execution_time").asInstant(), amount = it.getDecimal("amount"), + creditFee = it.getDecimal("credit_fee"), subject = it.getString("subject"), debtor = it.getString("debit_payto"), id = it.getString("bank_id"), @@ -135,6 +138,7 @@ class ListDAO(private val db: Database) { data class IncomingTxMetadata( val date: Instant, val amount: DecimalNumber, + val creditFee: DecimalNumber, val subject: String, val debtor: String, val id: String?, diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/db/PaymentDAO.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/db/PaymentDAO.kt @@ -83,19 +83,21 @@ class PaymentDAO(private val db: Database) { ): IncomingBounceRegistrationResult = db.serializable( """ SELECT out_found, out_tx_id, out_bounce_id - FROM register_and_bounce_incoming((?,?)::taler_amount,?,?,?,?,(?,?)::taler_amount,?,?) + FROM register_and_bounce_incoming((?,?)::taler_amount,(?,?)::taler_amount,?,?,?,?,(?,?)::taler_amount,?,?) """ ) { setLong(1, payment.amount.value) setInt(2, payment.amount.frac) - setString(3, payment.subject) - setLong(4, payment.executionTime.micros()) - setString(5, payment.debtorPayto.toString()) - setString(6, payment.bankId) - setLong(7, bounceAmount.value) - setInt(8, bounceAmount.frac) - setLong(9, timestamp.micros()) - setString(10, bounceEndToEndId) + setLong(3, payment.creditFee?.value ?: 0L) + setInt(4, payment.creditFee?.frac ?: 0) + setString(5, payment.subject) + setLong(6, payment.executionTime.micros()) + setString(7, payment.debtorPayto.toString()) + setString(8, payment.bankId) + setLong(9, bounceAmount.value) + setInt(10, bounceAmount.frac) + setLong(11, timestamp.micros()) + setString(12, bounceEndToEndId) one { IncomingBounceRegistrationResult( it.getLong("out_tx_id"), @@ -118,25 +120,27 @@ class PaymentDAO(private val db: Database) { ): IncomingRegistrationResult = db.serializable( """ SELECT out_reserve_pub_reuse, out_found, out_tx_id - FROM register_incoming((?,?)::taler_amount,?,?,?,?,?::taler_incoming_type,?,?) + FROM register_incoming((?,?)::taler_amount,(?,?)::taler_amount,?,?,?,?,?::taler_incoming_type,?,?) """ ) { val executionTime = payment.executionTime.micros() setLong(1, payment.amount.value) setInt(2, payment.amount.frac) - setString(3, payment.subject) - setLong(4, executionTime) - setString(5, payment.debtorPayto.toString()) - setString(6, payment.bankId) - setString(7, metadata.type.name) + setLong(3, payment.creditFee?.value ?: 0L) + setInt(4, payment.creditFee?.frac ?: 0) + setString(5, payment.subject) + setLong(6, executionTime) + setString(7, payment.debtorPayto.toString()) + setString(8, payment.bankId) + setString(9, metadata.type.name) when (metadata.type) { TalerIncomingType.reserve -> { - setBytes(8, metadata.key.raw) - setNull(9, Types.BINARY) + setBytes(10, metadata.key.raw) + setNull(11, Types.BINARY) } TalerIncomingType.kyc -> { - setNull(8, Types.BINARY) - setBytes(9, metadata.key.raw) + setNull(10, Types.BINARY) + setBytes(11, metadata.key.raw) } TalerIncomingType.wad -> throw UnsupportedOperationException() } @@ -157,16 +161,18 @@ class PaymentDAO(private val db: Database) { ): IncomingRegistrationResult.Success = db.serializable( """ SELECT out_found, out_tx_id - FROM register_incoming((?,?)::taler_amount,?,?,?,?,NULL,NULL,NULL) + FROM register_incoming((?,?)::taler_amount,(?,?)::taler_amount,?,?,?,?,NULL,NULL,NULL) """ ) { val executionTime = payment.executionTime.micros() setLong(1, payment.amount.value) setInt(2, payment.amount.frac) - setString(3, payment.subject) - setLong(4, executionTime) - setString(5, payment.debtorPayto.toString()) - setString(6, payment.bankId) + setLong(3, payment.creditFee?.value ?: 0L) + setInt(4, payment.creditFee?.frac ?: 0) + setString(5, payment.subject) + setLong(6, executionTime) + setString(7, payment.debtorPayto.toString()) + setString(8, payment.bankId) one { IncomingRegistrationResult.Success( it.getLong("out_tx_id"), @@ -185,6 +191,8 @@ class PaymentDAO(private val db: Database) { ,execution_time ,(amount).val AS amount_val ,(amount).frac AS amount_frac + ,(credit_fee).val AS credit_fee_val + ,(credit_fee).frac AS credit_fee_frac ,debit_payto ,subject FROM incoming_transactions WHERE @@ -193,6 +201,7 @@ class PaymentDAO(private val db: Database) { row_id = it.getLong("incoming_transaction_id"), date = it.getTalerTimestamp("execution_time"), amount = it.getAmount("amount", db.bankCurrency), + credit_fee = it.getAmount("credit_fee", db.bankCurrency).notZeroOrNull(), debit_account = it.getString("debit_payto"), subject = it.getString("subject") ) diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/camt.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/camt.kt @@ -34,12 +34,14 @@ data class IncomingPayment( /** ISO20022 UETR or TxID */ val bankId: String? = null, // Null when TxID is wrong with Atruvia's implementation of instant transactions val amount: TalerAmount, + val creditFee: TalerAmount? = null, val subject: String, override val executionTime: Instant, val debtorPayto: IbanPayto ): TxNotification { override fun toString(): String { - return "IN ${executionTime.fmtDate()} $amount $bankId debitor=$debtorPayto subject=\"$subject\"" + val fee = if (creditFee == null) "" else "$creditFee " + return "IN ${executionTime.fmtDate()} $amount$fee $bankId debitor=$debtorPayto subject=\"$subject\"" } } @@ -170,8 +172,7 @@ private fun XmlDestructor.returnReason(): String = opt("RtrInf") { /** Parse amount */ private fun XmlDestructor.amount(acceptedCurrency: String) = one("Amt") { val currency = attr("Ccy") - /** FIXME: test by sending non-CHF to PoFi and see which currency gets here. */ - if (currency != acceptedCurrency) throw Exception("Currency $currency not supported") + require(currency == acceptedCurrency) { "Currency $currency not supported" } val amount = text() val concat = if (amount.startsWith('.')) { "$currency:0$amount" @@ -181,6 +182,24 @@ private fun XmlDestructor.amount(acceptedCurrency: String) = one("Amt") { TalerAmount(concat) } +/** Parse amounts and compute fees */ +private fun XmlDestructor.amountAndFee(acceptedCurrency: String): Pair<TalerAmount, TalerAmount?> { + val amount = amount(acceptedCurrency) + var charges = TalerAmount.zero(amount.currency) + opt("Chrgs")?.each("Rcrd") { + if (one("ChrgInclInd").bool() && opt("Br")?.text() == "CRED") { + if (one("CdtDbtInd").text() == "DBIT") { + charges += amount(acceptedCurrency) + } + } + } + return if (charges.isZero()) { + Pair(amount, null) + } else { + Pair(amount + charges, charges) + } +} + /** Parse bank transaction code */ private fun XmlDestructor.bankTransactionCode(): BankTransactionCode { return one("BkTxCd").one("Domn") { @@ -285,7 +304,8 @@ fun parseTx( amount = amount, subject = subject, debtorPayto = debtorPayto, - code = code + code = code, + creditFee = null )) } Kind.DBIT -> { @@ -342,7 +362,8 @@ fun parseTx( amount = amount, subject = subject, debtorPayto = debtorPayto, - code = code + code = code, + creditFee = null )) } } @@ -416,7 +437,8 @@ fun parseTx( amount = amount, subject = subject, debtorPayto = debtorPayto, - code = code + code = code, + creditFee = null )) } Kind.DBIT -> { @@ -449,11 +471,11 @@ fun parseTx( val entryRef = opt("AcctSvcrRef")?.text() val bookDate = executionDate() - val amount = amount(acceptedCurrency) 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(acceptedCurrency) val subject = wireTransferSubject() if (!code.isReversal() && kind == Kind.CRDT) { val bankId = opt("Refs")?.opt("UETR")?.text() @@ -465,7 +487,8 @@ fun parseTx( amount = amount, subject = subject, debtorPayto = debtorPayto, - code = code + code = code, + creditFee = fee )) } } @@ -507,6 +530,7 @@ private sealed interface TxInfo { // Unique ID generated by payment provider val bankId: String?, val amount: TalerAmount, + val creditFee: TalerAmount?, val subject: String?, val debtorPayto: IbanPayto? ): TxInfo @@ -543,10 +567,11 @@ private fun parseTxLogic(info: TxInfo): TxNotification { throw TxErr("missing debtor info for Credit ${info.ref}") IncomingPayment( amount = info.amount, + creditFee = info.creditFee, bankId = info.bankId, debtorPayto = info.debtorPayto, executionTime = info.bookDate, - subject = info.subject + subject = info.subject, ) } is TxInfo.Debit -> { diff --git a/nexus/src/test/kotlin/Iso20022Test.kt b/nexus/src/test/kotlin/Iso20022Test.kt @@ -337,14 +337,16 @@ class Iso20022Test { listOf( IncomingPayment( bankId = "adbe4a5a-6cea-4263-b259-8ab964561a32", - amount = TalerAmount("CHF:0.8"), + amount = TalerAmount("CHF:1"), + creditFee = TalerAmount("CHF:0.2"), subject = "SFHP6H24C16A5J05Q3FJW2XN1PB3EK70ZPY 5SJ30ADGY68FWN68G", executionTime = dateToInstant("2024-11-04"), debtorPayto = ibanPayto("CH7389144832588726658", "Mr Test") ), IncomingPayment( bankId = "7371795e-62fa-42dd-93b7-da89cc120faa", - amount = TalerAmount("CHF:0.8"), + amount = TalerAmount("CHF:1"), + creditFee = TalerAmount("CHF:0.2"), subject = "Random subject", executionTime = dateToInstant("2024-11-04"), debtorPayto = ibanPayto("CH7389144832588726658", "Mr Test") diff --git a/nexus/src/test/kotlin/RegistrationTest.kt b/nexus/src/test/kotlin/RegistrationTest.kt @@ -106,6 +106,8 @@ class RegistrationTest { SELECT bank_id ,(amount).val as amount_val ,(amount).frac AS amount_frac + ,(credit_fee).val AS credit_fee_val + ,(credit_fee).frac AS credit_fee_frac ,subject ,execution_time ,debit_payto @@ -117,6 +119,7 @@ class RegistrationTest { IncomingPayment( bankId = it.getString("bank_id"), amount = it.getAmount("amount", this@check.bankCurrency), + creditFee = it.getAmount("credit_fee", this@check.bankCurrency).notZeroOrNull(), subject = it.getString("subject"), executionTime = it.getLong("execution_time").asInstant(), debtorPayto = it.getIbanPayto("debit_payto"), @@ -411,14 +414,16 @@ class RegistrationTest { incoming = listOf( IncomingPayment( bankId = "adbe4a5a-6cea-4263-b259-8ab964561a32", - amount = TalerAmount("CHF:0.8"), + amount = TalerAmount("CHF:1"), + creditFee = TalerAmount("CHF:0.2"), subject = "SFHP6H24C16A5J05Q3FJW2XN1PB3EK70ZPY 5SJ30ADGY68FWN68G", executionTime = dateToInstant("2024-11-04"), debtorPayto = ibanPayto("CH7389144832588726658", "Mr Test") ), IncomingPayment( bankId = "7371795e-62fa-42dd-93b7-da89cc120faa", - amount = TalerAmount("CHF:0.8"), + amount = TalerAmount("CHF:1"), + creditFee = TalerAmount("CHF:0.2"), subject = "Random subject", executionTime = dateToInstant("2024-11-04"), debtorPayto = ibanPayto("CH7389144832588726658", "Mr Test")