summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMS <ms@taler.net>2023-09-21 09:02:52 +0200
committerMS <ms@taler.net>2023-09-21 09:02:52 +0200
commit36f3e24e88b91903b5d9e821ca88d6fc6c29888a (patch)
tree3993f90818d628180f120c88313426a424040b0f
parent10190a79c63b97ca9e2c241775050e18c06f07ea (diff)
downloadlibeufin-36f3e24e88b91903b5d9e821ca88d6fc6c29888a.tar.gz
libeufin-36f3e24e88b91903b5d9e821ca88d6fc6c29888a.tar.bz2
libeufin-36f3e24e88b91903b5d9e821ca88d6fc6c29888a.zip
Implementing TWG /history/incoming.
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/Database.kt49
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/helpers.kt39
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/talerWireGatewayHandlers.kt30
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/transactionsHandlers.kt26
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/types.kt24
-rw-r--r--bank/src/test/kotlin/DatabaseTest.kt10
-rw-r--r--bank/src/test/kotlin/LibeuFinApiTest.kt2
-rw-r--r--bank/src/test/kotlin/TalerApiTest.kt41
8 files changed, 178 insertions, 43 deletions
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
index ebc827bd..1ceb2763 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
@@ -33,6 +33,8 @@ private const val DB_CTR_LIMIT = 1000000
fun Customer.expectRowId(): Long = this.dbRowId ?: throw internalServerError("Cutsomer '$login' had no DB row ID.")
fun BankAccount.expectBalance(): TalerAmount = this.balance ?: throw internalServerError("Bank account '${this.internalPaytoUri}' lacks balance.")
fun BankAccount.expectRowId(): Long = this.bankAccountId ?: throw internalServerError("Bank account '${this.internalPaytoUri}' lacks database row ID.")
+fun BankAccountTransaction.expectRowId(): Long = this.dbRowId ?: throw internalServerError("Bank account transaction (${this.subject}) lacks database row ID.")
+
class Database(private val dbConfig: String) {
@@ -510,6 +512,15 @@ class Database(private val dbConfig: String) {
)
}
}
+
+ /**
+ * The following function returns the list of transactions, according
+ * to the history parameters. The parameters take at least the 'start'
+ * and 'delta' values, and _optionally_ the payment direction. At the
+ * moment, only the TWG uses the direction, to provide the /incoming
+ * and /outgoing endpoints.
+ */
+ // Helper type to collect the history parameters.
private data class HistoryParams(
val cmpOp: String, // < or >
val orderBy: String // ASC or DESC
@@ -518,6 +529,7 @@ class Database(private val dbConfig: String) {
start: Long,
delta: Long,
bankAccountId: Long,
+ withDirection: TransactionDirection? = null
): List<BankAccountTransaction> {
reconnect()
val ops = if (delta < 0)
@@ -536,22 +548,45 @@ class Database(private val dbConfig: String) {
,account_servicer_reference
,payment_information_id
,end_to_end_id
- ,direction
+ ${if (withDirection != null) "" else ",direction"}
,bank_account_id
,bank_transaction_id
FROM bank_account_transactions
- WHERE bank_transaction_id ${ops.cmpOp} ? AND bank_account_id=?
+ WHERE bank_transaction_id ${ops.cmpOp} ?
+ AND bank_account_id=?
+ ${if (withDirection != null) "AND direction=?::direction_enum" else ""}
ORDER BY bank_transaction_id ${ops.orderBy}
LIMIT ?
""")
stmt.setLong(1, start)
stmt.setLong(2, bankAccountId)
- stmt.setLong(3, abs(delta))
+ /**
+ * The LIMIT parameter index might change, according to
+ * the presence of the direction filter.
+ */
+ val limitParamIndex = if (withDirection != null) {
+ stmt.setString(3, withDirection.name)
+ 4
+ }
+ else
+ 3
+ stmt.setLong(limitParamIndex, abs(delta))
val rs = stmt.executeQuery()
rs.use {
val ret = mutableListOf<BankAccountTransaction>()
if (!it.next()) return ret
do {
+ val direction = if (withDirection == null) {
+ it.getString("direction").run {
+ when (this) {
+ "credit" -> TransactionDirection.credit
+ "debit" -> TransactionDirection.debit
+ else -> throw internalServerError("Wrong direction in transaction: $this")
+ }
+ }
+ }
+ else
+ withDirection
ret.add(
BankAccountTransaction(
creditorPaytoUri = it.getString("creditor_payto_uri"),
@@ -564,13 +599,7 @@ class Database(private val dbConfig: String) {
),
accountServicerReference = it.getString("account_servicer_reference"),
endToEndId = it.getString("end_to_end_id"),
- direction = it.getString("direction").run {
- when(this) {
- "credit" -> TransactionDirection.credit
- "debit" -> TransactionDirection.debit
- else -> throw internalServerError("Wrong direction in transaction: $this")
- }
- },
+ direction = direction,
bankAccountId = it.getLong("bank_account_id"),
paymentInformationId = it.getString("payment_information_id"),
subject = it.getString("subject"),
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
index a71cf992..240b454d 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
@@ -20,7 +20,10 @@
package tech.libeufin.bank
import io.ktor.http.*
+import io.ktor.http.cio.*
import io.ktor.server.application.*
+import io.ktor.server.plugins.*
+import io.ktor.server.request.*
import io.ktor.server.util.*
import net.taler.common.errorcodes.TalerErrorCode
import net.taler.wallet.crypto.Base32Crockford
@@ -51,7 +54,6 @@ fun ApplicationCall.getAuthToken(): String? {
return null // Not a Bearer token case.
}
-
/**
* Performs the HTTP basic authentication. Returns the
* authenticated customer on success, or null otherwise.
@@ -77,7 +79,10 @@ fun doBasicAuth(encodedCredentials: String): Customer? {
)
val login = userAndPassSplit[0]
val plainPassword = userAndPassSplit[1]
- val maybeCustomer = db.customerGetFromLogin(login) ?: return null
+ val maybeCustomer = db.customerGetFromLogin(login) ?: throw notFound(
+ "User not found",
+ TalerErrorCode.TALER_EC_END // FIXME: define EC.
+ )
if (!CryptoUtil.checkpw(plainPassword, maybeCustomer.passwordHash)) return null
return maybeCustomer
}
@@ -372,6 +377,7 @@ fun getTalerWithdrawUri(baseUrl: String, woId: String) =
this.appendPathSegments(pathSegments)
}
+// Builds a withdrawal confirm URL.
fun getWithdrawalConfirmUrl(
baseUrl: String,
wopId: String,
@@ -411,3 +417,32 @@ fun getWithdrawal(opIdParam: String): TalerWithdrawalOperation {
)
return op
}
+
+data class HistoryParams(
+ val delta: Long,
+ val start: Long
+)
+/**
+ * Extracts the query parameters from "history-like" endpoints,
+ * providing the defaults according to the API specification.
+ */
+fun getHistoryParams(req: ApplicationRequest): HistoryParams {
+ val deltaParam: String = req.queryParameters["delta"] ?: throw MissingRequestParameterException(parameterName = "delta")
+ val delta: Long = try {
+ deltaParam.toLong()
+ } catch (e: Exception) {
+ logger.error(e.message)
+ throw badRequest("Param 'delta' not a number")
+ }
+ // Note: minimum 'start' is zero, as database IDs start from 1.
+ val start: Long = when (val param = req.queryParameters["start"]) {
+ null -> if (delta >= 0) 0L else Long.MAX_VALUE
+ else -> try {
+ param.toLong()
+ } catch (e: Exception) {
+ logger.error(e.message)
+ throw badRequest("Param 'start' not a number")
+ }
+ }
+ return HistoryParams(delta = delta, start = start)
+}
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/talerWireGatewayHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/talerWireGatewayHandlers.kt
index 672d9bdb..dc460928 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/talerWireGatewayHandlers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/talerWireGatewayHandlers.kt
@@ -21,6 +21,7 @@
package tech.libeufin.bank
+import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
@@ -36,6 +37,35 @@ fun Routing.talerWireGatewayHandlers() {
return@get
}
get("/accounts/{USERNAME}/taler-wire-gateway/history/incoming") {
+ val c = call.myAuth(TokenScope.readonly) ?: throw unauthorized()
+ if (!call.getResourceName("USERNAME").canI(c, withAdmin = true)) throw forbidden()
+ val params = getHistoryParams(call.request)
+ val bankAccount = db.bankAccountGetFromOwnerId(c.expectRowId())
+ ?: throw internalServerError("Customer '${c.login}' lacks bank account.")
+ if (!bankAccount.isTalerExchange) throw forbidden("History is not related to a Taler exchange.")
+ val bankAccountId = bankAccount.expectRowId()
+
+ val history: List<BankAccountTransaction> = db.bankTransactionGetHistory(
+ start = params.start,
+ delta = params.delta,
+ bankAccountId = bankAccountId,
+ withDirection = TransactionDirection.credit
+ )
+ if (history.isEmpty()) {
+ call.respond(HttpStatusCode.NoContent)
+ return@get
+ }
+ val resp = IncomingHistory(credit_account = bankAccount.internalPaytoUri)
+ history.forEach {
+ resp.incoming_transactions.add(IncomingReserveTransaction(
+ row_id = it.expectRowId(),
+ amount = it.amount.toString(),
+ date = it.transactionDate,
+ debit_account = it.debtorPaytoUri,
+ reserve_pub = it.subject
+ ))
+ }
+ call.respond(resp)
return@get
}
post("/accounts/{USERNAME}/taler-wire-gateway/transfer") {
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/transactionsHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/transactionsHandlers.kt
index aafd6682..8b2f1a7d 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/transactionsHandlers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/transactionsHandlers.kt
@@ -17,32 +17,14 @@ fun Routing.transactionsHandlers() {
val resourceName = call.expectUriComponent("USERNAME")
if (c.login != resourceName && c.login != "admin") throw forbidden()
// Collecting params.
- val deltaParam: String = call.request.queryParameters["delta"] ?: throw MissingRequestParameterException(parameterName = "delta")
- val delta: Long = try {
- deltaParam.toLong()
- } catch (e: Exception) {
- logger.error(e.message)
- throw badRequest("Param 'delta' not a number")
- }
- // Note: minimum 'start' is zero, as database IDs start from 1.
- val start: Long = when (val param = call.request.queryParameters["start"]) {
- null -> if (delta >= 0) 0L else Long.MAX_VALUE
- else -> try {
- param.toLong()
- } catch (e: Exception) {
- logger.error(e.message)
- throw badRequest("Param 'start' not a number")
- }
- }
- logger.info("Param long_poll_ms not supported")
+ val historyParams = getHistoryParams(call.request)
// Making the query.
val bankAccount = db.bankAccountGetFromOwnerId(c.expectRowId())
?: throw internalServerError("Customer '${c.login}' lacks bank account.")
- val bankAccountId = bankAccount.bankAccountId
- ?: throw internalServerError("Bank account lacks row ID.")
+ val bankAccountId = bankAccount.expectRowId()
val history: List<BankAccountTransaction> = db.bankTransactionGetHistory(
- start = start,
- delta = delta,
+ start = historyParams.start,
+ delta = historyParams.delta,
bankAccountId = bankAccountId
)
val res = BankAccountTransactionsResponse(transactions = mutableListOf())
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/types.kt b/bank/src/main/kotlin/tech/libeufin/bank/types.kt
index 6311e3e9..bd64def0 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/types.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/types.kt
@@ -410,8 +410,11 @@ data class BankAccountGetWithdrawalResponse(
typealias ResourceName = String
-
-// Checks if the input Customer has the rights over ResourceName
+/**
+ * Checks if the input Customer has the rights over ResourceName.
+ * FIXME: myAuth() gives null on failures, but this gives false.
+ * Should they return the same, for consistency?
+ */
fun ResourceName.canI(c: Customer, withAdmin: Boolean = true): Boolean {
if (c.login == this) return true
if (c.login == "admin" && withAdmin) return true
@@ -525,4 +528,21 @@ data class TWGConfigResponse(
val name: String = "taler-wire-gateway",
val version: String = "0:0:0:",
val currency: String
+)
+
+// Response of a TWG /history/incoming call.
+@Serializable
+data class IncomingHistory(
+ val incoming_transactions: MutableList<IncomingReserveTransaction> = mutableListOf(),
+ val credit_account: String // Receiver's Payto URI.
+)
+// TWG's incoming payment record.
+@Serializable
+data class IncomingReserveTransaction(
+ val type: String = "RESERVE",
+ val row_id: Long, // DB row ID of the payment.
+ val date: Long, // microseconds timestamp.
+ val amount: String,
+ val debit_account: String, // Payto of the sender.
+ val reserve_pub: String
) \ No newline at end of file
diff --git a/bank/src/test/kotlin/DatabaseTest.kt b/bank/src/test/kotlin/DatabaseTest.kt
index 35b0b9d5..f38e9559 100644
--- a/bank/src/test/kotlin/DatabaseTest.kt
+++ b/bank/src/test/kotlin/DatabaseTest.kt
@@ -25,10 +25,14 @@ import java.util.Random
import java.util.UUID
// Foo pays Bar with custom subject.
-fun genTx(subject: String = "test"): BankInternalTransaction =
+fun genTx(
+ subject: String = "test",
+ creditorId: Long = 2,
+ debtorId: Long = 1
+): BankInternalTransaction =
BankInternalTransaction(
- creditorAccountId = 2,
- debtorAccountId = 1,
+ creditorAccountId = creditorId,
+ debtorAccountId = debtorId,
subject = subject,
amount = TalerAmount( 10, 0, "KUDOS"),
accountServicerReference = "acct-svcr-ref",
diff --git a/bank/src/test/kotlin/LibeuFinApiTest.kt b/bank/src/test/kotlin/LibeuFinApiTest.kt
index cdbb2c0c..048947b6 100644
--- a/bank/src/test/kotlin/LibeuFinApiTest.kt
+++ b/bank/src/test/kotlin/LibeuFinApiTest.kt
@@ -197,7 +197,7 @@ class LibeuFinApiTest {
basicAuth("not", "not")
expectSuccess = false
}
- assert(shouldNot.status == HttpStatusCode.Unauthorized)
+ assert(shouldNot.status == HttpStatusCode.NotFound)
}
}
/**
diff --git a/bank/src/test/kotlin/TalerApiTest.kt b/bank/src/test/kotlin/TalerApiTest.kt
index 7f5c407b..7617467e 100644
--- a/bank/src/test/kotlin/TalerApiTest.kt
+++ b/bank/src/test/kotlin/TalerApiTest.kt
@@ -32,17 +32,52 @@ class TalerApiTest {
lastNexusFetchRowId = 1L,
owningCustomerId = 2L,
hasDebt = false,
- maxDebt = TalerAmount(10, 1, "KUDOS")
+ maxDebt = TalerAmount(10, 1, "KUDOS"),
+ isTalerExchange = true
)
val customerBar = Customer(
login = "bar",
- passwordHash = "hash",
+ passwordHash = CryptoUtil.hashpw("secret"),
name = "Bar",
phone = "+00",
email = "foo@b.ar",
cashoutPayto = "payto://external-IBAN",
cashoutCurrency = "KUDOS"
)
+ // Testing the /history/incoming call from the TWG API.
+ @Test
+ fun historyIncoming() {
+ val db = initDb()
+ assert(db.customerCreate(customerFoo) != null)
+ assert(db.bankAccountCreate(bankAccountFoo))
+ assert(db.customerCreate(customerBar) != null)
+ assert(db.bankAccountCreate(bankAccountBar))
+ // Give Foo reasonable debt allowance:
+ assert(db.bankAccountSetMaxDebt(
+ 1L,
+ TalerAmount(1000, 0)
+ ))
+ // Foo pays Bar (the exchange) twice.
+ assert(db.bankTransactionCreate(genTx("withdrawal 1")) == Database.BankTransactionResult.SUCCESS)
+ assert(db.bankTransactionCreate(genTx("withdrawal 2")) == Database.BankTransactionResult.SUCCESS)
+ // Bar pays Foo once, but that should not appear in the result.
+ assert(
+ db.bankTransactionCreate(genTx("payout", creditorId = 1, debtorId = 2)) ==
+ Database.BankTransactionResult.SUCCESS
+ )
+ // Bar expects two entries in the incoming history
+ testApplication {
+ application(webApp)
+ val resp = client.get("/accounts/bar/taler-wire-gateway/history/incoming?delta=5") {
+ basicAuth("bar", "secret")
+ expectSuccess = true
+ }
+ val j: IncomingHistory = Json.decodeFromString(resp.bodyAsText())
+ assert(j.incoming_transactions.size == 2)
+ }
+ }
+
+ // Testing the /admin/add-incoming call from the TWG API.
@Test
fun addIncoming() {
val db = initDb()
@@ -50,6 +85,7 @@ class TalerApiTest {
assert(db.bankAccountCreate(bankAccountFoo))
assert(db.customerCreate(customerBar) != null)
assert(db.bankAccountCreate(bankAccountBar))
+ // Give Bar reasonable debt allowance:
assert(db.bankAccountSetMaxDebt(
2L,
TalerAmount(1000, 0)
@@ -68,7 +104,6 @@ class TalerApiTest {
""".trimIndent())
}
}
-
}
// Selecting withdrawal details from the Integrtion API endpoint.
@Test