libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

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 }