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 }