diff options
-rw-r--r-- | database-versioning/new/procedures.sql | 20 | ||||
-rw-r--r-- | sandbox/src/main/kotlin/tech/libeufin/sandbox/Database.kt | 123 | ||||
-rw-r--r-- | sandbox/src/test/kotlin/DatabaseTest.kt | 72 |
3 files changed, 186 insertions, 29 deletions
diff --git a/database-versioning/new/procedures.sql b/database-versioning/new/procedures.sql index 17fbde32..8fd3c8c1 100644 --- a/database-versioning/new/procedures.sql +++ b/database-versioning/new/procedures.sql @@ -330,4 +330,24 @@ SET WHERE bank_account_id=in_creditor_account_id; RETURN; END $$; + +CREATE OR REPLACE FUNCTION cashout_delete( + IN in_cashout_uuid UUID, + OUT out_already_confirmed BOOLEAN +) +LANGUAGE plpgsql +AS $$ +BEGIN + PERFORM + FROM cashout_operations + WHERE cashout_uuid=in_cashout_uuid AND tan_confirmation_time IS NOT NULL; + IF FOUND + THEN + out_already_confirmed=TRUE; + RETURN; + END IF; + out_already_confirmed=FALSE; + DELETE FROM cashout_operations WHERE cashout_uuid=in_cashout_uuid; + RETURN; +END $$; COMMIT; diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Database.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Database.kt index ec6a3196..243efe2c 100644 --- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Database.kt +++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Database.kt @@ -330,9 +330,18 @@ class Database(private val dbConfig: String) { val rs = stmt.executeQuery() rs.use { if (!rs.next()) throw internalServerError("Bank transaction didn't properly return") - if (rs.getBoolean("out_nx_debtor")) return BankTransactionResult.NO_DEBTOR - if (rs.getBoolean("out_nx_creditor")) return BankTransactionResult.NO_CREDITOR - if (rs.getBoolean("out_balance_insufficient")) return BankTransactionResult.CONFLICT + if (rs.getBoolean("out_nx_debtor")) { + logger.error("No debtor account found") + return BankTransactionResult.NO_DEBTOR + } + if (rs.getBoolean("out_nx_creditor")) { + logger.error("No creditor account found") + return BankTransactionResult.NO_CREDITOR + } + if (rs.getBoolean("out_balance_insufficient")) { + logger.error("Balance insufficient") + return BankTransactionResult.CONFLICT + } return BankTransactionResult.SUCCESS } } @@ -509,7 +518,7 @@ class Database(private val dbConfig: String) { ,bank_account ,cashout_address ,cashout_currency - ) + ) VALUES ( ? ,(?,?)::taler_amount @@ -548,6 +557,112 @@ class Database(private val dbConfig: String) { return myExecute(stmt) } + fun cashoutConfirm( + opUuid: UUID, + tanConfirmationTimestamp: Long, + bankTransaction: Long // regional payment backing the operation + ): Boolean { + reconnect() + val stmt = prepare(""" + UPDATE cashout_operations + SET tan_confirmation_time = ?, local_transaction = ? + WHERE cashout_uuid=?; + """) + stmt.setLong(1, tanConfirmationTimestamp) + stmt.setLong(2, bankTransaction) + stmt.setObject(3, opUuid) + return myExecute(stmt) + } + // used by /abort + enum class CashoutDeleteResult { + SUCCESS, + CONFLICT_ALREADY_CONFIRMED + } + fun cashoutDelete(opUuid: UUID): CashoutDeleteResult { + val stmt = prepare(""" + SELECT out_already_confirmed + FROM cashout_delete(?) + """) + stmt.setObject(1, opUuid) + stmt.executeQuery().use { + if (!it.next()) { + throw internalServerError("Cashout deletion gave no result") + } + if (it.getBoolean("out_already_confirmed")) return CashoutDeleteResult.CONFLICT_ALREADY_CONFIRMED + return CashoutDeleteResult.SUCCESS + } + } + fun cashoutGetFromUuid(opUuid: UUID): Cashout? { + val stmt = prepare(""" + SELECT + (amount_debit).val as amount_debit_val + ,(amount_debit).frac as amount_debit_frac + ,(amount_credit).val as amount_credit_val + ,(amount_credit).frac as amount_credit_frac + ,buy_at_ratio + ,(buy_in_fee).val as buy_in_fee_val + ,(buy_in_fee).frac as buy_in_fee_frac + ,sell_at_ratio + ,(sell_out_fee).val as sell_out_fee_val + ,(sell_out_fee).frac as sell_out_fee_frac + ,subject + ,creation_time + ,tan_channel + ,tan_code + ,bank_account + ,cashout_address + ,cashout_currency + ,tan_confirmation_time + ,local_transaction + FROM cashout_operations + WHERE cashout_uuid=?; + """) + stmt.setObject(1, opUuid) + stmt.executeQuery().use { + if (!it.next()) return null + return Cashout( + amountDebit = TalerAmount( + value = it.getLong("amount_debit_val"), + frac = it.getInt("amount_debit_frac") + ), + amountCredit = TalerAmount( + value = it.getLong("amount_credit_val"), + frac = it.getInt("amount_credit_frac") + ), + bankAccount = it.getLong("bank_account"), + buyAtRatio = it.getInt("buy_at_ratio"), + buyInFee = TalerAmount( + value = it.getLong("buy_in_fee_val"), + frac = it.getInt("buy_in_fee_frac") + ), + cashoutAddress = it.getString("cashout_address"), + cashoutCurrency = it.getString("cashout_currency"), + cashoutUuid = opUuid, + creationTime = it.getLong("creation_time"), + sellAtRatio = it.getInt("sell_at_ratio"), + sellOutFee = TalerAmount( + value = it.getLong("sell_out_fee_val"), + frac = it.getInt("sell_out_fee_frac") + ), + subject = it.getString("subject"), + tanChannel = it.getString("tan_channel").run { + when(this) { + "sms" -> TanChannel.sms + "email" -> TanChannel.email + "file" -> TanChannel.file + else -> throw internalServerError("TAN channel $this unsupported") + } + }, + tanCode = it.getString("tan_code"), + localTransaction = it.getLong("local_transaction"), + tanConfirmationTime = it.getLong("tan_confirmation_time").run { + if (this == 0L) return@run null + return@run this + } + ) + } + } + // NOTE: EBICS not needed for BFH and NB. } diff --git a/sandbox/src/test/kotlin/DatabaseTest.kt b/sandbox/src/test/kotlin/DatabaseTest.kt index cbe59d08..7716cd26 100644 --- a/sandbox/src/test/kotlin/DatabaseTest.kt +++ b/sandbox/src/test/kotlin/DatabaseTest.kt @@ -50,28 +50,27 @@ class DatabaseTest { throwIfFails = true ) val db = Database("jdbc:postgresql:///libeufincheck") - // Need accounts first. - db.customerCreate(customerFoo) - db.customerCreate(customerBar) - db.bankAccountCreate(bankAccountFoo) - db.bankAccountCreate(bankAccountBar) - db.bankAccountSetMaxDebt( - "foo", - TalerAmount(100, 0) - ) - db.bankAccountSetMaxDebt( - "bar", - TalerAmount(50, 0) - ) return db } @Test fun bankTransactionsTest() { val db = initDb() + assert(db.customerCreate(customerFoo)) + assert(db.customerCreate(customerBar)) + assert(db.bankAccountCreate(bankAccountFoo)) + assert(db.bankAccountCreate(bankAccountBar)) var fooAccount = db.bankAccountGetFromLabel("foo") assert(fooAccount?.hasDebt == false) // Foo has NO debit. // Preparing the payment data. + db.bankAccountSetMaxDebt( + "foo", + TalerAmount(100, 0) + ) + db.bankAccountSetMaxDebt( + "bar", + TalerAmount(50, 0) + ) val fooPaysBar = BankInternalTransaction( creditorAccountId = 2, debtorAccountId = 1, @@ -163,17 +162,9 @@ class DatabaseTest { fun bankAccountTest() { val db = initDb() assert(db.bankAccountGetFromLabel("foo") == null) - val bankAccount = BankAccount( - iban = "not used", - bic = "not used", - bankAccountLabel = "foo", - lastNexusFetchRowId = 1L, - owningCustomerId = 1L, - hasDebt = false - ) - db.customerCreate(customerFoo) // Satisfies the REFERENCE - assert(db.bankAccountCreate(bankAccount)) - assert(!db.bankAccountCreate(bankAccount)) // Triggers conflict. + assert(db.customerCreate(customerFoo)) + assert(db.bankAccountCreate(bankAccountFoo)) + assert(!db.bankAccountCreate(bankAccountFoo)) // Triggers conflict. assert(db.bankAccountGetFromLabel("foo")?.bankAccountLabel == "foo") assert(db.bankAccountGetFromLabel("foo")?.balance?.equals(TalerAmount(0, 0)) == true) } @@ -182,6 +173,8 @@ class DatabaseTest { fun withdrawalTest() { val db = initDb() val uuid = UUID.randomUUID() + assert(db.customerCreate(customerFoo)) + assert(db.bankAccountCreate(bankAccountFoo)) // insert new. assert(db.talerWithdrawalCreate( uuid, @@ -234,6 +227,35 @@ class DatabaseTest { tanChannel = TanChannel.sms, tanCode = "secret", ) + assert(db.customerCreate(customerFoo)) + assert(db.bankAccountCreate(bankAccountFoo)) + assert(db.customerCreate(customerBar)) + assert(db.bankAccountCreate(bankAccountBar)) assert(db.cashoutCreate(op)) - } + val fromDb = db.cashoutGetFromUuid(op.cashoutUuid) + assert(fromDb?.subject == op.subject && fromDb.tanConfirmationTime == null) + assert(db.cashoutDelete(op.cashoutUuid) == Database.CashoutDeleteResult.SUCCESS) + assert(db.cashoutCreate(op)) + db.bankAccountSetMaxDebt( + "foo", + TalerAmount(100, 0) + ) + assert(db.bankTransactionCreate(BankInternalTransaction( + creditorAccountId = 2, + debtorAccountId = 1, + subject = "backing the cash-out", + amount = TalerAmount(10, 0), + accountServicerReference = "acct-svcr-ref", + endToEndId = "end-to-end-id", + paymentInformationId = "pmtinfid", + transactionDate = 100000L + )) == Database.BankTransactionResult.SUCCESS) + // Confirming the cash-out + assert(db.cashoutConfirm(op.cashoutUuid, 1L, 1L)) + // Checking the confirmation took place. + assert(db.cashoutGetFromUuid(op.cashoutUuid)?.tanConfirmationTime != null) + // Deleting the operation. + assert(db.cashoutDelete(op.cashoutUuid) == Database.CashoutDeleteResult.CONFLICT_ALREADY_CONFIRMED) + assert(db.cashoutGetFromUuid(op.cashoutUuid) != null) // previous didn't delete. + } }
\ No newline at end of file |