libeufin

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

commit f801adf2769c29b65a67d6a9f626617c597bd611
parent 3e3912bfc1e7af67756046379bc62ba4e292e374
Author: Florian Dold <florian.dold@gmail.com>
Date:   Tue,  7 Jul 2020 14:41:05 +0530

mandatory, stringified bank transaction code, even for GBIC rulebook

Diffstat:
Anexus/src/main/kotlin/tech/libeufin/nexus/iso20022/GbicRules.kt | 286+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Iso20022.kt | 55++++++++++++++++++++++++++++++++-----------------------
Mnexus/src/test/kotlin/Iso20022Test.kt | 6+-----
3 files changed, 319 insertions(+), 28 deletions(-)

diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/GbicRules.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/GbicRules.kt @@ -0,0 +1,285 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2020 Taler Systems S.A. + * + * LibEuFin is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation; either version 3, or + * (at your option) any later version. + * + * LibEuFin 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 Affero General + * Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with LibEuFin; see the file COPYING. If not, see + * <http://www.gnu.org/licenses/> + */ + +package tech.libeufin.nexus.iso20022 + +/** + * Extra rules for German Banking Industry Committee (GBIC) for ISO 20022. + */ +object GbicRules { + /** + * Map credit/debit indicator and the German GVC code to a ISO 20022 bank transaction code. + * When multiple alternatives are available, we always choose the least specific one. + * + * Mapping taken from "Anhang1 zu Anlage 3 - Datenformatstandards-Version 3.3 Final Version-2019-04-11" + */ + @Suppress("SpellCheckingInspection") + fun getBtcFromGvc(c: CreditDebitIndicator, s: String): String { + val cd = when (c) { + CreditDebitIndicator.CRDT -> "C" + CreditDebitIndicator.DBIT -> "D" + } + return when ("${cd}-${s}") { + "D-006" -> "PMNT-CCRD-POSC" + "C-058" -> "PMNT-RCDT-FICT" + "C-072" -> "PMNT-DRFT-STLR" + "D-073" -> "PMNT-DRFT-STAM" + "C-079" -> "PMNT-MCOP-OTHR" + "D-079" -> "PMNT-MDOP-OTHR" + "C-082" -> "PMNT-CNTR-CDPT" + "D-083" -> "PMNT-CNTR-CWDL" + "D-084" -> "PMNT-RDDT-OODD" + "D-087" -> "PMNT-ICDT-SDVA" + "C-088" -> "PMNT-RCDT-SDVA" + "C-093" -> "PMNT-DRFT-DDFT" + "C-095" -> "TRAD-GUAR-OTHR" + "D-095" -> "TRAD-GUAR-OTHR" + "C-098" -> "PMNT-MCRD-SMCD" + "D-101" -> "PMNT-ICHQ-CCHQ" + "D-102" -> "PMNT-ICHQ-ORCQ" + "D-103" -> "PMNT-ICHQ-CCHQ" + "D-104" -> "PMNT-RDDT-BBDD" + "D-105" -> "PMNT-RDDT-ESDD" + // Alternatives: + // "D-106" -> "PMNT-CCRD-CWDL" + // "D-106" -> "PMNT-CCRD-SMRT" + // "D-106" -> "PMNT-CCRD-POSD" + // "D-106" -> "PMNT-MCRD-CHRG" + "D-106" -> "PMNT-CCRD-OTHR" + "D-107" -> "PMNT-CCRD-OTHR" + "D-108" -> "PMNT-IDDT-UPDD" + "D-109" -> "PMNT-IDDT-UPDD" + "D-110" -> "PMNT-MCRD-UPCT" + "D-111" -> "PMNT-ICHQ-UPCQ" + "D-112" -> "PMNT-ICHQ-OTHR" + "C-112" -> "PMNT-RCHQ-OTHR" + "D-116" -> "PMNT-ICDT-ESCT" + "D-118" -> "PMNT-IRCT-ESCT" + "D-117" -> "PMNT-ICDT-STDO" + "D-119" -> "PMNT-ICDT-ESCT" + "D-122" -> "PMNT-ICHQ-CCHQ" + "C-152" -> "PMNT-RCDT-STDO" + "C-153" -> "PMNT-RCDT-SALA" + "C-154" -> "PMNT-RCDT-ESCT" + "C-155" -> "PMNT-RCDT-ESCT" + "C-156" -> "PMNT-RCDT-ESCT" + "C-157" -> "PMNT-RRCT-SALA" + "C-159" -> "PMNT-ICDT-RRTN" + "D-159" -> "PMNT-RCDT-RRTN" + "C-160" -> "PMNT-IRCT-RRTN" + "D-160" -> "PMNT-RRCT-RRTN" + "C-161" -> "PMNT-RRCT-ESCT" + "C-162" -> "PMNT-RRCT-ESCT" + "C-163" -> "PMNT-RRCT-ESCT" + "C-164" -> "PMNT-RRCT-ESCT" + "C-165" -> "PMNT-RRCT-ESCT" + "C-166" -> "PMNT-RCDT-ESCT" + "C-167" -> "PMNT-RCDT-ESCT" + "C-168" -> "PMNT-RRCT-ESCT" + "C-169" -> "PMNT-RCDT-ESCT" + "C-170" -> "PMNT-RCHQ-URCQ" + "C-171" -> "PMNT-IDDT-ESDD" + "C-174" -> "PMNT-IDDT-BBDD" + "D-177" -> "PMNT-ICDT-ESCT" + "C-181" -> "PMNT-RDDT-UPDD" + "C-182" -> "PMNT-CCRD-RIMB" + "C-183" -> "PMNT-RCHQ-UPCQ" + "C-184" -> "PMNT-RDDT-UPDD" + "D-185" -> "PMNT-ICHQ-CCHQ" + "D-188" -> "PMNT-IRCT-ESCT" + "C-189" -> "PMNT-RRCT-ESCT" + "D-190" -> "PMNT-CCRD-OTHR" + "D-191" -> "PMNT-ICDT-ESCT" + "C-192" -> "PMNT-IDDT-ESDD" + "D-193" -> "PMNT-IDDT-RCDD" + "C-194" -> "PMNT-RCDT-ESCT" + "D-195" -> "PMNT-RDDT-ESDD" + "C-196" -> "PMNT-IDDT-BBDD" + "D-197" -> "PMNT-RDDT-BBDD" + "C-198" -> "PMNT-MCRD-POSP" + "D-199" -> "PMNT-MCRD-DAJT" + "D-201" -> "PMNT-ICDT-XBCT" + "C-202" -> "PMNT-RCDT-XBCT" + "C-203" -> "TRAD-CLNC-OTHR" + "D-203" -> "TRAD-CLNC-OTHR" + "C-204" -> "TRAD-DCCT-OTHR" + "D-204" -> "TRAD-DCCT-OTHR" + "C-205" -> "TRAD-GUAR-OTHR" + "D-205" -> "TRAD-GUAR-OTHR" + "C-206" -> "PMNT-RCDT-XBCT" + "C-208" -> "TRAD-MCOP-OTHR" + "D-208" -> "TRAD-MDOP-OTHR" + "D-209" -> "PMNT-ICHQ-XBCQ" + "D-210" -> "PMNT-ICDT-XBCT" + "C-211" -> "PMNT-RCDT-XBCT" + "D-212" -> "PMNT-ICDT-XBST" + "D-213" -> "PMNT-RDDT-XBDD" + "D-214" -> "TRAD-DOCC-OTHR" + "C-215" -> "TRAD-DOCC-OTHR" + "D-216" -> "PMNT-DRFT-STAM" + "C-217" -> "PMNT-DRFT-STAM" + // Alternative: + // "C-217" -> "PMNT-DRFT-STLR" + "D-218" -> "TRAD-DCCT-OTHR" + "C-219" -> "TRAD-DCCT-OTHR" + "C-220" -> "PMNT-RCHQ-XRCQ" + "C-221" -> "PMNT-RCHQ-XBCQ" + "D-222" -> "PMNT-ICHQ-XBCQ" + "D-223" -> "PMNT-ICHQ-XBCQ" + "C-224" -> "PMNT-CNTR-FCDP" + "D-225" -> "PMNT-CNTR-FCWD" + "C-301" -> "SECU-CUST-REDM" + "C-302" -> "SECU-CUST-DVCA" + "C-303" -> "SECU-SETT-TRAD" + "D-303" -> "SECU-SETT-TRAD" + "C-304" -> "SECU-OTHR-OTHR" + "D-304" -> "SECU-OTHR-OTHR" + "D-305" -> "SECU-OTHR-OTHR" + "D-306" -> "SECU-OTHR-OTHR" + "D-307" -> "SECU-SETT-SUBS" + "C-308" -> "SECU-CORP-EXWA" + "D-308" -> "SECU-CORP-EXWA" + "C-309" -> "SECU-CORP-BONU" + "D-309" -> "SECU-CORP-BONU" + "C-310" -> "SECU-MCOP-OTHR" + "D-310" -> "SECU-MDOP-OTHR" + "C-311" -> "DERV-OTHR-OTHR" + "D-311" -> "DERV-OTHR-OTHR" + "D-320" -> "SECU-CASH-TRFE" + "D-321" -> "SECU-CUST-CHRG" + "C-321" -> "SECU-CUST-CHRG" + "C-330" -> "SECU-CUST-INTR" + "C-340" -> "SECU-CUST-REDM" + "C-399" -> "ACMT-ACOP-PSTE" + "D-399" -> "ACMT-ADOP-PSTE" + "C-401" -> "FORX-SPOT-OTHR" + "D-401" -> "FORX-SPOT-OTHR" + "C-402" -> "FORX-FWRD-OTHR" + "D-402" -> "FORX-FWRD-OTHR" + "D-403" -> "FORX-MDOP-OTHR" + "D-404" -> "FORX-OTHR-OTHR" + "D-405" -> "FORX-OTHR-OTHR" + "C-406" -> "FORX-SPOT-OTHR" + "D-406" -> "FORX-SPOT-OTHR" + "C-407" -> "FORX-OTHR-OTHR" + "D-407" -> "FORX-OTHR-OTHR" + "C-408" -> "FORX-OTHR-OTHR" + "C-409" -> "FORX-OTHR-OTHR" + "D-411" -> "FORX-SPOT-OTHR" + "C-412" -> "FORX-SPOT-OTHR" + "D-413" -> "FORX-FWRD-OTHR" + "C-414" -> "FORX-FWRD-OTHR" + "D-415" -> "FORX-OTHR-OTHR" + "C-416" -> "FORX-OTHR-OTHR" + "D-417" -> "FORX-OTHR-OTHR" + "C-418" -> "FORX-OTHR-OTHR" + "D-419" -> "FORX-OTHR-OTHR" + "C-420" -> "FORX-OTHR-OTHR" + "C-421" -> "FORX-OTHR-OTHR" + "D-421" -> "FORX-OTHR-OTHR" + "C-422" -> "FORX-SWAP-OTHR" + "D-422" -> "FORX-SWAP-OTHR" + "C-423" -> "PMET-SPOT-OTHR" + "D-424" -> "PMET-SPOT-OTHR" + "D-601" -> "LDAS-FTLN-OTHR" + "C-602" -> "LDAS-FTLN-OTHR" + "D-603" -> "LDAS-FTLN-PPAY" + "D-604" -> "LDAS-MDOP-INTR" + "D-605" -> "LDAS-MDOP-INTR" + "C-606" -> "LDAS-FTLN-DDWN" + "D-606" -> "LDAS-FTLN-DDWN" + "D-607" -> "LDAS-OTHR-OTHR" + "D-801" -> "ACMT-MDOP-CHRG" + "D-802" -> "ACMT-MDOP-CHRG" + "D-803" -> "SECU-CUST-CHRG" + "D-804" -> "PMNT-MDOP-CHRG" + "C-804" -> "PMNT-MCOP-CHRG" + "C-805" -> "ACMT-OPCL-ACCC" + "D-805" -> "ACMT-OPCL-ACCC" + "C-806" -> "ACMT-MCOP-CHRG" + "D-806" -> "ACMT-MDOP-CHRG" + "C-807" -> "ACMT-MCOP-CHRG" + "D-807" -> "ACMT-MDOP-CHRG" + "C-808" -> "PMNT-MCOP-CHRG" + "D-808" -> "PMNT-MDOP-CHRG" + // Alternatives: + // "C-808" -> "TRAD-MCOP-CHRG" + // "D-808" -> "TRAD-MDOP-CHRG" + // "C-808" -> "ACMT-MCOP-CHRG" + // "D-808" -> "ACMT-MDOP-CHRG" + "D-809" -> "PMNT-MDOP-COMM" + "C-809" -> "PMNT-MCOP-COMM" + // Alternatives: + // "D-809" -> "ACMT-MDOP-COMM" + // "C-809" -> "ACMT-MCOP-COMM" + // "D-809" -> "TRAD-MDOP-COMM" + // "C-809" -> "TRAD-MCOP-COMM" + // "D-809" -> "LDAS-MDOP-COMM" + // "C-809" -> "LDAS-MCOP-COMM" + "D-810" -> "ACMT-MDOP-CHRG" + "C-810" -> "ACMT-MCOP-CHRG" + "D-811" -> "LDAS-MDOP-CHRG" + "C-811" -> "LDAS-MCOP-CHRG" + "D-812" -> "LDAS-MDOP-INTR" + "C-812" -> "LDAS-MCOP-INTR" + "D-813" -> "LDAS-MDOP-INTR" + "C-814" -> "ACMT-MCOP-INTR" + "D-814" -> "ACMT-MDOP-INTR" + "C-815" -> "ACMT-OTHR-OTHR" + "C-816" -> "ACMT-OTHR-OTHR" + "C-817" -> "ACMT-OTHR-OTHR" + "D-818" -> "PMNT-OTHR-OTHR" + "C-819" -> "PMNT-OTHR-OTHR" + "C-820" -> "PMNT-RCDT-BOOK" + "D-820" -> "PMNT-ICDT-BOOK" + "D-821" -> "PMNT-OTHR-OTHR" + "C-822" -> "PMNT-OTHR-OTHR" + "C-823" -> "LDAS-FTDP-RPMT" + "D-823" -> "LDAS-FTDP-DPST" + "D-824" -> "LDAS-OTHR-OTHR" + "D-825" -> "LDAS-OTHR-OTHR" + "D-826" -> "LDAS-OTHR-OTHR" + "D-827" -> "LDAS-OTHR-OTHR" + "C-828" -> "LDAS-FTDP-RPMT" + "D-828" -> "LDAS-FTDP-DPST" + "C-829" -> "LDAS-FTDP-RPMT" + "D-829" -> "LDAS-FTDP-DPST" + "C-830" -> "LDAS-FTDP-INTR" + "D-831" -> "XTND-NTAV-NTAV" + "D-832" -> "LDAS-OTHR-OTHR" + "C-833" -> "CAMT-ACCB-OTHR" + "D-833" -> "CAMT-ACCB-OTHR" + "C-834" -> "CAMT-ACCB-OTHR" + "D-834" -> "CAMT-ACCB-OTHR" + "C-835" -> "XTND-NTAV-NTAV" + "D-835" -> "XTND-NTAV-NTAV" + "C-836" -> "ACMT-MCOP-ADJT" + "D-836" -> "ACMT-MDOP-ADJT" + "D-837" -> "ACMT-MDOP-TAXE" + "C-888" -> "XTND-NTAV-NTAV" + "D-888" -> "XTND-NTAV-NTAV" + "C-899" -> "ACMT-ACOP-PSTE" + "D-899" -> "ACMT-ADOP-PSTE" + "D-997" -> "XTND-NTAV-NTAV" + "C-999" -> "XTND-NTAV-NTAV" + "D-999" -> "XTND-NTAV-NTAV" + else -> "XTND-NTAV-NTAV" + } + } +} +\ No newline at end of file diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Iso20022.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Iso20022.kt @@ -186,7 +186,7 @@ data class TransactionInfo( @JsonInclude(JsonInclude.Include.NON_NULL) data class ReturnInfo( - val originalBankTransactionCode: BankTransactionCode?, + val originalBankTransactionCode: String?, val originator: PartyIdentification?, val reason: String?, val proprietaryReason: String?, @@ -213,7 +213,7 @@ data class CamtBankAccountEntry( * in more detail */ - val bankTransactionCode: BankTransactionCode, + val bankTransactionCode: String, /** * Transaction details, if this entry contains a single transaction. */ @@ -224,14 +224,6 @@ data class CamtBankAccountEntry( val entryRef: String? ) -@JsonInclude(JsonInclude.Include.NON_NULL) -data class BankTransactionCode( - val domain: String?, - val family: String?, - val subfamily: String?, - val proprietaryCode: String?, - val proprietaryIssuer: String? -) class CamtParsingError(msg: String) : Exception(msg) @@ -587,7 +579,11 @@ private fun XmlElementDestructor.extractTransactionInfos( returnInfo = maybeUniqueChildNamed("RtrInf") { ReturnInfo( originalBankTransactionCode = maybeUniqueChildNamed("OrgnlBkTxCd") { - extractInnerBkTxCd() + extractInnerBkTxCd( + when (creditDebitIndicator) { + CreditDebitIndicator.DBIT -> CreditDebitIndicator.CRDT + CreditDebitIndicator.CRDT -> CreditDebitIndicator.DBIT + }) }, originator = maybeUniqueChildNamed("Orgtr") { extractParty() }, reason = maybeUniqueChildNamed("Rsn") { maybeUniqueChildNamed("Cd") { it.textContent } }, @@ -600,26 +596,39 @@ private fun XmlElementDestructor.extractTransactionInfos( } } -private fun XmlElementDestructor.extractInnerBkTxCd(): BankTransactionCode { - return BankTransactionCode( - domain = maybeUniqueChildNamed("Domn") { maybeUniqueChildNamed("Cd") { it.textContent } }, - family = maybeUniqueChildNamed("Domn") { +private fun XmlElementDestructor.extractInnerBkTxCd(creditDebitIndicator: CreditDebitIndicator): String { + + val domain = maybeUniqueChildNamed("Domn") { maybeUniqueChildNamed("Cd") { it.textContent } } + val family = maybeUniqueChildNamed("Domn") { maybeUniqueChildNamed("Fmly") { maybeUniqueChildNamed("Cd") { it.textContent } } - }, - subfamily = maybeUniqueChildNamed("Domn") { + } + val subfamily = maybeUniqueChildNamed("Domn") { maybeUniqueChildNamed("Fmly") { maybeUniqueChildNamed("SubFmlyCd") { it.textContent } } - }, - proprietaryCode = maybeUniqueChildNamed("Prtry") { + } + val proprietaryCode = maybeUniqueChildNamed("Prtry") { maybeUniqueChildNamed("Cd") { it.textContent } - }, - proprietaryIssuer = maybeUniqueChildNamed("Prtry") { + } + val proprietaryIssuer = maybeUniqueChildNamed("Prtry") { maybeUniqueChildNamed("Issr") { it.textContent } } - ) + + if (domain != null && family != null && subfamily != null) { + return "$domain-$family-$subfamily" + } + if (proprietaryIssuer == "DK" && proprietaryCode != null) { + val components = proprietaryCode.split("+") + if (components.size == 1) { + return GbicRules.getBtcFromGvc(creditDebitIndicator, components[0]) + } else { + return GbicRules.getBtcFromGvc(creditDebitIndicator, components[1]) + } + } + // FIXME: log/raise this somewhere? + return "XTND-NTAV-NTAV" } private fun XmlElementDestructor.extractInnerTransactions(): CamtReport { @@ -633,7 +642,7 @@ private fun XmlElementDestructor.extractInnerTransactions(): CamtReport { CreditDebitIndicator.valueOf(it) } val btc = requireUniqueChildNamed("BkTxCd") { - extractInnerBkTxCd() + extractInnerBkTxCd(creditDebitIndicator) } val acctSvcrRef = maybeUniqueChildNamed("AcctSvcrRef") { it.textContent } val entryRef = maybeUniqueChildNamed("NtryRef") { it.textContent } diff --git a/nexus/src/test/kotlin/Iso20022Test.kt b/nexus/src/test/kotlin/Iso20022Test.kt @@ -34,11 +34,7 @@ class Iso20022Test { assertEquals(EntryStatus.BOOK, r.reports[0].entries[0].status) assertEquals(null, r.reports[0].entries[0].entryRef) assertEquals("acctsvcrref-001", r.reports[0].entries[0].accountServicerRef) - assertEquals("PMNT", r.reports[0].entries[0].bankTransactionCode.domain) - assertEquals("RCDT", r.reports[0].entries[0].bankTransactionCode.family) - assertEquals("ESCT", r.reports[0].entries[0].bankTransactionCode.subfamily) - assertEquals("166", r.reports[0].entries[0].bankTransactionCode.proprietaryCode) - assertEquals("DK", r.reports[0].entries[0].bankTransactionCode.proprietaryIssuer) + assertEquals("PMNT-RCDT-ESCT", r.reports[0].entries[0].bankTransactionCode) assertEquals(1, r.reports[0].entries[0].transactionInfos.size) assertEquals("EUR", r.reports[0].entries[0].transactionInfos[0].amount.currency) assertTrue(BigDecimal(100).compareTo(r.reports[0].entries[0].transactionInfos[0].amount.value) == 0)