libeufin

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

commit cbb687136fbefc4871a996b6b14a8d4d0528b533
parent 5bc40ce0dab5114766d9a4ae227c786ed381889c
Author: Marcello Stanisci <stanisci.m@gmail.com>
Date:   Wed, 15 Jan 2020 11:03:22 +0100

Including more details into C52 and C53 responses.

Diffstat:
Msandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt | 245+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Msandbox/src/test/kotlin/XmlCombinatorsTest.kt | 32++++++++++++++++++++++++++++++++
Mutil/src/main/kotlin/XmlCombinators.kt | 11++++++-----
3 files changed, 265 insertions(+), 23 deletions(-)

diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt @@ -41,6 +41,8 @@ import tech.libeufin.util.CryptoUtil import tech.libeufin.util.EbicsOrderUtil import tech.libeufin.util.XMLUtil import tech.libeufin.util.* +import java.awt.List +import java.math.BigDecimal import java.security.interfaces.RSAPrivateCrtKey import java.util.* import java.util.zip.DeflaterInputStream @@ -111,7 +113,153 @@ private suspend fun ApplicationCall.respondEbicsKeyManagement( respondText(text, ContentType.Application.Xml, HttpStatusCode.OK) } -/* intra-day account traffic */ +/** + * This function populates the "history" content of both a CAMT.052 and CAMT.053. + * FIXME: There might be needed some filter to exclude non-booked entries from a + * query-set (as CAMT.053 should do, see #6046) + * + * @param cusromerId unique identifier of a bank's customer: not EBICS-relevant. + * @param request the EBICS request carrying a "history" message. + * @param base the sub-node where to start attaching history elements. + * + */ +private fun iterHistory(customerId: Int, request: EbicsRequest, base: XmlElementBuilder) { + + extractHistoryForEach( + customerId, + try { + (request.header.static.orderDetails?.orderParams as EbicsRequest.StandardOrderParams).dateRange?.start.toString() + } catch (e: Exception) { + getGregorianDate().toString() + }, + try { + (request.header.static.orderDetails?.orderParams as EbicsRequest.StandardOrderParams).dateRange?.end.toString() + } catch (e: Exception) { + getGregorianDate().toString() + } + ) { + + base.element("Ntry") { + /* FIXME: one entry in an account history. + * NOTE: this element can appear from 0 to unbounded number of times. + * */ + element("Amt") { + /* FIXME: amount of this entry */ + } + element("CdtDbtInd") { + /* FIXME: as above, whether the entry witnesses debit or credit */ + } + element("Sts") { + /* FIXME: status of the entry (see 2.4.2.15.5 from the ISO20022 reference document.) + * + * From the original text: + * "Status of an entry on the books of the account servicer" + */ + } + element("BkTxCd") { + /* FIXME: Bank-transaction-code, see section 2.4.2.15.10. + * From the original text: + * + * "Set of elements used to fully identify the type of underlying + * transaction resulting in an entry" + */ + } + element("BookgDt") { + /** + * FIXME, Booking-date: when the entry was posted on the books + * of the account servicer; do not necessarily implies that assets + * become available. NOTE: this element is optional. + */ + } + element("ValDt") { + /** + * FIXME, Value-date: when the asset corresponding to one entry + * becomes available (or unavailable, in case of debit type entry) + * to the account owner. NOTE: this element is optional. + */ + } + } + } +} + +/** + * This function populates the content under "Rpt" or "Stmt" nodes, + * therefore is valid for generating both C52 and C53 responses. + * + * @param base the sub-node where starting to append content. + */ +private fun balance(base: XmlElementBuilder) { + + base.element("Id") { + // unique identificator for a report. + text("id under report") + } + base.element("Acct") { + // mandatory account identifier + text("account identifier") + } + base.element("Bal") { + element("Tp") { + // FIXME: type + element("CdOrPrTry") { + /** + * FIXME: code-or-proprietary + * This section specifies the 'balance type', either in a + * 'coded' format or in a proprietary one. + */ + } + } + element("Amt") { + /** + * FIXME: Amount + */ + attribute("Ccy", "EUR") + BigDecimal("1.00") + } + element("CdtDbtInd") { + /** + * FIXME: credit-debit-indicator + * Indicates whether the balance is a 'credit' ("CRDT") or a 'debit' ("DBIT") balance. + */ + } + element("Dt") { + /** + * FIXME: date, in YYYY-MM-DD format + */ + } + } +} + +/** + * Builds CAMT response. + * + * @param history the list of all the history elements + * @param type 52 or 53. + */ +private fun constructCamtResponse(type: Int, customerId: Int, request: EbicsRequest): String { + + val camt = constructXml(indent = true) { + + namespace("foo", "bar") // FIXME: set right namespace! + root("foo:BkToCstmrAcctRpt") { + element("GrpHdr") { + element("MsgId") { + // unique identifier for a message + text("id under group header") + } + } + + element(if (type == 52) "Rpt" else "Stmt") { + + balance(this) + iterHistory(customerId, request, this) + } + } + } + + return camt +} + private fun ApplicationCall.handleEbicsC52(header: EbicsRequest.Header): ByteArray { val userId = header.static.userID!! @@ -124,39 +272,100 @@ private fun ApplicationCall.handleEbicsC52(header: EbicsRequest.Header): ByteArr } }.firstOrNull() ?: throw Exception("Unknown subscriber") - val history = extractHistoryForEach( - subscriber.bankCustomer.id.value, - try { - (op as EbicsRequest.StandardOrderParams).dateRange?.start.toString() - } catch (e: Exception) { - getGregorianDate().toString() - }, - try { - (op as EbicsRequest.StandardOrderParams).dateRange?.end.toString() - } catch (e: Exception) { - getGregorianDate().toString() - } - ) { println(it) } + // call history builder here val ret = constructXml(indent = true) { namespace("foo", "bar") root("foo:BkToCstmrAcctRpt") { element("GrpHdr") { - element("MsgId") { + // unique identifier for a message text("id under group header") } - element("CreDtTm") { - text("now") - } } + /* + * NOTE: Rpt elements can be 1 or more + */ element("Rpt") { element("Id") { + // unique identificator for a report. text("id under report") } element("Acct") { + // mandatory account identifier text("account identifier") } + element("Bal") { + element("Tp") { + // FIXME: type + element("CdOrPrTry") { + /** + * FIXME: code-or-proprietary + * This section specifies the 'balance type', either in a + * 'coded' format or in a proprietary one. + */ + } + } + element("Amt") { + /** + * FIXME: Amount + */ + attribute("Ccy", "EUR") + BigDecimal("1.00") + } + element("CdtDbtInd") { + /** + * FIXME: credit-debit-indicator + * Indicates whether the balance is a 'credit' ("CRDT") or a 'debit' ("DBIT") balance. + */ + } + element("Dt") { + /** + * FIXME: date, in YYYY-MM-DD format + */ + } + } + element("Ntry") { + /* FIXME: one statement in an account history. + * NOTE: this element can appear from 0 to unbounded number of times. + * */ + element("Amt") { + /* FIXME: amount of this entry */ + } + element("CdtDbtInd") { + /* FIXME: as above, whether the entry witnesses debit or credit */ + } + element("Sts") { + /* FIXME: status of the entry (see 2.4.2.15.5 from the ISO20022 reference document.) + * + * From the original text: + * "Status of an entry on the books of the account servicer" + */ + } + element("BkTxCd") { + /* FIXME: Bank-transaction-code, see section 2.4.2.15.10. + + * From the original text: + * + * "Set of elements used to fully identify the type of underlying + * transaction resulting in an entry" + */ + } + element("BookgDt") { + /** + * FIXME, Booking-date: when the entry was posted on the books + * of the account servicer; do not necessarily implies that assets + * become available. NOTE: this element is optional. + */ + } + element("ValDt") { + /** + * FIXME, Value-date: when the asset corresponding to one entry + * becomes available (or unavailable, in case of debit type entry) + * to the account owner. NOTE: this element is optional. + */ + } + } } } } diff --git a/sandbox/src/test/kotlin/XmlCombinatorsTest.kt b/sandbox/src/test/kotlin/XmlCombinatorsTest.kt @@ -20,11 +20,43 @@ package tech.libeufin.sandbox import org.junit.Test +import tech.libeufin.util.XmlElementBuilder import tech.libeufin.util.constructXml class XmlCombinatorsTest { @Test + fun testWithModularity() { + fun module(base: XmlElementBuilder) { + base.element("module") + } + val s = constructXml { + root("root") { + module(this) + } + } + println(s) + } + + @Test + fun testWithIterable() { + val s = constructXml(indent = true) { + namespace("iter", "able") + root("iterable") { + element("endOfDocument") { + for (i in 1..10) + element("$i") { + element("$i$i") { + text("$i$i$i") + } + } + } + } + } + println(s) + } + + @Test fun testBasicXmlBuilding() { val s = constructXml(indent = true) { namespace("ebics", "urn:org:ebics:H004") diff --git a/util/src/main/kotlin/XmlCombinators.kt b/util/src/main/kotlin/XmlCombinators.kt @@ -7,17 +7,18 @@ import javax.xml.stream.XMLStreamWriter class XmlElementBuilder(val w: XMLStreamWriter) { + /** + * First consumes all the path's components, and _then_ starts applying f. + */ fun element(path: MutableList<String>, f: XmlElementBuilder.() -> Unit = {}) { - + /* the wanted path got constructed, go on with f's logic now. */ if (path.isEmpty()) { - f(this) + f() return } - w.writeStartElement(path.removeAt(0)) this.element(path, f) w.writeEndElement() - } fun element(path: String, f: XmlElementBuilder.() -> Unit = {}) { @@ -59,7 +60,7 @@ class XmlDocumentBuilder { fun root(name: String, f: XmlElementBuilder.() -> Unit) { val elementBuilder = XmlElementBuilder(writer) writer.writeStartElement(name) - f(elementBuilder) + elementBuilder.f() writer.writeEndElement() } }