libeufin

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

commit bc1b7a3ee37698bcac5979b180062886feeb626d
parent a6ad71bb7054b84ab4b8f141cb89265b7c37dede
Author: Marcello Stanisci <stanisci.m@gmail.com>
Date:   Tue, 17 Mar 2020 17:05:27 +0100

Camt generation.

Avoid spreading the logic through multiple helper
functions.  And also, separaing the data retrieval
from database and the actual (CAMT) string construction.

Diffstat:
Msandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt | 240+++++++++++++++++++++++++++++++++++++------------------------------------------
Msandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt | 26+++++++++-----------------
2 files changed, 120 insertions(+), 146 deletions(-)

diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt @@ -136,19 +136,118 @@ private suspend fun ApplicationCall.respondEbicsKeyManagement( respondText(text, ContentType.Application.Xml, HttpStatusCode.OK) } +private fun buildCamtString(history: SizedIterable<BankTransactionEntity>, type: Int): String { + + return constructXml(indent = true) { + root("Document") { + attribute("xmlns", "urn:iso:std:iso:20022:tech:xsd:camt.053.001.08") + attribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") + attribute("xmlns:schemaLocation", "urn:iso:std:iso:20022:tech:xsd:camt.053.001.02 camt.053.001.02.xsd") + element("BkToCstmrStmt") { + element("GrpHdr") { + element("MsgId") { + // unique identifier for a message + text("id under group header") + } + element("CreDtTm") + } + element(if (type == 52) "Rpt" else "Stmt") { + 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") + text(Amount("0.99").toPlainString()) + } + 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 + */ + } + } + + history.forEach { + 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 "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. + * Builds CAMT response. * + * @param history the list of all the history elements + * @param type 52 or 53. */ -private fun iterHistory(customerId: Int, header: EbicsRequest.Header, base: XmlElementBuilder) { +private fun constructCamtResponse(type: Int, customerId: Int, header: EbicsRequest.Header): String { - extractHistoryForEach( + val history = extractHistory( customerId, try { (header.static.orderDetails?.orderParams as EbicsRequest.StandardOrderParams).dateRange!!.start.toString() @@ -162,125 +261,8 @@ private fun iterHistory(customerId: Int, header: EbicsRequest.Header, base: XmlE LOGGER.debug("Asked to iterate over history with NO end date; default to now") DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar()).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, header: EbicsRequest.Header): String { - val camt = constructXml(indent = true) { - root("Document") { - attribute("xmlns", "urn:iso:std:iso:20022:tech:xsd:camt.053.001.08") - element("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, header, this) - } - } - } - } - return camt + ) + return buildCamtString(history, type) } @@ -299,7 +281,7 @@ private fun handleEbicsC52(requestContext: RequestContext): ByteArray { val baos = ByteArrayOutputStream() val asf = ArchiveStreamFactory().createArchiveOutputStream(ArchiveStreamFactory.ZIP, baos) - val zae = ZipArchiveEntry("Singleton C53 Entry") + val zae = ZipArchiveEntry("Singleton C5{2,3} Entry") asf.putArchiveEntry(zae) val bais = ByteArrayInputStream(camt.toByteArray()) diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt @@ -38,10 +38,7 @@ import io.ktor.routing.post import io.ktor.routing.routing import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty -import org.jetbrains.exposed.sql.Date -import org.jetbrains.exposed.sql.StdOutSqlLogger -import org.jetbrains.exposed.sql.addLogger -import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.transactions.transaction import org.joda.time.DateTime import org.slf4j.Logger @@ -163,29 +160,23 @@ fun sampleData() { } /** - * Finds the history for a customer, given dates of 'value'. This date - * specifies the point in time where a operation settled. The other type - * of date is 'operation', that indicates the point in time where the - * transaction was created in the bank's system. - * * @param id the customer whose history must be returned. This * id is local to the bank and is not reused/encoded into other * EBICS id values. - * @param builder lambda function that will loop over all the results. + * + * @return result set of all the operations related to the customer + * identified by @p id. */ -fun extractHistoryForEach(id: Int, start: String?, end: String?, builder: (BankTransactionEntity) -> Any) { +fun extractHistory(id: Int, start: String?, end: String?): SizedIterable<BankTransactionEntity> { val s = if (start != null) DateTime.parse(start) else DateTime(0) val e = if (end != null) DateTime.parse(end) else DateTime.now() LOGGER.debug("Fetching history from $s to $e") - transaction { + return transaction { addLogger(StdOutSqlLogger) BankTransactionEntity.find { BankTransactionsTable.localCustomer eq id and BankTransactionsTable.valueDate.between(s.millis, e.millis) - }.forEach { - LOGGER.debug("Found history element: $it") - builder(it) } } } @@ -244,8 +235,9 @@ fun main() { val req = call.receive<CustomerHistoryRequest>() val customer = findCustomer(call.parameters["id"]) val ret = CustomerHistoryResponse() - - extractHistoryForEach(customer.id.value, req.start, req.end) { + val history = extractHistory(customer.id.value, req.start, req.end) + + history.forEach { ret.history.add( CustomerHistoryResponseElement( subject = it.subject,