libeufin

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

commit d61fa04108b0a278b42877b6d9cf70f3d70f8cf3
parent b171cf9cd2d1607e68580bbc7e65136ae186cdd3
Author: MS <ms@taler.net>
Date:   Thu,  2 Sep 2021 21:53:46 +0000

Implement balance accounting at Nexus.

Nexus never calculates balances, but stores the amount as
returned by the bank each time it downloads a "history" message.

The API /bank-accounts/{my-acct} got extended to include the balance.

Diffstat:
Mnexus/src/main/kotlin/tech/libeufin/nexus/DB.kt | 14+++++++-------
Mnexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt | 39+++++++++++++++++++++++++++++++++------
Mnexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt | 1-
Mnexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Iso20022.kt | 4++++
Mnexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt | 3+++
Mnexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt | 12++++++++++++
6 files changed, 59 insertions(+), 14 deletions(-)

diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt @@ -113,7 +113,7 @@ class TalerIncomingPaymentEntity(id: EntityID<Long>) : LongEntity(id) { } /** - * This table logs all the balances as returned by the bank for one particular bank account. + * This table logs all the balances as returned by the bank for all the bank accounts. */ object NexusBankBalancesTable : LongIdTable() { /** @@ -123,19 +123,16 @@ object NexusBankBalancesTable : LongIdTable() { */ val balance = text("balance") // $currency:x.y val creditDebitIndicator = text("creditDebitIndicator") // CRDT or DBIT. - /** - * Message downloaded from the bank. Must be of "history" type. - */ - val bankMessage = reference("bankMessage", NexusBankMessagesTable) val bankAccount = reference("bankAccount", NexusBankAccountsTable) + val date = text("date") // in the YYYY-MM-DD format } class NexusBankBalanceEntity(id: EntityID<Long>) : LongEntity(id) { companion object : LongEntityClass<NexusBankBalanceEntity>(NexusBankBalancesTable) var balance by NexusBankBalancesTable.balance var creditDebitIndicator by NexusBankBalancesTable.creditDebitIndicator - var bankMessage by NexusBankMessageEntity referencedOn NexusBankBalancesTable.bankMessage var bankAccount by NexusBankAccountEntity referencedOn NexusBankBalancesTable.bankAccount + var date by NexusBankBalancesTable.date } /** @@ -497,11 +494,13 @@ fun dbDropTables(dbConnectionString: String) { TalerInvalidIncomingPaymentsTable, NexusBankConnectionsTable, NexusBankMessagesTable, + NexusBankBalancesTable, FacadesTable, FacadeStateTable, NexusScheduledTasksTable, OfferedBankAccountsTable, NexusPermissionsTable, + AnastasisIncomingPaymentsTable ) } } @@ -516,6 +515,7 @@ fun dbCreateTables(dbConnectionString: String) { PaymentInitiationsTable, NexusEbicsSubscribersTable, NexusBankAccountsTable, + NexusBankBalancesTable, NexusBankTransactionsTable, AnastasisIncomingPaymentsTable, TalerIncomingPaymentsTable, @@ -526,7 +526,7 @@ fun dbCreateTables(dbConnectionString: String) { NexusBankMessagesTable, FacadesTable, OfferedBankAccountsTable, - NexusPermissionsTable, + NexusPermissionsTable ) } } 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,7 @@ import tech.libeufin.nexus.iso20022.parseCamtMessage import tech.libeufin.nexus.server.FetchSpecJson import tech.libeufin.nexus.server.Pain001Data import tech.libeufin.nexus.server.requireBankConnection +import tech.libeufin.nexus.server.toPlainString import tech.libeufin.util.XMLUtil import java.time.Instant import java.time.ZonedDateTime @@ -127,7 +128,7 @@ private fun findDuplicate(bankAccountId: String, acctSvcrRef: String): NexusBank * NOTE: this type can be used BOTH for one Camt document OR * for a set of those. */ -data class CamtProcessingResult( +data class CamtTransactionsCount( /** * Number of transactions that are new to the database. * Note that transaction T can be downloaded multiple times; @@ -143,7 +144,10 @@ data class CamtProcessingResult( */ val downloadedTransactions: Int ) -fun processCamtMessage(bankAccountId: String, camtDoc: Document, code: String): CamtProcessingResult { + +fun processCamtMessage( + bankAccountId: String, camtDoc: Document, code: String +): CamtTransactionsCount { logger.info("processing CAMT message") var newTransactions = 0 var downloadedTransactions = 0 @@ -159,6 +163,29 @@ fun processCamtMessage(bankAccountId: String, camtDoc: Document, code: String): newTransactions = -1 return@transaction } + res.reports.forEach { + NexusAssert( + it.account.iban == acct.iban, + "Neuxs hit a report or statement of a wrong IBAN!" + ) + it.balances.forEach { b -> + var clbdCount = 0 + if (b.type == "CLBD") { + clbdCount++ + NexusBankBalanceEntity.new { + bankAccount = acct + balance = b.amount.toPlainString() + creditDebitIndicator = b.creditDebitIndicator.name + date = b.date + } + } + if (clbdCount == 0) { + logger.warn("The bank didn't return ANY CLBD balances," + + " in the message: ${res.messageId}. Please clarify!") + } + } + } + val stamp = ZonedDateTime.parse(res.creationDateTime, DateTimeFormatter.ISO_DATE_TIME).toInstant().toEpochMilli() when (code) { @@ -226,7 +253,7 @@ fun processCamtMessage(bankAccountId: String, camtDoc: Document, code: String): } } } - return CamtProcessingResult( + return CamtTransactionsCount( newTransactions = newTransactions, downloadedTransactions = downloadedTransactions ) @@ -236,7 +263,7 @@ fun processCamtMessage(bankAccountId: String, camtDoc: Document, code: String): * Create new transactions for an account based on bank messages it * did not see before. */ -fun ingestBankMessagesIntoAccount(bankConnectionId: String, bankAccountId: String): CamtProcessingResult { +fun ingestBankMessagesIntoAccount(bankConnectionId: String, bankAccountId: String): CamtTransactionsCount { var totalNew = 0 var downloadedTransactions = 0 transaction { @@ -267,7 +294,7 @@ fun ingestBankMessagesIntoAccount(bankConnectionId: String, bankAccountId: Strin acct.highestSeenBankMessageSerialId = lastId } // return totalNew - return CamtProcessingResult( + return CamtTransactionsCount( newTransactions = totalNew, downloadedTransactions = downloadedTransactions ) @@ -318,7 +345,7 @@ fun addPaymentInitiation(paymentData: Pain001Data, debtorAccount: NexusBankAccou suspend fun fetchBankAccountTransactions( client: HttpClient, fetchSpec: FetchSpecJson, accountId: String -): CamtProcessingResult { +): CamtTransactionsCount { val res = transaction { val acct = NexusBankAccountEntity.findByName(accountId) if (acct == null) { diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt @@ -70,7 +70,6 @@ private data class EbicsFetchSpec( val orderParams: EbicsOrderParams ) -// Moved eventually in a tucked "camt" file. fun storeCamt(bankConnectionId: String, camt: String, historyType: String) { val camt53doc = XMLUtil.parseStringIntoDom(camt) val msgId = camt53doc.pickStringWithRootNs("/*[1]/*[1]/root:GrpHdr/root:MsgId") diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Iso20022.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Iso20022.kt @@ -110,6 +110,10 @@ data class CamtParseResult( val messageType: CashManagementResponseType, val messageId: String, val creationDateTime: String, + /** + * One Camt document can contain multiple reports/statements + * for each account being owned by the requester. + */ val reports: List<CamtReport> ) diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt @@ -441,6 +441,9 @@ data class CurrencyAmount( val currency: String, val value: BigDecimal // allows calculations ) +fun CurrencyAmount.toPlainString(): String { + return "${this.currency}:${this.value.toPlainString()}" +} data class InitiatedPayments( val initiatedPayments: MutableList<PaymentStatus> = mutableListOf() diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt @@ -563,9 +563,21 @@ fun serverMain(host: String, port: Int) { throw NexusError(HttpStatusCode.NotFound, "unknown bank account") } val holderEnc = URLEncoder.encode(bankAccount.accountHolder, "UTF-8") + val lastSeenBalance = NexusBankBalanceEntity.find { + NexusBankBalancesTable.bankAccount eq bankAccount.id + }.lastOrNull() return@transaction makeJsonObject { prop("defaultBankConnection", bankAccount.defaultBankConnection?.id?.value) prop("accountPaytoUri", "payto://iban/${bankAccount.iban}?receiver-name=$holderEnc") + prop( + "lastSeenBalance", + if (lastSeenBalance != null) { + val sign = if (lastSeenBalance.creditDebitIndicator == "DBIT") "-" else "" + "${sign}${lastSeenBalance.balance}" + } else { + "not downloaded from the bank yet" + } + ) } } call.respond(res)