/* * This file is part of LibEuFin. * Copyright (C) 2024 Taler Systems S.A. * 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. * You should have received a copy of the GNU Affero General Public * License along with LibEuFin; see the file COPYING. If not, see * */ 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.common.* import java.time.Instant import java.time.LocalDateTime import kotlin.test.assertEquals class StatsTest { @Test fun register() = bankSetup { db -> setMaxDebt("merchant", "KUDOS:1000") setMaxDebt("exchange", "KUDOS:1000") setMaxDebt("customer", "KUDOS:1000") suspend fun cashin(amount: String) { db.conn { conn -> val stmt = conn.prepareStatement("SELECT 0 FROM cashin(?, ?, (?, ?)::taler_amount, ?)") stmt.setLong(1, Instant.now().micros()) stmt.setBytes(2, ShortHashCode.rand().raw) val amount = TalerAmount(amount) stmt.setLong(3, amount.value) stmt.setInt(4, amount.frac) stmt.setString(5, "") stmt.executeQueryCheck() } } suspend fun monitor( dbCount: (MonitorWithConversion) -> Long, count: Long, regionalVolume: (MonitorWithConversion) -> TalerAmount, regionalAmount: String, fiatVolume: ((MonitorWithConversion) -> TalerAmount)? = null, fiatAmount: String? = null ) { Timeframe.entries.forEach { timestamp -> client.get("/monitor?timestamp=${timestamp.name}") { pwAuth("admin") }.assertOkJson { val resp = it as MonitorWithConversion assertEquals(count, dbCount(resp)) assertEquals(TalerAmount(regionalAmount), regionalVolume(resp)) fiatVolume?.run { assertEquals(TalerAmount(fiatAmount!!), this(resp)) } } } } suspend fun monitorTalerIn(count: Long, amount: String) = monitor({it.talerInCount}, count, {it.talerInVolume}, amount) suspend fun monitorTalerOut(count: Long, amount: String) = monitor({it.talerOutCount}, count, {it.talerOutVolume}, amount) suspend fun monitorCashin(count: Long, regionalAmount: String, fiatAmount: String) = monitor({it.cashinCount}, count, {it.cashinRegionalVolume}, regionalAmount, {it.cashinFiatVolume}, fiatAmount) suspend fun monitorCashout(count: Long, regionalAmount: String, fiatAmount: String) = monitor({it.cashoutCount}, count, {it.cashoutRegionalVolume}, regionalAmount, {it.cashoutFiatVolume}, fiatAmount) monitorTalerIn(0, "KUDOS:0") monitorTalerOut(0, "KUDOS:0") monitorCashin(0, "KUDOS:0", "EUR:0") monitorCashout(0, "KUDOS:0", "EUR:0") addIncoming("KUDOS:3") monitorTalerIn(1, "KUDOS:3") addIncoming("KUDOS:7.6") monitorTalerIn(2, "KUDOS:10.6") addIncoming("KUDOS:12.3") monitorTalerIn(3, "KUDOS:22.9") transfer("KUDOS:10.0") monitorTalerOut(1, "KUDOS:10.0") transfer("KUDOS:30.5") monitorTalerOut(2, "KUDOS:40.5") transfer("KUDOS:42") monitorTalerOut(3, "KUDOS:82.5") cashin("EUR:10") monitorCashin(1, "KUDOS:7.98", "EUR:10") monitorTalerIn(4, "KUDOS:30.88") cashin("EUR:20") monitorCashin(2, "KUDOS:23.96", "EUR:30") monitorTalerIn(5, "KUDOS:46.86") cashin("EUR:40") monitorCashin(3, "KUDOS:55.94", "EUR:70") monitorTalerIn(6, "KUDOS:78.84") cashout("KUDOS:3") monitorCashout(1, "KUDOS:3", "EUR:3.747") cashout("KUDOS:7.6") monitorCashout(2, "KUDOS:10.6", "EUR:13.244") cashout("KUDOS:12.3") monitorCashout(3, "KUDOS:22.9", "EUR:28.616") monitorTalerIn(6, "KUDOS:78.84") monitorTalerOut(3, "KUDOS:82.5") monitorCashin(3, "KUDOS:55.94", "EUR:70") monitorCashout(3, "KUDOS:22.9", "EUR:28.616") } @Test fun timeframe() = bankSetup { db -> db.conn { conn -> suspend fun register(now: LocalDateTime, amount: TalerAmount) { val stmt = conn.prepareStatement( "CALL stats_register_payment('taler_out', ?::timestamp, (?, ?)::taler_amount, null)" ) stmt.setObject(1, now) stmt.setLong(2, amount.value) stmt.setInt(3, amount.frac) stmt.executeUpdate() } suspend fun check( now: LocalDateTime, timeframe: Timeframe, which: Int?, 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 now = LocalDateTime.now() val otherHour = now.withHour((now.hour + 1) % 24) val otherDay = now.withDayOfMonth((now.dayOfMonth) % 28 + 1) val otherMonth = now.withMonth((now.monthValue) % 12 + 1) val otherYear = now.minusYears(1) 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")) // 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")) } } }