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:
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)