StatsTest.kt (9586B)
1 /* 2 * This file is part of LibEuFin. 3 * Copyright (C) 2024-2025 Taler Systems S.A. 4 5 * LibEuFin is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU Affero General Public License as 7 * published by the Free Software Foundation; either version 3, or 8 * (at your option) any later version. 9 10 * LibEuFin is distributed in the hope that it will be useful, but 11 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 12 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General 13 * Public License for more details. 14 15 * You should have received a copy of the GNU Affero General Public 16 * License along with LibEuFin; see the file COPYING. If not, see 17 * <http://www.gnu.org/licenses/> 18 */ 19 20 import io.ktor.client.request.* 21 import org.junit.Test 22 import tech.libeufin.bank.MonitorParams 23 import tech.libeufin.bank.MonitorResponse 24 import tech.libeufin.bank.MonitorWithConversion 25 import tech.libeufin.bank.Timeframe 26 import tech.libeufin.common.ShortHashCode 27 import tech.libeufin.common.TalerAmount 28 import tech.libeufin.common.assertOkJson 29 import tech.libeufin.common.db.* 30 import tech.libeufin.common.micros 31 import tech.libeufin.common.test.* 32 import java.time.Instant 33 import java.time.LocalDateTime 34 import java.time.ZoneOffset 35 import kotlin.test.assertEquals 36 37 class StatsTest { 38 @Test 39 fun register() = bankSetup { db -> 40 setMaxDebt("merchant", "KUDOS:1000") 41 setMaxDebt("exchange", "KUDOS:1000") 42 setMaxDebt("customer", "KUDOS:1000") 43 44 suspend fun cashin(amount: String) { 45 db.conn { conn -> 46 val stmt = conn.talerStatement("SELECT 0 FROM cashin(?, ?, (?, ?)::taler_amount, ?)") 47 stmt.bind(Instant.now()) 48 stmt.bind(ShortHashCode.rand()) 49 val amount = TalerAmount(amount) 50 stmt.bind(amount) 51 stmt.bind("") 52 stmt.executeQueryCheck() 53 } 54 } 55 56 suspend fun monitor( 57 dbCount: (MonitorWithConversion) -> Long, 58 count: Long, 59 regionalVolume: (MonitorWithConversion) -> TalerAmount, 60 regionalAmount: String, 61 fiatVolume: ((MonitorWithConversion) -> TalerAmount)? = null, 62 fiatAmount: String? = null 63 ) { 64 Timeframe.entries.forEach { timeframe -> 65 client.getAdmin("/monitor?timestamp=${timeframe.name}").assertOkJson<MonitorResponse> { 66 val resp = it as MonitorWithConversion 67 assertEquals(count, dbCount(resp)) 68 assertEquals(TalerAmount(regionalAmount), regionalVolume(resp)) 69 fiatVolume?.run { assertEquals(TalerAmount(fiatAmount!!), this(resp)) } 70 } 71 } 72 } 73 74 suspend fun monitorTalerIn(count: Long, amount: String) = 75 monitor({it.talerInCount}, count, {it.talerInVolume}, amount) 76 suspend fun monitorTalerOut(count: Long, amount: String) = 77 monitor({it.talerOutCount}, count, {it.talerOutVolume}, amount) 78 suspend fun monitorCashin(count: Long, regionalAmount: String, fiatAmount: String) = 79 monitor({it.cashinCount}, count, {it.cashinRegionalVolume}, regionalAmount, {it.cashinFiatVolume}, fiatAmount) 80 suspend fun monitorCashout(count: Long, regionalAmount: String, fiatAmount: String) = 81 monitor({it.cashoutCount}, count, {it.cashoutRegionalVolume}, regionalAmount, {it.cashoutFiatVolume}, fiatAmount) 82 83 monitorTalerIn(0, "KUDOS:0") 84 monitorTalerOut(0, "KUDOS:0") 85 monitorCashin(0, "KUDOS:0", "EUR:0") 86 monitorCashout(0, "KUDOS:0", "EUR:0") 87 88 addIncoming("KUDOS:3") 89 monitorTalerIn(1, "KUDOS:3") 90 addIncoming("KUDOS:7.6") 91 monitorTalerIn(2, "KUDOS:10.6") 92 addIncoming("KUDOS:12.3") 93 monitorTalerIn(3, "KUDOS:22.9") 94 95 // KYC are ignored 96 addKyc("KUDOS:3") 97 monitorTalerIn(3, "KUDOS:22.9") 98 99 transfer("KUDOS:10.0") 100 monitorTalerOut(1, "KUDOS:10.0") 101 transfer("KUDOS:30.5") 102 monitorTalerOut(2, "KUDOS:40.5") 103 transfer("KUDOS:42") 104 monitorTalerOut(3, "KUDOS:82.5") 105 106 cashin("EUR:10") 107 monitorCashin(1, "KUDOS:7.98", "EUR:10") 108 monitorTalerIn(4, "KUDOS:30.88") 109 cashin("EUR:20") 110 monitorCashin(2, "KUDOS:23.96", "EUR:30") 111 monitorTalerIn(5, "KUDOS:46.86") 112 cashin("EUR:40") 113 monitorCashin(3, "KUDOS:55.94", "EUR:70") 114 monitorTalerIn(6, "KUDOS:78.84") 115 116 cashout("KUDOS:3") 117 monitorCashout(1, "KUDOS:3", "EUR:3.77") 118 cashout("KUDOS:7.6") 119 monitorCashout(2, "KUDOS:10.6", "EUR:13.34") 120 cashout("KUDOS:12.3") 121 monitorCashout(3, "KUDOS:22.9", "EUR:28.83") 122 123 monitorTalerIn(6, "KUDOS:78.84") 124 monitorTalerOut(3, "KUDOS:82.5") 125 monitorCashin(3, "KUDOS:55.94", "EUR:70") 126 monitorCashout(3, "KUDOS:22.9", "EUR:28.83") 127 } 128 129 @Test 130 fun timeframe() = bankSetup { db -> 131 db.conn { conn -> 132 fun register(timestamp: LocalDateTime, amount: TalerAmount) { 133 val stmt = conn.talerStatement( 134 "CALL stats_register_payment('taler_out', ?::timestamp, (?, ?)::taler_amount, null)" 135 ) 136 stmt.bind(timestamp) 137 stmt.bind(amount) 138 stmt.executeUpdate() 139 } 140 141 suspend fun check( 142 params: MonitorParams, 143 count: Long, 144 amount: TalerAmount 145 ) { 146 val res = db.monitor(params) 147 assertEquals(count, res.talerOutCount, "taler count") 148 assertEquals(amount, res.talerOutVolume, "taler volume") 149 } 150 151 suspend fun checkSimple( 152 timestamp: LocalDateTime, 153 timeframe: Timeframe, 154 count: Long, 155 amount: TalerAmount 156 ) = check(MonitorParams(timeframe, timestamp), count, amount) 157 suspend fun checkWhich( 158 timestamp: LocalDateTime, 159 timeframe: Timeframe, 160 which: Int, 161 count: Long, 162 amount: TalerAmount 163 ) = check(MonitorParams(timeframe, timestamp, which), count, amount) 164 suspend fun checkDate( 165 secs: Long, 166 timeframe: Timeframe, 167 count: Long, 168 amount: TalerAmount 169 ) = check(MonitorParams(timeframe, secs), count, amount) 170 171 val now = LocalDateTime.now(ZoneOffset.UTC) 172 val otherHour = now.withHour((now.hour + 1) % 24) 173 val otherDay = now.withDayOfMonth((now.dayOfMonth) % 28 + 1) 174 val otherMonth = now.withMonth((now.monthValue) % 12 + 1) 175 val otherYear = now.minusYears(1) 176 177 register(now, TalerAmount("KUDOS:10.0")) 178 register(otherHour, TalerAmount("KUDOS:20.0")) 179 register(otherDay, TalerAmount("KUDOS:35.0")) 180 register(otherMonth, TalerAmount("KUDOS:40.0")) 181 register(otherYear, TalerAmount("KUDOS:50.0")) 182 183 // Check with timestamp and truncating 184 checkSimple(now, Timeframe.hour, 1, TalerAmount("KUDOS:10.0")) 185 checkSimple(otherHour, Timeframe.hour, 1, TalerAmount("KUDOS:20.0")) 186 checkSimple(otherDay, Timeframe.day, 1, TalerAmount("KUDOS:35.0")) 187 checkSimple(otherMonth, Timeframe.month, 1, TalerAmount("KUDOS:40.0")) 188 checkSimple(otherYear, Timeframe.year, 1, TalerAmount("KUDOS:50.0")) 189 190 // Check with timestamp and intervals 191 checkWhich(now, Timeframe.hour, now.hour, 1, TalerAmount("KUDOS:10.0")) 192 checkWhich(now, Timeframe.hour, otherHour.hour, 1, TalerAmount("KUDOS:20.0")) 193 checkWhich(now, Timeframe.day, otherDay.dayOfMonth, 1, TalerAmount("KUDOS:35.0")) 194 checkWhich(now, Timeframe.month, otherMonth.monthValue, 1, TalerAmount("KUDOS:40.0")) 195 checkWhich(now, Timeframe.year, otherYear.year, 1, TalerAmount("KUDOS:50.0")) 196 197 // Check with date seconds 198 checkDate(now.toEpochSecond(ZoneOffset.UTC), Timeframe.hour, 1, TalerAmount("KUDOS:10.0")) 199 checkDate(otherHour.toEpochSecond(ZoneOffset.UTC), Timeframe.hour, 1, TalerAmount("KUDOS:20.0")) 200 checkDate(otherDay.toEpochSecond(ZoneOffset.UTC), Timeframe.day, 1, TalerAmount("KUDOS:35.0")) 201 checkDate(otherMonth.toEpochSecond(ZoneOffset.UTC), Timeframe.month, 1, TalerAmount("KUDOS:40.0")) 202 checkDate(otherYear.toEpochSecond(ZoneOffset.UTC), Timeframe.year, 1, TalerAmount("KUDOS:50.0")) 203 204 // Check timestamp aggregation 205 checkSimple(now, Timeframe.day, 2, TalerAmount("KUDOS:30.0")) 206 checkSimple(now, Timeframe.month, 3, TalerAmount("KUDOS:65.0")) 207 checkSimple(now, Timeframe.year, 4, TalerAmount("KUDOS:105.0")) 208 checkWhich(now, Timeframe.day, now.dayOfMonth, 2, TalerAmount("KUDOS:30.0")) 209 checkWhich(now, Timeframe.month, now.monthValue, 3, TalerAmount("KUDOS:65.0")) 210 checkWhich(now, Timeframe.year, now.year, 4, TalerAmount("KUDOS:105.0")) 211 checkDate(now.toEpochSecond(ZoneOffset.UTC), Timeframe.day, 2, TalerAmount("KUDOS:30.0")) 212 checkDate(now.toEpochSecond(ZoneOffset.UTC), Timeframe.month, 3, TalerAmount("KUDOS:65.0")) 213 checkDate(now.toEpochSecond(ZoneOffset.UTC), Timeframe.year, 4, TalerAmount("KUDOS:105.0")) 214 } 215 } 216 }