DatabaseTest.kt (30564B)
1 /* 2 * This file is part of LibEuFin. 3 * Copyright (C) 2024-2025 Taler Systems S.A. 4 5 * LibEuFin is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU Affero General Public License as 7 * published by the Free Software Foundation; either version 3, or 8 * (at your option) any later version. 9 10 * LibEuFin is distributed in the hope that it will be useful, but 11 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 12 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General 13 * Public License for more details. 14 15 * You should have received a copy of the GNU Affero General Public 16 * License along with LibEuFin; see the file COPYING. If not, see 17 * <http://www.gnu.org/licenses/> 18 */ 19 20 import 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.ebics.* 31 import java.time.Instant 32 import java.util.UUID; 33 import kotlin.test.* 34 35 suspend fun Database.checkInCount(nbIncoming: Int, nbBounce: Int, nbTalerable: Int) = serializable( 36 """ 37 SELECT (SELECT count(*) FROM incoming_transactions) AS incoming, 38 (SELECT count(*) FROM bounced_transactions) AS bounce, 39 (SELECT count(*) FROM talerable_incoming_transactions) AS talerable; 40 """ 41 ) { 42 one { 43 assertEquals( 44 Triple(nbIncoming, nbBounce, nbTalerable), 45 Triple(it.getInt("incoming"), it.getInt("bounce"), it.getInt("talerable")) 46 ) 47 } 48 } 49 50 suspend fun Database.checkOutCount(nbIncoming: Int, nbTalerable: Int) = serializable( 51 """ 52 SELECT (SELECT count(*) FROM outgoing_transactions) AS incoming, 53 (SELECT count(*) FROM talerable_outgoing_transactions) AS talerable; 54 """ 55 ) { 56 one { 57 assertEquals( 58 Pair(nbIncoming, nbTalerable), 59 Pair(it.getInt("incoming"), it.getInt("talerable")) 60 ) 61 } 62 } 63 64 class OutgoingPaymentsTest { 65 @Test 66 fun register() = setup { db, _ -> 67 // Register initiated transaction 68 for (subject in sequenceOf( 69 "initiated by nexus", 70 "${ShortHashCode.rand()} https://exchange.com/" 71 )) { 72 val pay = genOutPay(subject) 73 assertIs<PaymentInitiationResult.Success>( 74 db.initiated.create(genInitPay(pay.id.endToEndId!!, subject)) 75 ) 76 val first = registerOutgoingPayment(db, pay) 77 assertEquals(OutgoingRegistrationResult(id = first.id, initiated = true, new = true), first) 78 assertEquals( 79 OutgoingRegistrationResult(id = first.id, initiated = true, new = false), 80 registerOutgoingPayment(db, pay) 81 ) 82 83 val refOnly = pay.copy(id = OutgoingId(null, null, acctSvcrRef = pay.id.endToEndId)) 84 val second = registerOutgoingPayment(db, refOnly) 85 assertEquals(OutgoingRegistrationResult(id = first.id + 1, initiated = false, new = true), second) 86 assertEquals( 87 OutgoingRegistrationResult(id = second.id, initiated = false, new = false), 88 registerOutgoingPayment(db, refOnly) 89 ) 90 } 91 db.checkOutCount(nbIncoming = 4, nbTalerable = 1) 92 93 // Register unknown 94 for (subject in sequenceOf( 95 "not initiated by nexus", 96 "${ShortHashCode.rand()} https://exchange.com/" 97 )) { 98 val pay = genOutPay(subject) 99 val first = registerOutgoingPayment(db, pay) 100 assertEquals(OutgoingRegistrationResult(id = first.id, initiated = false, new = true), first) 101 assertEquals( 102 OutgoingRegistrationResult(id = first.id, initiated = false, new = false), 103 registerOutgoingPayment(db, pay) 104 ) 105 } 106 db.checkOutCount(nbIncoming = 6, nbTalerable = 2) 107 108 // Register wtid reuse 109 val wtid = ShortHashCode.rand() 110 for (subject in sequenceOf( 111 "$wtid https://exchange.com/", 112 "$wtid https://exchange.com/" 113 )) { 114 val pay = genOutPay(subject) 115 val first = registerOutgoingPayment(db, pay) 116 assertEquals(OutgoingRegistrationResult(id = first.id, initiated = false, new = true), first) 117 assertEquals( 118 OutgoingRegistrationResult(id = first.id, initiated = false, new = false), 119 db.payment.registerOutgoing(pay, null, null) 120 ) 121 } 122 db.checkOutCount(nbIncoming = 8, nbTalerable = 3) 123 } 124 125 @Test 126 fun registerBatch() = setup { db, _ -> 127 // Init batch 128 val wtid = ShortHashCode.rand() 129 for (subject in sequenceOf( 130 "initiated by nexus", 131 "${ShortHashCode.rand()} https://exchange.com/", 132 "$wtid https://exchange.com/", 133 "$wtid https://exchange.com/" 134 )) { 135 assertIs<PaymentInitiationResult.Success>( 136 db.initiated.create(genInitPay(randEbicsId(), subject=subject)) 137 ) 138 } 139 db.initiated.batch(Instant.now(), "BATCH", false) 140 141 // Register batch 142 registerOutgoingBatch(db, OutgoingBatch("BATCH", Instant.now())); 143 db.checkOutCount(nbIncoming = 4, nbTalerable = 2) 144 145 // Test manual ack 146 val txs = List(3) { nb -> 147 assertIs<PaymentInitiationResult.Success>( 148 db.initiated.create(genInitPay(randEbicsId(), subject="tx $nb")) 149 ).id 150 } 151 152 // Check not sent without ack 153 db.initiated.batch(Instant.now(), "BATCH_MANUAL", true) 154 registerOutgoingBatch(db, OutgoingBatch("BATCH_MANUAL", Instant.now())); 155 db.checkOutCount(nbIncoming = 4, nbTalerable = 2) 156 157 // Check sent with ack 158 for (tx in txs) { 159 db.initiated.ack(tx) 160 } 161 db.initiated.batch(Instant.now(), "BATCH_MANUAL", true) 162 registerOutgoingBatch(db, OutgoingBatch("BATCH_MANUAL", Instant.now())); 163 db.checkOutCount(nbIncoming = 7, nbTalerable = 2) 164 } 165 } 166 167 class IncomingPaymentsTest { 168 // Tests creating and bouncing incoming payments in one DB transaction 169 @Test 170 fun bounce() = setup { db, _ -> 171 // creating and bouncing one incoming transaction. 172 val payment = genInPay("incoming and bounced") 173 val id = randEbicsId() 174 db.payment.registerMalformedIncoming( 175 payment, 176 TalerAmount("KUDOS:2.53"), 177 id, 178 Instant.now(), 179 "manual bounce" 180 ).run { 181 assertIs<IncomingBounceRegistrationResult.Success>(this) 182 assertTrue(new) 183 assertEquals(id, bounceId) 184 } 185 db.payment.registerMalformedIncoming( 186 payment, 187 TalerAmount("KUDOS:2.53"), 188 randEbicsId(), 189 Instant.now(), 190 "manual bounce" 191 ).run { 192 assertIs<IncomingBounceRegistrationResult.Success>(this) 193 assertFalse(new) 194 assertEquals(id, bounceId) 195 } 196 db.conn { 197 // Checking one incoming got created 198 val checkIncoming = it.talerStatement(""" 199 SELECT (amount).val as amount_value, (amount).frac as amount_frac 200 FROM incoming_transactions WHERE incoming_transaction_id = 1 201 """).executeQuery() 202 assertTrue(checkIncoming.next()) 203 assertEquals(payment.amount.value, checkIncoming.getLong("amount_value")) 204 assertEquals(payment.amount.frac, checkIncoming.getInt("amount_frac")) 205 // Checking the bounced table got its row. 206 val checkBounced = it.talerStatement(""" 207 SELECT 1 FROM bounced_transactions 208 WHERE incoming_transaction_id = 1 AND initiated_outgoing_transaction_id = 1 209 """).executeQuery() 210 assertTrue(checkBounced.next()) 211 // check the related initiated payment exists. 212 val checkInitiated = it.talerStatement(""" 213 SELECT 214 (amount).val as amount_value 215 ,(amount).frac as amount_frac 216 FROM initiated_outgoing_transactions 217 WHERE initiated_outgoing_transaction_id = 1 218 """).executeQuery() 219 assertTrue(checkInitiated.next()) 220 assertEquals( 221 53000000, 222 checkInitiated.getInt("amount_frac") 223 ) 224 assertEquals( 225 2, 226 checkInitiated.getInt("amount_value") 227 ) 228 } 229 } 230 231 // Test creating an incoming reserve transaction without and ID and reconcile it later again 232 @Test 233 fun simple() = setup { db, _ -> 234 val cfg = NexusIngestConfig.default(AccountType.exchange) 235 val subject = "test" 236 237 // Register 238 val incoming = genInPay(subject) 239 registerIncomingPayment(db, cfg, incoming) 240 db.checkInCount(1, 1, 0) 241 242 // Idempotent 243 registerIncomingPayment(db, cfg, incoming) 244 db.checkInCount(1, 1, 0) 245 246 // No key reuse 247 registerIncomingPayment(db, cfg, genInPay(subject, "KUDOS:9")) 248 registerIncomingPayment(db, cfg, genInPay("another $subject")) 249 db.checkInCount(3, 3, 0) 250 251 // Admin balance adjust is ignored 252 registerIncomingPayment(db, cfg, genInPay("ADMIN BALANCE ADJUST")) 253 db.checkInCount(4, 3, 0) 254 255 val original = genInPay("test 2") 256 val incomplete = original.copy(subject = null, debtor = null) 257 // Register incomplete transaction 258 registerIncomingPayment(db, cfg, incomplete) 259 db.checkInCount(5, 3, 0) 260 261 // Idempotent 262 registerIncomingPayment(db, cfg, incomplete) 263 db.checkInCount(5, 3, 0) 264 265 // Recover info when complete 266 registerIncomingPayment(db, cfg, original) 267 db.checkInCount(5, 4, 0) 268 } 269 270 // Test creating an incoming reserve taler transaction without and ID and reconcile it later again 271 @Test 272 fun talerable() = setup { db, _ -> 273 val cfg = NexusIngestConfig.default(AccountType.exchange) 274 val subject = "test with ${EddsaPublicKey.randEdsaKey()} reserve pub" 275 276 // Register 277 val incoming = genInPay(subject) 278 registerIncomingPayment(db, cfg, incoming) 279 db.checkInCount(1, 0, 1) 280 281 // Idempotent 282 registerIncomingPayment(db, cfg, incoming) 283 db.checkInCount(1, 0, 1) 284 285 // Key reuse is bounced 286 registerIncomingPayment(db, cfg, genInPay(subject, "KUDOS:9")) 287 registerIncomingPayment(db, cfg, genInPay("another $subject")) 288 db.checkInCount(3, 2, 1) 289 290 // Admin balance adjust is ignored 291 registerIncomingPayment(db, cfg, genInPay("ADMIN BALANCE ADJUST")) 292 db.checkInCount(4, 2, 1) 293 294 val original = genInPay("test 2 with ${EddsaPublicKey.randEdsaKey()} reserve pub") 295 val incomplete = original.copy(subject = null, debtor = null) 296 // Register incomplete transaction 297 registerIncomingPayment(db, cfg, incomplete) 298 db.checkInCount(5, 2, 1) 299 300 // Idempotent 301 registerIncomingPayment(db, cfg, incomplete) 302 db.checkInCount(5, 2, 1) 303 304 // Recover info when complete 305 registerIncomingPayment(db, cfg, original) 306 db.checkInCount(5, 2, 2) 307 } 308 309 @Test 310 fun recoverInfo() = setup { db, _ -> 311 val cfg = NexusIngestConfig.default(AccountType.exchange) 312 313 suspend fun Database.checkContent(payment: IncomingPayment) = serializable( 314 """ 315 SELECT 316 uetr IS NOT DISTINCT FROM ? AND 317 tx_id IS NOT DISTINCT FROM ? AND 318 acct_svcr_ref IS NOT DISTINCT FROM ? AND 319 subject IS NOT DISTINCT FROM ? AND 320 debit_payto IS NOT DISTINCT FROM ? 321 FROM incoming_transactions ORDER BY incoming_transaction_id DESC LIMIT 1 322 """ 323 ) { 324 bind(payment.id.uetr) 325 bind(payment.id.txId) 326 bind(payment.id.acctSvcrRef) 327 bind(payment.subject) 328 bind(payment.debtor?.toString()) 329 one { 330 assertTrue(it.getBoolean(1)) 331 } 332 } 333 334 // Non talerable 335 for ((index, partialId) in sequenceOf( 336 IncomingId(UUID.randomUUID(), null, null), 337 IncomingId(null, randEbicsId(), null), 338 IncomingId(null, null, randEbicsId()), 339 ).withIndex()) { 340 val payment = genInPay("subject") 341 342 // Register minimal 343 val partialPayment = payment.copy(id = partialId, subject = null, debtor = null) 344 registerIncomingPayment(db, cfg, partialPayment) 345 db.checkContent(partialPayment) 346 db.checkInCount(index + 1, index, 0) 347 348 // Recover ID 349 val fullId = IncomingId( 350 partialId.uetr ?: UUID.randomUUID(), 351 partialId.txId ?: randEbicsId(), 352 partialId.acctSvcrRef ?: randEbicsId() 353 ) 354 val idPayment = partialPayment.copy(id = fullId) 355 registerIncomingPayment(db, cfg, idPayment) 356 db.checkContent(idPayment) 357 db.checkInCount(index + 1, index, 0) 358 359 // Recover subject & debtor 360 val fullPayment = payment.copy(id = fullId) 361 registerIncomingPayment(db, cfg, fullPayment) 362 db.checkContent(fullPayment) 363 db.checkInCount(index + 1, index + 1, 0) 364 } 365 366 // Talerable 367 for ((index, partialId) in sequenceOf( 368 IncomingId(UUID.randomUUID(), null, null), 369 IncomingId(null, randEbicsId(), null), 370 IncomingId(null, null, randEbicsId()), 371 ).withIndex()) { 372 val payment = genInPay("test with ${EddsaPublicKey.randEdsaKey()} reserve pub") 373 374 // Register minimal 375 val partialPayment = payment.copy(id = partialId, subject = null, debtor = null) 376 registerIncomingPayment(db, cfg, partialPayment) 377 db.checkContent(partialPayment) 378 db.checkInCount(index + 4, 3, index) 379 380 // Recover ID 381 val fullId = IncomingId( 382 partialId.uetr ?: UUID.randomUUID(), 383 partialId.txId ?: randEbicsId(), 384 partialId.acctSvcrRef ?: randEbicsId() 385 ) 386 val idPayment = partialPayment.copy(id = fullId) 387 registerIncomingPayment(db, cfg, idPayment) 388 db.checkContent(idPayment) 389 db.checkInCount(index + 4, 3, index) 390 391 // Recover subject & debtor 392 val fullPayment = payment.copy(id = fullId) 393 registerIncomingPayment(db, cfg, fullPayment) 394 db.checkContent(fullPayment) 395 db.checkInCount(index + 4, 3, index + 1) 396 } 397 } 398 399 @Test 400 fun horror() = setup { db, _ -> 401 val cfg = NexusIngestConfig.default(AccountType.exchange) 402 403 // Check we do not bounce already registered talerable transaction 404 val talerablePayment = genInPay("test with ${EddsaPublicKey.randEdsaKey()} reserve pub") 405 registerIncomingPayment(db, cfg, talerablePayment) 406 db.payment.registerMalformedIncoming( 407 talerablePayment, 408 TalerAmount("KUDOS:2.53"), 409 randEbicsId(), 410 Instant.now(), 411 "manual bounce" 412 ).run { 413 assertEquals(IncomingBounceRegistrationResult.Talerable, this) 414 } 415 registerIncomingPayment(db, cfg, talerablePayment.copy(subject=null)) 416 registerIncomingPayment(db, cfg, talerablePayment) 417 registerIncomingPayment(db, cfg, talerablePayment.copy(subject=null)) 418 db.checkInCount(1, 0, 1) 419 420 // Check we do not register as talerable bounced transaction 421 val bouncedPayment = genInPay("bounced ${EddsaPublicKey.randEdsaKey()}") 422 registerIncomingPayment(db, cfg, bouncedPayment.copy(subject=null)) 423 registerIncomingPayment(db, cfg, bouncedPayment) 424 registerIncomingPayment(db, cfg, bouncedPayment.copy(subject=null)) 425 registerIncomingPayment(db, cfg, bouncedPayment) 426 db.checkInCount(2, 1, 1) 427 } 428 } 429 430 class PaymentInitiationsTest { 431 432 // Test skipping transaction based on config 433 @Test 434 fun skipping() = setup("skip.conf") { db, cfg -> 435 suspend fun checkCount(nbTxs: Int, nbBounce: Int) { 436 db.serializable( 437 """ 438 SELECT (SELECT count(*) FROM incoming_transactions) + (SELECT count(*) FROM outgoing_transactions) AS transactions, 439 (SELECT count(*) FROM bounced_transactions) AS bounce 440 """ 441 ) { 442 one { 443 assertEquals( 444 Pair(nbTxs, nbBounce), 445 Pair(it.getInt("transactions"), it.getInt("bounce")) 446 ) 447 } 448 } 449 } 450 451 suspend fun ingest(executionTime: Instant) { 452 for (tx in sequenceOf( 453 genInPay("test at $executionTime", executionTime = executionTime), 454 genOutPay("test at $executionTime", executionTime = executionTime) 455 )) { 456 registerTransaction(db, cfg.ingest, tx) 457 } 458 } 459 460 assertEquals(cfg.fetch.ignoreTransactionsBefore, dateToInstant("2024-04-04")) 461 assertEquals(cfg.fetch.ignoreBouncesBefore, dateToInstant("2024-06-12")) 462 463 // No transaction at the beginning 464 checkCount(0, 0) 465 466 // Skipped transactions 467 ingest(cfg.fetch.ignoreTransactionsBefore.minusMillis(10)) 468 checkCount(0, 0) 469 470 // Skipped bounces 471 ingest(cfg.fetch.ignoreTransactionsBefore) 472 ingest(cfg.fetch.ignoreTransactionsBefore.plusMillis(10)) 473 ingest(cfg.fetch.ignoreBouncesBefore.minusMillis(10)) 474 checkCount(6, 0) 475 476 // Bounces 477 ingest(cfg.fetch.ignoreBouncesBefore) 478 ingest(cfg.fetch.ignoreBouncesBefore.plusMillis(10)) 479 checkCount(10, 2) 480 } 481 482 @Test 483 fun status() = setup { db, _ -> 484 suspend fun checkPart( 485 batchId: Long, 486 batchStatus: SubmissionState, 487 batchMsg: String?, 488 txStatus: SubmissionState, 489 txMsg: String?, 490 settledStatus: SubmissionState, 491 settledMsg: String?, 492 ) { 493 // Check batch status 494 val msgId = db.serializable( 495 """ 496 SELECT message_id, status, status_msg FROM initiated_outgoing_batches WHERE initiated_outgoing_batch_id=? 497 """ 498 ) { 499 bind(batchId) 500 one { 501 val msgId = it.getString("message_id") 502 assertEquals( 503 batchStatus to batchMsg, 504 it.getEnum<SubmissionState>("status") to it.getString("status_msg"), 505 msgId 506 ) 507 msgId 508 } 509 } 510 // Check tx status 511 db.serializable( 512 """ 513 SELECT end_to_end_id, status, status_msg FROM initiated_outgoing_transactions WHERE initiated_outgoing_batch_id=? 514 """ 515 ) { 516 bind(batchId) 517 all { 518 val endToEndId = it.getString("end_to_end_id") 519 val expected = when (endToEndId) { 520 "TX" -> Pair(txStatus, txMsg) 521 "TX_SETTLED" -> Pair(settledStatus, settledMsg) 522 else -> throw Exception("Unexpected tx $endToEndId") 523 } 524 assertEquals( 525 expected, 526 it.getEnum<SubmissionState>("status") to it.getString("status_msg"), 527 "$msgId.$endToEndId" 528 ) 529 } 530 } 531 } 532 suspend fun checkBatch(batchId: Long, status: SubmissionState, msg: String?, txStatus: SubmissionState? = null) { 533 val txStatus = txStatus ?: status 534 checkPart(batchId, status, msg, txStatus, msg, txStatus, msg) 535 } 536 suspend fun checkOrder(orderId: String, status: SubmissionState, msg: String?, txStatus: SubmissionState? = null) { 537 val batchId = db.serializable( 538 "SELECT initiated_outgoing_batch_id FROM initiated_outgoing_batches WHERE order_id=?" 539 ) { 540 bind(orderId) 541 one { 542 it.getLong("initiated_outgoing_batch_id") 543 } 544 } 545 checkBatch(batchId, status, msg, txStatus) 546 } 547 548 suspend fun test(lambda: suspend (Long) -> Unit) { 549 // Reset DB 550 db.conn { conn -> 551 conn.execSQLUpdate("DELETE FROM initiated_outgoing_transactions"); 552 conn.execSQLUpdate("DELETE FROM initiated_outgoing_batches"); 553 } 554 // Create a test batch with three transactions 555 for (id in sequenceOf("TX", "TX_SETTLED")) { 556 assertIs<PaymentInitiationResult.Success>( 557 db.initiated.create(genInitPay(id)) 558 ) 559 } 560 db.initiated.batch(Instant.now(), "BATCH", false) 561 // Create witness transactions and batch 562 for (id in sequenceOf("WITNESS_1", "WITNESS_2")) { 563 assertIs<PaymentInitiationResult.Success>( 564 db.initiated.create(genInitPay(id)) 565 ) 566 } 567 db.initiated.batch(Instant.now(), "BATCH_WITNESS", false) 568 for (id in sequenceOf("WITNESS_3", "WITNESS_4")) { 569 assertIs<PaymentInitiationResult.Success>( 570 db.initiated.create(genInitPay(id)) 571 ) 572 } 573 // Check everything is unsubmitted 574 db.serializable( 575 """ 576 SELECT (SELECT bool_and(status = 'unsubmitted') FROM initiated_outgoing_batches) 577 AND (SELECT bool_and(status = 'unsubmitted') FROM initiated_outgoing_transactions) 578 """ 579 ) { 580 one { assertTrue(it.getBoolean(1)) } 581 } 582 // Run test 583 lambda(db.initiated.submittable().find { it.messageId == "BATCH" }!!.id) 584 // Check witness status is unaltered 585 db.serializable( 586 """ 587 SELECT (SELECT bool_and(status = 'unsubmitted') FROM initiated_outgoing_batches WHERE message_id != 'BATCH') 588 AND (SELECT bool_and(initiated_outgoing_transactions.status = 'unsubmitted') 589 FROM initiated_outgoing_transactions JOIN initiated_outgoing_batches USING (initiated_outgoing_batch_id) 590 WHERE message_id != 'BATCH') 591 """ 592 ) { 593 one { assertTrue(it.getBoolean(1)) } 594 } 595 } 596 597 // Submission retry status 598 test { batchId -> 599 db.initiated.batchSubmissionFailure(batchId, Instant.now(), "First failure") 600 checkBatch(batchId, SubmissionState.transient_failure, "First failure") 601 db.initiated.batchSubmissionFailure(batchId, Instant.now(), "Second failure") 602 checkBatch(batchId, SubmissionState.transient_failure, "Second failure") 603 db.initiated.batchSubmissionSuccess(batchId, Instant.now(), "ORDER") 604 checkOrder("ORDER", SubmissionState.pending, null) 605 db.initiated.batchSubmissionSuccess(batchId, Instant.now(), "ORDER") 606 checkOrder("ORDER", SubmissionState.pending, null) 607 db.initiated.orderStep("ORDER", "step msg") 608 checkOrder("ORDER", SubmissionState.pending, "step msg") 609 db.initiated.orderStep("ORDER", "success msg") 610 checkOrder("ORDER", SubmissionState.pending, "success msg") 611 db.initiated.orderSuccess("ORDER") 612 checkOrder("ORDER", SubmissionState.success, "success msg", SubmissionState.pending) 613 db.initiated.orderStep("ORDER", "late msg") 614 checkOrder("ORDER", SubmissionState.success, "success msg", SubmissionState.pending) 615 } 616 617 // Order step message on failure 618 test { batchId -> 619 db.initiated.batchSubmissionSuccess(batchId, Instant.now(), "ORDER") 620 checkOrder("ORDER", SubmissionState.pending, null) 621 db.initiated.orderStep("ORDER", "step msg") 622 checkOrder("ORDER", SubmissionState.pending, "step msg") 623 db.initiated.orderStep("ORDER", "failure msg") 624 checkOrder("ORDER", SubmissionState.pending, "failure msg") 625 assertEquals("failure msg", db.initiated.orderFailure("ORDER")!!.second) 626 checkOrder("ORDER", SubmissionState.permanent_failure, "failure msg") 627 db.initiated.orderStep("ORDER", "late msg") 628 checkOrder("ORDER", SubmissionState.permanent_failure, "failure msg") 629 } 630 631 // Payment & batch status 632 test { batchId -> 633 checkBatch(batchId, SubmissionState.unsubmitted, null) 634 db.initiated.batchStatusUpdate("BATCH", StatusUpdate.pending, "progress") 635 checkBatch(batchId, SubmissionState.pending, "progress") 636 db.initiated.txStatusUpdate("TX_SETTLED", null, StatusUpdate.success, "success") 637 checkPart(batchId, SubmissionState.pending, "progress", SubmissionState.pending, "progress", SubmissionState.success, "success") 638 db.initiated.batchStatusUpdate("BATCH", StatusUpdate.transient_failure, "waiting") 639 checkPart(batchId, SubmissionState.transient_failure, "waiting", SubmissionState.transient_failure, "waiting", SubmissionState.success, "success") 640 db.initiated.txStatusUpdate("TX", "BATCH", StatusUpdate.permanent_failure, "failure") 641 checkPart(batchId, SubmissionState.success, null, SubmissionState.permanent_failure, "failure", SubmissionState.success, "success") 642 db.initiated.txStatusUpdate("TX_SETTLED", "BATCH", StatusUpdate.permanent_failure, "late") 643 checkPart(batchId, SubmissionState.success, null, SubmissionState.permanent_failure, "failure", SubmissionState.late_failure, "late") 644 } 645 646 // Registration 647 test { batchId -> 648 checkBatch(batchId, SubmissionState.unsubmitted, null) 649 registerOutgoingPayment(db, genOutPay("", endToEndId = "TX_SETTLED")) 650 checkPart(batchId, SubmissionState.unsubmitted, null, SubmissionState.unsubmitted, null, SubmissionState.success, null) 651 registerOutgoingPayment(db, genOutPay("", endToEndId = "TX", msgId = "BATCH")) 652 checkPart(batchId, SubmissionState.success, null, SubmissionState.success, null, SubmissionState.success, null) 653 } 654 655 // Transaction failure take over batch failures 656 test { batchId -> 657 checkBatch(batchId, SubmissionState.unsubmitted, null) 658 db.initiated.batchStatusUpdate("BATCH", StatusUpdate.permanent_failure, "batch") 659 checkPart(batchId, SubmissionState.permanent_failure, "batch", SubmissionState.permanent_failure, "batch", SubmissionState.permanent_failure, "batch") 660 db.initiated.txStatusUpdate("TX", "BATCH", StatusUpdate.permanent_failure, "tx") 661 db.initiated.batchStatusUpdate("BATCH", StatusUpdate.permanent_failure, "batch2") 662 checkPart(batchId, SubmissionState.permanent_failure, "batch", SubmissionState.permanent_failure, "tx", SubmissionState.permanent_failure, "batch") 663 } 664 665 // Unknown order and batch 666 db.initiated.batchSubmissionSuccess(42, Instant.now(), "ORDER_X") 667 db.initiated.batchSubmissionFailure(42, Instant.now(), null) 668 db.initiated.orderStep("ORDER_X", "msg") 669 db.initiated.batchStatusUpdate("BATCH_X", StatusUpdate.success, null) 670 db.initiated.txStatusUpdate("TX_X", "BATCH_X", StatusUpdate.success, "msg") 671 assertNull(db.initiated.orderSuccess("ORDER_X")) 672 assertNull(db.initiated.orderFailure("ORDER_X")) 673 } 674 675 @Test 676 fun submittable() = setup { db, _ -> 677 repeat(6) { 678 assertIs<PaymentInitiationResult.Success>( 679 db.initiated.create(genInitPay("PAY$it")) 680 ) 681 db.initiated.batch(Instant.now(), "BATCH$it", false) 682 } 683 suspend fun checkIds(vararg ids: String) { 684 assertEquals( 685 listOf(*ids), 686 db.initiated.submittable().flatMap { it.payments.map { it.endToEndId } } 687 ) 688 } 689 checkIds("PAY0", "PAY1", "PAY2", "PAY3", "PAY4", "PAY5") 690 691 // Check submitted not submitable 692 db.initiated.batchSubmissionSuccess(1, Instant.now(), "ORDER1") 693 checkIds("PAY1", "PAY2", "PAY3", "PAY4", "PAY5") 694 695 // Check transient failure submitable last 696 db.initiated.batchSubmissionFailure(2, Instant.now(), "Failure") 697 checkIds("PAY2", "PAY3", "PAY4", "PAY5", "PAY1") 698 699 // Check persistent failure not submitable 700 db.initiated.batchSubmissionSuccess(4, Instant.now(), "ORDER3") 701 db.initiated.orderFailure("ORDER3") 702 checkIds("PAY2", "PAY4", "PAY5", "PAY1") 703 db.initiated.batchSubmissionSuccess(5, Instant.now(), "ORDER4") 704 db.initiated.orderFailure("ORDER4") 705 checkIds("PAY2", "PAY5", "PAY1") 706 707 // Check rotation 708 db.initiated.batchSubmissionFailure(3, Instant.now(), "Failure") 709 checkIds("PAY5", "PAY1", "PAY2") 710 db.initiated.batchSubmissionFailure(6, Instant.now(), "Failure") 711 checkIds("PAY1", "PAY2", "PAY5") 712 db.initiated.batchSubmissionFailure(2, Instant.now(), "Failure") 713 checkIds("PAY2", "PAY5", "PAY1") 714 } 715 716 // TODO test for unsettledTxInBatch 717 } 718 719 class EbicsTxTest { 720 // Test pending transaction's id 721 @Test 722 fun pending() = setup { db, _ -> 723 val ids = setOf("first", "second", "third") 724 for (id in ids) { 725 db.ebics.register(id) 726 } 727 728 repeat(ids.size) { 729 val id = db.ebics.first() 730 assert(ids.contains(id)) 731 db.ebics.remove(id!!) 732 } 733 734 assertNull(db.ebics.first()) 735 } 736 }