aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMS <ms@taler.net>2023-11-19 10:43:24 +0100
committerMS <ms@taler.net>2023-11-19 10:43:24 +0100
commit0c21910217848dd34423951b2ce18cc8a5c29777 (patch)
tree8922d5b05b27955dd512ebd2ec21d204eb6df17d
parentd7270181fd148d43ecc34f1e8f1613af696d5f3c (diff)
downloadlibeufin-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.kt5
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt79
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt46
-rw-r--r--nexus/src/test/kotlin/Common.kt7
-rw-r--r--nexus/src/test/kotlin/DatabaseTest.kt46
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)