libeufin

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

commit 550bb76748e4008bd57fb0e323a553f7f5f3fe49
parent 7440a4b8ef2f24a95f0280e5b27bd88d93349365
Author: MS <ms@taler.net>
Date:   Thu,  3 Aug 2023 18:22:48 +0200

Using the wireTransfer() method for EBICS.

Also: avoiding to use the "fetch all" specification
for downloading EBICS reports in tests, because they
easily miss the very latest transactions from the same
day.  Instead, use large enough time frames when all
the transactions have to be returned.

Diffstat:
Mcli/bin/libeufin-cli | 1-
Mnexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt | 10++++++----
Mnexus/src/test/kotlin/EbicsTest.kt | 25+++++++++++++++----------
Mnexus/src/test/kotlin/NexusApiTest.kt | 2+-
Mnexus/src/test/kotlin/TalerTest.kt | 4+++-
Msandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt | 5++---
Msandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt | 53+++++++++++------------------------------------------
Msandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt | 12++++++------
Msandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt | 10+++++-----
Mutil/src/main/kotlin/time.kt | 11+++++++----
Mutil/src/test/kotlin/TimeTest.kt | 4+++-
11 files changed, 59 insertions(+), 78 deletions(-)

diff --git a/cli/bin/libeufin-cli b/cli/bin/libeufin-cli @@ -987,7 +987,6 @@ def fetch_transactions(obj, account_name, range_type, level, start, end): check_response_status(resp) tell_user(resp) - @accounts.command(help="Get transactions from the simplified nexus JSON API") @click.option( "--compact/--no-compact", diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt @@ -697,11 +697,13 @@ class EbicsBankConnectionProtocol: BankConnectionProtocol { addForLevel(fetchSpec.level, p) } is FetchSpecAllJson -> { + val start = ZonedDateTime.ofInstant( + Instant.EPOCH, + ZoneOffset.UTC + ) + val end = ZonedDateTime.ofInstant(Instant.now(), ZoneOffset.systemDefault()) val p = EbicsStandardOrderParams( - EbicsDateRange( - ZonedDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC), - ZonedDateTime.now(ZoneOffset.UTC) - ) + EbicsDateRange(start, end) ) addForLevel(fetchSpec.level, p) } diff --git a/nexus/src/test/kotlin/EbicsTest.kt b/nexus/src/test/kotlin/EbicsTest.kt @@ -6,6 +6,7 @@ import io.ktor.server.response.* import io.ktor.server.routing.* import io.ktor.server.testing.* import kotlinx.coroutines.runBlocking +import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.transactions.transaction import org.junit.Test import org.w3c.dom.Document @@ -16,15 +17,15 @@ import tech.libeufin.nexus.bankaccount.submitAllPaymentInitiations import tech.libeufin.nexus.ebics.* import tech.libeufin.nexus.iso20022.NexusPaymentInitiationData import tech.libeufin.nexus.iso20022.createPain001document -import tech.libeufin.nexus.server.FetchLevel -import tech.libeufin.nexus.server.FetchSpecAllJson -import tech.libeufin.nexus.server.Pain001Data +import tech.libeufin.nexus.server.* import tech.libeufin.sandbox.* import tech.libeufin.util.* import tech.libeufin.util.ebics_h004.EbicsRequest import tech.libeufin.util.ebics_h004.EbicsResponse import tech.libeufin.util.ebics_h004.EbicsTypes import tech.libeufin.util.ebics_h005.Ebics3Request +import java.time.LocalDate +import java.time.ZonedDateTime /** * These test cases run EBICS CCT and C52, mixing ordinary operations @@ -104,13 +105,16 @@ class DownloadAndSubmit { "Exist in logging!", "TESTKUDOS:5" ) + testApplication { application(sandboxApp) runBlocking { fetchBankAccountTransactions( client, - fetchSpec = FetchSpecAllJson( + fetchSpec = FetchSpecTimeRangeJson( level = FetchLevel.REPORT, + start = "2020-10-10", + end = "3000-10-10", bankConnection = "foo" ), accountId = "foo" @@ -138,7 +142,7 @@ class DownloadAndSubmit { // Create Pain.001 to be submitted. addPaymentInitiation( Pain001Data( - creditorIban = getIban(), + creditorIban = BAR_USER_IBAN, creditorBic = "SANDBOXX", creditorName = "Tester", subject = "test payment", @@ -157,11 +161,12 @@ class DownloadAndSubmit { ) } transaction { - val payment = BankAccountTransactionEntity[1] - assert(payment.debtorIban == FOO_USER_IBAN && - payment.subject == "test payment" && - payment.direction == "DBIT" - ) + val howMany = BankAccountTransactionEntity.find { + BankAccountTransactionsTable.debtorIban eq FOO_USER_IBAN and ( + BankAccountTransactionsTable.subject eq "test payment" + ) and (BankAccountTransactionsTable.direction eq "DBIT") + }.count() + assert(howMany == 1L) } } } diff --git a/nexus/src/test/kotlin/NexusApiTest.kt b/nexus/src/test/kotlin/NexusApiTest.kt @@ -256,7 +256,7 @@ class NexusApiTest { fetchSpec = FetchSpecTimeRangeJson( FetchLevel.REPORT, start = "2019-10-31", - end = "2020-11-30", + end = "2019-11-30", bankConnection = null ), accountId = "foo", diff --git a/nexus/src/test/kotlin/TalerTest.kt b/nexus/src/test/kotlin/TalerTest.kt @@ -68,8 +68,10 @@ class TalerTest { */ fetchBankAccountTransactions( client, - fetchSpec = FetchSpecAllJson( + fetchSpec = FetchSpecTimeRangeJson( level = if (testedAccount == "bar") FetchLevel.STATEMENT else FetchLevel.REPORT, + start = "2020-01-01", + end = "3000-01-01", bankConnection = testedAccount ), accountId = testedAccount diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt @@ -13,7 +13,6 @@ import tech.libeufin.util.* import java.io.File import java.io.InputStreamReader import java.math.BigDecimal -import java.math.RoundingMode import java.util.concurrent.TimeUnit import kotlin.text.toByteArray @@ -322,7 +321,7 @@ fun circuitApi(circuitRoute: Route) { amount = op.amountDebit ) op.status = CashoutOperationStatus.CONFIRMED - op.confirmationTime = getUTCnow().toInstant().toEpochMilli() + op.confirmationTime = getSystemTimeNow().toInstant().toEpochMilli() // TODO(signal this payment over LIBEUFIN_REGIO_INCOMING) } call.respond(HttpStatusCode.NoContent) @@ -538,7 +537,7 @@ fun circuitApi(circuitRoute: Route) { this.sellAtRatio = ratiosAndFees.sell_at_ratio.toString() this.sellOutFee = ratiosAndFees.sell_out_fee.toString() this.subject = cashoutSubject - this.creationTime = getUTCnow().toInstant().toEpochMilli() + this.creationTime = getSystemTimeNow().toInstant().toEpochMilli() this.tanChannel = SupportedTanChannels.valueOf(tanChannel) this.account = user this.tan = getRandomString(5) diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt @@ -30,7 +30,6 @@ import io.ktor.util.AttributeKey import io.ktor.util.date.* import org.apache.xml.security.binding.xmldsig.RSAKeyValueType import org.jetbrains.exposed.sql.* -import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.statements.api.ExposedBlob import org.jetbrains.exposed.sql.transactions.transaction import org.w3c.dom.Document @@ -297,7 +296,7 @@ fun buildCamtString( * - Proprietary code of the bank transaction * - Id of the servicer (Issuer and Code) */ - val camtCreationTime = getUTCnow() // FIXME: should this be the payment time? + val camtCreationTime = getSystemTimeNow() // FIXME: should this be the payment time? val dashedDate = camtCreationTime.toDashedDate() val zonedDateTime = camtCreationTime.toZonedString() val creationTimeMillis = camtCreationTime.toInstant().toEpochMilli() @@ -703,46 +702,15 @@ private fun handleCct( } if (maybeDebit(bankAccount.label, maybeAmount, bankAccount.demoBank.name)) throw EbicsAmountCheckError("The requested amount (${parseResult.amount}) would exceed the debit threshold") - - // Get the two parties. - BankAccountTransactionEntity.new { - account = bankAccount - demobank = bankAccount.demoBank - creditorIban = parseResult.creditorIban - creditorName = parseResult.creditorName - creditorBic = parseResult.creditorBic - debtorIban = parseResult.debtorIban - debtorName = parseResult.debtorName - debtorBic = parseResult.debtorBic - subject = parseResult.subject - amount = parseResult.amount - currency = parseResult.currency - date = getUTCnow().toInstant().toEpochMilli() - pmtInfId = parseResult.pmtInfId + logger.debug("Wire-transfer'ing endToEndId: ${parseResult.endToEndId}") + wireTransfer( + bankAccount.label, + getBankAccountFromIban(parseResult.creditorIban).label, + bankAccount.demoBank.name, + parseResult.subject, + "${parseResult.currency}:${parseResult.amount}", endToEndId = parseResult.endToEndId - accountServicerReference = "sandboxref-${getRandomString(16)}" - direction = "DBIT" - } - val maybeLocalCreditor = BankAccountEntity.find(BankAccountsTable.iban eq parseResult.creditorIban).firstOrNull() - if (maybeLocalCreditor != null) { - BankAccountTransactionEntity.new { - account = maybeLocalCreditor - demobank = maybeLocalCreditor.demoBank - creditorIban = parseResult.creditorIban - creditorName = parseResult.creditorName - creditorBic = parseResult.creditorBic - debtorIban = parseResult.debtorIban - debtorName = parseResult.debtorName - debtorBic = parseResult.debtorBic - subject = parseResult.subject - amount = parseResult.amount - currency = parseResult.currency - date = getUTCnow().toInstant().toEpochMilli() - pmtInfId = parseResult.pmtInfId - accountServicerReference = "sandboxref-${getRandomString(16)}" - direction = "CRDT" - } - } + ) } } @@ -755,8 +723,9 @@ private fun handleEbicsC52(requestContext: RequestContext): ByteArray { val dateRange: Pair<Long, Long>? = if (maybeDateRange is EbicsRequest.StandardOrderParams) { val start: Long? = maybeDateRange.dateRange?.start?.toGregorianCalendar()?.timeInMillis val end: Long? = maybeDateRange.dateRange?.end?.toGregorianCalendar()?.timeInMillis - Pair(start ?: 0L, end ?: getTimeMillis()) + Pair(start ?: 0L, end ?: Long.MAX_VALUE) } else null + logger.debug("Date range: $dateRange") val report = constructCamtResponse( 52, requestContext.subscriber, diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt @@ -272,7 +272,7 @@ class Camt053Tick : CliktCommand( ) BankAccountStatementEntity.new { statementId = camtData.messageId - creationTime = getUTCnow().toInstant().epochSecond + creationTime = getSystemTimeNow().toInstant().epochSecond xmlMessage = camtData.camtMessage bankAccount = accountIter } @@ -843,7 +843,7 @@ val sandboxApp: Application.() -> Unit = { debtorName = body.debtorName subject = body.subject this.amount = amount.amount - date = getUTCnow().toInstant().toEpochMilli() + date = getSystemTimeNow().toInstant().toEpochMilli() accountServicerReference = "sandbox-$randId" this.account = account direction = "CRDT" @@ -966,7 +966,7 @@ val sandboxApp: Application.() -> Unit = { debtorName = "Max Mustermann" subject = "sample transaction $transactionReferenceCrdt" this.amount = amount.toString() - date = getUTCnow().toInstant().toEpochMilli() + date = getSystemTimeNow().toInstant().toEpochMilli() accountServicerReference = transactionReferenceCrdt this.account = account direction = "CRDT" @@ -987,7 +987,7 @@ val sandboxApp: Application.() -> Unit = { creditorName = "Max Mustermann" subject = "sample transaction $transactionReferenceDbit" this.amount = amount.toString() - date = getUTCnow().toInstant().toEpochMilli() + date = getSystemTimeNow().toInstant().toEpochMilli() accountServicerReference = transactionReferenceDbit this.account = account direction = "DBIT" @@ -1530,9 +1530,9 @@ val sandboxApp: Application.() -> Unit = { val size: Int = expectInt(call.request.queryParameters["size"] ?: "5") if (size < 1) throw badRequest("'size' param is less than 1") // Time range filter values - val fromMs = expectLong(call.request.queryParameters["from_ms"] ?: "0") + val fromMs: Long = expectLong(call.request.queryParameters["from_ms"] ?: "0") if (fromMs < 0) throw badRequest("'from_ms' param is less than 0") - val untilMs = expectLong(call.request.queryParameters["until_ms"] ?: Long.MAX_VALUE.toString()) + val untilMs: Long = expectLong(call.request.queryParameters["until_ms"] ?: Long.MAX_VALUE.toString()) if (untilMs < 0) throw badRequest("'until_ms' param is less than 0") val longPollMs: Long? = call.maybeLong("long_poll_ms") // LISTEN, if Postgres. diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt @@ -1,8 +1,6 @@ package tech.libeufin.sandbox import io.ktor.http.* -import org.jetbrains.exposed.sql.StdOutSqlLogger -import org.jetbrains.exposed.sql.addLogger import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.transactions.transaction import tech.libeufin.util.* @@ -140,9 +138,10 @@ fun wireTransfer( demobank: String = "default", subject: String, amount: String, // $currency:x.y - pmtInfId: String? = null + pmtInfId: String? = null, + endToEndId: String? = null ): String { - logger.debug("Maybe wire transfer: $debitAccount -> $creditAccount, $subject, $amount") + logger.debug("Maybe wire transfer (endToEndId: $endToEndId): $debitAccount -> $creditAccount, $subject, $amount") return transaction { val demobankDb = ensureDemobank(demobank) val debitAccountDb = getBankAccountFromLabel(debitAccount, demobankDb) @@ -167,7 +166,7 @@ fun wireTransfer( logger.error("Account ${debitAccountDb.label} would surpass debit threshold. Rollback wire transfer") throw SandboxError(HttpStatusCode.Conflict, "Insufficient funds") } - val timeStamp = getUTCnow().toInstant().toEpochMilli() + val timeStamp = getNowMillis() val transactionRef = getRandomString(8) BankAccountTransactionEntity.new { creditorIban = creditAccountDb.iban @@ -202,6 +201,7 @@ fun wireTransfer( direction = "DBIT" this.demobank = demobankDb this.pmtInfId = pmtInfId + this.endToEndId = endToEndId } // Adjusting the balances (acceptable debit conditions checked before). diff --git a/util/src/main/kotlin/time.kt b/util/src/main/kotlin/time.kt @@ -22,16 +22,19 @@ package tech.libeufin.util import java.time.* import java.time.format.DateTimeFormatter -private var LIBEUFIN_CLOCK = Clock.system(ZoneOffset.UTC) +private var LIBEUFIN_CLOCK = Clock.system(ZoneId.systemDefault()) fun setClock(rel: Duration) { LIBEUFIN_CLOCK = Clock.offset(LIBEUFIN_CLOCK, rel) } fun getNow(): ZonedDateTime { - return ZonedDateTime.now(LIBEUFIN_CLOCK) + return ZonedDateTime.now(ZoneId.systemDefault()) } -fun getUTCnow(): ZonedDateTime { - return ZonedDateTime.now(ZoneOffset.UTC) +fun getNowMillis(): Long = getNow().toInstant().toEpochMilli() + +fun getSystemTimeNow(): ZonedDateTime { + // return ZonedDateTime.now(ZoneOffset.UTC) + return ZonedDateTime.now(ZoneId.systemDefault()) } fun ZonedDateTime.toZonedString(): String { diff --git a/util/src/test/kotlin/TimeTest.kt b/util/src/test/kotlin/TimeTest.kt @@ -1,6 +1,7 @@ import org.junit.Ignore import org.junit.Test import tech.libeufin.util.getNow +import tech.libeufin.util.millis import tech.libeufin.util.setClock import java.time.* import java.time.format.DateTimeFormatter @@ -58,7 +59,8 @@ class TimeTest { val dtf = DateTimeFormatter.ISO_LOCAL_DATE return LocalDate.parse(dashedDate, dtf) } - val ret = parse("1970-01-01") + val ret: LocalDate = parse("1970-01-01") println(ret.toString()) + ret.millis() // Just testing it doesn't raise Exception. } } \ No newline at end of file