libeufin

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

commit 236b6d7e0948121efe52fdbed4c108c9e5903fb5
parent d9d3db1b094f412c85ddbe52480df6289a144f33
Author: MS <ms@taler.net>
Date:   Fri, 11 Dec 2020 23:20:58 +0100

Taler facade.

Getting to the point where one payment issued via
the Taler Wire Gateway shows up in a Camt53 report,
and is subsequently returned along the /outgoing
endpoint offered by the Taler facade.

Diffstat:
Mintegration-tests/tests.py | 25++++++++++++++++++-------
Mnexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt | 20+++++++++++---------
Mnexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt | 33+++++++++++++++++++--------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt | 1+
Mnexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Iso20022.kt | 11++++-------
Mnexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt | 4++--
Msandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt | 1-
Msandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt | 15++++++---------
Msandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt | 1-
Msandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt | 4+++-
Mutil/src/main/kotlin/JSON.kt | 10++++++++--
11 files changed, 72 insertions(+), 53 deletions(-)

diff --git a/integration-tests/tests.py b/integration-tests/tests.py @@ -282,11 +282,7 @@ def test_taler_facade_config(make_taler_facade): ) -# This test makes one payment via the Taler facade, -# and expects too see it in the outgoing history. -@pytest.mark.skip("Needs more attention") -def test_taler_facade(make_taler_facade): - +def test_taler_facade_history(make_taler_facade): assertResponse( post( f"{N}/facades/{TALER_FACADE}/taler/transfer", @@ -295,13 +291,28 @@ def test_taler_facade(make_taler_facade): amount="EUR:1", exchange_base_url="http//url", wtid="nice", - credit_account="payto://iban/THEBIC/THEIBAN?receiver-name=theName" + credit_account="payto://iban/AGRIFRPP/FR7630006000011234567890189?receiver-name=theName" ), auth=NEXUS_AUTH ) ) - sleep(5) # Let automatic tasks ingest the history. + # normally done by background tasks: + assertResponse( + post( + f"{N}/bank-accounts/{NEXUS_BANK_LABEL}/payment-initiations/1/submit", + json=dict(), + auth=NEXUS_AUTH + ) + ) + # normally done by background tasks: + assertResponse( + post( + f"{N}/bank-accounts/{NEXUS_BANK_LABEL}/fetch-transactions", # _with_ ingestion + auth=NEXUS_AUTH + ) + ) + resp = assertResponse( get( f"{N}/facades/{TALER_FACADE}/taler/history/outgoing?delta=5", diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt @@ -419,15 +419,17 @@ fun ingestTalerTransactions() { (NexusBankTransactionsTable.id.greater(lastId)) }.orderBy(Pair(NexusBankTransactionsTable.id, SortOrder.ASC)).forEach { // Incoming payment. - val tx = jacksonObjectMapper().readValue(it.transactionJson, CamtBankAccountEntry::class.java) - val txDetails = tx.details - if (txDetails == null) { - // We don't support batch transactions at the moment! - logger.warn("batch transactions not supported") - } else { - when (tx.creditDebitIndicator) { - CreditDebitIndicator.CRDT -> ingestIncoming(it, txDtls = txDetails) - } + logger.debug("Taler checks payment: ${it.transactionJson}") + val tx = jacksonObjectMapper().readValue( + it.transactionJson, CamtBankAccountEntry::class.java + ) + val details = tx.batches?.get(0)?.batchTransactions?.get(0)?.details + if (details == null) { + logger.warn("Met a void money movement: VERY strange") + return@forEach + } + when (tx.creditDebitIndicator) { + CreditDebitIndicator.CRDT -> ingestIncoming(it, txDtls = details) } lastId = it.id.value } diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt @@ -148,9 +148,14 @@ fun processCamtMessage(bankAccountId: String, camtDoc: Document, code: String): } } val entries = res.reports.map { it.entries }.flatten() - logger.info("found ${entries.size} transactions") - txloop@ for (tx in entries) { - val acctSvcrRef = tx.accountServicerRef + logger.info("found ${entries.size} money movements") + txloop@ for (entry in entries) { + val singletonBatchedTransaction = entry.batches?.get(0)?.batchTransactions?.get(0) + ?: throw NexusError( + HttpStatusCode.InternalServerError, + "Singleton money movements policy wasn't respected" + ) + val acctSvcrRef = entry.accountServicerRef if (acctSvcrRef == null) { // FIXME(dold): Report this! logger.error("missing account servicer reference in transaction") @@ -165,18 +170,18 @@ fun processCamtMessage(bankAccountId: String, camtDoc: Document, code: String): val rawEntity = NexusBankTransactionEntity.new { bankAccount = acct accountTransactionId = "AcctSvcrRef:$acctSvcrRef" - amount = tx.amount.value.toPlainString() - currency = tx.amount.currency - transactionJson = jacksonObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(tx) - creditDebitIndicator = tx.creditDebitIndicator.name - status = tx.status + amount = singletonBatchedTransaction.amount.value.toPlainString() + currency = singletonBatchedTransaction.amount.currency + transactionJson = jacksonObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(entry) + creditDebitIndicator = singletonBatchedTransaction.creditDebitIndicator.name + status = entry.status } rawEntity.flush() - if (tx.creditDebitIndicator == CreditDebitIndicator.DBIT) { - val t0 = tx.details - val msgId = t0?.messageId - val pmtInfId = t0?.paymentInformationId - if (t0 != null && msgId != null && pmtInfId != null) { + if (singletonBatchedTransaction.creditDebitIndicator == CreditDebitIndicator.DBIT) { + val t0 = singletonBatchedTransaction.details + val msgId = t0.messageId + val pmtInfId = t0.paymentInformationId + if (msgId != null && pmtInfId != null) { val paymentInitiation = PaymentInitiationEntity.find { (PaymentInitiationsTable.messageId eq msgId) and (PaymentInitiationsTable.bankAccount eq acct.id) and @@ -184,6 +189,7 @@ fun processCamtMessage(bankAccountId: String, camtDoc: Document, code: String): }.firstOrNull() if (paymentInitiation != null) { + logger.info("Could confirm one initiated payment: $msgId") paymentInitiation.confirmationTransaction = rawEntity } } @@ -314,7 +320,6 @@ suspend fun fetchBankAccountTransactions( fun importBankAccount(call: ApplicationCall, offeredBankAccountId: String, nexusBankAccountId: String) { transaction { - addLogger(StdOutSqlLogger) val conn = requireBankConnection(call, "connid") // first get handle of the offered bank account val offeredAccount = OfferedBankAccountsTable.select { diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt @@ -195,6 +195,7 @@ private suspend fun fetchEbicsC5x( orderParams: EbicsOrderParams, subscriberDetails: EbicsClientSubscriberDetails ) { + logger.debug("Requesting $historyType") val response = doEbicsDownloadTransaction( client, subscriberDetails, diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Iso20022.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Iso20022.kt @@ -264,9 +264,9 @@ data class ReturnInfo( ) data class BatchTransaction( - val amount: CurrencyAmount?, - val creditDebitIndicator: CreditDebitIndicator?, - val details: TransactionDetails? + val amount: CurrencyAmount, + val creditDebitIndicator: CreditDebitIndicator, + val details: TransactionDetails ) @JsonInclude(JsonInclude.Include.NON_NULL) @@ -323,9 +323,6 @@ data class CamtBankAccountEntry( */ val instructedAmount: CurrencyAmount?, - // This field got recently obsoleted. - val details: TransactionDetails? = null, - // list of sub-transactions participating in this money movement. val batches: List<Batch>? ) @@ -656,7 +653,7 @@ private fun XmlElementDestructor.extractMaybeCurrencyExchange(): CurrencyExchang } private fun XmlElementDestructor.extractBatches( - inheritableAmount: CurrencyAmount?, + inheritableAmount: CurrencyAmount, outerCreditDebitIndicator: CreditDebitIndicator ): List<Batch> { if (mapEachChildNamed("NtryDtls") {}.size != 1) throw CamtParsingError("This money movement is not a singleton #0") diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt @@ -669,7 +669,7 @@ fun serverMain(dbName: String, host: String) { call.receive<FetchSpecJson>() } else { FetchSpecLatestJson( - FetchLevel.ALL, + FetchLevel.STATEMENT, null ) } @@ -678,7 +678,7 @@ fun serverMain(dbName: String, host: String) { fetchSpec, accountid ) - call.respondText("Collection performed") + call.respond(object {}) return@post } diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt @@ -323,7 +323,6 @@ fun dbCreateTables(dbConnectionString: String) { Database.connect("${dbConnectionString}") TransactionManager.manager.defaultIsolationLevel = Connection.TRANSACTION_SERIALIZABLE transaction { - addLogger(StdOutSqlLogger) SchemaUtils.create( EbicsSubscribersTable, EbicsHostsTable, diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt @@ -200,11 +200,7 @@ private fun getRelatedParty(branch: XmlElementBuilder, payment: RawPayment) { } } -fun buildCamtString( - type: Int, - subscriberIban: String, - history: List<RawPayment> -): String { +fun buildCamtString(type: Int, subscriberIban: String, history: List<RawPayment>): String { /** * ID types required: * @@ -387,10 +383,10 @@ fun buildCamtString( element("NtryDtls/TxDtls") { element("Refs") { element("MsgId") { - text("0") + text(it.msgId ?: "NOTPROVIDED") } element("PmtInfId") { - text("0") + text(it.pmtInfId ?: "NOTPROVIDED") } element("EndToEndId") { text("NOTPROVIDED") @@ -556,6 +552,8 @@ private fun parsePain001(paymentRequest: String, initiatorName: String): PainPar * Process a payment request in the pain.001 format. */ private fun handleCct(paymentRequest: String, initiatorName: String, ctx: RequestContext) { + LOGGER.debug("Handling CCT") + LOGGER.debug("Pain.001: $paymentRequest") val parseResult = parsePain001(paymentRequest, initiatorName) transaction { try { @@ -586,7 +584,7 @@ private fun handleCct(paymentRequest: String, initiatorName: String, ctx: Reques } private fun handleEbicsC53(requestContext: RequestContext): ByteArray { - logger.debug("Handling C53 request") + LOGGER.debug("Handling C53 request") val camt = constructCamtResponse( 53, requestContext.requestObject.header, @@ -757,7 +755,6 @@ private suspend fun ApplicationCall.handleEbicsHpb( */ private fun ApplicationCall.ensureEbicsHost(requestHostID: String): EbicsHostPublicInfo { return transaction { - addLogger(StdOutSqlLogger) val ebicsHost = EbicsHostEntity.find { EbicsHostsTable.hostID.upperCase() eq requestHostID.toUpperCase() }.firstOrNull() if (ebicsHost == null) { diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt @@ -392,7 +392,6 @@ fun serverMain(dbName: String) { val pairB = CryptoUtil.generateRsaKeyPair(2048) val pairC = CryptoUtil.generateRsaKeyPair(2048) transaction { - addLogger(StdOutSqlLogger) EbicsHostEntity.new { this.ebicsVersion = req.ebicsVersion this.hostId = req.hostID diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt @@ -41,7 +41,9 @@ fun historyForAccount(iban: String): List<RawPayment> { // and it makes the document invalid! // uid = "${it[pmtInfId]}-${it[msgId]}" uid = "${it[BankAccountTransactionsTable.pmtInfId]}", - direction = it[BankAccountTransactionsTable.direction] + direction = it[BankAccountTransactionsTable.direction], + pmtInfId = it[BankAccountTransactionsTable.pmtInfId], + msgId = it[BankAccountTransactionsTable.msgId] ) ) } diff --git a/util/src/main/kotlin/JSON.kt b/util/src/main/kotlin/JSON.kt @@ -35,6 +35,12 @@ data class RawPayment( val currency: String, val subject: String, val date: String? = null, - val uid: String? = null, - val direction: String + val uid: String? = null, // FIXME: explain this value. + val direction: String, + // the following two values are rather CAMT/PAIN + // specific, therefore do not need to be returned + // along every API call using this object. + val pmtInfId: String? = null, + val msgId: String? = null + ) \ No newline at end of file