commit 6ebe691de905c96b5bb30649b12822b5d606191a
parent 2576369aa04718c91c732dfa4e37da5006e5fa50
Author: Antoine A <>
Date: Thu, 21 Nov 2024 16:56:23 +0100
nexus: check account iban and currency
Diffstat:
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)
}
}
}