libeufin

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

commit ddf1f43c9396d7afa980649555e4ad6e24d42759
parent 997a8dce779b139d89a05c58a9e686aa5d78b086
Author: Antoine A <>
Date:   Wed, 25 Sep 2024 01:33:06 +0200

nexus: handle EBICS returns as late failures

Diffstat:
Mdatabase-versioning/libeufin-nexus-0007.sql | 1+
Mdatabase-versioning/libeufin-nexus-procedures.sql | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mnexus/sample/platform/gls_camt052.xml | 174+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt | 18+++++++++---------
Mnexus/src/main/kotlin/tech/libeufin/nexus/db/Database.kt | 23+++++++++++++++++++++--
Mnexus/src/main/kotlin/tech/libeufin/nexus/db/ExchangeDAO.kt | 14++------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/db/InitiatedDAO.kt | 80++++++++++++++++---------------------------------------------------------------
Mnexus/src/test/kotlin/DatabaseTest.kt | 14++++++++------
Mnexus/src/test/kotlin/Iso20022Test.kt | 15++++++++++++++-
Mnexus/src/test/kotlin/RegistrationTest.kt | 13+++++++++++++
Mtestbench/src/main/kotlin/Main.kt | 29++++++++++++++++-------------
11 files changed, 347 insertions(+), 109 deletions(-)

diff --git a/database-versioning/libeufin-nexus-0007.sql b/database-versioning/libeufin-nexus-0007.sql @@ -19,6 +19,7 @@ SELECT _v.register_patch('libeufin-nexus-0007', NULL, NULL); -- Add a new submission state reusing a currently unused slot ALTER TYPE submission_state RENAME VALUE 'never_heard_back' TO 'pending'; +ALTER TYPE submission_state ADD VALUE 'late_failure'; -- Batch of initiated_outgoing_transactions CREATE TABLE initiated_outgoing_batches( diff --git a/database-versioning/libeufin-nexus-procedures.sql b/database-versioning/libeufin-nexus-procedures.sql @@ -178,12 +178,13 @@ IF NOT out_found THEN outgoing_transaction_id = out_tx_id ,status = 'success' ,status_msg = null - WHERE initiated_outgoing_transaction_id = init_id; + WHERE initiated_outgoing_transaction_id = init_id + AND status != 'late_failure'; -- Reconciles the related initiated batch UPDATE initiated_outgoing_batches SET status = 'success', status_msg = null - WHERE message_id = in_msg_id AND status NOT IN ('success', 'permanent_failure'); + WHERE message_id = in_msg_id AND status NOT IN ('success', 'permanent_failure', 'late_failure'); END IF; END IF; END $$; @@ -481,4 +482,74 @@ IF (EXISTS(SELECT FROM initiated_outgoing_transactions WHERE initiated_outgoing_ -- Update the batch with the sum of amounts UPDATE initiated_outgoing_batches SET sum=local_sum WHERE initiated_outgoing_batch_id=batch_id; END IF; +END $$; + +CREATE FUNCTION batch_status_update( + IN in_message_id text, + IN in_status submission_state, + IN in_status_msg text +) +RETURNS void +LANGUAGE plpgsql AS $$ +DECLARE +local_batch_id INT8; +BEGIN + -- Check if there is a batch for this message id + SELECT initiated_outgoing_batch_id INTO local_batch_id + FROM initiated_outgoing_batches + WHERE message_id = in_message_id; + IF FOUND THEN + -- Update unsettled batch status + UPDATE initiated_outgoing_batches + SET status = in_status, status_msg = in_status_msg + WHERE initiated_outgoing_batch_id = local_batch_id + AND status NOT IN ('success', 'permanent_failure', 'late_failure'); + + -- When a batch succeed it doesn't mean that individual transaction also succeed + IF in_status = 'success' THEN + in_status = 'pending'; + END IF; + + -- Update unsettled batch's transaction status + UPDATE initiated_outgoing_transactions + SET status = in_status, status_msg = in_status_msg + WHERE initiated_outgoing_batch_id = local_batch_id + AND status NOT IN ('success', 'permanent_failure', 'late_failure'); + END IF; +END $$; + +CREATE FUNCTION tx_status_update( + IN in_end_to_end_id text, + IN in_message_id text, + IN in_status submission_state, + IN in_status_msg text +) +RETURNS void +LANGUAGE plpgsql AS $$ +DECLARE +local_status submission_state; +local_tx_id INT8; +BEGIN + -- Check current tx status + SELECT initiated_outgoing_transaction_id, status INTO local_tx_id, local_status + FROM initiated_outgoing_transactions + WHERE end_to_end_id = in_end_to_end_id; + IF FOUND THEN + -- Update unsettled transaction status + IF local_status = 'success' AND in_status = 'permanent_failure' THEN + UPDATE initiated_outgoing_transactions + SET status = 'late_failure', status_msg = in_status_msg + WHERE initiated_outgoing_transaction_id = local_tx_id; + ELSIF local_status NOT IN ('success', 'permanent_failure', 'late_failure') THEN + UPDATE initiated_outgoing_transactions + SET status = in_status, status_msg = in_status_msg + WHERE initiated_outgoing_transaction_id = local_tx_id; + END IF; + + -- Update unsettled batch status + UPDATE initiated_outgoing_batches + SET status = 'success', status_msg = NULL + WHERE message_id = in_message_id + AND status NOT IN ('success', 'permanent_failure', 'late_failure'); + END IF; END $$; \ No newline at end of file diff --git a/nexus/sample/platform/gls_camt052.xml b/nexus/sample/platform/gls_camt052.xml @@ -548,6 +548,180 @@ </NtryDtls> <AddtlNtryInf>Sammelüberweisung</AddtlNtryInf> </Ntry> + <Ntry> + <Amt Ccy="EUR">0.42</Amt> + <CdtDbtInd>DBIT</CdtDbtInd> + <Sts>BOOK</Sts> + <BookgDt> + <Dt>2024-09-23</Dt> + </BookgDt> + <ValDt> + <Dt>2024-09-23</Dt> + </ValDt> + <AcctSvcrRef>2024092100252498000</AcctSvcrRef> + <BkTxCd> + <Domn> + <Cd>PMNT</Cd> + <Fmly> + <Cd>ICDT</Cd> + <SubFmlyCd>ESCT</SubFmlyCd> + </Fmly> + </Domn> + <Prtry> + <Cd>NTRF+177+08381</Cd> + <Issr>DK</Issr> + </Prtry> + </BkTxCd> + <NtryDtls> + <TxDtls> + <Refs> + <MsgId>BATCH_SINGLE_RETURN</MsgId> + <PmtInfId>NOTPROVIDED</PmtInfId> + <EndToEndId>KLJJ28S1LVNDK1R2HCHLN884M7EKM5XGM5</EndToEndId> + <TxId>2024092374955203090200000010000001</TxId> + </Refs> + <AmtDtls> + <TxAmt> + <Amt Ccy="EUR">0.42</Amt> + </TxAmt> + </AmtDtls> + <BkTxCd> + <Domn> + <Cd>PMNT</Cd> + <Fmly> + <Cd>ICDT</Cd> + <SubFmlyCd>ESCT</SubFmlyCd> + </Fmly> + </Domn> + <Prtry> + <Cd>NTRF+177+08381</Cd> + <Issr>DK</Issr> + </Prtry> + </BkTxCd> + <RltdPties> + <Dbtr> + <Nm>Florian Dold</Nm> + </Dbtr> + <DbtrAcct> + <Id> + <IBAN>DE89500105171325381664</IBAN> + </Id> + </DbtrAcct> + <Cdtr> + <Nm>John Smith</Nm> + </Cdtr> + <CdtrAcct> + <Id> + <IBAN>DE18500105173385245163</IBAN> + </Id> + </CdtrAcct> + </RltdPties> + <RltdAgts> + <CdtrAgt> + <FinInstnId> + <BIC>INGDDEFFXXX</BIC> + </FinInstnId> + </CdtrAgt> + </RltdAgts> + <RmtInf> + <Ustrd>This should fail because bad iban</Ustrd> + </RmtInf> + </TxDtls> + </NtryDtls> + <AddtlNtryInf>Überweisungsauftrag</AddtlNtryInf> + </Ntry> + <Ntry> + <Amt Ccy="EUR">0.42</Amt> + <CdtDbtInd>CRDT</CdtDbtInd> + <Sts>BOOK</Sts> + <BookgDt> + <Dt>2024-09-24</Dt> + </BookgDt> + <ValDt> + <Dt>2024-09-24</Dt> + </ValDt> + <AcctSvcrRef>2024092409341766000</AcctSvcrRef> + <BkTxCd> + <Domn> + <Cd>PMNT</Cd> + <Fmly> + <Cd>ICDT</Cd> + <SubFmlyCd>RRTN</SubFmlyCd> + </Fmly> + </Domn> + <Prtry> + <Cd>NRTI+159+00931</Cd> + <Issr>DK</Issr> + </Prtry> + </BkTxCd> + <NtryDtls> + <TxDtls> + <Refs> + <EndToEndId>KLJJ28S1LVNDK1R2HCHLN884M7EKM5XGM5</EndToEndId> + <TxId>2024092374955203090200000010000001</TxId> + </Refs> + <AmtDtls> + <TxAmt> + <Amt Ccy="EUR">0.42</Amt> + </TxAmt> + </AmtDtls> + <BkTxCd> + <Domn> + <Cd>PMNT</Cd> + <Fmly> + <Cd>ICDT</Cd> + <SubFmlyCd>RRTN</SubFmlyCd> + </Fmly> + </Domn> + <Prtry> + <Cd>NRTI+159+00931</Cd> + <Issr>DK</Issr> + </Prtry> + </BkTxCd> + <RltdPties> + <Dbtr> + <Nm>Florian Dold</Nm> + </Dbtr> + <DbtrAcct> + <Id> + <IBAN>DE89500105171325381664</IBAN> + </Id> + </DbtrAcct> + <Cdtr> + <Nm>John Smith</Nm> + </Cdtr> + <CdtrAcct> + <Id> + <IBAN>DE18500105173385245163</IBAN> + </Id> + </CdtrAcct> + </RltdPties> + <RmtInf> + <Ustrd>Retoure ...</Ustrd> + </RmtInf> + <RtrInf> + <OrgnlBkTxCd> + <Prtry> + <Cd>116</Cd> + <Issr>DK</Issr> + </Prtry> + </OrgnlBkTxCd> + <Orgtr> + <Id> + <OrgId> + <BICOrBEI>GENODEM1GLS</BICOrBEI> + </OrgId> + </Id> + </Orgtr> + <Rsn> + <Cd>AC01</Cd> + </Rsn> + <AddtlInf>IBAN fehlerhaft und ungültig</AddtlInf> + </RtrInf> + </TxDtls> + </NtryDtls> + <AddtlNtryInf>Retouren</AddtlNtryInf> + </Ntry> </Rpt> </BkToCstmrAcctRpt> </Document> \ No newline at end of file diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt @@ -149,7 +149,7 @@ suspend fun registerTransaction( is OutgoingBatch -> registerOutgoingBatch(db, tx) is OutgoingReversal -> { logger.error("{}", tx) - db.initiated.txStatusUpdate(tx.endToEndId, tx.msgId, SubmissionState.permanent_failure, "Payment bounced: ${tx.reason}") + db.initiated.txStatusUpdate(tx.endToEndId, tx.msgId, StatusUpdate.permanent_failure, "Payment bounced: ${tx.reason}") } } } @@ -203,12 +203,12 @@ suspend fun registerFile( if (msgStatus.code != null) { val msg = msgStatus.msg() val state = when (msgStatus.code) { - ExternalPaymentGroupStatusCode.ACSC -> SubmissionState.success + ExternalPaymentGroupStatusCode.ACSC -> StatusUpdate.success ExternalPaymentGroupStatusCode.RJCT -> { logger.error("Batch ${msgStatus.id} failed: $msg") - SubmissionState.permanent_failure + StatusUpdate.permanent_failure } - else -> SubmissionState.pending + else -> StatusUpdate.pending } db.initiated.batchStatusUpdate(msgStatus.id, state, msg) } @@ -218,12 +218,12 @@ suspend fun registerFile( } else if (pmtStatus.code != null) { val msg = pmtStatus.msg() val state = when (pmtStatus.code) { - ExternalPaymentGroupStatusCode.ACSC -> SubmissionState.success + ExternalPaymentGroupStatusCode.ACSC -> StatusUpdate.success ExternalPaymentGroupStatusCode.RJCT -> { logger.error("Batch ${msgStatus.id} failed: $msg") - SubmissionState.permanent_failure + StatusUpdate.permanent_failure } - else -> SubmissionState.pending + else -> StatusUpdate.pending } db.initiated.batchStatusUpdate(msgStatus.id, state, msg) } @@ -233,9 +233,9 @@ suspend fun registerFile( ExternalPaymentTransactionStatusCode.RJCT, ExternalPaymentTransactionStatusCode.BLCK -> { logger.error("Transaction ${txStatus.endToEndId} failed: $msg") - SubmissionState.permanent_failure + StatusUpdate.permanent_failure } - else -> SubmissionState.pending + else -> StatusUpdate.pending } db.initiated.txStatusUpdate(txStatus.endToEndId, null, state, msg) } diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/db/Database.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/db/Database.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import org.slf4j.LoggerFactory import tech.libeufin.common.TalerAmount +import tech.libeufin.common.TransferStatusState import tech.libeufin.common.IbanPayto import tech.libeufin.common.db.DatabaseConfig import tech.libeufin.common.db.DbPool @@ -47,6 +48,13 @@ data class InitiatedPayment( val endToEndId: String ) +enum class StatusUpdate { + pending, + transient_failure, + permanent_failure, + success +} + /** Outgoing transactions and batches submission status */ enum class SubmissionState { // Initiated but not yet submitted @@ -58,12 +66,23 @@ enum class SubmissionState { // Definitive failure, will never succeed permanent_failure, // Definitive success, booked and settled - success ; + success, + // Late failure after a success, happens when a payment is returned + late_failure; companion object { - val SETTLED = listOf(SubmissionState.success, SubmissionState.permanent_failure) + val SETTLED = listOf(SubmissionState.success, SubmissionState.permanent_failure, SubmissionState.late_failure) val PENDING = listOf(SubmissionState.unsubmitted, SubmissionState.pending) } + + fun toTransferStatus(): TransferStatusState { + return when (this) { + SubmissionState.unsubmitted, SubmissionState.pending -> TransferStatusState.pending + SubmissionState.transient_failure -> TransferStatusState.transient_failure + SubmissionState.permanent_failure -> TransferStatusState.permanent_failure + SubmissionState.success, SubmissionState.late_failure -> TransferStatusState.success + } + } } /** Collects database connection steps and any operation on the Nexus tables */ diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/db/ExchangeDAO.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/db/ExchangeDAO.kt @@ -159,12 +159,7 @@ class ExchangeDAO(private val db: Database) { setLong(1, id) oneOrNull { TransferStatus( - status = when (it.getEnum<SubmissionState>("status")) { - SubmissionState.unsubmitted, SubmissionState.pending -> TransferStatusState.pending - SubmissionState.transient_failure -> TransferStatusState.transient_failure - SubmissionState.permanent_failure -> TransferStatusState.permanent_failure - SubmissionState.success -> TransferStatusState.success - }, + status = it.getEnum<SubmissionState>("status").toTransferStatus(), status_msg = it.getString("status_msg"), amount = it.getAmount("amount", db.bankCurrency), origin_exchange_url = it.getString("exchange_base_url"), @@ -216,12 +211,7 @@ class ExchangeDAO(private val db: Database) { ) { TransferListStatus( row_id = it.getLong("initiated_outgoing_transaction_id"), - status = when (it.getEnum<SubmissionState>("status")) { - SubmissionState.unsubmitted, SubmissionState.pending -> TransferStatusState.pending - SubmissionState.transient_failure -> TransferStatusState.transient_failure - SubmissionState.permanent_failure -> TransferStatusState.permanent_failure - SubmissionState.success -> TransferStatusState.success - }, + status = it.getEnum<SubmissionState>("status").toTransferStatus(), amount = it.getAmount("amount", db.bankCurrency), credit_account = it.getString("credit_payto"), timestamp = it.getTalerTimestamp("initiation_time"), diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/db/InitiatedDAO.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/db/InitiatedDAO.kt @@ -240,75 +240,27 @@ class InitiatedDAO(private val db: Database) { } /** Register payment status [state] with [msg] for batch [msgId] */ - suspend fun batchStatusUpdate(msgId: String, state: SubmissionState, msg: String?) = db.serializableTransaction { tx -> - // Update unsettled batch status - val batchId = tx.withStatement( - """ - UPDATE initiated_outgoing_batches - SET status = ?::submission_state, status_msg = ? - WHERE message_id = ? AND $UNSETTLED_FILTER - RETURNING initiated_outgoing_batch_id - """ - ) { - setString(1, state.name) - setString(2, msg) - setString(3, msgId) - oneOrNull { it.getLong("initiated_outgoing_batch_id") } - } - // Update unsettled batch's transaction status - if (batchId != null) { - // When a batch succeed it doesn't mean that individual transaction also succeed - val txState = if (state == SubmissionState.success) { - SubmissionState.pending - } else { - state - } - tx.withStatement( - """ - UPDATE initiated_outgoing_transactions - SET status = ?::submission_state, status_msg = ? - WHERE initiated_outgoing_batch_id = ? AND $UNSETTLED_FILTER - """ - ) { - setString(1, txState.name) - setString(2, msg) - setLong(3, batchId) - execute() - } - } + suspend fun batchStatusUpdate(msgId: String, state: StatusUpdate, msg: String?) = db.serializable( + "SELECT FROM batch_status_update(?,?::submission_state,?)" + ) { + setString(1, msgId) + setString(2, state.name) + setString(3, msg) + execute() } /** Register payment status [state] with [msg] for transaction [endToEndId] in batch [msgId] */ - suspend fun txStatusUpdate(endToEndId: String, msgId: String?, state: SubmissionState, msg: String) = db.serializableTransaction { tx -> - // Update unsettled transaction status - tx.withStatement( - """ - UPDATE initiated_outgoing_transactions - SET status = ?::submission_state, status_msg = ? - WHERE end_to_end_id = ? AND $UNSETTLED_FILTER - """ - ) { - setString(1, state.name) - setString(2, msg) - setString(3, endToEndId) - execute() - } - // Update unsettled batch status - if (msgId != null) { - tx.withStatement( - """ - UPDATE initiated_outgoing_batches - SET status = 'success', status_msg = NULL - WHERE message_id = ? AND $UNSETTLED_FILTER - """ - ) { - setString(1, msgId) - execute() - } - } + suspend fun txStatusUpdate(endToEndId: String, msgId: String?, state: StatusUpdate, msg: String) = db.serializable( + "SELECT FROM tx_status_update(?,?,?::submission_state,?)" + ) { + setString(1, endToEndId) + setString(2, msgId) + setString(3, state.name) + setString(4, msg) + execute() } - /** Unsettled intiaited payment in batch [msgId] */ + /** Unsettled initiated payment in batch [msgId] */ suspend fun unsettledTxInBatch(msgId: String, executionTime: Instant) = db.serializable( """ SELECT end_to_end_id diff --git a/nexus/src/test/kotlin/DatabaseTest.kt b/nexus/src/test/kotlin/DatabaseTest.kt @@ -497,14 +497,16 @@ class PaymentInitiationsTest { // Payment & batch status test { batchId -> checkBatch(batchId, SubmissionState.unsubmitted, null) - db.initiated.batchStatusUpdate("BATCH", SubmissionState.pending, "progress") + db.initiated.batchStatusUpdate("BATCH", StatusUpdate.pending, "progress") checkBatch(batchId, SubmissionState.pending, "progress") - db.initiated.txStatusUpdate("TX_SETTLED", null, SubmissionState.success, "success") + db.initiated.txStatusUpdate("TX_SETTLED", null, StatusUpdate.success, "success") checkPart(batchId, SubmissionState.pending, "progress", SubmissionState.pending, "progress", SubmissionState.success, "success") - db.initiated.batchStatusUpdate("BATCH", SubmissionState.transient_failure, "waiting") + db.initiated.batchStatusUpdate("BATCH", StatusUpdate.transient_failure, "waiting") checkPart(batchId, SubmissionState.transient_failure, "waiting", SubmissionState.transient_failure, "waiting", SubmissionState.success, "success") - db.initiated.txStatusUpdate("TX", "BATCH", SubmissionState.permanent_failure, "failure") + db.initiated.txStatusUpdate("TX", "BATCH", StatusUpdate.permanent_failure, "failure") checkPart(batchId, SubmissionState.success, null, SubmissionState.permanent_failure, "failure", SubmissionState.success, "success") + db.initiated.txStatusUpdate("TX_SETTLED", "BATCH", StatusUpdate.permanent_failure, "late") + checkPart(batchId, SubmissionState.success, null, SubmissionState.permanent_failure, "failure", SubmissionState.late_failure, "late") } // Registration @@ -520,8 +522,8 @@ class PaymentInitiationsTest { db.initiated.batchSubmissionSuccess(42, Instant.now(), "ORDER_X") db.initiated.batchSubmissionFailure(42, Instant.now(), null) db.initiated.orderStep("ORDER_X", "msg") - db.initiated.batchStatusUpdate("BATCH_X", SubmissionState.success, null) - db.initiated.txStatusUpdate("TX_X", "BATCH_X", SubmissionState.success, "msg") + db.initiated.batchStatusUpdate("BATCH_X", StatusUpdate.success, null) + db.initiated.txStatusUpdate("TX_X", "BATCH_X", StatusUpdate.success, "msg") assertNull(db.initiated.orderSuccess("ORDER_X")) assertNull(db.initiated.orderFailure("ORDER_X")) } diff --git a/nexus/src/test/kotlin/Iso20022Test.kt b/nexus/src/test/kotlin/Iso20022Test.kt @@ -242,7 +242,20 @@ class Iso20022Test { OutgoingBatch( msgId = "BATCH_MANY_SUCCESS", executionTime = dateToInstant("2024-09-20"), - ) + ), + OutgoingPayment( + endToEndId = "KLJJ28S1LVNDK1R2HCHLN884M7EKM5XGM5", + msgId = "BATCH_SINGLE_RETURN", + amount = TalerAmount("EUR:0.42"), + subject = "This should fail because bad iban", + executionTime = dateToInstant("2024-09-23"), + creditorPayto = ibanPayto("DE18500105173385245163", "John Smith") + ), + OutgoingReversal( + endToEndId = "KLJJ28S1LVNDK1R2HCHLN884M7EKM5XGM5", + reason = "IncorrectAccountNumber 'Format of the account number specified is not correct' - 'IBAN fehlerhaft und ungültig'", + executionTime = dateToInstant("2024-09-24") + ), ) ) } diff --git a/nexus/src/test/kotlin/RegistrationTest.kt b/nexus/src/test/kotlin/RegistrationTest.kt @@ -262,6 +262,9 @@ class RegistrationTest { "BATCH_SINGLE_FAILURE" to listOf( genInitPay("DAFC3NEE4T48WVC560T76ABA2C"), ), + "BATCH_SINGLE_RETURN" to listOf( + genInitPay("KLJJ28S1LVNDK1R2HCHLN884M7EKM5XGM5"), + ), "BATCH_MANY_SUCCESS" to listOf( genInitPay("IVMIGCUIE7Q7VOF73R8GU3KGRYBZPAYC5V"), genInitPay("CDFN7I4FVIZ848DGDQ35DZ2K49H9EWXGAW"), @@ -300,6 +303,9 @@ class RegistrationTest { "BATCH_SINGLE_FAILURE" to Pair(SubmissionState.pending, mapOf( // TODO success "DAFC3NEE4T48WVC560T76ABA2C" to SubmissionState.pending, // TODO failure )), + "BATCH_SINGLE_RETURN" to Pair(SubmissionState.success, mapOf( + "KLJJ28S1LVNDK1R2HCHLN884M7EKM5XGM5" to SubmissionState.late_failure, + )), "BATCH_MANY_SUCCESS" to Pair(SubmissionState.success, mapOf( "IVMIGCUIE7Q7VOF73R8GU3KGRYBZPAYC5V" to SubmissionState.success, "CDFN7I4FVIZ848DGDQ35DZ2K49H9EWXGAW" to SubmissionState.success, @@ -376,6 +382,13 @@ class RegistrationTest { creditorPayto = ibanPayto("CH4189144589712575493", "Test") ), OutgoingPayment( + endToEndId = "KLJJ28S1LVNDK1R2HCHLN884M7EKM5XGM5", + amount = TalerAmount("EUR:0.42"), + subject = "This should fail because bad iban", + executionTime = dateToInstant("2024-09-23"), + creditorPayto = ibanPayto("DE18500105173385245163", "John Smith") + ), + OutgoingPayment( endToEndId = "27SK3166EG36SJ7VP7VFYP0MW8", amount = TalerAmount("EUR:44"), subject = "init payment", diff --git a/testbench/src/main/kotlin/Main.kt b/testbench/src/main/kotlin/Main.kt @@ -30,7 +30,7 @@ import io.ktor.client.engine.cio.* import io.ktor.http.* import kotlinx.coroutines.runBlocking import kotlinx.serialization.Serializable -import tech.libeufin.common.ANSI +import tech.libeufin.common.* import tech.libeufin.nexus.* import tech.libeufin.nexus.cli.* import java.time.Instant @@ -132,7 +132,7 @@ class Cli : CliktCommand() { val dummyPaytos = mapOf( "CHF" to "payto://iban/CH4189144589712575493?receiver-name=John%20Smith", - "EUR" to "payto://iban/FR6812739000706478491641U13?receiver-name=John%20Smith" + "EUR" to "payto://iban/DE18500105173385245162?receiver-name=John%20Smith" ) val dummyPayto = requireNotNull(dummyPaytos[currency]) { "Missing dummy payto for $currency" @@ -142,7 +142,7 @@ class Cli : CliktCommand() { val recoverDoc = "report statement notification" runBlocking { step("Init ${kind.name}") - + assert(nexusCmd.run("dbinit $flags")) val cmds = buildMap { @@ -182,13 +182,13 @@ class Cli : CliktCommand() { Unit }) put("tx", suspend { - step("Initiate one new transaction") + step("Initiate a new transaction") val now = Instant.now() nexusCmd.run("initiate-payment $flags --amount=$currency:0.1 --subject \"single $now\" \"$payto\"") Unit }) put("txs", suspend { - step("Submit many transaction") + step("Initiate four new transactions") val now = Instant.now() repeat(4) { nexusCmd.run("initiate-payment $flags --amount=$currency:${(10.0+it)/100} --subject \"multi $it $now\" \"$payto\"") @@ -197,20 +197,23 @@ class Cli : CliktCommand() { put("tx-bad-name", suspend { val badPayto = URLBuilder().takeFrom(payto) badPayto.parameters["receiver-name"] = "John Smith" - step("Submit new transaction with a bad name") - nexusCmd.run("initiate-payment $flags \"$badPayto&amount=$currency:0.21&message=This%20should%20fail%20because%20bad%20name\"") + step("Initiate a new transaction with a bad name") + val now = Instant.now() + nexusCmd.run("initiate-payment $flags --amount=$currency:0.21 --subject \"bad name $now\" \"$badPayto\"") Unit }) put("tx-bad-iban", suspend { - val badPayto = URLBuilder().takeFrom("payto://iban/DE18500105173385245162") + val badPayto = URLBuilder().takeFrom("payto://iban/XX18500105173385245165") badPayto.parameters["receiver-name"] = "John Smith" - step("Submit new transaction to a bad IBAN") - nexusCmd.run("initiate-payment $flags \"$badPayto&amount=$currency:0.22&message=This%20should%20fail%20because%20bad%20iban\"") + step("Initiate a new transaction to a bad IBAN") + val now = Instant.now() + nexusCmd.run("initiate-payment $flags --amount=$currency:0.22 --subject \"bad iban $now\" \"$badPayto\"") Unit }) - put("tx-dummy", suspend { - step("Submit new transaction to a dummy IBAN") - nexusCmd.run("initiate-payment $flags \"$dummyPayto&amount=$currency:0.23&message=This%20should%20fail%20because%20dummy\"") + put("tx-dummy-iban", suspend { + step("Initiate a new transaction to a dummy IBAN") + val now = Instant.now() + nexusCmd.run("initiate-payment $flags --amount=$currency:0.23 --subject \"dummy iban $now\" \"$dummyPayto\"") Unit }) put("tx-check", "Check transaction semantic", "testing tx-check $flags")