summaryrefslogtreecommitdiff
path: root/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt
diff options
context:
space:
mode:
authorMS <ms@taler.net>2022-12-20 17:25:48 +0100
committerMS <ms@taler.net>2022-12-20 17:25:48 +0100
commitffd714d263da95e59088906e1d7fe5165e39985c (patch)
treea8de6af6dc3daf019358f0ad1dd86096d688dfcb /sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt
parentb0d98e47e6e92e6ec179148f886422c241ecac53 (diff)
downloadlibeufin-ffd714d263da95e59088906e1d7fe5165e39985c.tar.gz
libeufin-ffd714d263da95e59088906e1d7fe5165e39985c.tar.bz2
libeufin-ffd714d263da95e59088906e1d7fe5165e39985c.zip
balance (1) and debit limit (2)
1. Get balances using the last statement as the base, then optionally adding pending transactions to it. 2. Enforce debit limit for wire transfers and Taler withdrawals.
Diffstat (limited to 'sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt')
-rw-r--r--sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt194
1 files changed, 156 insertions, 38 deletions
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt
index 852e4b39..74c04bfe 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt
@@ -2,57 +2,175 @@ package tech.libeufin.sandbox
import io.ktor.http.*
import org.jetbrains.exposed.sql.and
-import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
-import org.jetbrains.exposed.sql.transactions.experimental.suspendedTransactionAsync
import org.jetbrains.exposed.sql.transactions.transaction
-import org.slf4j.Logger
-import org.slf4j.LoggerFactory
import tech.libeufin.util.*
import java.math.BigDecimal
-// Mainly useful inside the Camt generator.
-fun balanceForAccount(
- history: MutableList<RawPayment>,
- baseBalance: BigDecimal
+/**
+ * The last balance is the one mentioned in the bank account's
+ * last statement. If the bank account does not have any statement
+ * yet, then zero is returned. When 'withPending' is true, it adds
+ * the pending transactions to it.
+ */
+fun getBalance(
+ bankAccount: BankAccountEntity,
+ withPending: Boolean = true
): BigDecimal {
- var ret = baseBalance
- history.forEach direction@ {
- if (it.direction == "CRDT") {
- val amount = parseDecimal(it.amount)
- ret += amount
- return@direction
+ val lastStatement = transaction {
+ BankAccountStatementEntity.find {
+ BankAccountStatementsTable.bankAccount eq bankAccount.id
+ }.lastOrNull()
+ }
+ var lastBalance = if (lastStatement == null) {
+ BigDecimal.ZERO
+ } else { BigDecimal(lastStatement.balanceClbd) }
+ if (!withPending) return lastBalance
+ /**
+ * Caller asks to include the pending transactions in the
+ * balance. The block below gets the transactions happened
+ * later than the last statement and adds them to the balance
+ * that was calculated so far.
+ */
+ transaction {
+ val pendingTransactions = BankAccountTransactionEntity.find {
+ BankAccountTransactionsTable.account eq bankAccount.id and (
+ BankAccountTransactionsTable.date.greater(lastStatement?.creationTime ?: 0L))
}
- if (it.direction == "DBIT") {
- val amount = parseDecimal(it.amount)
- ret -= amount
- return@direction
+ pendingTransactions.forEach { tx ->
+ when (tx.direction) {
+ "DBIT" -> lastBalance -= parseDecimal(tx.amount)
+ "CRDT" -> lastBalance += parseDecimal(tx.amount)
+ else -> {
+ logger.error("Transaction ${tx.id} is neither debit nor credit.")
+ throw SandboxError(
+ HttpStatusCode.InternalServerError,
+ "Error in transactions state."
+ )
+ }
+ }
}
- throw SandboxError(
- HttpStatusCode.InternalServerError,
- "A payment direction was found neither CRDT nor DBIT",
- LibeufinErrorCode.LIBEUFIN_EC_INVALID_STATE
+ }
+ return lastBalance
+}
+
+// Wrapper offering to get bank accounts from a string.
+fun getBalance(accountLabel: String, withPending: Boolean = false): BigDecimal {
+ val account = transaction {
+ BankAccountEntity.find { BankAccountsTable.label.eq(accountLabel) }.firstOrNull()
+ }
+ if (account == null) throw notFound("Bank account $accountLabel not found")
+ return getBalance(account, withPending)
+}
+
+fun wireTransfer(
+ debitAccount: String,
+ creditAccount: String,
+ demobank: String,
+ subject: String,
+ amount: String, // $currency:x.y
+ pmtInfId: String? = null
+): String {
+ val args: Triple<BankAccountEntity, BankAccountEntity, DemobankConfigEntity> = transaction {
+ val debitAccountDb = BankAccountEntity.find {
+ BankAccountsTable.label eq debitAccount
+ }.firstOrNull() ?: throw SandboxError(
+ HttpStatusCode.NotFound,
+ "Debit account '$debitAccount' not found"
+ )
+ val creditAccountDb = BankAccountEntity.find {
+ BankAccountsTable.label eq creditAccount
+ }.firstOrNull() ?: throw SandboxError(
+ HttpStatusCode.NotFound,
+ "Credit account '$creditAccount' not found"
+ )
+ val demoBank = DemobankConfigEntity.find {
+ DemobankConfigsTable.name eq demobank
+ }.firstOrNull() ?: throw SandboxError(
+ HttpStatusCode.NotFound,
+ "Demobank '$demobank' not found"
)
+
+ Triple(debitAccountDb, creditAccountDb, demoBank)
}
- return ret
+
+ return wireTransfer(
+ debitAccount = args.first,
+ creditAccount = args.second,
+ demobank = args.third,
+ subject = subject,
+ amount = amount,
+ pmtInfId
+ )
}
+/**
+ * Book a CRDT and a DBIT transaction and return the unique reference thereof.
+ *
+ * At the moment there is redundancy because all the creditor / debtor details
+ * are contained (directly or indirectly) already in the BankAccount parameters.
+ *
+ * This is kept both not to break the existing tests and to allow future versions
+ * where one party of the transaction is not a customer of the running Sandbox.
+ */
-fun balanceForAccount(bankAccount: BankAccountEntity): BigDecimal {
- var balance = BigDecimal.ZERO
+fun wireTransfer(
+ debitAccount: BankAccountEntity,
+ creditAccount: BankAccountEntity,
+ demobank: DemobankConfigEntity,
+ subject: String,
+ amount: String, // $currency:$value
+ pmtInfId: String? = null
+): String {
+ val checkAmount = parseAmount(amount)
+ if (checkAmount.amount == BigDecimal.ZERO)
+ throw badRequest("Wire transfers of zero not possible.")
+ if (checkAmount.currency != demobank.currency)
+ throw badRequest("Won't wire transfer with currency: ${checkAmount.currency}")
+ // Check funds are sufficient.
+ val pendingBalance = getBalance(debitAccount, withPending = true)
+ val maxDebt = if (debitAccount.label == "bank") {
+ demobank.bankDebtLimit
+ } else demobank.usersDebtLimit
+ if ((pendingBalance - checkAmount.amount).abs() > BigDecimal.valueOf(maxDebt.toLong())) {
+ logger.info("Account ${debitAccount.label} would surpass debit threshold of $maxDebt. Rollback wire transfer")
+ throw SandboxError(HttpStatusCode.PreconditionFailed, "Insufficient funds")
+ }
+ val timeStamp = getUTCnow().toInstant().toEpochMilli()
+ val transactionRef = getRandomString(8)
transaction {
- BankAccountTransactionEntity.find {
- BankAccountTransactionsTable.direction eq "CRDT" and (
- BankAccountTransactionsTable.account eq bankAccount.id)
- }.forEach {
- val amount = parseDecimal(it.amount)
- balance += amount
+ BankAccountTransactionEntity.new {
+ creditorIban = creditAccount.iban
+ creditorBic = creditAccount.bic
+ this.creditorName = getPersonNameFromCustomer(creditAccount.owner)
+ debtorIban = debitAccount.iban
+ debtorBic = debitAccount.bic
+ debtorName = getPersonNameFromCustomer(debitAccount.owner)
+ this.subject = subject
+ this.amount = checkAmount.amount.toPlainString()
+ this.currency = demobank.currency
+ date = timeStamp
+ accountServicerReference = transactionRef
+ account = creditAccount
+ direction = "CRDT"
+ this.demobank = demobank
+ this.pmtInfId = pmtInfId
}
- BankAccountTransactionEntity.find {
- BankAccountTransactionsTable.direction eq "DBIT" and (
- BankAccountTransactionsTable.account eq bankAccount.id)
- }.forEach {
- val amount = parseDecimal(it.amount)
- balance -= amount
+ BankAccountTransactionEntity.new {
+ creditorIban = creditAccount.iban
+ creditorBic = creditAccount.bic
+ this.creditorName = getPersonNameFromCustomer(creditAccount.owner)
+ debtorIban = debitAccount.iban
+ debtorBic = debitAccount.bic
+ debtorName = getPersonNameFromCustomer(debitAccount.owner)
+ this.subject = subject
+ this.amount = checkAmount.amount.toPlainString()
+ this.currency = demobank.currency
+ date = timeStamp
+ accountServicerReference = transactionRef
+ account = debitAccount
+ direction = "DBIT"
+ this.demobank = demobank
+ this.pmtInfId = pmtInfId
}
}
- return balance
+ return transactionRef
} \ No newline at end of file