libeufin

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

commit a31d6b760ae636dc3f59ea35f75c779f297b8732
parent 0cd1658bc6178a7154937268a66a34a58028492b
Author: Florian Dold <florian.dold@gmail.com>
Date:   Thu, 18 Jun 2020 18:50:59 +0530

refactor payment initiations

Diffstat:
Mnexus/src/main/kotlin/tech/libeufin/nexus/DB.kt | 44++++++++++++++++++++++----------------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt | 16++++++++--------
Mnexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt | 47++++++++++++++++-------------------------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/Main.kt | 40+++++-----------------------------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt | 38+++++++++++++++++++++-----------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt | 5+++--
Msandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt | 4++--
Msandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt | 40+++++++++++++++++++---------------------
Mutil/src/main/kotlin/JSON.kt | 4++--
9 files changed, 98 insertions(+), 140 deletions(-)

diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt @@ -46,7 +46,7 @@ object TalerRequestedPayments : LongIdTable() { // in the sense that a "early" prepared payment might // get a "high" id because the bank confirmed it "late". val abstractId = long("abstractId").nullable() - val preparedPayment = reference("payment", InitiatedPaymentsTable) + val preparedPayment = reference("payment", PaymentInitiationsTable) val requestUId = text("request_uid") val amount = text("amount") val exchangeBaseUrl = text("exchange_base_url") @@ -57,7 +57,7 @@ object TalerRequestedPayments : LongIdTable() { class TalerRequestedPaymentEntity(id: EntityID<Long>) : LongEntity(id) { companion object : LongEntityClass<TalerRequestedPaymentEntity>(TalerRequestedPayments) var abstractId by TalerRequestedPayments.abstractId - var preparedPayment by InitiatedPaymentEntity referencedOn TalerRequestedPayments.preparedPayment + var preparedPayment by PaymentInitiationEntity referencedOn TalerRequestedPayments.preparedPayment var requestUId by TalerRequestedPayments.requestUId var amount by TalerRequestedPayments.amount var exchangeBaseUrl by TalerRequestedPayments.exchangeBaseUrl @@ -167,7 +167,7 @@ class RawBankTransactionEntity(id: EntityID<Long>) : LongEntity(id) { /** * Represents a prepared payment. */ -object InitiatedPaymentsTable : LongIdTable() { +object PaymentInitiationsTable : LongIdTable() { /** * Bank account that wants to initiate the payment. */ @@ -193,24 +193,24 @@ object InitiatedPaymentsTable : LongIdTable() { val rawConfirmation = reference("rawConfirmation", RawBankTransactionsTable).nullable() } -class InitiatedPaymentEntity(id: EntityID<Long>) : LongEntity(id) { - companion object : LongEntityClass<InitiatedPaymentEntity>(InitiatedPaymentsTable) - - var bankAccount by NexusBankAccountEntity referencedOn InitiatedPaymentsTable.bankAccount - var preparationDate by InitiatedPaymentsTable.preparationDate - var submissionDate by InitiatedPaymentsTable.submissionDate - var sum by InitiatedPaymentsTable.sum - var currency by InitiatedPaymentsTable.currency - var debitorIban by InitiatedPaymentsTable.debitorIban - var debitorBic by InitiatedPaymentsTable.debitorBic - var debitorName by InitiatedPaymentsTable.debitorName - var endToEndId by InitiatedPaymentsTable.endToEndId - var subject by InitiatedPaymentsTable.subject - var creditorIban by InitiatedPaymentsTable.creditorIban - var creditorBic by InitiatedPaymentsTable.creditorBic - var creditorName by InitiatedPaymentsTable.creditorName - var submitted by InitiatedPaymentsTable.submitted - var rawConfirmation by RawBankTransactionEntity optionalReferencedOn InitiatedPaymentsTable.rawConfirmation +class PaymentInitiationEntity(id: EntityID<Long>) : LongEntity(id) { + companion object : LongEntityClass<PaymentInitiationEntity>(PaymentInitiationsTable) + + var bankAccount by NexusBankAccountEntity referencedOn PaymentInitiationsTable.bankAccount + var preparationDate by PaymentInitiationsTable.preparationDate + var submissionDate by PaymentInitiationsTable.submissionDate + var sum by PaymentInitiationsTable.sum + var currency by PaymentInitiationsTable.currency + var debitorIban by PaymentInitiationsTable.debitorIban + var debitorBic by PaymentInitiationsTable.debitorBic + var debitorName by PaymentInitiationsTable.debitorName + var endToEndId by PaymentInitiationsTable.endToEndId + var subject by PaymentInitiationsTable.subject + var creditorIban by PaymentInitiationsTable.creditorIban + var creditorBic by PaymentInitiationsTable.creditorBic + var creditorName by PaymentInitiationsTable.creditorName + var submitted by PaymentInitiationsTable.submitted + var rawConfirmation by RawBankTransactionEntity optionalReferencedOn PaymentInitiationsTable.rawConfirmation } /** @@ -345,7 +345,7 @@ fun dbCreateTables(dbName: String) { addLogger(StdOutSqlLogger) SchemaUtils.create( NexusUsersTable, - InitiatedPaymentsTable, + PaymentInitiationsTable, EbicsSubscribersTable, NexusBankAccountsTable, RawBankTransactionsTable, diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt @@ -52,9 +52,9 @@ private fun findDuplicate(bankAccountId: String, acctSvcrRef: String): RawBankTr fun markInitiatedAsConfirmed(subject: String, debtorIban: String, rawUuid: Long) { // not introducing a 'transaction {}' block since // this function should be always be invoked from one. - val initiatedPayment = InitiatedPaymentEntity.find { - InitiatedPaymentsTable.subject eq subject and - (InitiatedPaymentsTable.debitorIban eq debtorIban) + val initiatedPayment = PaymentInitiationEntity.find { + PaymentInitiationsTable.subject eq subject and + (PaymentInitiationsTable.debitorIban eq debtorIban) }.firstOrNull() if (initiatedPayment == null) { logger.info("Payment '$subject' was never programmatically prepared") @@ -160,7 +160,7 @@ fun ingestBankMessagesIntoAccount( * Create a PAIN.001 XML document according to the input data. * Needs to be called within a transaction block. */ -fun createPain001document(paymentData: InitiatedPaymentEntity): String { +fun createPain001document(paymentData: PaymentInitiationEntity): String { /** * Every PAIN.001 document contains at least three IDs: * @@ -286,9 +286,9 @@ fun createPain001document(paymentData: InitiatedPaymentEntity): String { * Retrieve prepared payment from database, raising exception * if not found. */ -fun getPreparedPayment(uuid: Long): InitiatedPaymentEntity { +fun getPreparedPayment(uuid: Long): PaymentInitiationEntity { return transaction { - InitiatedPaymentEntity.findById(uuid) + PaymentInitiationEntity.findById(uuid) } ?: throw NexusError( HttpStatusCode.NotFound, "Payment '$uuid' not found" @@ -303,9 +303,9 @@ fun getPreparedPayment(uuid: Long): InitiatedPaymentEntity { * it will be the account whose money will pay the wire transfer being defined * by this pain document. */ -fun addPreparedPayment(paymentData: Pain001Data, debitorAccount: NexusBankAccountEntity): InitiatedPaymentEntity { +fun addPreparedPayment(paymentData: Pain001Data, debitorAccount: NexusBankAccountEntity): PaymentInitiationEntity { return transaction { - InitiatedPaymentEntity.new { + PaymentInitiationEntity.new { bankAccount = debitorAccount subject = paymentData.subject sum = paymentData.sum diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt @@ -17,18 +17,14 @@ * <http://www.gnu.org/licenses/> */ -package tech.libeufin.nexus - /** - * Parse ISO 20022 messages + * Parse and generate ISO 20022 messages */ +package tech.libeufin.nexus import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonSubTypes import com.fasterxml.jackson.annotation.JsonTypeInfo -import io.ktor.http.HttpStatusCode -import org.jetbrains.exposed.sql.and -import org.jetbrains.exposed.sql.transactions.transaction import org.w3c.dom.Document import tech.libeufin.util.* import java.time.Instant @@ -224,6 +220,7 @@ class CamtParsingError(msg: String) : Exception(msg) data class NexusPaymentInitiationData( val debtorIban: String, val debtorBic: String, + val debtorName: String, val messageId: String, val paymentInformationId: String, val amount: String, @@ -239,29 +236,17 @@ data class NexusPaymentInitiationData( * Needs to be called within a transaction block. */ fun createPain001document(paymentData: NexusPaymentInitiationData): String { - /** - * Every PAIN.001 document contains at least three IDs: - * - * 1) MsgId: a unique id for the message itself - * 2) PmtInfId: the unique id for the payment's set of information - * 3) EndToEndId: a unique id to be shared between the debtor and - * creditor that uniquely identifies the transaction - * - * For now and for simplicity, since every PAIN entry in the database - * has a unique ID, and the three values aren't required to be mutually different, - * we'll assign the SAME id (= the row id) to all the three aforementioned - * PAIN id types. - */ - val debitorBankAccountLabel = run { - val debitorBankAcount = NexusBankAccountEntity.find { - NexusBankAccountsTable.iban eq paymentData.debtorIban and - (NexusBankAccountsTable.bankCode eq paymentData.debtorBic) - }.firstOrNull() ?: throw NexusError( - HttpStatusCode.NotFound, - "Please download bank accounts details first (HTD)" - ) - debitorBankAcount.id.value - } + // Every PAIN.001 document contains at least three IDs: + // + // 1) MsgId: a unique id for the message itself + // 2) PmtInfId: the unique id for the payment's set of information + // 3) EndToEndId: a unique id to be shared between the debtor and + // creditor that uniquely identifies the transaction + // + // For now and for simplicity, since every PAIN entry in the database + // has a unique ID, and the three values aren't required to be mutually different, + // we'll assign the SAME id (= the row id) to all the three aforementioned + // PAIN id types. val s = constructXml(indent = true) { root("Document") { @@ -287,7 +272,7 @@ fun createPain001document(paymentData: NexusPaymentInitiationData): String { text(paymentData.amount) } element("InitgPty/Nm") { - text(debitorBankAccountLabel) + text(paymentData.debtorName) } } element("PmtInf") { @@ -314,7 +299,7 @@ fun createPain001document(paymentData: NexusPaymentInitiationData): String { text(importDateFromMillis(dateMillis).toDashedDate()) } element("Dbtr/Nm") { - text(debitorBankAccountLabel) + text(paymentData.debtorName) } element("DbtrAcct/Id/IBAN") { text(paymentData.debtorIban) diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt @@ -66,7 +66,8 @@ import org.jetbrains.exposed.sql.transactions.transaction import org.slf4j.Logger import org.slf4j.LoggerFactory import org.slf4j.event.Level -import tech.libeufin.nexus.bankaccount.submitPreparedPayments +import tech.libeufin.nexus.bankaccount.submitAllPreparedPayments +import tech.libeufin.nexus.bankaccount.submitPreparedPayment import tech.libeufin.nexus.ebics.* import tech.libeufin.util.* import tech.libeufin.util.CryptoUtil.hashpw @@ -246,7 +247,7 @@ fun moreFrequentBackgroundTasks(httpClient: HttpClient) { } // FIXME: should be done automatically after raw ingestion reportAndIgnoreErrors { ingestTalerTransactions() } - reportAndIgnoreErrors { submitPreparedPayments(httpClient) } + reportAndIgnoreErrors { submitAllPreparedPayments(httpClient) } logger.debug("More frequent background jobs done") delay(Duration.ofSeconds(1)) } @@ -518,40 +519,9 @@ fun serverMain(dbName: String) { val uuid = ensureLong(call.parameters["uuid"]) val accountId = ensureNonNull(call.parameters["accountid"]) val res = transaction { - val user = authenticateRequest(call.request) - val preparedPayment = getPreparedPayment(uuid) - if (preparedPayment.submitted) { - throw NexusError( - HttpStatusCode.PreconditionFailed, - "Payment ${uuid} was submitted already" - ) - } - val bankAccount = NexusBankAccountEntity.findById(accountId) - if (bankAccount == null) { - throw NexusError(HttpStatusCode.NotFound, "unknown bank account") - } - val defaultBankConnection = bankAccount.defaultBankConnection - ?: throw NexusError(HttpStatusCode.NotFound, "needs a default connection") - return@transaction object { - val pain001document = createPain001document(preparedPayment) - val bankConnectionType = defaultBankConnection.type - val connId = defaultBankConnection.id.value - } - } - // type and name aren't null - when (res.bankConnectionType) { - "ebics" -> { - submitEbicsPaymentInitiation(client, res.connId, res.pain001document) - } - else -> throw NexusError( - HttpStatusCode.NotFound, - "Transport type '${res.bankConnectionType}' not implemented" - ) - } - transaction { - val preparedPayment = getPreparedPayment(uuid) - preparedPayment.submitted = true + authenticateRequest(call.request) } + submitPreparedPayment(client, uuid) call.respondText("Payment ${uuid} submitted") return@post } diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt @@ -25,19 +25,29 @@ import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.not import org.jetbrains.exposed.sql.transactions.transaction import tech.libeufin.nexus.* -import tech.libeufin.nexus.ebics.doEbicsUploadTransaction import tech.libeufin.nexus.ebics.submitEbicsPaymentInitiation -import tech.libeufin.util.EbicsClientSubscriberDetails -import tech.libeufin.util.EbicsStandardOrderParams +suspend fun submitPreparedPayment(httpClient: HttpClient, paymentInitiationId: Long) { + val type = transaction { + val paymentInitiation = PaymentInitiationEntity.findById(paymentInitiationId) + if (paymentInitiation == null) { + throw NexusError(HttpStatusCode.NotFound, "prepared payment not found") + } + paymentInitiation.bankAccount.defaultBankConnection?.type + } + when (type) { + null -> throw NexusError(HttpStatusCode.NotFound, "no default bank connection") + "ebics" -> submitEbicsPaymentInitiation(httpClient, paymentInitiationId) + } +} + /** * Submit all pending prepared payments. */ -suspend fun submitPreparedPayments(httpClient: HttpClient) { +suspend fun submitAllPreparedPayments(httpClient: HttpClient) { data class Submission( - val id: Long, - val type: String + val id: Long ) logger.debug("auto-submitter started") val workQueue = mutableListOf<Submission>() @@ -56,21 +66,15 @@ suspend fun submitPreparedPayments(httpClient: HttpClient) { return@forEach } val bankAccount: NexusBankAccountEntity = it - InitiatedPaymentEntity.find { - InitiatedPaymentsTable.debitorIban eq bankAccount.iban and - not(InitiatedPaymentsTable.submitted) + PaymentInitiationEntity.find { + PaymentInitiationsTable.debitorIban eq bankAccount.iban and + not(PaymentInitiationsTable.submitted) }.forEach { - workQueue.add(Submission(it.id.value, bankConnection.type)) + workQueue.add(Submission(it.id.value)) } } } workQueue.forEach { - when (it.type) { - "ebics" -> { - submitEbicsPaymentInitiation(httpClient, it.id) - } - else -> throw NexusError(HttpStatusCode.NotImplemented, "submission for ${it.type }not supported") - } - + submitPreparedPayment(httpClient, it.id) } } \ No newline at end of file diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt @@ -635,7 +635,7 @@ fun getEbicsKeyLetterPdf(conn: NexusBankConnectionEntity): ByteArray { suspend fun submitEbicsPaymentInitiation(httpClient: HttpClient, paymentInitiationId: Long) { val r = transaction { - val paymentInitiation = InitiatedPaymentEntity.findById(paymentInitiationId) + val paymentInitiation = PaymentInitiationEntity.findById(paymentInitiationId) ?: throw NexusError(HttpStatusCode.NotFound, "payment initiation not found") val connId = paymentInitiation.bankAccount.defaultBankConnection?.id ?: throw NexusError(HttpStatusCode.NotFound, "no default bank connection available for submission") @@ -653,7 +653,8 @@ suspend fun submitEbicsPaymentInitiation(httpClient: HttpClient, paymentInitiati // FIXME(dold): Put date in here as well paymentInformationId = paymentInitiation.id.toString(), preparationTimestamp = paymentInitiation.preparationDate, - subject = paymentInitiation.subject + subject = paymentInitiation.subject, + debtorName = paymentInitiation.bankAccount.accountHolder )) object { val subscriberDetails = subscriberDetails diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt @@ -251,10 +251,10 @@ class EbicsUploadTransactionChunkEntity(id: EntityID<String>) : Entity<String>(i */ object PaymentsTable : IntIdTable() { val creditorIban = text("creditorIban") - val creditorBic = text("creditorBic") + val creditorBic = text("creditorBic").nullable() val creditorName = text("creditorName") val debitorIban = text("debitorIban") - val debitorBic = text("debitorBic") + val debitorBic = text("debitorBic").nullable() val debitorName = text("debitorName") val subject = text("subject") val amount = text("amount") diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt @@ -376,23 +376,23 @@ fun buildCamtString(type: Int, subscriberIban: String, history: MutableList<RawP text(it.creditorIban) } } - element("RltdAgts") { - element("CdtrAgt/FinInstnId/BIC") { - // FIXME: explain this! - text( - if (subscriberIban.equals(it.creditorIban)) - it.debitorBic else it.creditorBic - ) - } - element("DbtrAgt/FinInstnId/BIC") { - // FIXME: explain this! - text( - if (subscriberIban.equals(it.creditorIban)) - it.creditorBic else it.debitorBic - ) - } - - } +// element("RltdAgts") { +// element("CdtrAgt/FinInstnId/BIC") { +// // FIXME: explain this! +// text( +// if (subscriberIban.equals(it.creditorIban)) +// it.debitorBic else it.creditorBic +// ) +// } +// element("DbtrAgt/FinInstnId/BIC") { +// // FIXME: explain this! +// text( +// if (subscriberIban.equals(it.creditorIban)) +// it.creditorBic else it.debitorBic +// ) +// } +// +// } element("RmtInf/Ustrd") { text(it.subject) } @@ -488,10 +488,10 @@ private fun handleCct(paymentRequest: String, initiatorName: String) { */ val painDoc = XMLUtil.parseStringIntoDom(paymentRequest) val creditorIban = painDoc.pickString("//*[local-name()='CdtrAcct']//*[local-name()='IBAN']") - val creditorBic = painDoc.pickString("//*[local-name()='CdtrAgt']//*[local-name()='BIC']") + //val creditorBic = painDoc.pickString("//*[local-name()='CdtrAgt']//*[local-name()='BIC']") val creditorName = painDoc.pickString("//*[local-name()='Cdtr']//*[local-name()='Nm']") val debitorIban = painDoc.pickString("//*[local-name()='DbtrAcct']//*[local-name()='IBAN']") - val debitorBic = painDoc.pickString("//*[local-name()='DbtrAgt']//*[local-name()='BIC']") + //val debitorBic = painDoc.pickString("//*[local-name()='DbtrAgt']//*[local-name()='BIC']") val debitorName = initiatorName val subject = painDoc.pickString("//*[local-name()='Ustrd']") val amount = painDoc.pickString("//*[local-name()='InstdAmt']") @@ -500,10 +500,8 @@ private fun handleCct(paymentRequest: String, initiatorName: String) { transaction { PaymentEntity.new { this.creditorIban = creditorIban - this.creditorBic = creditorBic this.creditorName = creditorName this.debitorIban = debitorIban - this.debitorBic = debitorBic this.debitorName = debitorName this.subject = subject this.amount = amount diff --git a/util/src/main/kotlin/JSON.kt b/util/src/main/kotlin/JSON.kt @@ -26,10 +26,10 @@ package tech.libeufin.util */ data class RawPayment( val creditorIban: String, - val creditorBic: String, + val creditorBic: String?, val creditorName: String, val debitorIban: String, - val debitorBic: String, + val debitorBic: String?, val debitorName: String, val amount: String, val currency: String,