commit f44f0ee78843532d36ad71461ab594395a97f758
parent 74187aaded0b0181427940abfb9db6bf3fbf46c7
Author: Antoine A <>
Date: Sat, 2 Dec 2023 00:45:39 +0000
Add new withdrawal GET endpoint
Diffstat:
7 files changed, 180 insertions(+), 156 deletions(-)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/BankIntegrationApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/BankIntegrationApi.kt
@@ -96,8 +96,6 @@ fun Routing.bankIntegrationApi(db: Database, ctx: BankConfig) {
confirm_transfer_url = confirmUrl
))
}
- // Make IntelliJ happy.
- else -> throw AssertionError("not reached")
}
}
}
\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt
@@ -435,7 +435,8 @@ private fun Routing.coreBankWithdrawalApi(db: Database, ctx: BankConfig) {
}
get("/withdrawals/{withdrawal_id}") {
val uuid = call.uuidUriComponent("withdrawal_id")
- val op = db.withdrawal.get(uuid) ?: throw notFound(
+ val params = StatusParams.extract(call.request.queryParameters)
+ val op = db.withdrawal.pollInfo(uuid, params) ?: throw notFound(
"Withdrawal operation '$uuid' not found",
TalerErrorCode.BANK_TRANSACTION_NOT_FOUND
)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt
@@ -343,19 +343,15 @@ data class BankAccountCreateWithdrawalResponse(
@Serializable
data class WithdrawalPublicInfo (
- val username: String
-)
-
-// Taler withdrawal details response // TODO remove
-@Serializable
-data class BankAccountGetWithdrawalResponse(
+ val status: WithdrawalStatus,
val amount: TalerAmount,
+ val username: String,
+ val selected_reserve_pub: EddsaPublicKey? = null,
+ val selected_exchange_account: String? = null,
+ // TODO remove
val aborted: Boolean,
val confirmation_done: Boolean,
val selection_done: Boolean,
- val selected_reserve_pub: EddsaPublicKey? = null,
- val selected_exchange_account: String? = null,
- val username: String
)
@Serializable
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt
@@ -188,78 +188,12 @@ class WithdrawalDAO(private val db: Database) {
stmt.oneOrNull { it.getString(1) }
}
- /** Get withdrawal operation [uuid] */
- suspend fun get(uuid: UUID): BankAccountGetWithdrawalResponse? = db.conn { conn ->
- val stmt = conn.prepareStatement("""
- SELECT
- (amount).val as amount_val
- ,(amount).frac as amount_frac
- ,selection_done
- ,aborted
- ,confirmation_done
- ,reserve_pub
- ,selected_exchange_payto
- ,login
- FROM taler_withdrawal_operations
- JOIN bank_accounts ON wallet_bank_account=bank_account_id
- JOIN customers ON customer_id=owning_customer_id
- WHERE withdrawal_uuid=?
- """)
- stmt.setObject(1, uuid)
- stmt.oneOrNull {
- BankAccountGetWithdrawalResponse(
- amount = it.getAmount("amount", db.bankCurrency),
- selection_done = it.getBoolean("selection_done"),
- confirmation_done = it.getBoolean("confirmation_done"),
- aborted = it.getBoolean("aborted"),
- selected_exchange_account = it.getString("selected_exchange_payto"),
- selected_reserve_pub = it.getBytes("reserve_pub")?.run(::EddsaPublicKey),
- username = it.getString("login")
- )
- }
- }
-
- /** Pool public status of operation [uuid] */
- suspend fun pollStatus(uuid: UUID, params: StatusParams): BankWithdrawalOperationStatus? {
- suspend fun load(): BankWithdrawalOperationStatus? = db.conn { conn ->
- val stmt = conn.prepareStatement("""
- SELECT
- CASE
- WHEN confirmation_done THEN 'confirmed'
- WHEN aborted THEN 'aborted'
- WHEN selection_done THEN 'selected'
- ELSE 'pending'
- END as status
- ,(amount).val as amount_val
- ,(amount).frac as amount_frac
- ,selection_done
- ,aborted
- ,confirmation_done
- ,internal_payto_uri
- ,reserve_pub
- ,selected_exchange_payto
- FROM taler_withdrawal_operations
- JOIN bank_accounts ON (wallet_bank_account=bank_account_id)
- WHERE withdrawal_uuid=?
- """)
- stmt.setObject(1, uuid)
- stmt.oneOrNull {
- BankWithdrawalOperationStatus(
- status = WithdrawalStatus.valueOf(it.getString("status")),
- amount = it.getAmount("amount", db.bankCurrency),
- selection_done = it.getBoolean("selection_done"),
- transfer_done = it.getBoolean("confirmation_done"),
- aborted = it.getBoolean("aborted"),
- sender_wire = it.getString("internal_payto_uri"),
- confirm_transfer_url = null,
- suggested_exchange = null,
- selected_exchange_account = it.getString("selected_exchange_payto"),
- selected_reserve_pub = it.getBytes("reserve_pub")?.run(::EddsaPublicKey),
- )
- }
- }
-
-
+ private suspend fun <T> poll(
+ uuid: UUID,
+ params: StatusParams,
+ status: (T) -> WithdrawalStatus,
+ load: suspend () -> T?
+ ): T? {
return if (params.polling.poll_ms > 0) {
db.notifWatcher.listenWithdrawals(uuid) { flow ->
coroutineScope {
@@ -272,7 +206,7 @@ class WithdrawalDAO(private val db: Database) {
// Initial loading
val init = load()
// Long polling if there is no operation or its not confirmed
- if (init?.run { status == params.old_state } ?: true) {
+ if (init?.run { status(this) == params.old_state } ?: true) {
polling.join()
load()
} else {
@@ -285,4 +219,87 @@ class WithdrawalDAO(private val db: Database) {
load()
}
}
+
+ /** Pool public info of operation [uuid] */
+ suspend fun pollInfo(uuid: UUID, params: StatusParams): WithdrawalPublicInfo? =
+ poll(uuid, params, status = { it.status }) {
+ db.conn { conn ->
+ val stmt = conn.prepareStatement("""
+ SELECT
+ CASE
+ WHEN confirmation_done THEN 'confirmed'
+ WHEN aborted THEN 'aborted'
+ WHEN selection_done THEN 'selected'
+ ELSE 'pending'
+ END as status
+ ,(amount).val as amount_val
+ ,(amount).frac as amount_frac
+ ,selection_done
+ ,aborted
+ ,confirmation_done
+ ,reserve_pub
+ ,selected_exchange_payto
+ ,login
+ FROM taler_withdrawal_operations
+ JOIN bank_accounts ON wallet_bank_account=bank_account_id
+ JOIN customers ON customer_id=owning_customer_id
+ WHERE withdrawal_uuid=?
+ """)
+ stmt.setObject(1, uuid)
+ stmt.oneOrNull {
+ WithdrawalPublicInfo(
+ status = WithdrawalStatus.valueOf(it.getString("status")),
+ amount = it.getAmount("amount", db.bankCurrency),
+ username = it.getString("login"),
+ selected_exchange_account = it.getString("selected_exchange_payto"),
+ selected_reserve_pub = it.getBytes("reserve_pub")?.run(::EddsaPublicKey),
+ selection_done = it.getBoolean("selection_done"),
+ confirmation_done = it.getBoolean("confirmation_done"),
+ aborted = it.getBoolean("aborted"),
+ )
+ }
+ }
+ }
+
+ /** Pool public status of operation [uuid] */
+ suspend fun pollStatus(uuid: UUID, params: StatusParams): BankWithdrawalOperationStatus? =
+ poll(uuid, params, status = { it.status }) {
+ db.conn { conn ->
+ val stmt = conn.prepareStatement("""
+ SELECT
+ CASE
+ WHEN confirmation_done THEN 'confirmed'
+ WHEN aborted THEN 'aborted'
+ WHEN selection_done THEN 'selected'
+ ELSE 'pending'
+ END as status
+ ,(amount).val as amount_val
+ ,(amount).frac as amount_frac
+ ,selection_done
+ ,aborted
+ ,confirmation_done
+ ,internal_payto_uri
+ ,reserve_pub
+ ,selected_exchange_payto
+ FROM taler_withdrawal_operations
+ JOIN bank_accounts ON (wallet_bank_account=bank_account_id)
+ WHERE withdrawal_uuid=?
+ """)
+ stmt.setObject(1, uuid)
+ stmt.oneOrNull {
+ BankWithdrawalOperationStatus(
+ status = WithdrawalStatus.valueOf(it.getString("status")),
+ amount = it.getAmount("amount", db.bankCurrency),
+ selection_done = it.getBoolean("selection_done"),
+ transfer_done = it.getBoolean("confirmation_done"),
+ aborted = it.getBoolean("aborted"),
+ sender_wire = it.getString("internal_payto_uri"),
+ confirm_transfer_url = null,
+ suggested_exchange = null,
+ selected_exchange_account = it.getString("selected_exchange_payto"),
+ selected_reserve_pub = it.getBytes("reserve_pub")?.run(::EddsaPublicKey),
+ )
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/bank/src/test/kotlin/BankIntegrationApiTest.kt b/bank/src/test/kotlin/BankIntegrationApiTest.kt
@@ -58,76 +58,7 @@ class BankIntegrationApiTest {
}
// Check polling
- client.postA("/accounts/customer/withdrawals") {
- json { "amount" to amount }
- }.assertOkJson<BankAccountCreateWithdrawalResponse> { resp ->
- val aborted_uuid = resp.taler_withdraw_uri.split("/").last()
- val confirmed_uuid = client.postA("/accounts/customer/withdrawals") {
- json { "amount" to amount }
- }.assertOkJson<BankAccountCreateWithdrawalResponse>()
- .taler_withdraw_uri.split("/").last()
-
- // Check no useless polling
- assertTime(0, 100) {
- client.get("/taler-integration/withdrawal-operation/$confirmed_uuid?long_poll_ms=1000&old_state=selected")
- .assertOkJson<BankWithdrawalOperationStatus> { assertEquals(WithdrawalStatus.pending, it.status) }
- }
-
- // Polling selected
- coroutineScope {
- launch { // Check polling succeed
- assertTime(100, 200) {
- client.get("/taler-integration/withdrawal-operation/$confirmed_uuid?long_poll_ms=1000")
- .assertOkJson<BankWithdrawalOperationStatus> { assertEquals(WithdrawalStatus.selected, it.status) }
- }
- }
- launch { // Check polling succeed
- assertTime(100, 200) {
- client.get("/taler-integration/withdrawal-operation/$aborted_uuid?long_poll_ms=1000")
- .assertOkJson<BankWithdrawalOperationStatus> { assertEquals(WithdrawalStatus.selected, it.status) }
- }
- }
- delay(100)
- withdrawalSelect(confirmed_uuid)
- withdrawalSelect(aborted_uuid)
- }
-
- // Polling confirmed
- coroutineScope {
- launch { // Check polling succeed
- assertTime(100, 200) {
- client.get("/taler-integration/withdrawal-operation/$confirmed_uuid?long_poll_ms=1000&old_state=selected")
- .assertOkJson<BankWithdrawalOperationStatus> { assertEquals(WithdrawalStatus.confirmed, it.status)}
- }
- }
- launch { // Check polling timeout
- assertTime(200, 300) {
- client.get("/taler-integration/withdrawal-operation/$aborted_uuid?long_poll_ms=200&old_state=selected")
- .assertOkJson<BankWithdrawalOperationStatus> { assertEquals(WithdrawalStatus.selected, it.status) }
- }
- }
- delay(100)
- client.post("/withdrawals/$confirmed_uuid/confirm").assertNoContent()
- }
-
- // Polling abort
- coroutineScope {
- launch {
- assertTime(200, 300) {
- client.get("/taler-integration/withdrawal-operation/$confirmed_uuid?long_poll_ms=200&old_state=confirmed")
- .assertOkJson<BankWithdrawalOperationStatus> { assertEquals(WithdrawalStatus.confirmed, it.status)}
- }
- }
- launch {
- assertTime(100, 200) {
- client.get("/taler-integration/withdrawal-operation/$aborted_uuid?long_poll_ms=1000&old_state=selected")
- .assertOkJson<BankWithdrawalOperationStatus> { assertEquals(WithdrawalStatus.aborted, it.status) }
- }
- }
- delay(100)
- client.post("/withdrawals/$aborted_uuid/abort").assertNoContent()
- }
- }
+ statusRoutine<BankWithdrawalOperationStatus>("/taler-integration/withdrawal-operation") { it.status }
// Check unknown
client.get("/taler-integration/withdrawal-operation/${UUID.randomUUID()}")
diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt
@@ -762,7 +762,7 @@ class CoreBankWithdrawalApiTest {
}.assertOkJson<BankAccountCreateWithdrawalResponse> {
client.get("/withdrawals/${it.withdrawal_id}") {
pwAuth("merchant")
- }.assertOkJson<BankAccountGetWithdrawalResponse> {
+ }.assertOkJson<WithdrawalPublicInfo> {
assert(!it.selection_done)
assert(!it.aborted)
assert(!it.confirmation_done)
@@ -771,6 +771,9 @@ class CoreBankWithdrawalApiTest {
}
}
+ // Check polling
+ statusRoutine<WithdrawalPublicInfo>("/withdrawals") { it.status }
+
// Check bad UUID
client.get("/withdrawals/chocolate").assertBadRequest()
diff --git a/bank/src/test/kotlin/routines.kt b/bank/src/test/kotlin/routines.kt
@@ -23,6 +23,7 @@ import io.ktor.client.statement.HttpResponse
import io.ktor.server.testing.ApplicationTestBuilder
import io.ktor.client.request.*
import io.ktor.http.*
+import kotlin.test.*
import kotlinx.coroutines.*
import kotlinx.serialization.json.*
import net.taler.common.errorcodes.TalerErrorCode
@@ -199,4 +200,81 @@ inline suspend fun <reified B> ApplicationTestBuilder.historyRoutine(
// backward range:
history("delta=-10").assertHistory(10)
history("delta=-10&start=${id-4}").assertHistory(10)
+}
+
+inline suspend fun <reified B> ApplicationTestBuilder.statusRoutine(
+ url: String,
+ crossinline status: (B) -> WithdrawalStatus
+) {
+ val amount = TalerAmount("KUDOS:9.0")
+ client.postA("/accounts/customer/withdrawals") {
+ json { "amount" to amount }
+ }.assertOkJson<BankAccountCreateWithdrawalResponse> { resp ->
+ val aborted_uuid = resp.taler_withdraw_uri.split("/").last()
+ val confirmed_uuid = client.postA("/accounts/customer/withdrawals") {
+ json { "amount" to amount }
+ }.assertOkJson<BankAccountCreateWithdrawalResponse>()
+ .taler_withdraw_uri.split("/").last()
+
+ // Check no useless polling
+ assertTime(0, 100) {
+ client.get("$url/$confirmed_uuid?long_poll_ms=1000&old_state=selected")
+ .assertOkJson<B> { assertEquals(WithdrawalStatus.pending, status(it)) }
+ }
+
+ // Polling selected
+ coroutineScope {
+ launch { // Check polling succeed
+ assertTime(100, 200) {
+ client.get("$url/$confirmed_uuid?long_poll_ms=1000")
+ .assertOkJson<B> { assertEquals(WithdrawalStatus.selected, status(it)) }
+ }
+ }
+ launch { // Check polling succeed
+ assertTime(100, 200) {
+ client.get("$url/$aborted_uuid?long_poll_ms=1000")
+ .assertOkJson<B> { assertEquals(WithdrawalStatus.selected, status(it)) }
+ }
+ }
+ delay(100)
+ withdrawalSelect(confirmed_uuid)
+ withdrawalSelect(aborted_uuid)
+ }
+
+ // Polling confirmed
+ coroutineScope {
+ launch { // Check polling succeed
+ assertTime(100, 200) {
+ client.get("$url/$confirmed_uuid?long_poll_ms=1000&old_state=selected")
+ .assertOkJson<B> { assertEquals(WithdrawalStatus.confirmed, status(it))}
+ }
+ }
+ launch { // Check polling timeout
+ assertTime(200, 300) {
+ client.get("$url/$aborted_uuid?long_poll_ms=200&old_state=selected")
+ .assertOkJson<B> { assertEquals(WithdrawalStatus.selected, status(it)) }
+ }
+ }
+ delay(100)
+ client.post("/withdrawals/$confirmed_uuid/confirm").assertNoContent()
+ }
+
+ // Polling abort
+ coroutineScope {
+ launch {
+ assertTime(200, 300) {
+ client.get("$url/$confirmed_uuid?long_poll_ms=200&old_state=confirmed")
+ .assertOkJson<B> { assertEquals(WithdrawalStatus.confirmed, status(it))}
+ }
+ }
+ launch {
+ assertTime(100, 200) {
+ client.get("$url/$aborted_uuid?long_poll_ms=1000&old_state=selected")
+ .assertOkJson<B> { assertEquals(WithdrawalStatus.aborted, status(it)) }
+ }
+ }
+ delay(100)
+ client.post("/withdrawals/$aborted_uuid/abort").assertNoContent()
+ }
+ }
}
\ No newline at end of file