commit ce82d2aea3a2919d019bd37c89e4d4346811fc17
parent be4ffcbcc55d656fbbf983de10d4e483949b127d
Author: Florian Dold <florian.dold@gmail.com>
Date: Fri, 19 Jun 2020 18:23:30 +0530
store date
Diffstat:
6 files changed, 54 insertions(+), 18 deletions(-)
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt
@@ -220,14 +220,11 @@ object NexusBankAccountsTable : IdTable<String>() {
val bankCode = text("bankCode")
val defaultBankConnection = reference("defaultBankConnection", NexusBankConnectionsTable).nullable()
- // ISO-8601 zoned date time
- val lastStatementCreationTimestamp = text("lastStatementCreationTimestamp").nullable()
+ val lastStatementCreationTimestamp = long("lastStatementCreationTimestamp").nullable()
- // ISO-8601 zoned date time
- val lastReportCreationTimestamp = text("lastReportCreationTimestamp").nullable()
+ val lastReportCreationTimestamp = long("lastReportCreationTimestamp").nullable()
- // ISO-8601 zoned date time
- val lastNotificationCreationTimestamp = text("lastNotificationCreationTimestamp").nullable()
+ val lastNotificationCreationTimestamp = long("lastNotificationCreationTimestamp").nullable()
// Highest bank message ID that this bank account is aware of.
val highestSeenBankMessageId = integer("highestSeenBankMessageId")
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt
@@ -513,14 +513,20 @@ private fun XmlElementDestructor.extractInnerTransactions(): List<BankTransactio
}
}
+data class CamtParseResult(
+ val transactions: List<BankTransaction>,
+ val messageId: String,
+ val creationDateTime: String
+)
+
/**
* Extract a list of transactions from an ISO20022 camt.052 / camt.053 message.
*/
-fun getTransactions(doc: Document): List<BankTransaction> {
+fun parseCamtMessage(doc: Document): CamtParseResult {
return destructXml(doc) {
requireRootElement("Document") {
// Either bank to customer statement or report
- requireOnlyChild {
+ val transactions = requireOnlyChild {
when (it.localName) {
"BkToCstmrAcctRpt" -> {
mapEachChildNamed("Rpt") {
@@ -536,7 +542,18 @@ fun getTransactions(doc: Document): List<BankTransaction> {
throw CamtParsingError("expected statement or report")
}
}
+ }.flatten()
+ val messageId = requireOnlyChild {
+ requireUniqueChildNamed("GrpHdr") {
+ requireUniqueChildNamed("MsgId") { it.textContent }
+ }
}
+ val creationDateTime = requireOnlyChild {
+ requireUniqueChildNamed("GrpHdr") {
+ requireUniqueChildNamed("CreDtTm") { it.textContent }
+ }
+ }
+ CamtParseResult(transactions, messageId, creationDateTime)
}
- }.flatten()
+ }
}
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
@@ -33,6 +33,8 @@ import tech.libeufin.nexus.server.FetchSpecJson
import tech.libeufin.nexus.server.Pain001Data
import tech.libeufin.util.XMLUtil
import java.time.Instant
+import java.time.ZonedDateTime
+import java.time.format.DateTimeFormatter
suspend fun submitPaymentInitiation(httpClient: HttpClient, paymentInitiationId: Long) {
@@ -104,7 +106,8 @@ private fun findDuplicate(bankAccountId: String, acctSvcrRef: String): NexusBank
fun processCamtMessage(
bankAccountId: String,
- camtDoc: Document
+ camtDoc: Document,
+ code: String
) {
logger.info("processing CAMT message")
transaction {
@@ -112,7 +115,26 @@ fun processCamtMessage(
if (acct == null) {
throw NexusError(HttpStatusCode.NotFound, "user not found")
}
- val transactions = getTransactions(camtDoc)
+ val res = parseCamtMessage(camtDoc)
+
+ val stamp = ZonedDateTime.parse(res.creationDateTime, DateTimeFormatter.ISO_DATE_TIME).toInstant().toEpochMilli()
+
+ when (code) {
+ "C52" -> {
+ val s = acct.lastReportCreationTimestamp
+ if (s != null && stamp > s) {
+ acct.lastReportCreationTimestamp = stamp
+ }
+ }
+ "C53" -> {
+ val s = acct.lastStatementCreationTimestamp
+ if (s != null && stamp > s) {
+ acct.lastStatementCreationTimestamp = stamp
+ }
+ }
+ }
+
+ val transactions = res.transactions
logger.info("found ${transactions.size} transactions")
txloop@ for (tx in transactions) {
val acctSvcrRef = tx.accountServicerReference
@@ -183,7 +205,7 @@ fun ingestBankMessagesIntoAccount(
}.orderBy(Pair(NexusBankMessagesTable.id, SortOrder.ASC)).forEach {
// FIXME: check if it's CAMT first!
val doc = XMLUtil.parseStringIntoDom(it.message.bytes.toString(Charsets.UTF_8))
- processCamtMessage(bankAccountId, doc)
+ processCamtMessage(bankAccountId, doc, it.code)
lastId = it.id.value
}
acct.highestSeenBankMessageId = lastId
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt
@@ -82,10 +82,10 @@ suspend fun fetchEbicsBySpec(
}
object {
val lastStatement = acct.lastStatementCreationTimestamp?.let {
- ZonedDateTime.parse(it, DateTimeFormatter.ISO_DATE_TIME)
+ ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneOffset.UTC)
}
val lastReport = acct.lastReportCreationTimestamp?.let {
- ZonedDateTime.parse(it, DateTimeFormatter.ISO_DATE_TIME)
+ ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneOffset.UTC)
}
}
}
diff --git a/nexus/src/test/kotlin/Iso20022Test.kt b/nexus/src/test/kotlin/Iso20022Test.kt
@@ -18,8 +18,8 @@ class Iso20022Test {
@Test
fun testTransactionsImport() {
val camt53 = loadXmlResource("iso20022-samples/camt.053.001.02.gesamtbeispiel.xml")
- val txs = getTransactions(camt53)
- for (tx in txs) {
+ val r = parseCamtMessage(camt53)
+ for (tx in r.transactions) {
val txStr = jacksonObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(tx)
println(txStr)
val tx2 = jacksonObjectMapper().readValue(txStr, BankTransaction::class.java)
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_OFFSET_DATE.format(this)
+ return DateTimeFormatter.ISO_DATE.format(this)
}
fun parseDashedDate(date: String): LocalDateTime {
- val dtf = DateTimeFormatter.ISO_OFFSET_DATE
+ val dtf = DateTimeFormatter.ISO_DATE
val asDate = LocalDate.parse(date, dtf)
return asDate.atStartOfDay()
}