diff options
author | MS <ms@taler.net> | 2023-11-19 10:43:24 +0100 |
---|---|---|
committer | MS <ms@taler.net> | 2023-11-19 10:43:24 +0100 |
commit | 0c21910217848dd34423951b2ce18cc8a5c29777 (patch) | |
tree | 8922d5b05b27955dd512ebd2ec21d204eb6df17d | |
parent | d7270181fd148d43ecc34f1e8f1613af696d5f3c (diff) | |
download | libeufin-0c21910217848dd34423951b2ce18cc8a5c29777.tar.gz libeufin-0c21910217848dd34423951b2ce18cc8a5c29777.tar.bz2 libeufin-0c21910217848dd34423951b2ce18cc8a5c29777.zip |
nexus submit: adjusting pain.001 after PoFi
-rw-r--r-- | nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt | 5 | ||||
-rw-r--r-- | nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt | 79 | ||||
-rw-r--r-- | nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt | 46 | ||||
-rw-r--r-- | nexus/src/test/kotlin/Common.kt | 7 | ||||
-rw-r--r-- | nexus/src/test/kotlin/DatabaseTest.kt | 46 |
5 files changed, 120 insertions, 63 deletions
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt index 64e2c9f7..10ec9f8c 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt @@ -519,7 +519,7 @@ class Database(dbConfig: String): java.io.Closeable { * @param currency in which currency should the payment be submitted to the bank. * @return [Map] of the initiated payment row ID and [InitiatedPayment] */ - suspend fun initiatedPaymentsUnsubmittedGet(currency: String): Map<Long, InitiatedPayment> = runConn { conn -> + suspend fun initiatedPaymentsSubmittableGet(currency: String): Map<Long, InitiatedPayment> = runConn { conn -> val stmt = conn.prepareStatement(""" SELECT initiated_outgoing_transaction_id @@ -530,7 +530,8 @@ class Database(dbConfig: String): java.io.Closeable { ,initiation_time ,request_uid FROM initiated_outgoing_transactions - WHERE submitted='unsubmitted'; + WHERE submitted='unsubmitted' + OR submitted='transient_failure'; """) val maybeMap = mutableMapOf<Long, InitiatedPayment>() stmt.executeQuery().use { diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt index c1dc2c4b..e11e39c7 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt @@ -92,10 +92,49 @@ class NexusSubmitException( val stage: NexusSubmissionStage ) : Exception(msg, cause) + +/** + * Optionally logs the pain.001 in the log directory, if the + * configuration had this latter. + * + * @param maybeLogDir log directory. Null if the configuration + * lacks it. + * @param xml the pain.001 document to log. + * @param requestUid UID of the payment request (normally equals + * the pain.001 MsgId element), will be part of + * the filename. + */ +fun maybeLog( + maybeLogDir: String?, + xml: String, + requestUid: String +) { + if (maybeLogDir == null) { + logger.info("Logging pain.001 to files is disabled") + return + } + logger.debug("Logging to $maybeLogDir") + val now = Instant.now() + val asUtcDate = LocalDate.ofInstant(now, ZoneId.of("UTC")) + val subDir = "${asUtcDate.year}-${asUtcDate.monthValue}-${asUtcDate.dayOfMonth}" + val dirs = Path.of(maybeLogDir, subDir) + doOrFail { dirs.createDirectories() } + val f = File( + dirs.toString(), + "${now.toDbMicros()}_requestUid_${requestUid}_pain.001.xml" + ) + // Very rare: same pain.001 should not be submitted twice in the same microsecond. + if (f.exists()) { + logger.error("pain.001 log file exists already at: $f") + exitProcess(1) + } + doOrFail { f.writeText(xml) } +} + /** - * Takes the initiated payment data, as it was returned from the - * database, sanity-checks it, makes the pain.001 document and finally - * submits it via EBICS to the bank. + * Takes the initiated payment data as it was returned from the + * database, sanity-checks it, gets the pain.001 from the helper + * function and finally submits it via EBICS to the bank. * * @param ctx [SubmissionContext] * @return true on success, false otherwise. @@ -118,6 +157,16 @@ private suspend fun submitInitiatedPayment( debitAccount = ctx.cfg.myIbanAccount, wireTransferSubject = initiatedPayment.wireTransferSubject ) + // Logging first! + val maybeLogDir: String? = ctx.cfg.config.lookupString( + "nexus-submit", + "SUBMISSIONS_LOG_DIRECTORY" + ) + maybeLog( + maybeLogDir, + xml, + initiatedPayment.requestUid + ) try { submitPain001( xml, @@ -150,28 +199,6 @@ private suspend fun submitInitiatedPayment( cause = permanent ) } - // Submission succeeded, storing the pain.001 to file. - val logDir: String? = ctx.cfg.config.lookupString( - "neuxs-submit", - "SUBMISSIONS_LOG_DIRECTORY" - ) - if (logDir != null) { - val now = Instant.now() - val asUtcDate = LocalDate.ofInstant(now, ZoneId.of("UTC")) - val subDir = "${asUtcDate.year}-${asUtcDate.monthValue}-${asUtcDate.dayOfMonth}" - val dirs = Path.of(logDir, subDir) - doOrFail { dirs.createDirectories() } - val f = File( - dirs.toString(), - "${now.toDbMicros()}_requestUid_${initiatedPayment.requestUid}_pain.001.xml" - ) - // Very rare: same pain.001 should not be submitted twice in the same microsecond. - if (f.exists()) { - logger.error("pain.001 log file exists already at: $f") - exitProcess(1) - } - doOrFail { f.writeText(xml) } - } } /** @@ -190,7 +217,7 @@ private fun submitBatch( ) { logger.debug("Running submit at: ${Instant.now()}") runBlocking { - db.initiatedPaymentsUnsubmittedGet(ctx.cfg.currency).forEach { + db.initiatedPaymentsSubmittableGet(ctx.cfg.currency).forEach { logger.debug("Submitting payment initiation with row ID: ${it.key}") val submissionState = try { submitInitiatedPayment(ctx, initiatedPayment = it.value) diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt index 3c291b99..1ad6c50b 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt @@ -8,6 +8,10 @@ import java.time.ZonedDateTime import java.time.format.DateTimeFormatter +/** + * Collects details to define the pain.001 namespace + * XML attributes. + */ data class Pain001Namespaces( val fullNamespace: String, val xsdFilename: String @@ -73,8 +77,14 @@ fun createPain001( ) return constructXml(indent = true) { root("Document") { - attribute("xmlns", namespace.fullNamespace) - attribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") + attribute( + "xmlns", + namespace.fullNamespace + ) + attribute( + "xmlns:xsi", + "http://www.w3.org/2001/XMLSchema-instance" + ) attribute( "xsi:schemaLocation", "${namespace.fullNamespace} ${namespace.xsdFilename}" @@ -100,7 +110,7 @@ fun createPain001( } element("PmtInf") { element("PmtInfId") { - text("NOT GIVEN") + text("NOTPROVIDED") } element("PmtMtd") { text("TRF") @@ -108,15 +118,6 @@ fun createPain001( element("BtchBookg") { text("true") } - element("NbOfTxs") { - text("1") - } - element("CtrlSum") { - text(amountWithoutCurrency) - } - element("PmtTpInf/SvcLvl/Cd") { - text("SDVA") - } element("ReqdExctnDt") { element("Dt") { text(DateTimeFormatter.ISO_DATE.format(zonedTimestamp)) @@ -128,31 +129,18 @@ fun createPain001( element("DbtrAcct/Id/IBAN") { text(debitAccount.iban) } - element("DbtrAgt/FinInstnId") { - element("BICFI") { - text(debitAccount.bic) - } - } - element("ChrgBr") { - text("SLEV") + element("DbtrAgt/FinInstnId/BICFI") { + text(debitAccount.bic) } element("CdtTrfTxInf") { element("PmtId") { - element("InstrId") { text("NOT PROVIDED") } - element("EndToEndId") { text("NOT PROVIDED") } + element("InstrId") { text("NOTPROVIDED") } + element("EndToEndId") { text("NOTPROVIDED") } } element("Amt/InstdAmt") { attribute("Ccy", amount.currency) text(amountWithoutCurrency) } - creditAccount.bic.apply { - if (this != null) - element("CdtrAgt/FinInstnId") { - element("BICFI") { - text(this@apply) - } - } - } element("Cdtr/Nm") { text(creditorName) } diff --git a/nexus/src/test/kotlin/Common.kt b/nexus/src/test/kotlin/Common.kt index ecfa8c6e..ea9e67fd 100644 --- a/nexus/src/test/kotlin/Common.kt +++ b/nexus/src/test/kotlin/Common.kt @@ -77,13 +77,16 @@ fun getPofiConfig( """.trimIndent() // Generates a payment initiation, given its subject. -fun genInitPay(subject: String = "init payment", rowUid: String = "unique") = +fun genInitPay( + subject: String = "init payment", + requestUid: String = "unique" +) = InitiatedPayment( amount = TalerAmount(44, 0, "KUDOS"), creditPaytoUri = "payto://iban/TEST-IBAN?receiver-name=Test", wireTransferSubject = subject, initiationTime = Instant.now(), - requestUid = rowUid + requestUid = requestUid ) // Generates an incoming payment, given its subject. diff --git a/nexus/src/test/kotlin/DatabaseTest.kt b/nexus/src/test/kotlin/DatabaseTest.kt index 9017f950..0f2cc4fa 100644 --- a/nexus/src/test/kotlin/DatabaseTest.kt +++ b/nexus/src/test/kotlin/DatabaseTest.kt @@ -161,7 +161,7 @@ class PaymentInitiationsTest { fun paymentInitiation() { val db = prepDb(TalerConfig(NEXUS_CONFIG_SOURCE)) runBlocking { - val beEmpty = db.initiatedPaymentsUnsubmittedGet("KUDOS")// expect no records. + val beEmpty = db.initiatedPaymentsSubmittableGet("KUDOS") // expect no records. assertEquals(beEmpty.size, 0) } val initPay = InitiatedPayment( @@ -175,17 +175,55 @@ class PaymentInitiationsTest { assertNull(db.initiatedPaymentGetFromUid("unique")) assertEquals(db.initiatedPaymentCreate(initPay), PaymentInitiationOutcome.SUCCESS) assertEquals(db.initiatedPaymentCreate(initPay), PaymentInitiationOutcome.UNIQUE_CONSTRAINT_VIOLATION) - val haveOne = db.initiatedPaymentsUnsubmittedGet("KUDOS") + val haveOne = db.initiatedPaymentsSubmittableGet("KUDOS") assertTrue { haveOne.size == 1 && haveOne.containsKey(1) && haveOne[1]?.requestUid == "unique" } - db.initiatedPaymentSetSubmittedState(1, DatabaseSubmissionState.success) + assertTrue(db.initiatedPaymentSetSubmittedState(1, DatabaseSubmissionState.success)) assertNotNull(db.initiatedPaymentGetFromUid("unique")) } } + /** + * The SQL that gets submittable payments checks multiple + * statuses from them. Checking it here. + */ + @Test + fun submittablePayments() { + val db = prepDb(TalerConfig(NEXUS_CONFIG_SOURCE)) + runBlocking { + val beEmpty = db.initiatedPaymentsSubmittableGet("KUDOS") + assertEquals(0, beEmpty.size) + assertEquals( + db.initiatedPaymentCreate(genInitPay(requestUid = "first")), + PaymentInitiationOutcome.SUCCESS + ) + assertEquals( + db.initiatedPaymentCreate(genInitPay(requestUid = "second")), + PaymentInitiationOutcome.SUCCESS + ) + assertEquals( + db.initiatedPaymentCreate(genInitPay(requestUid = "third")), + PaymentInitiationOutcome.SUCCESS + ) + + // Setting the first as "transient_failure", must be found. + assertTrue(db.initiatedPaymentSetSubmittedState( + 1, DatabaseSubmissionState.transient_failure + )) + // Setting the second as "success", must not be found. + assertTrue(db.initiatedPaymentSetSubmittedState( + 2, DatabaseSubmissionState.success + )) + val expectTwo = db.initiatedPaymentsSubmittableGet("KUDOS") + // the third initiation keeps the default "unsubmitted" + // state, must be found. Total 2. + assertEquals(2, expectTwo.size) + } + } + // Tests how the fetch method gets the list of // multiple unsubmitted payment initiations. @Test @@ -207,7 +245,7 @@ class PaymentInitiationsTest { } // Expecting all the payments BUT the #3 in the result. - db.initiatedPaymentsUnsubmittedGet("KUDOS").apply { + db.initiatedPaymentsSubmittableGet("KUDOS").apply { assertEquals(3, this.size) assertEquals("#1", this[1]?.wireTransferSubject) assertEquals("#2", this[2]?.wireTransferSubject) |