diff options
author | Antoine A <> | 2024-04-04 16:19:20 +0200 |
---|---|---|
committer | Antoine A <> | 2024-04-04 16:19:20 +0200 |
commit | a4e1b86575e75960fd5d0b158994adef0096a8bf (patch) | |
tree | 8c8dd4bfdf7e8981130f128d59490be1ccdb1ce3 | |
parent | 42eb74f888d156a16414ec44746addd512a0e9e0 (diff) | |
download | libeufin-a4e1b86575e75960fd5d0b158994adef0096a8bf.tar.gz libeufin-a4e1b86575e75960fd5d0b158994adef0096a8bf.tar.bz2 libeufin-a4e1b86575e75960fd5d0b158994adef0096a8bf.zip |
Improve monitor API
-rw-r--r-- | API_CHANGES.md | 4 | ||||
-rw-r--r-- | bank/src/main/kotlin/tech/libeufin/bank/Constants.kt | 2 | ||||
-rw-r--r-- | bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt | 11 | ||||
-rw-r--r-- | bank/src/main/kotlin/tech/libeufin/bank/params.kt | 34 | ||||
-rw-r--r-- | bank/src/test/kotlin/StatsTest.kt | 106 | ||||
-rw-r--r-- | database-versioning/libeufin-bank-procedures.sql | 18 |
6 files changed, 97 insertions, 78 deletions
diff --git a/API_CHANGES.md b/API_CHANGES.md index ff80790f..5feb4353 100644 --- a/API_CHANGES.md +++ b/API_CHANGES.md @@ -46,9 +46,11 @@ This files contains all the API changes for the current release: - GET /accounts: add row_id field - GET /public-accounts: add row_id field - GET /config: new bank_name field for the bank name -- POST /accounts/USERNAME/transactions: new request_uid field for idempotency and new idempotency error +- POST /accounts/USERNAME/transactions: new request_uid field for idempotency + and new idempotency error - GET /accounts: new status field - GET /accounts/USERNAME: new status field +- GET /monitor: new date_s params ## bank cli diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt b/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt index 15fe5be1..504f068f 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt @@ -40,7 +40,7 @@ const val IBAN_ALLOCATION_RETRY_COUNTER: Int = 5 const val MAX_BODY_LENGTH: Long = 4 * 1024 // 4kB // API version -const val COREBANK_API_VERSION: String = "4:5:0" +const val COREBANK_API_VERSION: String = "4:6:0" const val CONVERSION_API_VERSION: String = "0:0:0" const val INTEGRATION_API_VERSION: String = "2:0:2" const val WIRE_GATEWAY_API_VERSION: String = "0:2:0" diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt index c843d355..b021ff6c 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt @@ -32,6 +32,7 @@ import tech.libeufin.common.db.* import java.sql.PreparedStatement import java.sql.ResultSet import java.sql.Types +import java.time.LocalDateTime import kotlin.math.abs private val logger: Logger = LoggerFactory.getLogger("libeufin-bank-db") @@ -70,14 +71,10 @@ class Database(dbConfig: String, internal val bankCurrency: String, internal val ,taler_out_count ,(taler_out_volume).val as taler_out_volume_val ,(taler_out_volume).frac as taler_out_volume_frac - FROM stats_get_frame(NULL, ?::stat_timeframe_enum, ?) + FROM stats_get_frame(?::timestamp, ?::stat_timeframe_enum) """) - stmt.setString(1, params.timeframe.name) - if (params.which != null) { - stmt.setInt(2, params.which) - } else { - stmt.setNull(2, Types.INTEGER) - } + stmt.setObject(1, params.date) + stmt.setString(2, params.timeframe.name) stmt.oneOrNull { fiatCurrency?.run { MonitorWithConversion( diff --git a/bank/src/main/kotlin/tech/libeufin/bank/params.kt b/bank/src/main/kotlin/tech/libeufin/bank/params.kt index 26cb2eba..4972ef99 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/params.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/params.kt @@ -22,7 +22,8 @@ package tech.libeufin.bank import io.ktor.http.* import tech.libeufin.common.TalerAmount import tech.libeufin.common.TalerErrorCode -import java.time.OffsetDateTime +import java.time.Instant +import java.time.LocalDateTime import java.time.ZoneOffset import java.time.temporal.TemporalAdjusters import java.util.UUID @@ -59,20 +60,39 @@ fun Parameters.amount(name: String): TalerAmount? data class MonitorParams( val timeframe: Timeframe, - val which: Int? + val date: LocalDateTime ) { + constructor(timeframe: Timeframe, now: LocalDateTime, which: Int) : this( + timeframe, + when (timeframe) { + Timeframe.hour -> now.withHour(which) + Timeframe.day -> now.withDayOfMonth(which) + Timeframe.month -> now.withMonth(which) + Timeframe.year -> now.withYear(which) + } + ) + constructor(timeframe: Timeframe, secs: Long) : this( + timeframe, + LocalDateTime.ofInstant(Instant.ofEpochSecond(secs), ZoneOffset.UTC) + ) companion object { val names = Timeframe.entries.map { it.name } val names_fmt = names.joinToString() + fun extract(params: Parameters): MonitorParams { val raw = params.get("timeframe") ?: "hour" if (!names.contains(raw)) { throw badRequest("Param 'timeframe' must be one of $names_fmt", TalerErrorCode.GENERIC_PARAMETER_MALFORMED) } val timeframe = Timeframe.valueOf(raw) + val now = LocalDateTime.now(ZoneOffset.UTC) val which = params.int("which") - if (which != null) { - val lastDayOfMonth = OffsetDateTime.now(ZoneOffset.UTC).with(TemporalAdjusters.lastDayOfMonth()).dayOfMonth + val dateS = params.long("date_s") + if (which != null && dateS != null) { + throw badRequest("Cannot use both 'date_s' and deprecated 'which'", TalerErrorCode.GENERIC_PARAMETER_MALFORMED) + } + return if (which != null) { + val lastDayOfMonth = now.with(TemporalAdjusters.lastDayOfMonth()).dayOfMonth when { timeframe == Timeframe.hour && (0 > which || which > 23) -> throw badRequest("For hour timestamp param 'which' must be between 00 to 23", TalerErrorCode.GENERIC_PARAMETER_MALFORMED) @@ -84,8 +104,12 @@ data class MonitorParams( throw badRequest("For year timestamp param 'which' must be between 0001 to 9999", TalerErrorCode.GENERIC_PARAMETER_MALFORMED) else -> {} } + MonitorParams(timeframe, now, which) + } else if (dateS != null) { + MonitorParams(timeframe, dateS) + } else { + MonitorParams(timeframe, now) } - return MonitorParams(timeframe, which) } } } diff --git a/bank/src/test/kotlin/StatsTest.kt b/bank/src/test/kotlin/StatsTest.kt index 71f35d0a..4dd52e3d 100644 --- a/bank/src/test/kotlin/StatsTest.kt +++ b/bank/src/test/kotlin/StatsTest.kt @@ -19,13 +19,13 @@ import io.ktor.client.request.* import org.junit.Test -import tech.libeufin.bank.MonitorResponse -import tech.libeufin.bank.MonitorWithConversion -import tech.libeufin.bank.Timeframe +import tech.libeufin.bank.* import tech.libeufin.common.* import tech.libeufin.common.db.* import java.time.Instant +import java.time.ZoneOffset import java.time.LocalDateTime +import java.time.OffsetDateTime import kotlin.test.assertEquals class StatsTest { @@ -56,9 +56,10 @@ class StatsTest { fiatVolume: ((MonitorWithConversion) -> TalerAmount)? = null, fiatAmount: String? = null ) { - Timeframe.entries.forEach { timestamp -> - client.get("/monitor?timestamp=${timestamp.name}") { pwAuth("admin") }.assertOkJson<MonitorResponse> { + Timeframe.entries.forEach { timeframe -> + client.get("/monitor?timestamp=${timeframe.name}") { pwAuth("admin") }.assertOkJson<MonitorResponse> { val resp = it as MonitorWithConversion + println("$resp") assertEquals(count, dbCount(resp)) assertEquals(TalerAmount(regionalAmount), regionalVolume(resp)) fiatVolume?.run { assertEquals(TalerAmount(fiatAmount!!), this(resp)) } @@ -131,39 +132,36 @@ class StatsTest { } suspend fun check( - now: LocalDateTime, - timeframe: Timeframe, - which: Int?, + params: MonitorParams, count: Long, amount: TalerAmount ) { - val stmt = conn.prepareStatement(""" - SELECT - taler_out_count - ,(taler_out_volume).val as taler_out_volume_val - ,(taler_out_volume).frac as taler_out_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 talerOutCount = it.getLong("taler_out_count") - val talerOutVolume = TalerAmount( - value = it.getLong("taler_out_volume_val"), - frac = it.getInt("taler_out_volume_frac"), - currency = "KUDOS" - ) - assertEquals(count, talerOutCount, "taler count") - assertEquals(amount, talerOutVolume, "taler volume") - }!! + val res = db.monitor(params) + assertEquals(count, res.talerOutCount, "taler count") + assertEquals(amount, res.talerOutVolume, "taler volume") } - val now = LocalDateTime.now() + suspend fun checkSimple( + now: LocalDateTime, + timeframe: Timeframe, + count: Long, + amount: TalerAmount + ) = check(MonitorParams(timeframe, now), count, amount) + suspend fun checkWhich( + now: LocalDateTime, + timeframe: Timeframe, + which: Int, + count: Long, + amount: TalerAmount + ) = check(MonitorParams(timeframe, now, which), count, amount) + suspend fun checkDate( + secs: Long, + timeframe: Timeframe, + count: Long, + amount: TalerAmount + ) = check(MonitorParams(timeframe, secs), count, amount) + + val now = LocalDateTime.now(ZoneOffset.UTC) val otherHour = now.withHour((now.hour + 1) % 24) val otherDay = now.withDayOfMonth((now.dayOfMonth) % 28 + 1) val otherMonth = now.withMonth((now.monthValue) % 12 + 1) @@ -176,26 +174,36 @@ class StatsTest { 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")) + checkSimple(now, Timeframe.hour, 1, TalerAmount("KUDOS:10.0")) + checkSimple(otherHour, Timeframe.hour, 1, TalerAmount("KUDOS:20.0")) + checkSimple(otherDay, Timeframe.day, 1, TalerAmount("KUDOS:35.0")) + checkSimple(otherMonth, Timeframe.month, 1, TalerAmount("KUDOS:40.0")) + checkSimple(otherYear, Timeframe.year, 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")) + checkWhich(now, Timeframe.hour, now.hour, 1, TalerAmount("KUDOS:10.0")) + checkWhich(now, Timeframe.hour, otherHour.hour, 1, TalerAmount("KUDOS:20.0")) + checkWhich(now, Timeframe.day, otherDay.dayOfMonth, 1, TalerAmount("KUDOS:35.0")) + checkWhich(now, Timeframe.month, otherMonth.monthValue, 1, TalerAmount("KUDOS:40.0")) + checkWhich(now, Timeframe.year, otherYear.year, 1, TalerAmount("KUDOS:50.0")) + + // Check with date seconds + checkDate(now.toEpochSecond(ZoneOffset.UTC), Timeframe.hour, 1, TalerAmount("KUDOS:10.0")) + checkDate(otherHour.toEpochSecond(ZoneOffset.UTC), Timeframe.hour, 1, TalerAmount("KUDOS:20.0")) + checkDate(otherDay.toEpochSecond(ZoneOffset.UTC), Timeframe.day, 1, TalerAmount("KUDOS:35.0")) + checkDate(otherMonth.toEpochSecond(ZoneOffset.UTC), Timeframe.month, 1, TalerAmount("KUDOS:40.0")) + checkDate(otherYear.toEpochSecond(ZoneOffset.UTC), Timeframe.year, 1, TalerAmount("KUDOS:50.0")) // 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")) + checkSimple(now, Timeframe.day, 2, TalerAmount("KUDOS:30.0")) + checkSimple(now, Timeframe.month, 3, TalerAmount("KUDOS:65.0")) + checkSimple(now, Timeframe.year, 4, TalerAmount("KUDOS:105.0")) + checkWhich(now, Timeframe.day, now.dayOfMonth, 2, TalerAmount("KUDOS:30.0")) + checkWhich(now, Timeframe.month, now.monthValue, 3, TalerAmount("KUDOS:65.0")) + checkWhich(now, Timeframe.year, now.year, 4, TalerAmount("KUDOS:105.0")) + checkDate(now.toEpochSecond(ZoneOffset.UTC), Timeframe.day, 2, TalerAmount("KUDOS:30.0")) + checkDate(now.toEpochSecond(ZoneOffset.UTC), Timeframe.month, 3, TalerAmount("KUDOS:65.0")) + checkDate(now.toEpochSecond(ZoneOffset.UTC), Timeframe.year, 4, TalerAmount("KUDOS:105.0")) } } } diff --git a/database-versioning/libeufin-bank-procedures.sql b/database-versioning/libeufin-bank-procedures.sql index 7ef7c3f1..0dceea3c 100644 --- a/database-versioning/libeufin-bank-procedures.sql +++ b/database-versioning/libeufin-bank-procedures.sql @@ -1281,9 +1281,8 @@ END $$; COMMENT ON FUNCTION tan_challenge_try IS 'Try to confirm a challenge, return true if the challenge have been confirmed'; CREATE FUNCTION stats_get_frame( - IN now TIMESTAMP, + IN date TIMESTAMP, IN in_timeframe stat_timeframe_enum, - IN which INTEGER, OUT cashin_count INT8, OUT cashin_regional_volume taler_amount, OUT cashin_fiat_volume taler_amount, @@ -1296,19 +1295,8 @@ CREATE FUNCTION stats_get_frame( OUT taler_out_volume taler_amount ) LANGUAGE plpgsql AS $$ -DECLARE - local_start_time TIMESTAMP; BEGIN - IF now IS NULL THEN - now = timezone('utc', now())::TIMESTAMP; - END IF; - 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; + date = date_trunc(in_timeframe::text, date); SELECT s.cashin_count ,(s.cashin_regional_volume).val @@ -1345,7 +1333,7 @@ BEGIN ,taler_out_volume.frac FROM bank_stats AS s WHERE s.timeframe = in_timeframe - AND s.start_time = local_start_time; + AND s.start_time = date; END $$; CREATE PROCEDURE stats_register_payment( |