libeufin

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

DatabaseTest.kt (50741B)


      1 /*
      2  * This file is part of LibEuFin.
      3  * Copyright (C) 2024, 2025, 2026 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 org.junit.Test
     21 import tech.libeufin.common.*
     22 import tech.libeufin.common.db.*
     23 import tech.libeufin.nexus.AccountType
     24 import tech.libeufin.nexus.NexusIngestConfig
     25 import tech.libeufin.nexus.iso20022.*
     26 import tech.libeufin.nexus.cli.*
     27 import tech.libeufin.nexus.db.*
     28 import tech.libeufin.nexus.db.PaymentDAO.*
     29 import tech.libeufin.nexus.db.InitiatedDAO.*
     30 import tech.libeufin.nexus.db.TransferDAO.*
     31 import tech.libeufin.ebics.*
     32 import java.time.Instant
     33 import java.util.UUID;
     34 import kotlin.test.*
     35 
     36 suspend fun Database.checkInCount(nbIncoming: Int, nbBounce: Int, nbTalerable: Int) = serializable(
     37     """
     38         SELECT (SELECT count(*) FROM incoming_transactions) AS incoming,
     39                (SELECT count(*) FROM bounced_transactions) AS bounce,
     40                (SELECT count(*) FROM talerable_incoming_transactions) AS talerable;
     41     """
     42 ) {
     43     one {
     44         assertEquals(
     45             Triple(nbIncoming, nbBounce, nbTalerable),
     46             Triple(it.getInt("incoming"), it.getInt("bounce"), it.getInt("talerable"))
     47         )
     48     }
     49 }
     50 
     51 suspend fun Database.checkOutCount(nbIncoming: Int, nbTalerable: Int) = serializable(
     52     """
     53         SELECT (SELECT count(*) FROM outgoing_transactions) AS incoming,
     54                (SELECT count(*) FROM talerable_outgoing_transactions) AS talerable;
     55     """
     56 ) {
     57     one {
     58         assertEquals(
     59             Pair(nbIncoming, nbTalerable),
     60             Pair(it.getInt("incoming"), it.getInt("talerable"))
     61         )
     62     }
     63 }
     64 
     65 sealed interface Status {
     66     data object Simple : Status
     67     data object Pending : Status
     68     data object Bounced : Status
     69     data object Incomplete : Status
     70     data class Reserve(val key: EddsaPublicKey) : Status
     71     data class Kyc(val key: EddsaPublicKey) : Status
     72 }
     73 
     74 suspend fun Database.checkIn(vararg expected: Status) {
     75     val current = this.serializable(
     76         """
     77             SELECT pending_recurrent_incoming_transactions.authorization_pub IS NOT NULL, initiated_outgoing_transaction_id IS NOT NULL, debit_payto IS NULL OR subject IS NULL, type, metadata 
     78             FROM incoming_transactions
     79                 LEFT JOIN talerable_incoming_transactions USING (incoming_transaction_id)
     80                 LEFT JOIN pending_recurrent_incoming_transactions USING (incoming_transaction_id)
     81                 LEFT JOIN bounced_transactions USING (incoming_transaction_id)
     82             ORDER BY incoming_transaction_id
     83         """
     84     ) {
     85         all {
     86             if (it.getBoolean(1)) {
     87                 Status.Pending
     88             } else if (it.getBoolean(2)) {
     89                 Status.Bounced
     90             } else if (it.getBoolean(3)) {
     91                 Status.Incomplete
     92             } else {
     93                 when (it.getOptEnum<TransferType>(4)) {
     94                     null -> Status.Simple
     95                     TransferType.reserve -> Status.Reserve(EddsaPublicKey(it.getBytes(5)))
     96                     TransferType.kyc -> Status.Kyc(EddsaPublicKey(it.getBytes(5)))
     97                 }
     98             }
     99         }.toList()
    100     }
    101     assertContentEquals(listOf(*expected), current)
    102 }
    103 
    104 class OutgoingPaymentsTest {
    105     @Test
    106     fun register() = setup { db, _ -> 
    107         // Register initiated transaction
    108         for (subject in sequenceOf(
    109             "initiated by nexus",
    110             "${ShortHashCode.rand()} https://exchange.com/"
    111         )) {
    112             val pay = genOutPay(subject)
    113             assertIs<PaymentInitiationResult.Success>(
    114                 db.initiated.create(genInitPay(pay.id.endToEndId!!, subject))
    115             )
    116             val first = registerOutgoingPayment(db, pay)
    117             assertEquals(OutgoingRegistrationResult(id = first.id, initiated = true, new = true), first)
    118             assertEquals(
    119                 OutgoingRegistrationResult(id = first.id, initiated = true, new = false),
    120                 registerOutgoingPayment(db, pay)
    121             )
    122 
    123             val refOnly = pay.copy(id = OutgoingId(null, null, acctSvcrRef = pay.id.endToEndId))
    124             val second = registerOutgoingPayment(db, refOnly)
    125             assertEquals(OutgoingRegistrationResult(id = first.id + 1, initiated = false, new = true), second)
    126             assertEquals(
    127                 OutgoingRegistrationResult(id = second.id, initiated = false, new = false),
    128                 registerOutgoingPayment(db, refOnly)
    129             )
    130         }
    131         db.checkOutCount(nbIncoming = 4, nbTalerable = 1)
    132 
    133         // Register unknown
    134         for (subject in sequenceOf(
    135             "not initiated by nexus",
    136             "${ShortHashCode.rand()} https://exchange.com/"
    137         )) {
    138             val pay = genOutPay(subject)
    139             val first = registerOutgoingPayment(db, pay)
    140             assertEquals(OutgoingRegistrationResult(id = first.id, initiated = false, new = true), first)
    141             assertEquals(
    142                 OutgoingRegistrationResult(id = first.id, initiated = false, new = false),
    143                 registerOutgoingPayment(db, pay)
    144             )
    145         }
    146         db.checkOutCount(nbIncoming = 6, nbTalerable = 2)
    147 
    148         // Register wtid reuse
    149         val wtid = ShortHashCode.rand()
    150         for (subject in sequenceOf(
    151             "$wtid https://exchange.com/",
    152             "$wtid https://exchange.com/"
    153         )) {
    154             val pay = genOutPay(subject)
    155             val first = registerOutgoingPayment(db, pay)
    156             assertEquals(OutgoingRegistrationResult(id = first.id, initiated = false, new = true), first)
    157             assertEquals(
    158                 OutgoingRegistrationResult(id = first.id, initiated = false, new = false),
    159                 db.payment.registerOutgoing(pay, null, null, null)
    160             )
    161         }
    162         db.checkOutCount(nbIncoming = 8, nbTalerable = 3)
    163     }
    164 
    165     @Test
    166     fun registerBatch() = setup { db, _ ->
    167         // Init batch
    168         val wtid = ShortHashCode.rand()
    169         for (subject in sequenceOf(
    170             "initiated by nexus",
    171             "${ShortHashCode.rand()} https://exchange.com/",
    172             "$wtid https://exchange.com/",
    173             "$wtid https://exchange.com/"
    174         )) {
    175             assertIs<PaymentInitiationResult.Success>(
    176                 db.initiated.create(genInitPay(randEbicsId(), subject=subject))
    177             )
    178         }
    179         db.initiated.batch(Instant.now(), "BATCH", false)
    180 
    181         // Register batch
    182         registerOutgoingBatch(db, OutgoingBatch("BATCH", Instant.now()));
    183         db.checkOutCount(nbIncoming = 4, nbTalerable = 2)
    184 
    185         // Test manual ack
    186         val txs = List(3) { nb ->
    187             assertIs<PaymentInitiationResult.Success>(
    188                 db.initiated.create(genInitPay(randEbicsId(), subject="tx $nb"))
    189             ).id
    190         }
    191 
    192         // Check not sent without ack
    193         db.initiated.batch(Instant.now(), "BATCH_MANUAL", true)
    194         registerOutgoingBatch(db, OutgoingBatch("BATCH_MANUAL", Instant.now()));
    195         db.checkOutCount(nbIncoming = 4, nbTalerable = 2)
    196 
    197         // Check sent with ack
    198         for (tx in txs) {
    199             db.initiated.ack(tx)
    200         }
    201         db.initiated.batch(Instant.now(), "BATCH_MANUAL", true)
    202         registerOutgoingBatch(db, OutgoingBatch("BATCH_MANUAL", Instant.now()));
    203         db.checkOutCount(nbIncoming = 7, nbTalerable = 2)
    204     }
    205 }
    206 
    207 class IncomingPaymentsTest {
    208     // Tests creating and bouncing incoming payments in one DB transaction
    209     @Test
    210     fun bounce() = setup { db, _ -> 
    211         // creating and bouncing one incoming transaction.
    212         val payment = genInPay("incoming and bounced")
    213         val id = randEbicsId()
    214         db.payment.registerMalformedIncoming(
    215             payment,
    216             TalerAmount("KUDOS:2.53"),
    217             id,
    218             Instant.now(),
    219             "manual bounce"
    220         ).run {
    221             assertIs<IncomingBounceRegistrationResult.Success>(this)
    222             assertTrue(new)
    223             assertEquals(id, bounceId)
    224         }
    225         db.payment.registerMalformedIncoming(
    226             payment,
    227             TalerAmount("KUDOS:2.53"),
    228             randEbicsId(),
    229             Instant.now(),
    230             "manual bounce"
    231         ).run {
    232             assertIs<IncomingBounceRegistrationResult.Success>(this)
    233             assertFalse(new)
    234             assertEquals(id, bounceId)
    235         }
    236         db.conn {
    237             // Checking one incoming got created
    238             val checkIncoming = it.talerStatement("""
    239                 SELECT (amount).val as amount_value, (amount).frac as amount_frac 
    240                 FROM incoming_transactions WHERE incoming_transaction_id = 1
    241             """).executeQuery()
    242             assertTrue(checkIncoming.next())
    243             assertEquals(payment.amount.value, checkIncoming.getLong("amount_value"))
    244             assertEquals(payment.amount.frac, checkIncoming.getInt("amount_frac"))
    245             // Checking the bounced table got its row.
    246             val checkBounced = it.talerStatement("""
    247                 SELECT 1 FROM bounced_transactions 
    248                 WHERE incoming_transaction_id = 1 AND initiated_outgoing_transaction_id = 1
    249             """).executeQuery()
    250             assertTrue(checkBounced.next())
    251             // check the related initiated payment exists.
    252             val checkInitiated = it.talerStatement("""
    253                 SELECT
    254                     (amount).val as amount_value
    255                     ,(amount).frac as amount_frac
    256                 FROM initiated_outgoing_transactions
    257                 WHERE initiated_outgoing_transaction_id = 1
    258             """).executeQuery()
    259             assertTrue(checkInitiated.next())
    260             assertEquals(
    261                 53000000,
    262                 checkInitiated.getInt("amount_frac")
    263             )
    264             assertEquals(
    265                 2,
    266                 checkInitiated.getInt("amount_value")
    267             )
    268         }
    269     }
    270 
    271     // Test creating an incoming reserve transaction without and ID and reconcile it later again
    272     @Test
    273     fun simple() = setup { db, _ ->
    274         val cfg = NexusIngestConfig.default(AccountType.exchange)
    275         val subject = "test"  
    276         
    277         // Register
    278         val incoming = genInPay(subject)
    279         registerIncomingPayment(db, cfg, incoming)
    280         db.checkIn(Status.Bounced)
    281 
    282         // Idempotent
    283         registerIncomingPayment(db, cfg, incoming)
    284         db.checkIn(Status.Bounced)
    285 
    286         // No key reuse
    287         registerIncomingPayment(db, cfg, genInPay(subject, "KUDOS:9"))
    288         registerIncomingPayment(db, cfg, genInPay("another $subject"))
    289         db.checkIn(Status.Bounced, Status.Bounced, Status.Bounced)
    290 
    291         // Admin balance adjust is ignored
    292         registerIncomingPayment(db, cfg, genInPay("ADMIN BALANCE ADJUST"))
    293         db.checkIn(Status.Bounced, Status.Bounced, Status.Bounced, Status.Simple)
    294 
    295         val original = genInPay("test 2")
    296         val incomplete = original.copy(subject = null, debtor = null)
    297         // Register incomplete transaction
    298         registerIncomingPayment(db, cfg, incomplete)
    299         db.checkIn(Status.Bounced, Status.Bounced, Status.Bounced, Status.Simple, Status.Incomplete)
    300 
    301         // Idempotent
    302         registerIncomingPayment(db, cfg, incomplete)
    303         db.checkIn(Status.Bounced, Status.Bounced, Status.Bounced, Status.Simple, Status.Incomplete)
    304 
    305         // Recover info when complete
    306         registerIncomingPayment(db, cfg, original)
    307         db.checkIn(Status.Bounced, Status.Bounced, Status.Bounced, Status.Simple, Status.Bounced)
    308     }
    309 
    310     // Test creating an incoming reserve taler transaction without and ID and reconcile it later again
    311     @Test
    312     fun talerable() = setup { db, _ ->
    313         val cfg = NexusIngestConfig.default(AccountType.exchange)
    314         val key = EddsaPublicKey.randEdsaKey()
    315         val subject = "test with $key reserve pub"  
    316         
    317         // Register
    318         val incoming = genInPay(subject)
    319         registerIncomingPayment(db, cfg, incoming)
    320         db.checkIn(Status.Reserve(key))
    321 
    322         // Idempotent
    323         registerIncomingPayment(db, cfg, incoming)
    324         db.checkIn(Status.Reserve(key))
    325 
    326         // Key reuse is bounced
    327         registerIncomingPayment(db, cfg, genInPay(subject, "KUDOS:9"))
    328         registerIncomingPayment(db, cfg, genInPay("another $subject"))
    329         db.checkIn(Status.Reserve(key), Status.Bounced, Status.Bounced)
    330 
    331         // Admin balance adjust is ignored
    332         registerIncomingPayment(db, cfg, genInPay("ADMIN BALANCE ADJUST"))
    333         db.checkIn(Status.Reserve(key), Status.Bounced, Status.Bounced, Status.Simple)
    334 
    335         val newKey = EddsaPublicKey.randEdsaKey()
    336         val original = genInPay("test 2 with $newKey reserve pub")
    337         val incomplete = original.copy(subject = null, debtor = null)
    338         // Register incomplete transaction
    339         registerIncomingPayment(db, cfg, incomplete)
    340         db.checkIn(Status.Reserve(key), Status.Bounced, Status.Bounced, Status.Simple, Status.Incomplete)
    341 
    342         // Idempotent
    343         registerIncomingPayment(db, cfg, incomplete)
    344         db.checkIn(Status.Reserve(key), Status.Bounced, Status.Bounced, Status.Simple, Status.Incomplete)
    345 
    346         // Recover info when complete
    347         registerIncomingPayment(db, cfg, original)
    348         db.checkIn(Status.Reserve(key), Status.Bounced, Status.Bounced, Status.Simple, Status.Reserve(newKey))
    349     }
    350 
    351     // Test creating an mapped reserve taler transaction without and ID and reconcile it later again
    352     @Test
    353     fun mapping() = setup { db, _ ->
    354         val cfg = NexusIngestConfig.default(AccountType.exchange)
    355         val firstKey = EddsaPublicKey.randEdsaKey()
    356         val authPub = EddsaPublicKey.randEdsaKey()
    357         val sig = EddsaSignature.rand()
    358         val referenceNumber = subjectFmtQrBill(authPub)
    359         val subject = "test with MAP:$authPub auth pub"
    360 
    361         assertEquals(
    362             RegistrationResult.Success,
    363             db.transfer.register(
    364                 type = TransferType.reserve,
    365                 accountPub = firstKey,
    366                 authPub = authPub,
    367                 authSig = sig,
    368                 referenceNumber = referenceNumber,
    369                 timestamp = Instant.now(),
    370                 recurrent = true
    371             )
    372         )
    373         
    374         // Register
    375         val incoming = genInPay(subject)
    376         registerIncomingPayment(db, cfg, incoming)
    377         db.checkIn(Status.Reserve(firstKey))
    378 
    379         // Idempotent
    380         registerIncomingPayment(db, cfg, incoming)
    381         db.checkIn(Status.Reserve(firstKey))
    382 
    383         // Admin balance adjust is ignored
    384         registerIncomingPayment(db, cfg, genInPay("ADMIN BALANCE ADJUST"))
    385         db.checkIn(Status.Reserve(firstKey), Status.Simple)
    386 
    387         val original = genInPay("test 2 for $subject")
    388         val incomplete = original.copy(subject = null, debtor = null)
    389         // Register incomplete transaction
    390         registerIncomingPayment(db, cfg, incomplete)
    391         db.checkIn(Status.Reserve(firstKey), Status.Simple, Status.Incomplete)
    392 
    393         // Idempotent
    394         registerIncomingPayment(db, cfg, incomplete)
    395         db.checkIn(Status.Reserve(firstKey), Status.Simple, Status.Incomplete)
    396 
    397         // Recover info when complete
    398         registerIncomingPayment(db, cfg, original)
    399         db.checkIn(Status.Reserve(firstKey), Status.Simple, Status.Pending)
    400 
    401         val secondKey = EddsaPublicKey.randEdsaKey()
    402         assertEquals(
    403             RegistrationResult.Success,
    404             db.transfer.register(
    405                 type = TransferType.reserve,
    406                 accountPub = secondKey,
    407                 authPub = authPub,
    408                 authSig = sig,
    409                 referenceNumber = referenceNumber,
    410                 timestamp = Instant.now(),
    411                 recurrent = true
    412             )
    413         )
    414 
    415         db.checkIn(Status.Reserve(firstKey), Status.Simple, Status.Reserve(secondKey))
    416 
    417         // Key reuse is pending
    418         registerIncomingPayment(db, cfg, genInPay(subject, "KUDOS:9"))
    419         registerIncomingPayment(db, cfg, genInPay("another $subject"))
    420         db.checkIn(Status.Reserve(firstKey), Status.Simple, Status.Reserve(secondKey), Status.Pending, Status.Pending)
    421 
    422         // Finish pending
    423         val thirdKey = EddsaPublicKey.randEdsaKey()
    424         assertEquals(
    425             RegistrationResult.Success,
    426             db.transfer.register(
    427                 type = TransferType.reserve,
    428                 accountPub = thirdKey,
    429                 authPub = authPub,
    430                 authSig = sig,
    431                 referenceNumber = referenceNumber,
    432                 timestamp = Instant.now(),
    433                 recurrent = true
    434             )
    435         )
    436         db.checkIn(Status.Reserve(firstKey), Status.Simple, Status.Reserve(secondKey), Status.Reserve(thirdKey), Status.Pending)
    437     }
    438 
    439     // Test creating an mapped reserve taler transaction without and ID and reconcile it later again
    440     @Test
    441     fun reference() = setup { db, _ ->
    442         val cfg = NexusIngestConfig.default(AccountType.exchange)
    443         val firstKey = EddsaPublicKey.randEdsaKey()
    444         val authPub = EddsaPublicKey.randEdsaKey()
    445         val sig = EddsaSignature.rand()
    446         val referenceNumber = subjectFmtQrBill(authPub)
    447 
    448         assertEquals(
    449             RegistrationResult.Success,
    450             db.transfer.register(
    451                 type = TransferType.reserve,
    452                 accountPub = firstKey,
    453                 authPub = authPub,
    454                 authSig = sig,
    455                 referenceNumber = referenceNumber,
    456                 timestamp = Instant.now(),
    457                 recurrent = true
    458             )
    459         )
    460         
    461         // Register
    462         val incoming = genInPay(referenceNumber)
    463         registerIncomingPayment(db, cfg, incoming)
    464         db.checkIn(Status.Reserve(firstKey))
    465 
    466         // Idempotent
    467         registerIncomingPayment(db, cfg, incoming)
    468         db.checkIn(Status.Reserve(firstKey))
    469 
    470         // Admin balance adjust is ignored
    471         registerIncomingPayment(db, cfg, genInPay("ADMIN BALANCE ADJUST"))
    472         db.checkIn(Status.Reserve(firstKey), Status.Simple)
    473 
    474         val original = genInPay(referenceNumber)
    475         val incomplete = original.copy(subject = null, debtor = null)
    476         // Register incomplete transaction
    477         registerIncomingPayment(db, cfg, incomplete)
    478         db.checkIn(Status.Reserve(firstKey), Status.Simple, Status.Incomplete)
    479 
    480         // Idempotent
    481         registerIncomingPayment(db, cfg, incomplete)
    482         db.checkIn(Status.Reserve(firstKey), Status.Simple, Status.Incomplete)
    483 
    484         // Recover info when complete
    485         registerIncomingPayment(db, cfg, original)
    486         db.checkIn(Status.Reserve(firstKey), Status.Simple, Status.Pending)
    487 
    488         val secondKey = EddsaPublicKey.randEdsaKey()
    489         assertEquals(
    490             RegistrationResult.Success,
    491             db.transfer.register(
    492                 type = TransferType.reserve,
    493                 accountPub = secondKey,
    494                 authPub = authPub,
    495                 authSig = sig,
    496                 referenceNumber = referenceNumber,
    497                 timestamp = Instant.now(),
    498                 recurrent = true
    499             )
    500         )
    501 
    502         db.checkIn(Status.Reserve(firstKey), Status.Simple, Status.Reserve(secondKey))
    503 
    504         // Key reuse is pending
    505         registerIncomingPayment(db, cfg, genInPay(referenceNumber, "KUDOS:9"))
    506         registerIncomingPayment(db, cfg, genInPay(referenceNumber))
    507         db.checkIn(Status.Reserve(firstKey), Status.Simple, Status.Reserve(secondKey), Status.Pending, Status.Pending)
    508 
    509         // Finish pending
    510         val thirdKey = EddsaPublicKey.randEdsaKey()
    511         assertEquals(
    512             RegistrationResult.Success,
    513             db.transfer.register(
    514                 type = TransferType.reserve,
    515                 accountPub = thirdKey,
    516                 authPub = authPub,
    517                 authSig = sig,
    518                 referenceNumber = referenceNumber,
    519                 timestamp = Instant.now(),
    520                 recurrent = true
    521             )
    522         )
    523         db.checkIn(Status.Reserve(firstKey), Status.Simple, Status.Reserve(secondKey), Status.Reserve(thirdKey), Status.Pending)
    524     }
    525 
    526     @Test 
    527     fun recoverInfo() = setup { db, _ ->
    528         val cfg = NexusIngestConfig.default(AccountType.exchange)
    529 
    530         suspend fun Database.checkContent(payment: IncomingPayment) = serializable(
    531             """
    532                 SELECT
    533                     uetr IS NOT DISTINCT FROM ? AND
    534                     tx_id IS NOT DISTINCT FROM ? AND
    535                     acct_svcr_ref IS NOT DISTINCT FROM ? AND
    536                     subject IS NOT DISTINCT FROM ? AND
    537                     debit_payto IS NOT DISTINCT FROM ?
    538                 FROM incoming_transactions ORDER BY incoming_transaction_id DESC LIMIT 1
    539             """
    540         ) {
    541             bind(payment.id.uetr)
    542             bind(payment.id.txId)
    543             bind(payment.id.acctSvcrRef)
    544             bind(payment.subject)
    545             bind(payment.debtor?.toString())
    546             one {
    547                 assertTrue(it.getBoolean(1))
    548             }
    549         }
    550 
    551         // Non talerable
    552         for ((index, partialId) in sequenceOf(
    553                 IncomingId(UUID.randomUUID(), null, null),
    554                 IncomingId(null, randEbicsId(), null),
    555                 IncomingId(null, null, randEbicsId()),
    556             ).withIndex()) {
    557             val payment = genInPay("subject")
    558 
    559             // Register minimal
    560             val partialPayment = payment.copy(id = partialId, subject = null, debtor = null)
    561             registerIncomingPayment(db, cfg, partialPayment)
    562             db.checkContent(partialPayment)
    563             db.checkInCount(index + 1, index, 0)
    564 
    565             // Recover ID
    566             val fullId = IncomingId(
    567                 partialId.uetr ?: UUID.randomUUID(),
    568                 partialId.txId ?: randEbicsId(),
    569                 partialId.acctSvcrRef ?: randEbicsId()
    570             )
    571             val idPayment = partialPayment.copy(id = fullId)
    572             registerIncomingPayment(db, cfg, idPayment)
    573             db.checkContent(idPayment)
    574             db.checkInCount(index + 1, index, 0)
    575 
    576             // Recover subject & debtor
    577             val fullPayment = payment.copy(id = fullId)
    578             registerIncomingPayment(db, cfg, fullPayment)
    579             db.checkContent(fullPayment)
    580             db.checkInCount(index + 1, index + 1, 0)
    581         }
    582 
    583         // Talerable
    584         for ((index, partialId) in sequenceOf(
    585                 IncomingId(UUID.randomUUID(), null, null),
    586                 IncomingId(null, randEbicsId(), null),
    587                 IncomingId(null, null, randEbicsId()),
    588             ).withIndex()) {
    589             val payment = genInPay("test with ${EddsaPublicKey.randEdsaKey()} reserve pub")
    590 
    591             // Register minimal
    592             val partialPayment = payment.copy(id = partialId, subject = null, debtor = null)
    593             registerIncomingPayment(db, cfg, partialPayment)
    594             db.checkContent(partialPayment)
    595             db.checkInCount(index + 4, 3, index)
    596 
    597             // Recover ID
    598             val fullId = IncomingId(
    599                 partialId.uetr ?: UUID.randomUUID(),
    600                 partialId.txId ?: randEbicsId(),
    601                 partialId.acctSvcrRef ?: randEbicsId()
    602             )
    603             val idPayment = partialPayment.copy(id = fullId)
    604             registerIncomingPayment(db, cfg, idPayment)
    605             db.checkContent(idPayment)
    606             db.checkInCount(index + 4, 3, index)
    607 
    608             // Recover subject & debtor
    609             val fullPayment = payment.copy(id = fullId)
    610             registerIncomingPayment(db, cfg, fullPayment)
    611             db.checkContent(fullPayment)
    612             db.checkInCount(index + 4, 3, index + 1)
    613         }
    614     }
    615 
    616     @Test 
    617     fun horror() = setup { db, _ ->
    618         val cfg = NexusIngestConfig.default(AccountType.exchange)
    619 
    620         // Check we do not bounce already registered talerable transaction
    621         val key = EddsaPublicKey.randEdsaKey()
    622         val talerablePayment = genInPay("test with $key reserve pub")
    623         registerIncomingPayment(db, cfg, talerablePayment)
    624         db.payment.registerMalformedIncoming(
    625             talerablePayment,
    626             TalerAmount("KUDOS:2.53"),
    627             randEbicsId(),
    628             Instant.now(),
    629             "manual bounce"
    630         ).run {
    631             assertEquals(IncomingBounceRegistrationResult.Talerable, this)
    632         }
    633         registerIncomingPayment(db, cfg, talerablePayment.copy(subject=null))
    634         registerIncomingPayment(db, cfg, talerablePayment)
    635         registerIncomingPayment(db, cfg, talerablePayment.copy(subject=null))
    636         db.checkIn(Status.Reserve(key))
    637 
    638         // Check we do not register as talerable bounced transaction
    639         val newKey = EddsaPublicKey.randEdsaKey()
    640         val bouncedPayment = genInPay("bounced $key")
    641         registerIncomingPayment(db, cfg, bouncedPayment.copy(subject=null))
    642         registerIncomingPayment(db, cfg, bouncedPayment)
    643         registerIncomingPayment(db, cfg, bouncedPayment.copy(subject=null))
    644         registerIncomingPayment(db, cfg, bouncedPayment)
    645         db.checkIn(Status.Reserve(key), Status.Bounced)
    646     }
    647 }
    648 
    649 class PaymentInitiationsTest {
    650 
    651     // Test skipping transaction based on config
    652     @Test
    653     fun skipping() = setup("skip.conf") { db, cfg ->
    654         suspend fun checkCount(nbTxs: Int, nbBounce: Int) {
    655             db.serializable(
    656                 """
    657                     SELECT (SELECT count(*) FROM incoming_transactions) + (SELECT count(*) FROM outgoing_transactions) AS transactions,
    658                         (SELECT count(*) FROM bounced_transactions) AS bounce
    659                 """
    660             ) {
    661                 one {
    662                     assertEquals(
    663                         Pair(nbTxs, nbBounce),
    664                         Pair(it.getInt("transactions"), it.getInt("bounce"))
    665                     )
    666                 }
    667             }
    668         }
    669 
    670         suspend fun ingest(executionTime: Instant) {
    671             for (tx in sequenceOf(
    672                 genInPay("test at $executionTime", executionTime = executionTime),
    673                 genOutPay("test at $executionTime", executionTime = executionTime)
    674             )) {
    675                 registerTransaction(db, cfg.ingest, tx)
    676             }
    677         }
    678 
    679         assertEquals(cfg.fetch.ignoreTransactionsBefore, dateToInstant("2024-04-04"))
    680         assertEquals(cfg.fetch.ignoreBouncesBefore, dateToInstant("2024-06-12"))
    681 
    682         // No transaction at the beginning
    683         checkCount(0, 0)
    684 
    685         // Skipped transactions
    686         ingest(cfg.fetch.ignoreTransactionsBefore.minusMillis(10))
    687         checkCount(0, 0)
    688 
    689         // Skipped bounces
    690         ingest(cfg.fetch.ignoreTransactionsBefore)
    691         ingest(cfg.fetch.ignoreTransactionsBefore.plusMillis(10))
    692         ingest(cfg.fetch.ignoreBouncesBefore.minusMillis(10))
    693         checkCount(6, 0)
    694 
    695         // Bounces
    696         ingest(cfg.fetch.ignoreBouncesBefore)
    697         ingest(cfg.fetch.ignoreBouncesBefore.plusMillis(10))
    698         checkCount(10, 2)
    699     }
    700 
    701     @Test
    702     fun status() = setup { db, _ ->
    703         suspend fun checkPart(
    704             batchId: Long, 
    705             batchStatus: SubmissionState, 
    706             batchMsg: String?, 
    707             txStatus: SubmissionState, 
    708             txMsg: String?,
    709             settledStatus: SubmissionState, 
    710             settledMsg: String?,
    711         ) {
    712             // Check batch status
    713             val msgId = db.serializable(
    714                 """
    715                 SELECT message_id, status, status_msg FROM initiated_outgoing_batches WHERE initiated_outgoing_batch_id=?
    716                 """
    717             ) {
    718                 bind(batchId)
    719                 one {
    720                     val msgId = it.getString("message_id")
    721                     assertEquals(
    722                         batchStatus to batchMsg,
    723                         it.getEnum<SubmissionState>("status") to it.getString("status_msg"),
    724                         msgId
    725                     )
    726                     msgId
    727                 }
    728             }
    729             // Check tx status
    730             db.serializable(
    731                 """
    732                 SELECT end_to_end_id, status, status_msg FROM initiated_outgoing_transactions WHERE initiated_outgoing_batch_id=?
    733                 """
    734             ) {
    735                 bind(batchId)
    736                 all { 
    737                     val endToEndId = it.getString("end_to_end_id")
    738                     val expected = when (endToEndId) {
    739                         "TX" -> Pair(txStatus, txMsg)
    740                         "TX_SETTLED" -> Pair(settledStatus, settledMsg)
    741                         else -> throw Exception("Unexpected tx $endToEndId")
    742                     }
    743                     assertEquals(
    744                         expected,
    745                         it.getEnum<SubmissionState>("status") to it.getString("status_msg"),
    746                         "$msgId.$endToEndId"
    747                     )
    748                 }
    749             }
    750         }
    751         suspend fun checkBatch(batchId: Long, status: SubmissionState, msg: String?, txStatus: SubmissionState? = null) {
    752             val txStatus = txStatus ?: status
    753             checkPart(batchId, status, msg, txStatus, msg, txStatus, msg)
    754         }
    755         suspend fun checkOrder(orderId: String, status: SubmissionState, msg: String?, txStatus: SubmissionState? = null) {
    756             val batchId = db.serializable(
    757                 "SELECT initiated_outgoing_batch_id FROM initiated_outgoing_batches WHERE order_id=?"
    758             ) {
    759                 bind(orderId)
    760                 one { 
    761                     it.getLong("initiated_outgoing_batch_id")
    762                 }
    763             }
    764             checkBatch(batchId, status, msg, txStatus)
    765         }
    766 
    767         suspend fun test(lambda: suspend (Long) -> Unit) {
    768             // Reset DB
    769             db.conn { conn -> 
    770                 conn.execSQLUpdate("DELETE FROM initiated_outgoing_transactions");
    771                 conn.execSQLUpdate("DELETE FROM initiated_outgoing_batches");
    772             }
    773             // Create a test batch with three transactions
    774             for (id in sequenceOf("TX", "TX_SETTLED")) {
    775                 assertIs<PaymentInitiationResult.Success>(
    776                     db.initiated.create(genInitPay(id))
    777                 )
    778             }
    779             db.initiated.batch(Instant.now(), "BATCH", false)
    780             // Create witness transactions and batch
    781             for (id in sequenceOf("WITNESS_1", "WITNESS_2")) {
    782                 assertIs<PaymentInitiationResult.Success>(
    783                     db.initiated.create(genInitPay(id))
    784                 )
    785             }
    786             db.initiated.batch(Instant.now(), "BATCH_WITNESS", false)
    787             for (id in sequenceOf("WITNESS_3", "WITNESS_4")) {
    788                 assertIs<PaymentInitiationResult.Success>(
    789                     db.initiated.create(genInitPay(id))
    790                 )
    791             }
    792             // Check everything is unsubmitted
    793             db.serializable(
    794                 """
    795                 SELECT (SELECT bool_and(status = 'unsubmitted') FROM initiated_outgoing_batches)
    796                    AND (SELECT bool_and(status = 'unsubmitted') FROM initiated_outgoing_transactions)
    797                 """
    798             ) {
    799                 one { assertTrue(it.getBoolean(1)) }
    800             }
    801             // Run test
    802             lambda(db.initiated.submittable().find { it.messageId == "BATCH" }!!.id)
    803             // Check witness status is unaltered
    804             db.serializable(
    805                 """
    806                 SELECT (SELECT bool_and(status = 'unsubmitted') FROM initiated_outgoing_batches WHERE message_id != 'BATCH')
    807                    AND (SELECT bool_and(initiated_outgoing_transactions.status = 'unsubmitted') 
    808                             FROM initiated_outgoing_transactions JOIN initiated_outgoing_batches USING (initiated_outgoing_batch_id)
    809                             WHERE message_id != 'BATCH')
    810                 """
    811             ) {
    812                 one { assertTrue(it.getBoolean(1)) }
    813             }
    814         }
    815 
    816         // Submission retry status
    817         test { batchId ->
    818             db.initiated.batchSubmissionFailure(batchId, Instant.now(), "First failure")
    819             checkBatch(batchId, SubmissionState.transient_failure, "First failure")
    820             db.initiated.batchSubmissionFailure(batchId, Instant.now(), "Second failure")
    821             checkBatch(batchId, SubmissionState.transient_failure, "Second failure")
    822             db.initiated.batchSubmissionSuccess(batchId, Instant.now(), "ORDER")
    823             checkOrder("ORDER", SubmissionState.pending, null)
    824             db.initiated.batchSubmissionSuccess(batchId, Instant.now(), "ORDER")
    825             checkOrder("ORDER", SubmissionState.pending, null)
    826             db.initiated.orderStep("ORDER", "step msg")
    827             checkOrder("ORDER", SubmissionState.pending, "step msg")
    828             db.initiated.orderStep("ORDER", "success msg")
    829             checkOrder("ORDER", SubmissionState.pending, "success msg")
    830             db.initiated.orderSuccess("ORDER")
    831             checkOrder("ORDER", SubmissionState.success, "success msg", SubmissionState.pending)
    832             db.initiated.orderStep("ORDER", "late msg")
    833             checkOrder("ORDER", SubmissionState.success, "success msg", SubmissionState.pending)
    834         }
    835 
    836         // Order step message on failure
    837         test { batchId ->
    838             db.initiated.batchSubmissionSuccess(batchId, Instant.now(), "ORDER")
    839             checkOrder("ORDER", SubmissionState.pending, null)
    840             db.initiated.orderStep("ORDER", "step msg")
    841             checkOrder("ORDER", SubmissionState.pending, "step msg")
    842             db.initiated.orderStep("ORDER", "failure msg")
    843             checkOrder("ORDER", SubmissionState.pending, "failure msg")
    844             assertEquals("failure msg", db.initiated.orderFailure("ORDER")!!.second)
    845             checkOrder("ORDER", SubmissionState.permanent_failure, "failure msg")
    846             db.initiated.orderStep("ORDER", "late msg")
    847             checkOrder("ORDER", SubmissionState.permanent_failure, "failure msg")
    848         }
    849 
    850         // Payment & batch status
    851         test { batchId ->
    852             checkBatch(batchId, SubmissionState.unsubmitted, null)
    853             db.initiated.batchStatusUpdate("BATCH", StatusUpdate.pending, "progress")
    854             checkBatch(batchId, SubmissionState.pending, "progress")
    855             db.initiated.txStatusUpdate("TX_SETTLED", null, StatusUpdate.success, "success")
    856             checkPart(batchId, SubmissionState.pending, "progress", SubmissionState.pending, "progress", SubmissionState.success, "success")
    857             db.initiated.batchStatusUpdate("BATCH", StatusUpdate.transient_failure, "waiting")
    858             checkPart(batchId, SubmissionState.transient_failure, "waiting", SubmissionState.transient_failure, "waiting", SubmissionState.success, "success")
    859             db.initiated.txStatusUpdate("TX", "BATCH", StatusUpdate.permanent_failure, "failure")
    860             checkPart(batchId, SubmissionState.success, null, SubmissionState.permanent_failure, "failure", SubmissionState.success, "success")
    861             db.initiated.txStatusUpdate("TX_SETTLED", "BATCH", StatusUpdate.permanent_failure, "late")
    862             checkPart(batchId, SubmissionState.success, null, SubmissionState.permanent_failure, "failure", SubmissionState.late_failure, "late")
    863         }
    864 
    865         // Registration
    866         test { batchId ->
    867             checkBatch(batchId, SubmissionState.unsubmitted, null)
    868             registerOutgoingPayment(db, genOutPay("", endToEndId = "TX_SETTLED"))
    869             checkPart(batchId, SubmissionState.unsubmitted, null, SubmissionState.unsubmitted, null, SubmissionState.success, null)
    870             registerOutgoingPayment(db, genOutPay("", endToEndId = "TX", msgId = "BATCH"))
    871             checkPart(batchId, SubmissionState.success, null, SubmissionState.success, null, SubmissionState.success, null)
    872         }
    873 
    874         // Transaction failure take over batch failures
    875         test { batchId -> 
    876             checkBatch(batchId, SubmissionState.unsubmitted, null)
    877             db.initiated.batchStatusUpdate("BATCH", StatusUpdate.permanent_failure, "batch")
    878             checkPart(batchId, SubmissionState.permanent_failure, "batch", SubmissionState.permanent_failure, "batch", SubmissionState.permanent_failure, "batch")
    879             db.initiated.txStatusUpdate("TX", "BATCH", StatusUpdate.permanent_failure, "tx")
    880             db.initiated.batchStatusUpdate("BATCH", StatusUpdate.permanent_failure, "batch2")
    881             checkPart(batchId, SubmissionState.permanent_failure, "batch", SubmissionState.permanent_failure, "tx", SubmissionState.permanent_failure, "batch")
    882         }
    883        
    884         // Unknown order and batch
    885         db.initiated.batchSubmissionSuccess(42, Instant.now(), "ORDER_X")
    886         db.initiated.batchSubmissionFailure(42, Instant.now(), null)
    887         db.initiated.orderStep("ORDER_X", "msg")
    888         db.initiated.batchStatusUpdate("BATCH_X", StatusUpdate.success, null)
    889         db.initiated.txStatusUpdate("TX_X", "BATCH_X", StatusUpdate.success, "msg")
    890         assertNull(db.initiated.orderSuccess("ORDER_X"))
    891         assertNull(db.initiated.orderFailure("ORDER_X"))
    892     }
    893 
    894     @Test
    895     fun submittable() = setup { db, _ -> 
    896         repeat(6) {
    897             assertIs<PaymentInitiationResult.Success>(
    898                 db.initiated.create(genInitPay("PAY$it"))
    899             )
    900             db.initiated.batch(Instant.now(), "BATCH$it", false)
    901         }
    902         suspend fun checkIds(vararg ids: String) {
    903             assertEquals(
    904                 listOf(*ids),
    905                 db.initiated.submittable().flatMap { it.payments.map { it.endToEndId } }
    906             )
    907         }
    908         checkIds("PAY0", "PAY1", "PAY2", "PAY3", "PAY4", "PAY5")
    909 
    910         // Check submitted not submitable
    911         db.initiated.batchSubmissionSuccess(1, Instant.now(), "ORDER1")
    912         checkIds("PAY1", "PAY2", "PAY3", "PAY4", "PAY5")
    913 
    914         // Check transient failure submitable last
    915         db.initiated.batchSubmissionFailure(2, Instant.now(), "Failure")
    916         checkIds("PAY2", "PAY3", "PAY4", "PAY5", "PAY1")
    917 
    918         // Check persistent failure not submitable
    919         db.initiated.batchSubmissionSuccess(4, Instant.now(), "ORDER3")
    920         db.initiated.orderFailure("ORDER3")
    921         checkIds("PAY2", "PAY4", "PAY5", "PAY1")
    922         db.initiated.batchSubmissionSuccess(5, Instant.now(), "ORDER4")
    923         db.initiated.orderFailure("ORDER4")
    924         checkIds("PAY2", "PAY5", "PAY1")
    925 
    926         // Check rotation
    927         db.initiated.batchSubmissionFailure(3, Instant.now(), "Failure")
    928         checkIds("PAY5", "PAY1", "PAY2")
    929         db.initiated.batchSubmissionFailure(6, Instant.now(), "Failure")
    930         checkIds("PAY1", "PAY2", "PAY5")
    931         db.initiated.batchSubmissionFailure(2, Instant.now(), "Failure")
    932         checkIds("PAY2", "PAY5", "PAY1")
    933     }
    934 
    935     // TODO test for unsettledTxInBatch
    936 }
    937 
    938 class EbicsTxTest {
    939     // Test pending transaction's id
    940     @Test
    941     fun pending() = setup { db, _ ->
    942         val ids = setOf("first", "second", "third")
    943         for (id in ids) {
    944             db.ebics.register(id)
    945         }
    946 
    947         repeat(ids.size) {
    948             val id = db.ebics.first()
    949             assert(ids.contains(id))
    950             db.ebics.remove(id!!)
    951         }
    952 
    953         assertNull(db.ebics.first())
    954     }
    955 }
    956 
    957 class TransferTest {
    958     suspend fun Database.mapTx(authPub: EddsaPublicKey) = this.payment.registerTalerableIncoming(
    959         genInPay("subject"), IncomingSubject.Map(authPub)
    960     )
    961 
    962     suspend fun Database.qrTx(reference: String) = this.payment.registerQrBillIncoming(
    963         genInPay(reference), reference
    964     )
    965 
    966     @Test
    967     fun registration() = setup { db, cfg ->
    968         val now = Instant.now()
    969         val accountPub = EddsaPublicKey.randEdsaKey()
    970         val authPub = EddsaPublicKey.randEdsaKey()
    971         val sig = EddsaSignature.rand()
    972         val referenceNumber = subjectFmtQrBill(authPub)
    973 
    974         // Register
    975         assertEquals(
    976             RegistrationResult.Success,
    977             db.transfer.register(
    978                 type = TransferType.reserve,
    979                 accountPub = accountPub,
    980                 authPub = authPub,
    981                 authSig = sig,
    982                 referenceNumber = referenceNumber,
    983                 timestamp = now,
    984                 recurrent = false
    985             )
    986         )
    987         // Idempotent
    988         assertEquals(
    989             RegistrationResult.Success,
    990             db.transfer.register(
    991                 type = TransferType.reserve,
    992                 accountPub = accountPub,
    993                 authPub = authPub,
    994                 authSig = sig,
    995                 referenceNumber = referenceNumber,
    996                 timestamp = now,
    997                 recurrent = false
    998             )
    999         )
   1000 
   1001         // Reference number reuse
   1002         assertEquals(
   1003             RegistrationResult.SubjectReuse,
   1004             db.transfer.register(
   1005                 type = TransferType.reserve,
   1006                 accountPub = accountPub,
   1007                 authPub = EddsaPublicKey.randEdsaKey(),
   1008                 authSig = sig,
   1009                 referenceNumber = referenceNumber,
   1010                 timestamp = now,
   1011                 recurrent = false
   1012             )
   1013         )
   1014 
   1015         // Auth pub reuse replace existing one
   1016         assertEquals(
   1017             RegistrationResult.Success,
   1018             db.transfer.register(
   1019                 type = TransferType.reserve,
   1020                 accountPub = accountPub,
   1021                 authPub = authPub,
   1022                 authSig = sig,
   1023                 referenceNumber = "032847109247158302947510329",
   1024                 timestamp = now,
   1025                 recurrent = false
   1026             )
   1027         )
   1028 
   1029         // Reserve pub reuse
   1030         assertEquals(
   1031             RegistrationResult.ReservePubReuse,
   1032             db.transfer.register(
   1033                 type = TransferType.reserve,
   1034                 accountPub = accountPub,
   1035                 authPub = EddsaPublicKey.randEdsaKey(),
   1036                 authSig = sig,
   1037                 referenceNumber = "032847109247158302947510330",
   1038                 timestamp = now,
   1039                 recurrent = true
   1040             )
   1041         )
   1042 
   1043         // Non recurrent accept one then bounce
   1044         assertEquals(
   1045             RegistrationResult.Success,
   1046             db.transfer.register(
   1047                 type = TransferType.reserve,
   1048                 accountPub = accountPub,
   1049                 authPub = authPub,
   1050                 authSig = sig,
   1051                 referenceNumber = referenceNumber,
   1052                 timestamp = now,
   1053                 recurrent = false
   1054             )
   1055         )
   1056         assertEquals(
   1057             IncomingRegistrationResult.Success(1, true, false, null, false),
   1058             db.mapTx(authPub)
   1059         )
   1060         db.checkIn(
   1061             Status.Reserve(accountPub)
   1062         )
   1063         assertEquals(
   1064             IncomingRegistrationResult.MappingReuse,
   1065             db.mapTx(authPub)
   1066         )
   1067        
   1068         // Recurrent accept one and delay
   1069         val newKey = EddsaPublicKey.randEdsaKey()
   1070         assertEquals(
   1071             RegistrationResult.Success,
   1072             db.transfer.register(
   1073                 type = TransferType.reserve,
   1074                 accountPub = newKey,
   1075                 authPub = authPub,
   1076                 authSig = sig,
   1077                 referenceNumber = referenceNumber,
   1078                 timestamp = now,
   1079                 recurrent = true
   1080             )
   1081         )
   1082         assertEquals(
   1083             IncomingRegistrationResult.Success(2, true, false, null, false),
   1084             db.mapTx(authPub)
   1085         )
   1086         assertEquals(
   1087             IncomingRegistrationResult.Success(3, true, false, null, true),
   1088             db.mapTx(authPub)
   1089         )
   1090         assertEquals(
   1091             IncomingRegistrationResult.Success(4, true, false, null, true),
   1092             db.mapTx(authPub)
   1093         )
   1094         assertEquals(
   1095             IncomingRegistrationResult.Success(5, true, false, null, true),
   1096             db.mapTx(authPub)
   1097         )
   1098         assertEquals(
   1099             IncomingRegistrationResult.Success(6, true, false, null, true),
   1100             db.mapTx(authPub)
   1101         )
   1102         db.checkIn(
   1103             Status.Reserve(accountPub),
   1104             Status.Reserve(newKey),
   1105             Status.Pending,
   1106             Status.Pending,
   1107             Status.Pending,
   1108             Status.Pending
   1109         )
   1110 
   1111         // Complete pending on recurrent update
   1112         val kycKey = EddsaPublicKey.randEdsaKey()
   1113         assertEquals(
   1114             RegistrationResult.Success,
   1115             db.transfer.register(
   1116                 type = TransferType.kyc,
   1117                 accountPub = kycKey,
   1118                 authPub = authPub,
   1119                 authSig = sig,
   1120                 referenceNumber = referenceNumber,
   1121                 timestamp = now,
   1122                 recurrent = true
   1123             )
   1124         )
   1125         assertEquals(
   1126             RegistrationResult.Success,
   1127             db.transfer.register(
   1128                 type = TransferType.reserve,
   1129                 accountPub = kycKey,
   1130                 authPub = authPub,
   1131                 authSig = sig,
   1132                 referenceNumber = referenceNumber,
   1133                 timestamp = now,
   1134                 recurrent = true
   1135             )
   1136         )
   1137         
   1138         db.checkIn(
   1139             Status.Reserve(accountPub),
   1140             Status.Reserve(newKey),
   1141             Status.Kyc(kycKey),
   1142             Status.Reserve(kycKey),
   1143             Status.Pending,
   1144             Status.Pending,
   1145         )
   1146 
   1147         // Kyc key reuse keep pending ones
   1148         registerIncomingPayment(db, NexusIngestConfig.default(AccountType.exchange), genInPay(fmtIncomingSubject(IncomingType.kyc, kycKey)))
   1149         db.checkIn(
   1150             Status.Reserve(accountPub),
   1151             Status.Reserve(newKey),
   1152             Status.Kyc(kycKey),
   1153             Status.Reserve(kycKey),
   1154             Status.Pending,
   1155             Status.Pending,
   1156             Status.Kyc(kycKey)
   1157         )
   1158 
   1159         // Switching to non recurrent cancel pending
   1160         assertEquals(
   1161             RegistrationResult.Success,
   1162             db.transfer.register(
   1163                 type = TransferType.kyc,
   1164                 accountPub = kycKey,
   1165                 authPub = authPub,
   1166                 authSig = sig,
   1167                 referenceNumber = referenceNumber,
   1168                 timestamp = now,
   1169                 recurrent = false
   1170             )
   1171         )
   1172         db.checkIn(
   1173             Status.Reserve(accountPub),
   1174             Status.Reserve(newKey),
   1175             Status.Kyc(kycKey),
   1176             Status.Reserve(kycKey),
   1177             Status.Bounced,
   1178             Status.Bounced,
   1179             Status.Kyc(kycKey)
   1180         )
   1181     }
   1182 
   1183     @Test
   1184     fun delete() = setup { db, _ ->
   1185         val authPub = EddsaPublicKey.randEdsaKey()
   1186         val sig = EddsaSignature.rand()
   1187         val referenceNumber = subjectFmtQrBill(authPub)
   1188         val payto = IbanPayto.rand("Sir Florian")
   1189         val amount = TalerAmount("KUDOS:2.53")
   1190 
   1191         // Unknown
   1192         assertFalse(db.transfer.unregister(authPub, Instant.now()))
   1193 
   1194         // Unused
   1195         assertEquals(
   1196             RegistrationResult.Success,
   1197             db.transfer.register(
   1198                 type = TransferType.reserve,
   1199                 accountPub = authPub,
   1200                 authPub = authPub,
   1201                 authSig = sig,
   1202                 referenceNumber = referenceNumber,
   1203                 timestamp = Instant.now(),
   1204                 recurrent = false
   1205             )
   1206         )
   1207         assertTrue(db.transfer.unregister(authPub, Instant.now()))
   1208         assertFalse(db.transfer.unregister(authPub, Instant.now()))
   1209 
   1210         assertEquals(
   1211             IncomingRegistrationResult.UnknownMapping,
   1212             db.mapTx(authPub)
   1213         )
   1214         assertEquals(
   1215             IncomingRegistrationResult.UnknownMapping,
   1216             db.qrTx(referenceNumber)
   1217         )
   1218 
   1219         // Register after deletion is idempotent if already known
   1220         assertEquals(
   1221             RegistrationResult.Success,
   1222             db.transfer.register(
   1223                 type = TransferType.reserve,
   1224                 accountPub = authPub,
   1225                 authPub = authPub,
   1226                 authSig = sig,
   1227                 referenceNumber = referenceNumber,
   1228                 timestamp = Instant.now(),
   1229                 recurrent = false
   1230             )
   1231         )
   1232         val cfg = NexusIngestConfig.default(AccountType.exchange)
   1233         val payment = genInPay(referenceNumber)
   1234         assertEquals(IncomingRegistrationResult.Success(1, true, false, null, false), db.payment.registerQrBillIncoming(payment, referenceNumber))
   1235         assertEquals(IncomingRegistrationResult.Success(1, false, false, null, false), db.payment.registerQrBillIncoming(payment, referenceNumber))
   1236         db.checkIn(Status.Reserve(authPub))
   1237         assertTrue(db.transfer.unregister(authPub, Instant.now()))
   1238         registerIncomingPayment(db, cfg, payment)
   1239         assertEquals(IncomingRegistrationResult.Success(1, false, false, null, false), db.payment.registerQrBillIncoming(payment, referenceNumber))
   1240         db.checkIn(Status.Reserve(authPub))
   1241 
   1242         // Test mapped transfers behavior after deletion
   1243         assertEquals(
   1244             RegistrationResult.Success,
   1245             db.transfer.register(
   1246                 type = TransferType.kyc,
   1247                 accountPub = authPub,
   1248                 authPub = authPub,
   1249                 authSig = sig,
   1250                 referenceNumber = referenceNumber,
   1251                 timestamp = Instant.now(),
   1252                 recurrent = true
   1253             )
   1254         )
   1255 
   1256         // First is registered
   1257         assertEquals(
   1258             IncomingRegistrationResult.Success(2, true, false, null, false),
   1259             db.qrTx(referenceNumber)
   1260         )
   1261         // Other are pending
   1262         assertEquals(
   1263             IncomingRegistrationResult.Success(3, true, false, null, true),
   1264             db.qrTx(referenceNumber)
   1265         )
   1266         assertEquals(
   1267             IncomingRegistrationResult.Success(4, true, false, null, true),
   1268             db.mapTx(authPub)
   1269         )
   1270         db.checkIn(Status.Reserve(authPub), Status.Kyc(authPub), Status.Pending, Status.Pending)
   1271 
   1272         assertTrue(db.transfer.unregister(authPub, Instant.now()))
   1273         db.checkIn(Status.Reserve(authPub), Status.Kyc(authPub), Status.Bounced, Status.Bounced)
   1274 
   1275         assertEquals(
   1276             IncomingRegistrationResult.UnknownMapping,
   1277             db.mapTx(authPub)
   1278         )
   1279         assertEquals(
   1280             IncomingRegistrationResult.UnknownMapping,
   1281             db.qrTx(referenceNumber)
   1282         )
   1283     }
   1284 }