libeufin

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

commit 6ebe691de905c96b5bb30649b12822b5d606191a
parent 2576369aa04718c91c732dfa4e37da5006e5fa50
Author: Antoine A <>
Date:   Thu, 21 Nov 2024 16:56:23 +0100

nexus: check account iban and currency

Diffstat:
Mcommon/src/main/kotlin/TalerCommon.kt | 1-
Mnexus/conf/gls.conf | 2+-
Mnexus/conf/maerki_baumann.conf | 2+-
Mnexus/sample/platform/gls_camt052.xml | 6++++++
Mnexus/sample/platform/gls_camt053.xml | 6++++++
Mnexus/sample/platform/gls_camt054.xml | 6++++++
Mnexus/sample/platform/maerki_baumann_camt053.xml | 6++++++
Mnexus/sample/platform/postfinance_camt053.xml | 6++++++
Mnexus/sample/platform/postfinance_camt054.xml | 6++++++
Mnexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt | 32+++++++++++++++++++++++++++++---
Mnexus/src/main/kotlin/tech/libeufin/nexus/cli/Testing.kt | 5+----
Mnexus/src/main/kotlin/tech/libeufin/nexus/iso20022/camt.kt | 190+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mnexus/src/test/kotlin/Iso20022Test.kt | 390++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Mtestbench/src/test/kotlin/Iso20022Test.kt | 6+++---
14 files changed, 381 insertions(+), 283 deletions(-)

diff --git a/common/src/main/kotlin/TalerCommon.kt b/common/src/main/kotlin/TalerCommon.kt @@ -228,7 +228,6 @@ class TalerAmount { fun normalize(): TalerAmount { val value = Math.addExact(this.value, (this.frac / FRACTION_BASE).toLong()) val frac = this.frac % FRACTION_BASE - println("${this.value}+${this.frac / FRACTION_BASE}=${value} ${MAX_VALUE}") if (value > MAX_VALUE) throw ArithmeticException("amount value overflowed") return TalerAmount(value, frac, currency) } diff --git a/nexus/conf/gls.conf b/nexus/conf/gls.conf @@ -11,7 +11,7 @@ HOST_ID = PFEBICS USER_ID = PFC00563 PARTNER_ID = PFC00563 -IBAN = DE89500105171325381664 +IBAN = DE84500105177118117964 BIC = BIC NAME = myname diff --git a/nexus/conf/maerki_baumann.conf b/nexus/conf/maerki_baumann.conf @@ -7,7 +7,7 @@ HOST_ID = PFEBICS USER_ID = PFC00563 PARTNER_ID = PFC00563 -IBAN = DE89500105171325381664 +IBAN = CH7389144832588726658 BIC = BIC NAME = myname diff --git a/nexus/sample/platform/gls_camt052.xml b/nexus/sample/platform/gls_camt052.xml @@ -4,6 +4,12 @@ xsi:schemaLocation="urn:iso:std:iso:20022:tech:xsd:camt.052.001.02 camt.052.001.02.xsd"> <BkToCstmrAcctRpt> <Rpt> + <Acct> + <Id> + <IBAN>DE84500105177118117964</IBAN> + </Id> + <Ccy>EUR</Ccy> + </Acct> <Ntry> <Amt Ccy="EUR">2.00</Amt> <CdtDbtInd>DBIT</CdtDbtInd> diff --git a/nexus/sample/platform/gls_camt053.xml b/nexus/sample/platform/gls_camt053.xml @@ -4,6 +4,12 @@ xsi:schemaLocation="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02 camt.053.001.02.xsd"> <BkToCstmrStmt> <Stmt> + <Acct> + <Id> + <IBAN>DE84500105177118117964</IBAN> + </Id> + <Ccy>EUR</Ccy> + </Acct> <Ntry> <Amt Ccy="EUR">2.00</Amt> <CdtDbtInd>DBIT</CdtDbtInd> diff --git a/nexus/sample/platform/gls_camt054.xml b/nexus/sample/platform/gls_camt054.xml @@ -5,6 +5,12 @@ <MsgId>IS11PGENODEFF2DA8899900378806</MsgId> </GrpHdr> <Ntfctn> + <Acct> + <Id> + <IBAN>DE84500105177118117964</IBAN> + </Id> + <Ccy>EUR</Ccy> + </Acct> <Ntry> <Amt Ccy="EUR">2.50</Amt> <CdtDbtInd>CRDT</CdtDbtInd> diff --git a/nexus/sample/platform/maerki_baumann_camt053.xml b/nexus/sample/platform/maerki_baumann_camt053.xml @@ -10,6 +10,12 @@ </MsgPgntn> </GrpHdr> <Stmt> + <Acct> + <Id> + <IBAN>CH7389144832588726658</IBAN> + </Id> + <Ccy>CHF</Ccy> + </Acct> <Ntry> <Amt Ccy="CHF">.8</Amt> <CdtDbtInd>CRDT</CdtDbtInd> diff --git a/nexus/sample/platform/postfinance_camt053.xml b/nexus/sample/platform/postfinance_camt053.xml @@ -4,6 +4,12 @@ xsi:schemaLocation="urn:iso:std:iso:20022:tech:xsd:camt.053.001.08 camt.053.001.08.xsd"> <BkToCstmrStmt> <Stmt> + <Acct> + <Id> + <IBAN>CH9289144596463965762</IBAN> + </Id> + <Ccy>CHF</Ccy> + </Acct> <Ntry> <Amt Ccy="CHF">1.00</Amt> <CdtDbtInd>CRDT</CdtDbtInd> diff --git a/nexus/sample/platform/postfinance_camt054.xml b/nexus/sample/platform/postfinance_camt054.xml @@ -7,6 +7,12 @@ <MsgId>20240115375204422237387</MsgId> </GrpHdr> <Ntfctn> + <Acct> + <Id> + <IBAN>CH9289144596463965762</IBAN> + </Id> + <Ccy>CHF</Ccy> + </Acct> <Ntry> <Amt Ccy="CHF">3.00</Amt> <CdtDbtInd>DBIT</CdtDbtInd> diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt @@ -161,6 +161,34 @@ suspend fun registerTransaction( } } +/** Register a single EBICS [xml] txs [document] into [db] */ +suspend fun registerTxs( + db: Database, + cfg: NexusConfig, + xml: InputStream +): Int { + var nbTx: Int = 0 + parseTx(xml, cfg.ebics.dialect).forEach { accountTx -> + if (accountTx.iban == cfg.ebics.account.iban) { + require(accountTx.currency == cfg.currency) { "Expected transactions of currency ${cfg.currency} for ${accountTx.currency}" } + accountTx.txs.forEach { tx -> + when (tx) { + is IncomingPayment -> + require(tx.amount.currency == cfg.currency) { "Expected transactions of currency ${cfg.currency} for ${tx.amount.currency}" } + is OutgoingPayment -> + require(tx.amount.currency == cfg.currency) { "Expected transactions of currency ${cfg.currency} for ${tx.amount.currency}" } + is OutgoingBatch, is OutgoingReversal -> {} + } + registerTransaction(db, cfg.ingest, tx) + nbTx += 1 + } + } else { + logger.debug("Skip transaction for unknown account ${accountTx.iban}") + } + } + return nbTx +} + /** Register a single EBICS [xml] [document] into [db] */ suspend fun registerFile( db: Database, @@ -171,9 +199,7 @@ suspend fun registerFile( when (doc) { OrderDoc.report, OrderDoc.statement, OrderDoc.notification -> { try { - parseTx(xml, cfg.currency, cfg.ebics.dialect).forEach { tx -> - registerTransaction(db, cfg.ingest, tx) - } + registerTxs(db, cfg, xml) } catch (e: Exception) { throw Exception("Ingesting notifications failed", e) } diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/cli/Testing.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/cli/Testing.kt @@ -317,10 +317,7 @@ class ImportCmt: CliktCommand("import") { nexusConfig(common.config).withDb { db, cfg -> var nbTx: Int = 0 source.inputStream().use { xml -> - parseTx(xml, cfg.currency, cfg.ebics.dialect).forEach { tx -> - registerTransaction(db, cfg.ingest, tx) - nbTx += 1 - } + nbTx += registerTxs(db, cfg, xml) } println("Imported $nbTx transactions from $source") } diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/camt.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/camt.kt @@ -96,7 +96,7 @@ private enum class Kind { } /** Unique ID generated by libeufin-nexus */ -private data class OutgoingId( +data class OutgoingId( // Unique msg ID generated by libeufin-nexus val msgId: String?, // Unique end-to-end ID generated by libeufin-nexus @@ -170,9 +170,8 @@ private fun XmlDestructor.returnReason(): String = opt("RtrInf") { } ?: "" /** Parse amount */ -private fun XmlDestructor.amount(acceptedCurrency: String) = one("Amt") { +private fun XmlDestructor.amount() = one("Amt") { val currency = attr("Ccy") - require(currency == acceptedCurrency) { "Currency $currency not supported" } val amount = text() val concat = if (amount.startsWith('.')) { "$currency:0$amount" @@ -183,13 +182,13 @@ private fun XmlDestructor.amount(acceptedCurrency: String) = one("Amt") { } /** Parse amounts and compute fees */ -private fun XmlDestructor.amountAndFee(acceptedCurrency: String): Pair<TalerAmount, TalerAmount?> { - val amount = amount(acceptedCurrency) +private fun XmlDestructor.amountAndFee(): Pair<TalerAmount, TalerAmount?> { + val amount = amount() 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) + charges += amount() } } } @@ -231,12 +230,39 @@ private fun XmlDestructor.wireTransferSubject(): String? { return opt("RmtInf")?.map("Ustrd") { text() }?.joinToString("")?.trim() } +/** Parse account information */ +private fun XmlDestructor.account(): Pair<String, String?> = one("Acct") { + Pair( + one("Id") { + (opt("IBAN") ?: one("Othr").one("Id")).text() + }, + opt("Ccy")?.text() + ) +} + +data class AccountTransactions( + val iban: String?, + val currency: String?, + val txs: List<TxNotification> +) { + companion object { + internal fun fromParts(iban: String?, currency: String?, txsInfos: List<TxInfo>): AccountTransactions { + val txs = txsInfos.mapNotNull { + try { + it.parse() + } catch (e: TxErr) { + // TODO: add more info in doc or in log message? + logger.warn("skip incomplete tx: ${e.msg}") + null + } + } + return AccountTransactions(iban, currency, txs) + } + } +} + /** Parse camt.054 or camt.053 file */ -fun parseTx( - notifXml: InputStream, - acceptedCurrency: String, - dialect: Dialect -): List<TxNotification> { +fun parseTx(notifXml: InputStream, dialect: Dialect): List<AccountTransactions> { /* In ISO 20022 specifications, most fields are optional and the same information can be written several times in different places. For libeufin, we're only @@ -259,15 +285,13 @@ fun parseTx( disadvantage of being known only by the account servicing institution. They should therefore only be used as a last resort. */ - - val txsInfo = mutableListOf<TxInfo>() + val accountTxs = mutableListOf<AccountTransactions>() XmlDestructor.fromStream(notifXml, "Document") { when (dialect) { Dialect.gls -> { /** Common parsing logic for camt.052 and camt.053 */ fun XmlDestructor.parseGlsInner() { - opt("Acct") { - // Sanity check on currency and IBAN ? - } + val (iban, currency) = account() + val txsInfo = mutableListOf<TxInfo>() each("Ntry") { if (!isBooked()) return@each val code = bankTransactionCode() @@ -275,7 +299,7 @@ fun parseTx( val entryRef = opt("AcctSvcrRef")?.text() val bookDate = executionDate() val kind = one("CdtDbtInd").enum<Kind>() - val amount = amount(acceptedCurrency) + val amount = amount() one("NtryDtls").one("TxDtls") { // TODO handle batches val code = optBankTransactionCode() ?: code val txRef = opt("Refs")?.opt("AcctSvcrRef")?.text() @@ -325,6 +349,7 @@ fun parseTx( } } } + accountTxs.add(AccountTransactions.fromParts(iban, currency, txsInfo)) } opt("BkToCstmrStmt")?.each("Stmt") { // Camt.053 // All transactions appear here the day after they are booked @@ -336,9 +361,8 @@ fun parseTx( } opt("BkToCstmrDbtCdtNtfctn")?.each("Ntfctn") { // Camt.054 // Instant transactions appear here a few seconds after being booked - opt("Acct") { - // Sanity check on currency and IBAN ? - } + val (iban, currency) = account() + val txsInfo = mutableListOf<TxInfo>() each("Ntry") { if (!isBooked()) return@each val code = bankTransactionCode() @@ -346,7 +370,7 @@ fun parseTx( val entryRef = opt("AcctSvcrRef")?.text() val bookDate = executionDate() val kind = one("CdtDbtInd").enum<Kind>() - val amount = amount(acceptedCurrency) + val amount = amount() one("NtryDtls").one("TxDtls") { val code = optBankTransactionCode() ?: code val txRef = opt("Refs")?.opt("AcctSvcrRef")?.text() @@ -368,6 +392,7 @@ fun parseTx( } } } + accountTxs.add(AccountTransactions.fromParts(iban, currency, txsInfo)) } } Dialect.postfinance -> { @@ -378,9 +403,8 @@ fun parseTx( this file contains the structured return reasons that are missing from the camt.054 files. That's why we only use this file for this purpose. */ - opt("Acct") { - // Sanity check on currency and IBAN ? - } + val (iban, currency) = account() + val txsInfo = mutableListOf<TxInfo>() each("Ntry") { if (!isBooked()) return@each val code = bankTransactionCode() @@ -406,12 +430,12 @@ fun parseTx( } } } + accountTxs.add(AccountTransactions.fromParts(iban, currency, txsInfo)) } opt("BkToCstmrDbtCdtNtfctn")?.each("Ntfctn") { // Camt.054 // Instant transactions appear here a moment after being booked - opt("Acct") { - // Sanity check on currency and IBAN ? - } + val (iban, currency) = account() + val txsInfo = mutableListOf<TxInfo>() each("Ntry") { if (!isBooked()) return@each val code = bankTransactionCode() @@ -423,7 +447,7 @@ fun parseTx( one("NtryDtls").each("TxDtls") { val kind = one("CdtDbtInd").enum<Kind>() val code = optBankTransactionCode() ?: code - val amount = amount(acceptedCurrency) + val amount = amount() val txRef = opt("Refs")?.opt("AcctSvcrRef")?.text() val subject = wireTransferSubject() when (kind) { @@ -457,13 +481,13 @@ fun parseTx( } } } + accountTxs.add(AccountTransactions.fromParts(iban, currency, txsInfo)) } } Dialect.maerki_baumann -> { opt("BkToCstmrStmt")?.each("Stmt") { // Camt.053 - opt("Acct") { - // Sanity check on currency and IBAN ? - } + val (iban, currency) = account() + val txsInfo = mutableListOf<TxInfo>() each("Ntry") { if (!isBooked()) return@each val code = bankTransactionCode() @@ -475,7 +499,7 @@ fun parseTx( val txRef = opt("Refs")?.opt("AcctSvcrRef")?.text() val kind = one("CdtDbtInd").enum<Kind>() val code = optBankTransactionCode() ?: code - val (amount, fee) = amountAndFee(acceptedCurrency) + val (amount, fee) = amountAndFee() val subject = wireTransferSubject() if (!code.isReversal() && kind == Kind.CRDT) { val bankId = opt("Refs")?.opt("UETR")?.text() @@ -493,22 +517,14 @@ fun parseTx( } } } + accountTxs.add(AccountTransactions.fromParts(iban, currency, txsInfo)) } } }} - - return txsInfo.mapNotNull { - try { - parseTxLogic(it) - } catch (e: TxErr) { - // TODO: add more info in doc or in log message? - logger.warn("skip incomplete tx: ${e.msg}") - null - } - } + return accountTxs } -private sealed interface TxInfo { +sealed interface TxInfo { // Bank provider ref for debugging val ref: String? // When was this transaction booked @@ -544,54 +560,54 @@ private sealed interface TxInfo { val subject: String?, val creditorPayto: IbanPayto? ): TxInfo -} -private fun parseTxLogic(info: TxInfo): TxNotification { - return when (info) { - is TxInfo.CreditReversal -> { - if (info.id.endToEndId == null) - throw TxErr("missing end-to-end ID for Credit reversal ${info.ref}") - OutgoingReversal( - endToEndId = info.id.endToEndId!!, - msgId = info.id.msgId, - reason = info.reason, - executionTime = info.bookDate - ) - } - is TxInfo.Credit -> { - /*if (info.bankId == null) TODO use the bank ID again when Atruvia's implementation is fixed - throw TxErr("missing bank ID for Credit ${info.ref}")*/ - if (info.subject == null) - throw TxErr("missing subject for Credit ${info.ref}") - if (info.debtorPayto == null) - 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, - ) - } - is TxInfo.Debit -> { - if (info.id.endToEndId == null && info.id.msgId == null) { - throw TxErr("missing end-to-end ID for Debit ${info.ref}") - } else if (info.id.endToEndId != null) { - OutgoingPayment( - amount = info.amount, - endToEndId = info.id.endToEndId, - msgId = info.id.msgId, - executionTime = info.bookDate, - creditorPayto = info.creditorPayto, - subject = info.subject + fun parse(): TxNotification { + return when (this) { + is TxInfo.CreditReversal -> { + if (id.endToEndId == null) + throw TxErr("missing end-to-end ID for Credit reversal $ref") + OutgoingReversal( + endToEndId = id.endToEndId!!, + msgId = id.msgId, + reason = reason, + executionTime = bookDate ) - } else { - OutgoingBatch( - msgId = info.id.msgId!!, - executionTime = info.bookDate, + } + is TxInfo.Credit -> { + /*if (bankId == null) TODO use the bank ID again when Atruvia's implementation is fixed + throw TxErr("missing bank ID for Credit $ref")*/ + if (subject == null) + throw TxErr("missing subject for Credit $ref") + if (debtorPayto == null) + throw TxErr("missing debtor info for Credit $ref") + IncomingPayment( + amount = amount, + creditFee = creditFee, + bankId = bankId, + debtorPayto = debtorPayto, + executionTime = bookDate, + subject = subject, ) } + is TxInfo.Debit -> { + if (id.endToEndId == null && id.msgId == null) { + throw TxErr("missing end-to-end ID for Debit $ref") + } else if (id.endToEndId != null) { + OutgoingPayment( + amount = amount, + endToEndId = id.endToEndId, + msgId = id.msgId, + executionTime = bookDate, + creditorPayto = creditorPayto, + subject = subject + ) + } else { + OutgoingBatch( + msgId = id.msgId!!, + executionTime = bookDate, + ) + } + } } } } diff --git a/nexus/src/test/kotlin/Iso20022Test.kt b/nexus/src/test/kotlin/Iso20022Test.kt @@ -139,219 +139,243 @@ class Iso20022Test { @Test fun postfinance_camt054() { - assertContentEquals( - parseTx(Path("sample/platform/postfinance_camt054.xml").inputStream(), "CHF", Dialect.postfinance), - listOf( - OutgoingPayment( - endToEndId = "ZS1PGNTSV0ZNDFAJBBWWB8015G", - msgId = "ZS1PGNTSV0ZNDFAJBBWWB8015G", - amount = TalerAmount("CHF:3.00"), - subject = null, - executionTime = dateToInstant("2024-01-15"), - creditorPayto = null - ), - IncomingPayment( - bankId = "62e2b511-7313-4ccd-8d40-c9d8e612cd71", - amount = TalerAmount("CHF:10"), - subject = "G1XTY6HGWGMVRM7E6XQ4JHJK561ETFDFTJZ7JVGV543XZCB27YBG", - executionTime = dateToInstant("2023-12-19"), - debtorPayto = ibanPayto("CH7389144832588726658", "Mr Test") - ), - IncomingPayment( - bankId = "62e2b511-7313-4ccd-8d40-c9d8e612cd71", - amount = TalerAmount("CHF:2.53"), - subject = "G1XTY6HGWGMVRM7E6XQ4JHJK561ETFDFTJZ7JVGV543XZCB27YB", - executionTime = dateToInstant("2023-12-19"), - debtorPayto = ibanPayto("CH7389144832588726658", "Mr Test") - ), - OutgoingBatch( - msgId = "ZS1PGNTSV0ZNDFAJBBWWB8015G", - executionTime = dateToInstant("2024-01-15") + assertEquals( + parseTx(Path("sample/platform/postfinance_camt054.xml").inputStream(), Dialect.postfinance), + listOf(AccountTransactions( + iban = "CH9289144596463965762", + currency = "CHF", + txs = listOf( + OutgoingPayment( + endToEndId = "ZS1PGNTSV0ZNDFAJBBWWB8015G", + msgId = "ZS1PGNTSV0ZNDFAJBBWWB8015G", + amount = TalerAmount("CHF:3.00"), + subject = null, + executionTime = dateToInstant("2024-01-15"), + creditorPayto = null + ), + IncomingPayment( + bankId = "62e2b511-7313-4ccd-8d40-c9d8e612cd71", + amount = TalerAmount("CHF:10"), + subject = "G1XTY6HGWGMVRM7E6XQ4JHJK561ETFDFTJZ7JVGV543XZCB27YBG", + executionTime = dateToInstant("2023-12-19"), + debtorPayto = ibanPayto("CH7389144832588726658", "Mr Test") + ), + IncomingPayment( + bankId = "62e2b511-7313-4ccd-8d40-c9d8e612cd71", + amount = TalerAmount("CHF:2.53"), + subject = "G1XTY6HGWGMVRM7E6XQ4JHJK561ETFDFTJZ7JVGV543XZCB27YB", + executionTime = dateToInstant("2023-12-19"), + debtorPayto = ibanPayto("CH7389144832588726658", "Mr Test") + ), + OutgoingBatch( + msgId = "ZS1PGNTSV0ZNDFAJBBWWB8015G", + executionTime = dateToInstant("2024-01-15") + ) ) - ) + )) ) } @Test fun postfinance_camt053() { - assertContentEquals( - parseTx(Path("sample/platform/postfinance_camt053.xml").inputStream(), "CHF", Dialect.postfinance), - listOf( - OutgoingReversal( - endToEndId = "889d1a80-1267-49bd-8fcc-85701a", - msgId = "889d1a80-1267-49bd-8fcc-85701a", - reason = "InconsistenWithEndCustomer 'Identification of end customer is not consistent with associated account number, organisation ID or private ID.' - 'more info here ...'", - executionTime = dateToInstant("2023-11-22") - ), - OutgoingReversal( - endToEndId = "4cc61cc7-6230-49c2-b5e2-b40bbb", - msgId = "4cc61cc7-6230-49c2-b5e2-b40bbb", - reason = "MissingCreditorNameOrAddress 'Specification of the creditor’s name and/or address needed for regulatory requirements is insufficient or missing.' - 'more info here ...'", - executionTime = dateToInstant("2023-11-22") + assertEquals( + parseTx(Path("sample/platform/postfinance_camt053.xml").inputStream(), Dialect.postfinance), + listOf(AccountTransactions( + iban = "CH9289144596463965762", + currency = "CHF", + txs = listOf( + OutgoingReversal( + endToEndId = "889d1a80-1267-49bd-8fcc-85701a", + msgId = "889d1a80-1267-49bd-8fcc-85701a", + reason = "InconsistenWithEndCustomer 'Identification of end customer is not consistent with associated account number, organisation ID or private ID.' - 'more info here ...'", + executionTime = dateToInstant("2023-11-22") + ), + OutgoingReversal( + endToEndId = "4cc61cc7-6230-49c2-b5e2-b40bbb", + msgId = "4cc61cc7-6230-49c2-b5e2-b40bbb", + reason = "MissingCreditorNameOrAddress 'Specification of the creditor’s name and/or address needed for regulatory requirements is insufficient or missing.' - 'more info here ...'", + executionTime = dateToInstant("2023-11-22") + ) ) - ) + )) ) } @Test fun gls_camt052() { - assertContentEquals( - parseTx(Path("sample/platform/gls_camt052.xml").inputStream(), "EUR", Dialect.gls), - listOf( - OutgoingPayment( - endToEndId = "COMPAT_SUCCESS", - msgId = "COMPAT_SUCCESS", - amount = TalerAmount("EUR:2"), - subject = "TestABC123", - executionTime = dateToInstant("2024-04-18"), - creditorPayto = ibanPayto("DE20500105172419259181", "John Smith") - ), - OutgoingReversal( - endToEndId = "8XK8Z7RAX224FGWK832FD40GYC", - reason = "IncorrectAccountNumber 'Format of the account number specified is not correct' - 'IBAN fehlerhaft und ungültig'", - executionTime = dateToInstant("2024-09-05") - ), - IncomingPayment( - bankId = "BYLADEM1WOR-G2910276709458A2", - amount = TalerAmount("EUR:3"), - subject = "Taler FJDQ7W6G7NWX4H9M1MKA12090FRC9K7DA6N0FANDZZFXTR6QHX5G Test.,-", - executionTime = dateToInstant("2024-04-12"), - debtorPayto = ibanPayto("DE84500105177118117964", "John Smith") - ), - OutgoingReversal( - endToEndId = "COMPAT_FAILURE", - reason = "IncorrectAccountNumber 'Format of the account number specified is not correct' - 'IBAN ...'", - executionTime = dateToInstant("2024-04-12") - ), - OutgoingPayment( - endToEndId = "FD622SMXKT5QWSAHDY0H8NYG3G", - msgId = "BATCH_SINGLE_SUCCESS", - amount = TalerAmount("EUR:1.1"), - subject = "single 2024-09-02T14:29:52.875253314Z", - executionTime = dateToInstant("2024-09-02"), - creditorPayto = ibanPayto("DE89500105173198527518", "Grothoff Hans") - ), - OutgoingPayment( - endToEndId = "YF5QBARGQ0MNY0VK59S477VDG4", - msgId = "YF5QBARGQ0MNY0VK59S477VDG4", - amount = TalerAmount("EUR:1.1"), - subject = "Simple tx", - executionTime = dateToInstant("2024-04-18"), - creditorPayto = ibanPayto("DE20500105172419259181", "John Smith") - ), - OutgoingBatch( - msgId = "BATCH_MANY_SUCCESS", - executionTime = dateToInstant("2024-09-20"), - ), - OutgoingPayment( - endToEndId = "KLJJ28S1LVNDK1R2HCHLN884M7EKM5XGM5", - msgId = "BATCH_SINGLE_RETURN", - amount = TalerAmount("EUR:0.42"), - subject = "This should fail because bad iban", - executionTime = dateToInstant("2024-09-23"), - creditorPayto = ibanPayto("DE18500105173385245163", "John Smith") - ), - OutgoingReversal( - endToEndId = "KLJJ28S1LVNDK1R2HCHLN884M7EKM5XGM5", - reason = "IncorrectAccountNumber 'Format of the account number specified is not correct' - 'IBAN fehlerhaft und ungültig'", - executionTime = dateToInstant("2024-09-24") - ), - ) + assertEquals( + parseTx(Path("sample/platform/gls_camt052.xml").inputStream(), Dialect.gls), + listOf(AccountTransactions( + iban = "DE84500105177118117964", + currency = "EUR", + txs = listOf( + OutgoingPayment( + endToEndId = "COMPAT_SUCCESS", + msgId = "COMPAT_SUCCESS", + amount = TalerAmount("EUR:2"), + subject = "TestABC123", + executionTime = dateToInstant("2024-04-18"), + creditorPayto = ibanPayto("DE20500105172419259181", "John Smith") + ), + OutgoingReversal( + endToEndId = "8XK8Z7RAX224FGWK832FD40GYC", + reason = "IncorrectAccountNumber 'Format of the account number specified is not correct' - 'IBAN fehlerhaft und ungültig'", + executionTime = dateToInstant("2024-09-05") + ), + IncomingPayment( + bankId = "BYLADEM1WOR-G2910276709458A2", + amount = TalerAmount("EUR:3"), + subject = "Taler FJDQ7W6G7NWX4H9M1MKA12090FRC9K7DA6N0FANDZZFXTR6QHX5G Test.,-", + executionTime = dateToInstant("2024-04-12"), + debtorPayto = ibanPayto("DE84500105177118117964", "John Smith") + ), + OutgoingReversal( + endToEndId = "COMPAT_FAILURE", + reason = "IncorrectAccountNumber 'Format of the account number specified is not correct' - 'IBAN ...'", + executionTime = dateToInstant("2024-04-12") + ), + OutgoingPayment( + endToEndId = "FD622SMXKT5QWSAHDY0H8NYG3G", + msgId = "BATCH_SINGLE_SUCCESS", + amount = TalerAmount("EUR:1.1"), + subject = "single 2024-09-02T14:29:52.875253314Z", + executionTime = dateToInstant("2024-09-02"), + creditorPayto = ibanPayto("DE89500105173198527518", "Grothoff Hans") + ), + OutgoingPayment( + endToEndId = "YF5QBARGQ0MNY0VK59S477VDG4", + msgId = "YF5QBARGQ0MNY0VK59S477VDG4", + amount = TalerAmount("EUR:1.1"), + subject = "Simple tx", + executionTime = dateToInstant("2024-04-18"), + creditorPayto = ibanPayto("DE20500105172419259181", "John Smith") + ), + OutgoingBatch( + msgId = "BATCH_MANY_SUCCESS", + executionTime = dateToInstant("2024-09-20"), + ), + OutgoingPayment( + endToEndId = "KLJJ28S1LVNDK1R2HCHLN884M7EKM5XGM5", + msgId = "BATCH_SINGLE_RETURN", + amount = TalerAmount("EUR:0.42"), + subject = "This should fail because bad iban", + executionTime = dateToInstant("2024-09-23"), + creditorPayto = ibanPayto("DE18500105173385245163", "John Smith") + ), + OutgoingReversal( + endToEndId = "KLJJ28S1LVNDK1R2HCHLN884M7EKM5XGM5", + reason = "IncorrectAccountNumber 'Format of the account number specified is not correct' - 'IBAN fehlerhaft und ungültig'", + executionTime = dateToInstant("2024-09-24") + ), + ) + )) ) } @Test fun gls_camt053() { - assertContentEquals( - parseTx(Path("sample/platform/gls_camt053.xml").inputStream(), "EUR", Dialect.gls), - listOf( - OutgoingPayment( - endToEndId = "COMPAT_SUCCESS", - msgId = "COMPAT_SUCCESS", - amount = TalerAmount("EUR:2"), - subject = "TestABC123", - executionTime = dateToInstant("2024-04-18"), - creditorPayto = ibanPayto("DE20500105172419259181", "John Smith") - ), - OutgoingReversal( - endToEndId = "KGTDBASWTJ6JM89WXD3Q5KFQC4", - reason = "Retoure aus SEPA Überweisung multi line", - executionTime = dateToInstant("2024-09-04") - ), - OutgoingBatch( - msgId = "BATCH_MANY_PART", - executionTime = dateToInstant("2024-09-04") - ), - IncomingPayment( - bankId = "BYLADEM1WOR-G2910276709458A2", - amount = TalerAmount("EUR:3"), - subject = "Taler FJDQ7W6G7NWX4H9M1MKA12090FRC9K7DA6N0FANDZZFXTR6QHX5G Test.,-", - executionTime = dateToInstant("2024-04-12"), - debtorPayto = ibanPayto("DE84500105177118117964", "John Smith") - ), - OutgoingReversal( - endToEndId = "COMPAT_FAILURE", - reason = "IncorrectAccountNumber 'Format of the account number specified is not correct' - 'IBAN ...'", - executionTime = dateToInstant("2024-04-12") - ), - OutgoingPayment( - endToEndId = "FD622SMXKT5QWSAHDY0H8NYG3G", - msgId = "BATCH_SINGLE_SUCCESS", - amount = TalerAmount("EUR:1.1"), - subject = "single 2024-09-02T14:29:52.875253314Z", - executionTime = dateToInstant("2024-09-02"), - creditorPayto = ibanPayto("DE89500105173198527518", "Grothoff Hans") - ), - OutgoingPayment( - endToEndId = "YF5QBARGQ0MNY0VK59S477VDG4", - msgId = "YF5QBARGQ0MNY0VK59S477VDG4", - amount = TalerAmount("EUR:1.1"), - subject = "Simple tx", - executionTime = dateToInstant("2024-04-18"), - creditorPayto = ibanPayto("DE20500105172419259181", "John Smith") - ), + assertEquals( + parseTx(Path("sample/platform/gls_camt053.xml").inputStream(), Dialect.gls), + listOf(AccountTransactions( + iban = "DE84500105177118117964", + currency = "EUR", + txs = listOf( + OutgoingPayment( + endToEndId = "COMPAT_SUCCESS", + msgId = "COMPAT_SUCCESS", + amount = TalerAmount("EUR:2"), + subject = "TestABC123", + executionTime = dateToInstant("2024-04-18"), + creditorPayto = ibanPayto("DE20500105172419259181", "John Smith") + ), + OutgoingReversal( + endToEndId = "KGTDBASWTJ6JM89WXD3Q5KFQC4", + reason = "Retoure aus SEPA Überweisung multi line", + executionTime = dateToInstant("2024-09-04") + ), + OutgoingBatch( + msgId = "BATCH_MANY_PART", + executionTime = dateToInstant("2024-09-04") + ), + IncomingPayment( + bankId = "BYLADEM1WOR-G2910276709458A2", + amount = TalerAmount("EUR:3"), + subject = "Taler FJDQ7W6G7NWX4H9M1MKA12090FRC9K7DA6N0FANDZZFXTR6QHX5G Test.,-", + executionTime = dateToInstant("2024-04-12"), + debtorPayto = ibanPayto("DE84500105177118117964", "John Smith") + ), + OutgoingReversal( + endToEndId = "COMPAT_FAILURE", + reason = "IncorrectAccountNumber 'Format of the account number specified is not correct' - 'IBAN ...'", + executionTime = dateToInstant("2024-04-12") + ), + OutgoingPayment( + endToEndId = "FD622SMXKT5QWSAHDY0H8NYG3G", + msgId = "BATCH_SINGLE_SUCCESS", + amount = TalerAmount("EUR:1.1"), + subject = "single 2024-09-02T14:29:52.875253314Z", + executionTime = dateToInstant("2024-09-02"), + creditorPayto = ibanPayto("DE89500105173198527518", "Grothoff Hans") + ), + OutgoingPayment( + endToEndId = "YF5QBARGQ0MNY0VK59S477VDG4", + msgId = "YF5QBARGQ0MNY0VK59S477VDG4", + amount = TalerAmount("EUR:1.1"), + subject = "Simple tx", + executionTime = dateToInstant("2024-04-18"), + creditorPayto = ibanPayto("DE20500105172419259181", "John Smith") + ), + )) ) ) } @Test fun gls_camt054() { - assertContentEquals( - parseTx(Path("sample/platform/gls_camt054.xml").inputStream(), "EUR", Dialect.gls), - listOf( - IncomingPayment( - bankId = null, //"IS11PGENODEFF2DA8899900378806", - amount = TalerAmount("EUR:2.5"), - subject = "Test ICT", - executionTime = dateToInstant("2024-05-05"), - debtorPayto = ibanPayto("DE84500105177118117964", "Mr Test") + assertEquals( + parseTx(Path("sample/platform/gls_camt054.xml").inputStream(), Dialect.gls), + listOf(AccountTransactions( + iban = "DE84500105177118117964", + currency = "EUR", + txs = listOf<TxNotification>( + IncomingPayment( + bankId = null, //"IS11PGENODEFF2DA8899900378806", + amount = TalerAmount("EUR:2.5"), + subject = "Test ICT", + executionTime = dateToInstant("2024-05-05"), + debtorPayto = ibanPayto("DE84500105177118117964", "Mr Test") + ) ) - ) + )) ) } @Test fun maerki_baumann_camt053() { - assertContentEquals( - parseTx(Path("sample/platform/maerki_baumann_camt053.xml").inputStream(), "CHF", Dialect.maerki_baumann), - listOf( - IncomingPayment( - bankId = "adbe4a5a-6cea-4263-b259-8ab964561a32", - 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:1"), - creditFee = TalerAmount("CHF:0.2"), - subject = "Random subject", - executionTime = dateToInstant("2024-11-04"), - debtorPayto = ibanPayto("CH7389144832588726658", "Mr Test") + assertEquals( + parseTx(Path("sample/platform/maerki_baumann_camt053.xml").inputStream(), Dialect.maerki_baumann), + listOf(AccountTransactions( + iban = "CH7389144832588726658", + currency = "CHF", + txs = listOf<TxNotification>( + IncomingPayment( + bankId = "adbe4a5a-6cea-4263-b259-8ab964561a32", + 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:1"), + creditFee = TalerAmount("CHF:0.2"), + subject = "Random subject", + executionTime = dateToInstant("2024-11-04"), + debtorPayto = ibanPayto("CH7389144832588726658", "Mr Test") + ) ) - ) + )) ) } diff --git a/testbench/src/test/kotlin/Iso20022Test.kt b/testbench/src/test/kotlin/Iso20022Test.kt @@ -42,7 +42,7 @@ class Iso20022Test { } else if (name.contains("pain.002") || name.contains("pain002") ) { parseCustomerPaymentStatusReport(content) } else { - parseTx(content, "CHF", Dialect.postfinance) + parseTx(content, Dialect.postfinance) } } @@ -55,7 +55,7 @@ class Iso20022Test { } else if (name.contains("pain.002") || name.contains("pain002") ) { parseCustomerPaymentStatusReport(content) } else { - parseTx(content, "CHF", Dialect.postfinance) + parseTx(content, Dialect.postfinance) } } } @@ -121,7 +121,7 @@ class Iso20022Test { } else if ( !name.contains("camt.052") && !name.contains("_C52_") && !name.contains("_Z01_") ) { - parseTx(content, currency, dialect) + parseTx(content, dialect) } } }