libeufin

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

commit be4ffcbcc55d656fbbf983de10d4e483949b127d
parent 47e778d7d05c43e3f6b489143b2163082bae127b
Author: Florian Dold <florian.dold@gmail.com>
Date:   Fri, 19 Jun 2020 17:55:35 +0530

allow timezones, use ISO time stamp for last fetch date

Diffstat:
Mnexus/src/main/kotlin/tech/libeufin/nexus/DB.kt | 14+++++++++++---
Mnexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt | 9+++++----
Mnexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt | 91++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt | 22+++++++++++++++++++---
Mnexus/src/test/kotlin/authentication.kt | 1+
Mutil/src/main/kotlin/Ebics.kt | 14++++++++++----
Mutil/src/main/kotlin/time.kt | 4++--
7 files changed, 120 insertions(+), 35 deletions(-)

diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt @@ -220,9 +220,14 @@ object NexusBankAccountsTable : IdTable<String>() { val bankCode = text("bankCode") val defaultBankConnection = reference("defaultBankConnection", NexusBankConnectionsTable).nullable() - val lastStatementCreationTimestamp = long("lastStatementCreationTimestamp").nullable() - val lastReportCreationTimestamp = long("lastReportCreationTimestamp").nullable() - val lastNotificationCreationTimestamp = long("lastNotificationCreationTimestamp").nullable() + // ISO-8601 zoned date time + val lastStatementCreationTimestamp = text("lastStatementCreationTimestamp").nullable() + + // ISO-8601 zoned date time + val lastReportCreationTimestamp = text("lastReportCreationTimestamp").nullable() + + // ISO-8601 zoned date time + val lastNotificationCreationTimestamp = text("lastNotificationCreationTimestamp").nullable() // Highest bank message ID that this bank account is aware of. val highestSeenBankMessageId = integer("highestSeenBankMessageId") @@ -239,6 +244,9 @@ class NexusBankAccountEntity(id: EntityID<String>) : Entity<String>(id) { var defaultBankConnection by NexusBankConnectionEntity optionalReferencedOn NexusBankAccountsTable.defaultBankConnection var highestSeenBankMessageId by NexusBankAccountsTable.highestSeenBankMessageId var pain001Counter by NexusBankAccountsTable.pain001Counter + var lastStatementCreationTimestamp by NexusBankAccountsTable.lastStatementCreationTimestamp + var lastReportCreationTimestamp by NexusBankAccountsTable.lastReportCreationTimestamp + var lastNotificationCreationTimestamp by NexusBankAccountsTable.lastNotificationCreationTimestamp } object EbicsSubscribersTable : IntIdTable() { diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt @@ -236,10 +236,10 @@ fun addPaymentInitiation(paymentData: Pain001Data, debitorAccount: NexusBankAcco suspend fun fetchBankAccountTransactions( client: HttpClient, fetchSpec: FetchSpecJson, - accountid: String + accountId: String ) { val res = transaction { - val acct = NexusBankAccountEntity.findById(accountid) + val acct = NexusBankAccountEntity.findById(accountId) if (acct == null) { throw NexusError( HttpStatusCode.NotFound, @@ -263,7 +263,8 @@ suspend fun fetchBankAccountTransactions( fetchEbicsBySpec( fetchSpec, client, - res.connectionName + res.connectionName, + accountId ) } else -> throw NexusError( @@ -271,6 +272,6 @@ suspend fun fetchBankAccountTransactions( "Connection type '${res.connectionType}' not implemented" ) } - ingestBankMessagesIntoAccount(res.connectionName, accountid) + ingestBankMessagesIntoAccount(res.connectionName, accountId) ingestTalerTransactions() } diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt @@ -51,8 +51,10 @@ import tech.libeufin.util.ebics_h004.HTDResponseOrderData import java.io.ByteArrayOutputStream import java.security.interfaces.RSAPrivateCrtKey import java.security.interfaces.RSAPublicKey -import java.time.LocalDate +import java.time.Instant import java.time.LocalDateTime +import java.time.ZoneOffset +import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import java.util.* import javax.crypto.EncryptedPrivateKeyInfo @@ -63,37 +65,88 @@ private data class EbicsFetchSpec( val orderParams: EbicsOrderParams ) -suspend fun fetchEbicsBySpec(fetchSpec: FetchSpecJson, client: HttpClient, bankConnectionId: String) { +suspend fun fetchEbicsBySpec( + fetchSpec: FetchSpecJson, + client: HttpClient, + bankConnectionId: String, + accountId: String +) { val subscriberDetails = transaction { getEbicsSubscriberDetails(bankConnectionId) } + val lastTimes = transaction { + val acct = NexusBankAccountEntity.findById(accountId) + if (acct == null) { + throw NexusError( + HttpStatusCode.NotFound, + "Account not found" + ) + } + object { + val lastStatement = acct.lastStatementCreationTimestamp?.let { + ZonedDateTime.parse(it, DateTimeFormatter.ISO_DATE_TIME) + } + val lastReport = acct.lastReportCreationTimestamp?.let { + ZonedDateTime.parse(it, DateTimeFormatter.ISO_DATE_TIME) + } + } + } val specs = mutableListOf<EbicsFetchSpec>() + + fun addForLevel(l: FetchLevel, p: EbicsOrderParams) { + when (fetchSpec.level) { + FetchLevel.ALL -> { + specs.add(EbicsFetchSpec("C52", p)) + specs.add(EbicsFetchSpec("C53", p)) + } + FetchLevel.REPORT -> { + specs.add(EbicsFetchSpec("C52", p)) + } + FetchLevel.STATEMENT -> { + specs.add(EbicsFetchSpec("C53", p)) + } + } + } + when (fetchSpec) { is FetchSpecLatestJson -> { val p = EbicsStandardOrderParams() - when (fetchSpec.level) { - FetchLevel.ALL -> { - specs.add(EbicsFetchSpec("C52", p)) - specs.add(EbicsFetchSpec("C53", p)) - } - FetchLevel.REPORT -> { - specs.add(EbicsFetchSpec("C52", p)) - } - FetchLevel.STATEMENT -> { - specs.add(EbicsFetchSpec("C53", p)) - } - } + addForLevel(fetchSpec.level, p) } is FetchSpecAllJson -> { - val p = EbicsStandardOrderParams(EbicsDateRange(LocalDate.EPOCH, LocalDate.now())) + val p = EbicsStandardOrderParams( + EbicsDateRange( + ZonedDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC), + ZonedDateTime.now(ZoneOffset.UTC) + ) + ) + addForLevel(fetchSpec.level, p) + } + is FetchSpecSinceLastJson -> { + val pRep = EbicsStandardOrderParams( + EbicsDateRange( + lastTimes.lastReport ?: ZonedDateTime.ofInstant( + Instant.EPOCH, + ZoneOffset.UTC + ), ZonedDateTime.now(ZoneOffset.UTC) + ) + ) + val pStmt = EbicsStandardOrderParams( + EbicsDateRange( + lastTimes.lastStatement ?: ZonedDateTime.ofInstant( + Instant.EPOCH, + ZoneOffset.UTC + ), ZonedDateTime.now(ZoneOffset.UTC) + ) + ) when (fetchSpec.level) { FetchLevel.ALL -> { - specs.add(EbicsFetchSpec("C52", p)) - specs.add(EbicsFetchSpec("C53", p)) + specs.add(EbicsFetchSpec("C52", pRep)) + specs.add(EbicsFetchSpec("C53", pStmt)) } FetchLevel.REPORT -> { - specs.add(EbicsFetchSpec("C52", p)) + specs.add(EbicsFetchSpec("C52", pRep)) } FetchLevel.STATEMENT -> { - specs.add(EbicsFetchSpec("C53", p)) + specs.add(EbicsFetchSpec("C53", pStmt)) } } } diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt @@ -26,7 +26,14 @@ import com.fasterxml.jackson.annotation.JsonValue import com.fasterxml.jackson.databind.JsonNode import tech.libeufin.nexus.BankTransaction import tech.libeufin.util.* -import java.time.LocalDate +import java.time.Instant +import java.time.ZoneId +import java.time.ZoneOffset +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.time.format.DateTimeFormatterBuilder +import java.time.temporal.ChronoField + data class BackupRequestJson( val passphrase: String @@ -62,6 +69,15 @@ class EbicsStandardOrderParamsEmptyJson : EbicsOrderParamsJson() { } } +object EbicsDateFormat { + var fmt = DateTimeFormatterBuilder() + .append(DateTimeFormatter.ISO_DATE) + .parseDefaulting(ChronoField.HOUR_OF_DAY, 0) + .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0) + .parseDefaulting(ChronoField.OFFSET_SECONDS, ZoneId.systemDefault().rules.getOffset(Instant.now()).totalSeconds.toLong()) + .toFormatter() +} + @JsonTypeName("standard-date-range") class EbicsStandardOrderParamsDateJson( val start: String, @@ -70,8 +86,8 @@ class EbicsStandardOrderParamsDateJson( override fun toOrderParams(): EbicsOrderParams { val dateRange: EbicsDateRange? = EbicsDateRange( - LocalDate.parse(this.start), - LocalDate.parse(this.end) + ZonedDateTime.parse(this.start, EbicsDateFormat.fmt), + ZonedDateTime.parse(this.end, EbicsDateFormat.fmt) ) return EbicsStandardOrderParams(dateRange) } diff --git a/nexus/src/test/kotlin/authentication.kt b/nexus/src/test/kotlin/authentication.kt @@ -2,6 +2,7 @@ package tech.libeufin.nexus import org.junit.Test import junit.framework.TestCase.assertEquals +import tech.libeufin.nexus.server.extractUserAndPassword class AuthenticationTest { @Test diff --git a/util/src/main/kotlin/Ebics.kt b/util/src/main/kotlin/Ebics.kt @@ -33,14 +33,15 @@ import java.math.BigInteger import java.security.SecureRandom import java.security.interfaces.RSAPrivateCrtKey import java.security.interfaces.RSAPublicKey -import java.time.LocalDate +import java.time.ZonedDateTime import java.util.* import java.util.zip.DeflaterInputStream import javax.xml.datatype.DatatypeFactory +import javax.xml.datatype.XMLGregorianCalendar data class EbicsProtocolError(val statusCode: HttpStatusCode, val reason: String) : Exception(reason) -data class EbicsDateRange(val start: LocalDate, val end: LocalDate) +data class EbicsDateRange(val start: ZonedDateTime, val end: ZonedDateTime) sealed class EbicsOrderParams @@ -86,6 +87,11 @@ private fun getNonce(size: Int): ByteArray { return ret } +private fun getXmlDate(d: ZonedDateTime): XMLGregorianCalendar { + return DatatypeFactory.newInstance() + .newXMLGregorianCalendar(d.year, d.monthValue, d.dayOfMonth, 0, 0, 0, 0, d.offset.totalSeconds / 60) +} + private fun makeOrderParams(orderParams: EbicsOrderParams): EbicsRequest.OrderParams { return when (orderParams) { is EbicsStandardOrderParams -> { @@ -93,8 +99,8 @@ private fun makeOrderParams(orderParams: EbicsOrderParams): EbicsRequest.OrderPa val r = orderParams.dateRange if (r != null) { this.dateRange = EbicsRequest.DateRange().apply { - this.start = DatatypeFactory.newInstance().newXMLGregorianCalendar(r.start.toString()) - this.end = DatatypeFactory.newInstance().newXMLGregorianCalendar(r.end.toString()) + this.start = getXmlDate(r.start) + this.end = getXmlDate(r.end) } } } diff --git a/util/src/main/kotlin/time.kt b/util/src/main/kotlin/time.kt @@ -27,11 +27,11 @@ fun LocalDateTime.toZonedString(): String { } fun LocalDateTime.toDashedDate(): String { - return DateTimeFormatter.ISO_DATE.format(this) + return DateTimeFormatter.ISO_OFFSET_DATE.format(this) } fun parseDashedDate(date: String): LocalDateTime { - val dtf = DateTimeFormatter.ISO_DATE + val dtf = DateTimeFormatter.ISO_OFFSET_DATE val asDate = LocalDate.parse(date, dtf) return asDate.atStartOfDay() }