libeufin

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

commit e71407eb7689bc66d11ab3712b8e292c8f3fb74a
parent febabaeb08dcb1a51ea0ecb73d8404027081568c
Author: Marcello Stanisci <ms@taler.net>
Date:   Tue, 12 May 2020 13:53:10 +0200

Adjust transport retrieval.

In particular, the default case needed to be handled.
For now, the default is nothing more than a random transport
instance belonging to the querying user.

Diffstat:
Mnexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mnexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt | 20++++++++++++++++++--
Mnexus/src/main/kotlin/tech/libeufin/nexus/Main.kt | 123+++++++++++++++++++++++++++-----------------------------------------------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/MainDeprecated.kt | 2+-
4 files changed, 159 insertions(+), 87 deletions(-)

diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt @@ -1,5 +1,7 @@ package tech.libeufin.nexus +import io.ktor.application.ApplicationCall +import io.ktor.client.HttpClient import io.ktor.http.HttpStatusCode import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.transactions.transaction @@ -99,7 +101,7 @@ fun getBankAccountsFromNexusUserId(id: String): MutableList<BankAccountEntity> { return ret } -fun getSubscriberDetailsInternal(subscriber: EbicsSubscriberEntity): EbicsClientSubscriberDetails { +fun getSubscriberDetails(subscriber: EbicsSubscriberEntity): EbicsClientSubscriberDetails { var bankAuthPubValue: RSAPublicKey? = null if (subscriber.bankAuthenticationPublicKey != null) { bankAuthPubValue = CryptoUtil.loadRsaPublicKey( @@ -127,7 +129,102 @@ fun getSubscriberDetailsInternal(subscriber: EbicsSubscriberEntity): EbicsClient ) } -fun getTransport() +/** + * Retrieve Ebics subscriber details given a Transport + * object and handling the default case. + */ +fun getEbicsSubscriberDetails(userId: String, transportId: String?): EbicsClientSubscriberDetails { + val transport = transaction { + if (transportId == null) { + return@transaction EbicsSubscriberEntity.all().first() + } + return@transaction EbicsSubscriberEntity.findById(transportId) + } + ?: throw NexusError( + HttpStatusCode.NotFound, + "Could not find ANY Ebics transport for user $userId" + ) + if (transport.nexusUser.id.value != userId) { + throw NexusError( + HttpStatusCode.Forbidden, + "No rights over transport $transportId" + ) + } + // transport exists and belongs to caller. + return getSubscriberDetails(transport) +} + +suspend fun downloadAndPersistC5xEbics( + historyType: String, + client: HttpClient, + userId: String, + start: String?, // dashed date YYYY-MM(01-12)-DD(01-31) + end: String?, // dashed date YYYY-MM(01-12)-DD(01-31) + transportId: String? +) { + val subscriberDetails = getEbicsSubscriberDetails(userId, transportId) + val orderParamsJson = EbicsStandardOrderParamsJson( + EbicsDateRangeJson(start, end) + ) + /** More types C52/C54 .. forthcoming */ + if (historyType != "C53") throw NexusError( + HttpStatusCode.InternalServerError, + "Ebics query tried on unknown message $historyType" + ) + val response = doEbicsDownloadTransaction( + client, + subscriberDetails, + historyType, + orderParamsJson.toOrderParams() + ) + when (response) { + is EbicsDownloadSuccessResult -> { + response.orderData.unzipWithLambda { + logger.debug("Camt entry: ${it.second}") + val fileName = it.first + val camt53doc = XMLUtil.parseStringIntoDom(it.second) + transaction { + RawBankTransactionEntity.new { + bankAccount = getBankAccountFromIban(camt53doc.pickString("//*[local-name()='Stmt']/Acct/Id/IBAN")) + sourceFileName = fileName + unstructuredRemittanceInformation = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Ustrd']") + transactionType = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='CdtDbtInd']") + currency = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Amt']/@Ccy") + amount = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Amt']") + status = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Sts']") + bookingDate = parseDashedDate(camt53doc.pickString("//*[local-name()='BookgDt']//*[local-name()='Dt']")).millis + nexusUser = extractNexusUser(userId) + counterpartIban = camt53doc.pickString("//*[local-name()='${if (this.transactionType == "DBIT") "CdtrAcct" else "DbtrAcct"}']//*[local-name()='IBAN']") + counterpartName = camt53doc.pickString("//*[local-name()='RltdPties']//*[local-name()='${if (this.transactionType == "DBIT") "Cdtr" else "Dbtr"}']//*[local-name()='Nm']") + counterpartBic = camt53doc.pickString("//*[local-name()='RltdAgts']//*[local-name()='BIC']") + } + } + } + } + is EbicsDownloadBankErrorResult -> { + throw NexusError( + HttpStatusCode.BadGateway, + response.returnCode.errorCode + ) + } + } +} + +suspend fun submitPaymentEbics( + client: HttpClient, + userId: String, + transportId: String?, + pain001document: String +) { + logger.debug("Uploading PAIN.001: ${pain001document}") + doEbicsUploadTransaction( + client, + getEbicsSubscriberDetails(userId, transportId), + "CCT", + pain001document.toByteArray(Charsets.UTF_8), + EbicsStandardOrderParams() + ) +} /** diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt @@ -1,5 +1,6 @@ package tech.libeufin.nexus +import com.sun.jdi.connect.Transport import tech.libeufin.util.* import java.lang.NullPointerException import java.time.LocalDate @@ -137,15 +138,30 @@ data class Transactions( val transactions: MutableList<Transaction> = mutableListOf() ) +/** Specifies the transport to use. */ +data class Transport( + /** + * Must match one of the types implemented by nexus: + * 'ebics', 'local', possibly 'fints' in the near future! + */ + val type: String, + /** + * A mnemonic identifier given by the user to one + * transport instance. + */ + val name: String +) + /** Request type of "POST /prepared-payments/submit" */ data class SubmitPayment( val uuid: String, - val transport: String? + /** Policy to pick the default transport is still work in progress. */ + val transport: tech.libeufin.nexus.Transport? ) /** Request type of "POST /collected-transactions" */ data class CollectedTransaction( - val transport: String?, + val transport: tech.libeufin.nexus.Transport?, val start: String?, val end: String? ) diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt @@ -196,37 +196,27 @@ fun main() { ) } val pain001document = createPain001document(preparedPayment) - when (body.transport) { - "ebics" -> { - val subscriberDetails = getTransport(body.transport) - logger.debug("Uploading PAIN.001: ${pain001document}") - doEbicsUploadTransaction( - client, - subscriberDetails, - "CCT", - pain001document.toByteArray(Charsets.UTF_8), - EbicsStandardOrderParams() - ) - /** mark payment as 'submitted' */ - transaction { - val payment = PreparedPaymentEntity.findById(body.uuid) ?: throw NexusError( - HttpStatusCode.InternalServerError, - "Severe internal error: could not find payment in DB after having submitted it to the bank" + if (body.transport != null) { + // type and name aren't null + when (body.transport.type) { + "ebics" -> { + submitPaymentEbics( + client, userId, body.transport.name, pain001document ) - payment.submitted = true } - call.respondText( - "CCT message submitted to the bank", - ContentType.Text.Plain, - HttpStatusCode.OK + else -> throw NexusError( + HttpStatusCode.NotFound, + "Transport type '${body.transport.type}' not implemented" ) - preparedPayment.submissionDate = DateTime.now().millis } - else -> throw NexusError( - HttpStatusCode.NotImplemented, - "Bank transport ${body.transport} is not implemented" + } else { + // default to ebics and "first" transport from user + submitPaymentEbics( + client, userId, null, pain001document ) } + preparedPayment.submitted = true + call.respondText("Payment ${body.uuid} submitted") return@post } /** @@ -282,73 +272,42 @@ fun main() { } /** * Downloads new transactions from the bank. + * + * NOTE: 'accountid' is not used. Transaction are asked on + * the basis of a transport subscriber (regardless of their + * bank account details) */ post("/bank-accounts/{accountid}/collected-transactions") { val userId = authenticateRequest(call.request.headers["Authorization"]) val body = call.receive<CollectedTransaction>() - when (body.transport) { - "ebics" -> { - val orderParams = EbicsStandardOrderParamsJson( - EbicsDateRangeJson( + if (body.transport != null) { + when (body.transport.type) { + "ebics" -> { + downloadAndPersistC5xEbics( + "C53", + client, + userId, body.start, - body.end + body.end, + body.transport.name ) - ).toOrderParams() - val subscriberData = getSubscriberDetailsFromNexusUserId(userId) - when (val response = doEbicsDownloadTransaction(client, subscriberData, "C53", orderParams)) { - is EbicsDownloadSuccessResult -> { - /** - * The current code is _heavily_ dependent on the way GLS returns - * data. For example, GLS makes one ZIP entry for each "Ntry" element - * (a bank transfer), but per the specifications one bank can choose to - * return all the "Ntry" elements into one single ZIP entry, or even unzipped - * at all. - */ - response.orderData.unzipWithLambda { - logger.debug("C53 entry: ${it.second}") - val fileName = it.first - val camt53doc = XMLUtil.parseStringIntoDom(it.second) - transaction { - RawBankTransactionEntity.new { - bankAccount = getBankAccountFromIban(camt53doc.pickString("//*[local-name()='Stmt']/Acct/Id/IBAN")) - sourceFileName = fileName - unstructuredRemittanceInformation = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Ustrd']") - transactionType = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='CdtDbtInd']") - currency = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Amt']/@Ccy") - amount = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Amt']") - status = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Sts']") - bookingDate = parseDashedDate(camt53doc.pickString("//*[local-name()='BookgDt']//*[local-name()='Dt']")).millis - nexusUser = extractNexusUser(userId) - counterpartIban = camt53doc.pickString("//*[local-name()='${if (this.transactionType == "DBIT") "CdtrAcct" else "DbtrAcct"}']//*[local-name()='IBAN']") - counterpartName = camt53doc.pickString("//*[local-name()='RltdPties']//*[local-name()='${if (this.transactionType == "DBIT") "Cdtr" else "Dbtr"}']//*[local-name()='Nm']") - counterpartBic = camt53doc.pickString("//*[local-name()='RltdAgts']//*[local-name()='BIC']") - } - } - } - call.respondText( - "C53 data persisted into the database (WIP).", - ContentType.Text.Plain, - HttpStatusCode.OK - ) - } - is EbicsDownloadBankErrorResult -> { - call.respond( - HttpStatusCode.BadGateway, - EbicsErrorJson( - EbicsErrorDetailJson( - "bankError", - response.returnCode.errorCode - ) - ) - ) - } } + else -> throw NexusError( + HttpStatusCode.BadRequest, + "Transport type '${body.transport.type}' not implemented" + ) } - else -> throw NexusError( - HttpStatusCode.NotImplemented, - "Bank transport ${body.transport} is not implemented" + } else { + downloadAndPersistC5xEbics( + "C53", + client, + userId, + body.start, + body.end, + null ) } + call.respondText("Collection performed") return@post } /** diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/MainDeprecated.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/MainDeprecated.kt @@ -572,7 +572,7 @@ fun main() { logger.debug("Uploading PAIN.001: ${painDoc}") doEbicsUploadTransaction( client, - getSubscriberDetailsInternal(subscriber), + getSubscriberDetails(subscriber), "CCT", painDoc.toByteArray(Charsets.UTF_8), EbicsStandardOrderParams()