commit 297228f904554b09e320fa490035a0d68450d746
parent 4f0135998694fd67f34e587bbdd0ad3411208819
Author: Antoine A <>
Date: Mon, 23 Oct 2023 16:26:04 +0000
Fix /monitor
Diffstat:
3 files changed, 180 insertions(+), 72 deletions(-)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
@@ -60,7 +60,7 @@ private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.bank.Databas
private fun faultyTimestampByBank() = internalServerError("Bank took overflowing timestamp")
private fun faultyDurationByClient() = badRequest("Overflowing duration, please specify 'forever' instead.")
-private fun <T> PreparedStatement.all(lambda: (ResultSet) -> T): List<T> {
+fun <T> PreparedStatement.all(lambda: (ResultSet) -> T): List<T> {
executeQuery().use {
val ret = mutableListOf<T>()
while (it.next()) {
@@ -621,11 +621,10 @@ class Database(dbConfig: String, private val bankCurrency: String, private val f
if (it.getBoolean("out_debtor_is_exchange")) {
val rowId = it.getLong("out_debit_row_id")
if (metadata is OutgoingTxMetadata) {
- val stmt = conn.prepareStatement("CALL register_outgoing(NULL, ?, ?, ?, ?)")
+ val stmt = conn.prepareStatement("CALL register_outgoing(NULL, ?, ?, ?)")
stmt.setBytes(1, metadata.wtid.raw)
stmt.setString(2, metadata.exchangeBaseUrl.url)
stmt.setLong(3, rowId)
- stmt.setLong(4, debtorAccountId)
stmt.executeUpdate()
} else {
logger.warn("exchange account $debtorAccountId sent a transaction $rowId with malformed metadata")
diff --git a/bank/src/test/kotlin/StatsTest.kt b/bank/src/test/kotlin/StatsTest.kt
@@ -1,74 +1,160 @@
/*
- * This file is part of LibEuFin.
- * Copyright (C) 2019 Stanisci and Dold.
+* This file is part of LibEuFin.
+* Copyright (C) 2019 Stanisci and Dold.
- * LibEuFin is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation; either version 3, or
- * (at your option) any later version.
+* LibEuFin is free software; you can redistribute it and/or modify
+* it under the terms of the GNU Affero General Public License as
+* published by the Free Software Foundation; either version 3, or
+* (at your option) any later version.
- * LibEuFin is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
- * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
- * Public License for more details.
+* LibEuFin is distributed in the hope that it will be useful, but
+* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
+* Public License for more details.
- * You should have received a copy of the GNU Affero General Public
- * License along with LibEuFin; see the file COPYING. If not, see
- * <http://www.gnu.org/licenses/>
- */
+* You should have received a copy of the GNU Affero General Public
+* License along with LibEuFin; see the file COPYING. If not, see
+* <http://www.gnu.org/licenses/>
+*/
-import org.junit.Test
-import io.ktor.server.testing.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.http.content.*
+import io.ktor.server.testing.*
+import java.time.*
+import java.util.*
+import kotlin.test.*
import kotlinx.serialization.json.Json
+import org.junit.Test
import tech.libeufin.bank.*
import tech.libeufin.util.*
-import kotlin.test.*
-import java.time.Instant
-import java.util.*
class StatsTest {
@Test
- fun internalTalerPayment() = bankSetup { db ->
+ fun transfer() = bankSetup { db ->
+ assert(db.bankAccountSetMaxDebt(
+ 2L,
+ TalerAmount(1000, 0, "KUDOS")
+ ))
+
+ suspend fun transfer(amount: TalerAmount) {
+ client.post("/accounts/exchange/taler-wire-gateway/transfer") {
+ basicAuth("exchange", "exchange-password")
+ jsonBody(json {
+ "request_uid" to randHashCode()
+ "amount" to amount
+ "exchange_base_url" to "http://exchange.example.com/"
+ "wtid" to randShortHashCode()
+ "credit_account" to "payto://iban/MERCHANT-IBAN-XYZ"
+ })
+ }.assertOk()
+ }
+
+ suspend fun monitor(count: Long, amount: TalerAmount) {
+ Timeframe.entries.forEach { timestamp ->
+ client.get("/monitor?timestamp=${timestamp.name}") { basicAuth("admin", "admin-password") }.assertOk().run {
+ val resp = Json.decodeFromString<MonitorResponse>(bodyAsText())
+ assertEquals(count, resp.talerPayoutCount)
+ assertEquals(amount, resp.talerPayoutInternalVolume)
+ }
+ }
+ }
+
+ monitor(0, TalerAmount("KUDOS:0"))
+ transfer(TalerAmount("KUDOS:10.0"))
+ monitor(1, TalerAmount("KUDOS:10.0"))
+ transfer(TalerAmount("KUDOS:30.5"))
+ monitor(2, TalerAmount("KUDOS:40.5"))
+ transfer(TalerAmount("KUDOS:42"))
+ monitor(3, TalerAmount("KUDOS:82.5"))
+ }
+
+ @Test
+ fun timeframe() = bankSetup { db ->
db.conn { conn ->
- val stmt = conn.prepareStatement("CALL stats_register_internal_taler_payment(now()::timestamp, (?, ?)::taler_amount)")
-
- suspend fun register(amount: TalerAmount) {
- stmt.setLong(1, amount.value)
- stmt.setInt(2, amount.frac)
+ suspend fun register(now: OffsetDateTime, amount: TalerAmount) {
+ val stmt =
+ conn.prepareStatement(
+ "CALL stats_register_internal_taler_payment(?::timestamp, (?, ?)::taler_amount)"
+ )
+ stmt.setObject(1, now)
+ stmt.setLong(2, amount.value)
+ stmt.setInt(3, amount.frac)
stmt.executeUpdate()
}
- client.get("/monitor") {
- basicAuth("admin", "admin-password")
- }.assertOk().run {
- val resp = Json.decodeFromString<MonitorResponse>(bodyAsText())
- assertEquals(0, resp.talerPayoutCount)
- assertEquals(TalerAmount("KUDOS:0"), resp.talerPayoutInternalVolume)
+ suspend fun check(
+ now: OffsetDateTime,
+ timeframe: Timeframe,
+ which: Int?,
+ count: Long,
+ amount: TalerAmount
+ ) {
+ val stmt = conn.prepareStatement(
+ """
+ SELECT
+ internal_taler_payments_count
+ ,(internal_taler_payments_volume).val as internal_taler_payments_volume_val
+ ,(internal_taler_payments_volume).frac as internal_taler_payments_volume_frac
+ FROM stats_get_frame(?::timestamp, ?::stat_timeframe_enum, ?)
+ """
+ )
+ stmt.setObject(1, now)
+
+ stmt.setString(2, timeframe.name)
+ if (which != null) {
+ stmt.setInt(3, which)
+ } else {
+ stmt.setNull(3, java.sql.Types.INTEGER)
+ }
+ stmt.oneOrNull {
+ val talerPayoutCount = it.getLong("internal_taler_payments_count")
+ val talerPayoutInternalVolume =
+ TalerAmount(
+ value = it.getLong("internal_taler_payments_volume_val"),
+ frac = it.getInt("internal_taler_payments_volume_frac"),
+ currency = "KUDOS"
+ )
+ println("$timeframe $talerPayoutCount $talerPayoutInternalVolume")
+ assertEquals(count, talerPayoutCount)
+ assertEquals(amount, talerPayoutInternalVolume)
+ }!!
}
- register(TalerAmount("KUDOS:10.0"))
- client.get("/monitor") {
- basicAuth("admin", "admin-password")
- }.assertOk().run {
- val resp = Json.decodeFromString<MonitorResponse>(bodyAsText())
- assertEquals(1, resp.talerPayoutCount)
- assertEquals(TalerAmount("KUDOS:10"), resp.talerPayoutInternalVolume)
- }
+ val now = OffsetDateTime.now(ZoneOffset.UTC)
+ val otherHour = now.withHour((now.hour + 1) % 24)
+ val otherDay = now.withDayOfMonth((now.dayOfMonth + 1) % 28)
+ val otherMonth = now.withMonth((now.monthValue + 1) % 12)
+ val otherYear = now.minusYears(1)
- register(TalerAmount("KUDOS:30.5"))
- client.get("/monitor") {
- basicAuth("admin", "admin-password")
- }.assertOk().run {
- val resp = Json.decodeFromString<MonitorResponse>(bodyAsText())
- assertEquals(2, resp.talerPayoutCount)
- assertEquals(TalerAmount("KUDOS:40.5"), resp.talerPayoutInternalVolume)
- }
+ register(now, TalerAmount("KUDOS:10.0"))
+ register(otherHour, TalerAmount("KUDOS:20.0"))
+ register(otherDay, TalerAmount("KUDOS:35.0"))
+ register(otherMonth, TalerAmount("KUDOS:40.0"))
+ register(otherYear, TalerAmount("KUDOS:50.0"))
+
+ // Check with timestamp and truncating
+ check(now, Timeframe.hour, null, 1, TalerAmount("KUDOS:10.0"))
+ check(otherHour, Timeframe.hour, null, 1, TalerAmount("KUDOS:20.0"))
+ check(otherDay, Timeframe.day, null, 1, TalerAmount("KUDOS:35.0"))
+ check(otherMonth, Timeframe.month, null, 1, TalerAmount("KUDOS:40.0"))
+ check(otherYear, Timeframe.year, null, 1, TalerAmount("KUDOS:50.0"))
+
+ // Check with timestamp and intervals
+ check(now, Timeframe.hour, now.hour, 1, TalerAmount("KUDOS:10.0"))
+ check(now, Timeframe.hour, otherHour.hour, 1, TalerAmount("KUDOS:20.0"))
+ check(now, Timeframe.day, otherDay.dayOfMonth, 1, TalerAmount("KUDOS:35.0"))
+ check(now, Timeframe.month, otherMonth.monthValue, 1, TalerAmount("KUDOS:40.0"))
+ check(now, Timeframe.year, otherYear.year, 1, TalerAmount("KUDOS:50.0"))
- // TODO Test timeframe logic with different timestamps
+ // Check timestamp aggregation
+ check(now, Timeframe.day, now.dayOfMonth, 2, TalerAmount("KUDOS:30.0"))
+ check(now, Timeframe.month, now.monthValue, 3, TalerAmount("KUDOS:65.0"))
+ check(now, Timeframe.year, now.year, 4, TalerAmount("KUDOS:105.0"))
+ check(now, Timeframe.day, null, 2, TalerAmount("KUDOS:30.0"))
+ check(now, Timeframe.month, null, 3, TalerAmount("KUDOS:65.0"))
+ check(now, Timeframe.year, null, 4, TalerAmount("KUDOS:105.0"))
}
}
-}
-\ No newline at end of file
+}
diff --git a/database-versioning/procedures.sql b/database-versioning/procedures.sql
@@ -199,10 +199,12 @@ CREATE OR REPLACE PROCEDURE register_outgoing(
IN in_request_uid BYTEA,
IN in_wtid BYTEA,
IN in_exchange_base_url TEXT,
- IN in_tx_row_id BIGINT,
- IN in_exchange_bank_account_id BIGINT
+ IN in_tx_row_id BIGINT
)
LANGUAGE plpgsql AS $$
+DECLARE
+ local_amount taler_amount;
+ local_bank_account_id BIGINT;
BEGIN
-- Register outgoing transaction
INSERT
@@ -217,8 +219,13 @@ INSERT
in_exchange_base_url,
in_tx_row_id
);
+-- TODO check if not drain
+SELECT (amount).val, (amount).frac, bank_account_id
+INTO local_amount.val, local_amount.frac, local_bank_account_id
+FROM bank_account_transactions WHERE bank_transaction_id=in_tx_row_id;
+CALL stats_register_internal_taler_payment(now()::TIMESTAMP, local_amount);
-- notify new transaction
-PERFORM pg_notify('outgoing_tx', in_exchange_bank_account_id || ' ' || in_tx_row_id);
+PERFORM pg_notify('outgoing_tx', local_bank_account_id || ' ' || in_tx_row_id);
END $$;
COMMENT ON PROCEDURE register_outgoing
IS 'Register a bank transaction as a taler outgoing transaction';
@@ -339,7 +346,7 @@ IF out_exchange_balance_insufficient THEN
END IF;
out_timestamp=in_timestamp;
-- Register outgoing transaction
-CALL register_outgoing(in_request_uid, in_wtid, in_exchange_base_url, out_tx_row_id, exchange_bank_account_id);
+CALL register_outgoing(in_request_uid, in_wtid, in_exchange_base_url, out_tx_row_id);
END $$;
COMMENT ON FUNCTION taler_transfer(
bytea,
@@ -1021,7 +1028,7 @@ END $$;
CREATE OR REPLACE FUNCTION stats_get_frame(
IN now TIMESTAMP,
- IN timeframe stat_timeframe_enum,
+ IN in_timeframe stat_timeframe_enum,
IN which INTEGER,
OUT cashin_count BIGINT,
OUT cashin_volume_in_fiat taler_amount,
@@ -1030,24 +1037,41 @@ CREATE OR REPLACE FUNCTION stats_get_frame(
OUT internal_taler_payments_count BIGINT,
OUT internal_taler_payments_volume taler_amount
)
-LANGUAGE sql AS $$
+LANGUAGE plpgsql AS $$
+DECLARE
+ local_start_time TIMESTAMP;
+BEGIN
+ local_start_time = CASE
+ WHEN which IS NULL THEN date_trunc(in_timeframe::text, now)
+ WHEN in_timeframe = 'hour' THEN date_trunc('day' , now) + make_interval(hours => which)
+ WHEN in_timeframe = 'day' THEN date_trunc('month', now) + make_interval(days => which-1)
+ WHEN in_timeframe = 'month' THEN date_trunc('year' , now) + make_interval(months => which-1)
+ WHEN in_timeframe = 'year' THEN make_date(which, 1, 1)::TIMESTAMP
+ END;
SELECT
s.cashin_count
- ,s.cashin_volume_in_fiat
+ ,(s.cashin_volume_in_fiat).val
+ ,(s.cashin_volume_in_fiat).frac
,s.cashout_count
- ,s.cashout_volume_in_fiat
+ ,(s.cashout_volume_in_fiat).val
+ ,(s.cashout_volume_in_fiat).frac
,s.internal_taler_payments_count
- ,s.internal_taler_payments_volume
- FROM regional_stats AS s
- WHERE s.timeframe = timeframe
- AND start_time = CASE
- WHEN which IS NULL THEN date_trunc(timeframe::text, now)
- WHEN timeframe = 'hour' THEN date_trunc('day', now) + '1 hour'::interval * which
- WHEN timeframe = 'day' THEN date_trunc('month', now) + '1 day'::interval * which
- WHEN timeframe = 'month' THEN date_trunc('year', now) + '1 month'::interval * which
- WHEN timeframe = 'year' THEN make_date(which, 1, 1)::TIMESTAMP
- END
-$$;
+ ,(s.internal_taler_payments_volume).val
+ ,(s.internal_taler_payments_volume).frac
+ INTO
+ cashin_count
+ ,cashin_volume_in_fiat.val
+ ,cashin_volume_in_fiat.frac
+ ,cashout_count
+ ,cashout_volume_in_fiat.val
+ ,cashout_volume_in_fiat.frac
+ ,internal_taler_payments_count
+ ,internal_taler_payments_volume.val
+ ,internal_taler_payments_volume.frac
+ FROM regional_stats AS s
+ WHERE s.timeframe = in_timeframe
+ AND s.start_time = local_start_time;
+END $$;
CREATE OR REPLACE PROCEDURE stats_register_internal_taler_payment(
IN now TIMESTAMP,