gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[libeufin] branch master updated: nexus fetch: crafting camt.053 & pain.


From: gnunet
Subject: [libeufin] branch master updated: nexus fetch: crafting camt.053 & pain.002 requests
Date: Wed, 08 Nov 2023 21:55:54 +0100

This is an automated email from the git hooks/post-receive script.

ms pushed a commit to branch master
in repository libeufin.

The following commit(s) were added to refs/heads/master by this push:
     new 66a6cda6 nexus fetch: crafting camt.053 & pain.002 requests
66a6cda6 is described below

commit 66a6cda6d8dec57a75904392c63a87b191a676d8
Author: MS <ms@taler.net>
AuthorDate: Wed Nov 8 21:55:08 2023 +0100

    nexus fetch: crafting camt.053 & pain.002 requests
---
 .../main/kotlin/tech/libeufin/nexus/EbicsFetch.kt  | 89 ++++++++++++++++++++
 .../kotlin/tech/libeufin/nexus/ebics/Ebics2.kt     | 11 ++-
 .../kotlin/tech/libeufin/nexus/ebics/Ebics3.kt     |  4 +-
 .../tech/libeufin/nexus/ebics/EbicsCommon.kt       | 53 ++++++++----
 nexus/src/test/kotlin/PostFinance.kt               | 95 +++++++++++++++++++---
 5 files changed, 220 insertions(+), 32 deletions(-)

diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
index 23ccf7d4..ac1d522d 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
@@ -4,12 +4,35 @@ import com.github.ajalt.clikt.core.CliktCommand
 import com.github.ajalt.clikt.parameters.options.flag
 import com.github.ajalt.clikt.parameters.options.option
 import io.ktor.client.*
+import org.apache.commons.compress.archivers.zip.ZipFile
+import org.apache.commons.compress.utils.SeekableInMemoryByteChannel
 import tech.libeufin.util.ebics_h005.Ebics3Request
 import tech.libeufin.util.getXmlDate
 import java.time.Instant
 import kotlin.concurrent.fixedRateTimer
 import kotlin.system.exitProcess
 
+/**
+ * Unzips the ByteArray and runs the lambda over each entry.
+ *
+ * @param lambda function that gets the (fileName, fileContent) pair
+ *        for each entry in the ZIP archive as input.
+ */
+fun ByteArray.unzipForEach(lambda: (String, String) -> Unit) {
+    if (this.isEmpty()) {
+        logger.warn("Empty archive")
+        return
+    }
+    val mem = SeekableInMemoryByteChannel(this)
+    val zipFile = ZipFile(mem)
+    zipFile.getEntriesInPhysicalOrder().iterator().forEach {
+        lambda(
+            it.name, 
zipFile.getInputStream(it).readAllBytes().toString(Charsets.UTF_8)
+        )
+    }
+    zipFile.close()
+}
+
 /**
  * Crafts a date range object, when the caller needs a time range.
  *
@@ -27,6 +50,72 @@ fun getEbics3DateRange(
     }
 }
 
+/**
+ * Prepares the request for a pain.002 acknowledgement from the bank.
+ *
+ * @param startDate inclusive starting date for the returned acknowledgements.
+ * @param endDate inclusive ending date for the returned acknowledgements.  
NOTE:
+ *        if startDate is NOT null and endDate IS null, endDate gets defaulted
+ *        to the current UTC time.
+ *
+ * @return [Ebics3Request.OrderDetails.BTOrderParams]
+ */
+fun prepAckRequest(
+    startDate: Instant? = null,
+    endDate: Instant? = null
+): Ebics3Request.OrderDetails.BTOrderParams {
+    val service = Ebics3Request.OrderDetails.Service().apply {
+        serviceName = "PSR"
+        scope = "CH"
+        container = Ebics3Request.OrderDetails.Service.Container().apply {
+            containerType = "ZIP"
+        }
+        messageName = Ebics3Request.OrderDetails.Service.MessageName().apply {
+            value = "pain.002"
+            version = "03"
+        }
+    }
+    return Ebics3Request.OrderDetails.BTOrderParams().apply {
+        this.service = service
+        this.dateRange = if (startDate != null)
+            getEbics3DateRange(startDate, endDate ?: Instant.now())
+        else null
+    }
+}
+
+/**
+ * Prepares the request for (a) camt.053/statement(s).
+ *
+ * @param startDate inclusive starting date for the returned banking events.
+ * @param endDate inclusive ending date for the returned banking events.  NOTE:
+ *        if startDate is NOT null and endDate IS null, endDate gets defaulted
+ *        to the current UTC time.
+ *
+ * @return [Ebics3Request.OrderDetails.BTOrderParams]
+ */
+fun prepStatementRequest(
+    startDate: Instant? = null,
+    endDate: Instant? = null
+): Ebics3Request.OrderDetails.BTOrderParams {
+    val service = Ebics3Request.OrderDetails.Service().apply {
+        serviceName = "EOP"
+        scope = "CH"
+        container = Ebics3Request.OrderDetails.Service.Container().apply {
+            containerType = "ZIP"
+        }
+        messageName = Ebics3Request.OrderDetails.Service.MessageName().apply {
+            value = "camt.053"
+            version = "08"
+        }
+    }
+    return Ebics3Request.OrderDetails.BTOrderParams().apply {
+        this.service = service
+        this.dateRange = if (startDate != null)
+            getEbics3DateRange(startDate, endDate ?: Instant.now())
+        else null
+    }
+}
+
 /**
  * Prepares the request for camt.052/intraday records.
  *
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt
index cdda0c26..34cf5c5c 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt
@@ -24,6 +24,7 @@
 package tech.libeufin.nexus.ebics
 
 import io.ktor.client.*
+import org.bouncycastle.util.encoders.UTF8
 import tech.libeufin.nexus.BankPublicKeysFile
 import tech.libeufin.nexus.ClientPrivateKeysFile
 import tech.libeufin.nexus.EbicsSetupConfig
@@ -53,7 +54,7 @@ suspend fun doEbicsCustomDownload(
     clientKeys: ClientPrivateKeysFile,
     bankKeys: BankPublicKeysFile,
     client: HttpClient
-): String? {
+): ByteArray? {
     val xmlReq = createEbics25DownloadInit(cfg, clientKeys, bankKeys, 
messageType)
     return doEbicsDownload(client, cfg, clientKeys, bankKeys, xmlReq, false)
 }
@@ -77,19 +78,21 @@ suspend fun fetchBankAccounts(
     client: HttpClient
 ): HTDResponseOrderData? {
     val xmlReq = createEbics25DownloadInit(cfg, clientKeys, bankKeys, "HTD")
-    val xmlResp = doEbicsDownload(client, cfg, clientKeys, bankKeys, xmlReq, 
false)
-    if (xmlResp == null) {
+    val bytesResp = doEbicsDownload(client, cfg, clientKeys, bankKeys, xmlReq, 
false)
+    if (bytesResp == null) {
         logger.error("EBICS HTD transaction failed.")
         return null
     }
+    val xmlResp = bytesResp.toString(Charsets.UTF_8)
     return try {
-        logger.debug("Fetched accounts: $xmlResp")
+        logger.debug("Fetched accounts: $bytesResp")
         XMLUtil.convertStringToJaxb<HTDResponseOrderData>(xmlResp).value
     } catch (e: Exception) {
         logger.error("Could not parse the HTD payload, detail: ${e.message}")
         return null
     }
 }
+
 /**
  * Creates a EBICS 2.5 download init. message.  So far only used
  * to fetch the PostFinance bank accounts.
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt
index 2f141cbc..04cdbf4d 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt
@@ -55,9 +55,9 @@ fun createEbics3DownloadReceiptPhase(
 fun createEbics3DownloadTransferPhase(
     cfg: EbicsSetupConfig,
     clientKeys: ClientPrivateKeysFile,
-    transactionId: String,
+    howManySegments: Int,
     segmentNumber: Int,
-    howManySegments: Int
+    transactionId: String
 ): String {
     val req = Ebics3Request.createForDownloadTransferPhase(
         cfg.ebicsHostId,
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt
index 772192dc..fd72b87a 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt
@@ -63,7 +63,6 @@ import java.util.zip.DeflaterInputStream
  * @param encryptionInfo details related to the encrypted payload.
  * @param chunks the several chunks that constitute the whole encrypted 
payload.
  * @return the plain payload.  Errors throw, so the caller must handle those.
- *
  */
 fun decryptAndDecompressPayload(
     clientEncryptionKey: RSAPrivateCrtKey,
@@ -255,9 +254,14 @@ private fun areCodesOk(ebicsResponseContent: 
EbicsResponseContent) =
  * @param clientKeys client EBICS private keys.
  * @param bankKeys bank EBICS public keys.
  * @param reqXml raw EBICS XML request of the init phase.
- * @return the bank response as an XML string, or null if one
- *         error took place.  NOTE: any return code other than
- *         EBICS_OK constitutes an error.
+ * @param isEbics3 true for EBICS 3, false otherwise.
+ * @param tolerateEmptyResult true if the EC EBICS_NO_DOWNLOAD_DATA_AVAILABLE
+ *        should be tolerated as the bank-technical error, false otherwise.
+ * @return the bank response as an uncompressed [ByteArray], or null if one 
error took place.
+ *         If the request tolerates an empty download content, then the empty
+ *         array is returned.  If the request does not tolerate an empty 
response
+ *         any non-EBICS_OK error as the EBICS- or bank-technical EC 
constitutes
+ *         an error.
  */
 suspend fun doEbicsDownload(
     client: HttpClient,
@@ -265,13 +269,29 @@ suspend fun doEbicsDownload(
     clientKeys: ClientPrivateKeysFile,
     bankKeys: BankPublicKeysFile,
     reqXml: String,
-    isEbics3: Boolean
-): String? {
+    isEbics3: Boolean,
+    tolerateEmptyResult: Boolean = false
+): ByteArray? {
     val initResp = postEbics(client, cfg, bankKeys, reqXml, isEbics3)
-    if (!areCodesOk(initResp)) {
-        tech.libeufin.nexus.logger.error("EBICS download: could not get past 
the EBICS init phase, failing.")
+    logger.debug("Download init phase done.  EBICS- and bank-technical codes 
are: ${initResp.technicalReturnCode}, ${initResp.bankReturnCode}")
+    if (initResp.technicalReturnCode != EbicsReturnCode.EBICS_OK) {
+        logger.error("Download init phase has EBICS-technical error: 
${initResp.technicalReturnCode}")
+        return null
+    }
+    if (initResp.bankReturnCode == 
EbicsReturnCode.EBICS_NO_DOWNLOAD_DATA_AVAILABLE && tolerateEmptyResult) {
+        logger.info("Download content is empty")
+        return ByteArray(0)
+    }
+    if (initResp.bankReturnCode != EbicsReturnCode.EBICS_OK) {
+        logger.error("Download init phase has bank-technical error: 
${initResp.bankReturnCode}")
         return null
     }
+    val tId = initResp.transactionID
+    if (tId == null) {
+        tech.libeufin.nexus.logger.error("Transaction ID not found in the init 
response, cannot do transfer phase, failing.")
+        return null
+    }
+    logger.debug("EBICS download transaction got ID: $tId")
     val howManySegments = initResp.numSegments
     if (howManySegments == null) {
         tech.libeufin.nexus.logger.error("Init response lacks the quantity of 
segments, failing.")
@@ -289,15 +309,13 @@ suspend fun doEbicsDownload(
         return null
     }
     ebicsChunks.add(firstDataChunk)
-    val tId = initResp.transactionID
-    if (tId == null) {
-        tech.libeufin.nexus.logger.error("Transaction ID not found in the init 
response, cannot do transfer phase, failing.")
-        return null
-    }
     // proceed with the transfer phase.
     for (x in 2 .. howManySegments) {
         // request segment number x.
-        val transReq = createEbics25DownloadTransferPhase(cfg, clientKeys, x, 
howManySegments, tId)
+        val transReq = if (isEbics3)
+            createEbics3DownloadTransferPhase(cfg, clientKeys, x, 
howManySegments, tId)
+        else createEbics25DownloadTransferPhase(cfg, clientKeys, x, 
howManySegments, tId)
+
         val transResp = postEbics(client, cfg, bankKeys, transReq, isEbics3)
         if (!areCodesOk(transResp)) { // FIXME: consider tolerating 
EBICS_NO_DOWNLOAD_DATA_AVAILABLE.
             tech.libeufin.nexus.logger.error("EBICS transfer segment #$x 
failed.")
@@ -317,7 +335,10 @@ suspend fun doEbicsDownload(
         ebicsChunks
     )
     // payload reconstructed, ack to the bank.
-    val ackXml = createEbics25DownloadReceiptPhase(cfg, clientKeys, tId)
+    val ackXml = if (isEbics3)
+        createEbics3DownloadReceiptPhase(cfg, clientKeys, tId)
+    else createEbics25DownloadReceiptPhase(cfg, clientKeys, tId)
+
     try {
         postEbics(
             client,
@@ -332,7 +353,7 @@ suspend fun doEbicsDownload(
     }
     // receipt phase OK, can now return the payload as an XML string.
     return try {
-        payloadBytes.toString(Charsets.UTF_8)
+        payloadBytes
     } catch (e: Exception) {
         logger.error("Could not get the XML string out of payload bytes.")
         null
diff --git a/nexus/src/test/kotlin/PostFinance.kt 
b/nexus/src/test/kotlin/PostFinance.kt
index 55a59464..f5d95533 100644
--- a/nexus/src/test/kotlin/PostFinance.kt
+++ b/nexus/src/test/kotlin/PostFinance.kt
@@ -3,9 +3,8 @@ import kotlinx.coroutines.runBlocking
 import org.junit.Ignore
 import org.junit.Test
 import tech.libeufin.nexus.*
-import tech.libeufin.nexus.ebics.doEbicsCustomDownload
-import tech.libeufin.nexus.ebics.fetchBankAccounts
-import tech.libeufin.nexus.ebics.submitPain001
+import tech.libeufin.nexus.ebics.*
+import tech.libeufin.util.ebics_h005.Ebics3Request
 import tech.libeufin.util.parsePayto
 import java.io.File
 import java.time.Instant
@@ -22,8 +21,86 @@ private fun prep(): EbicsSetupConfig {
     return EbicsSetupConfig(handle)
 }
 
-@Ignore
 class Iso20022 {
+    // Asks a camt.052 report to the test platform.
+
+    @Test
+    fun simulateIncoming() {
+        val cfg = prep()
+        val orderService: Ebics3Request.OrderDetails.Service = 
Ebics3Request.OrderDetails.Service().apply {
+            serviceName = "OTH"
+            scope = "BIL"
+            messageName = 
Ebics3Request.OrderDetails.Service.MessageName().apply {
+                value = "csv"
+            }
+            serviceOption = "CH002LMF"
+        }
+        val instruction = """
+            
Product;Channel;Account;Currency;Amount;Reference;Name;Street;Number;Postcode;City;Country;DebtorAddressLine;DebtorAddressLine;DebtorAccount;ReferenceType;UltimateDebtorName;UltimateDebtorStreet;UltimateDebtorNumber;UltimateDebtorPostcode;UltimateDebtorTownName;UltimateDebtorCountry;UltimateDebtorAddressLine;UltimateDebtorAddressLine;RemittanceInformationText
+            
QRR;PO;CH9789144829733648596;CHF;1;;D009;Musterstrasse;1;1111;Musterstadt;CH;;;;NON;D009;Musterstrasse;1;1111;Musterstadt;CH;;;Taler-Demo
+        """.trimIndent()
+
+        runBlocking {
+            try {
+                doEbicsUpload(
+                    HttpClient(),
+                    cfg,
+                    loadPrivateKeysFromDisk(cfg.clientPrivateKeysFilename)!!,
+                    loadBankKeys(cfg.bankPublicKeysFilename)!!,
+                    orderService,
+                    instruction.toByteArray(Charsets.UTF_8)
+                )
+            }
+            catch (e: EbicsUploadException) {
+                logger.error(e.message)
+                logger.error("bank EC: ${e.bankErrorCode}, EBICS EC: 
${e.ebicsErrorCode}")
+            }
+        }
+    }
+
+    @Test // asks a pain.002
+    fun getAck() {
+        val pain002 = download(prepAckRequest())
+        println(pain002)
+    }
+
+    @Test
+    fun getStatement() {
+        val inflatedBytes = download(prepStatementRequest())
+        inflatedBytes?.unzipForEach { name, content ->
+            println(name)
+            println(content)
+        }
+    }
+
+    @Test
+    fun getReport() {
+        println(download(prepReportRequest()))
+    }
+
+    fun download(req: Ebics3Request.OrderDetails.BTOrderParams): ByteArray? {
+        val cfg = prep()
+        val bankKeys = loadBankKeys(cfg.bankPublicKeysFilename)!!
+        val myKeys = loadPrivateKeysFromDisk(cfg.clientPrivateKeysFilename)!!
+        val initXml = createEbics3DownloadInitialization(
+            cfg,
+            bankKeys,
+            myKeys,
+            orderParams = req
+        )
+        return runBlocking {
+            doEbicsDownload(
+                HttpClient(),
+                cfg,
+                myKeys,
+                bankKeys,
+                initXml,
+                isEbics3 = true,
+                tolerateEmptyResult = true
+            )
+        }
+    }
+
     @Test
     fun sendPayment() {
         val cfg = prep()
@@ -36,7 +113,6 @@ class Iso20022 {
             
parsePayto("payto://iban/CH9300762011623852957?receiver-name=NotGiven")!!
         )
         runBlocking {
-
             // Not asserting, as it throws in case of errors.
             submitPain001(
                 xml,
@@ -49,7 +125,6 @@ class Iso20022 {
     }
 }
 
-@Ignore
 class PostFinance {
     // Tests sending client keys to the PostFinance test platform.
     @Test
@@ -77,25 +152,25 @@ class PostFinance {
                 keys,
                 HttpClient(),
                 KeysOrderType.HPB
-            )
-            )
+            ))
         }
     }
 
+    // Arbitrary download request for manual tests.
     @Test
     fun customDownload() {
         val cfg = prep()
         val clientKeys = loadPrivateKeysFromDisk(cfg.clientPrivateKeysFilename)
         val bankKeys = loadBankKeys(cfg.bankPublicKeysFilename)
         runBlocking {
-            val xml = doEbicsCustomDownload(
+            val bytes = doEbicsCustomDownload(
                 messageType = "HTD",
                 cfg = cfg,
                 bankKeys = bankKeys!!,
                 clientKeys = clientKeys!!,
                 client = HttpClient()
             )
-            println(xml)
+            println(bytes.toString())
         }
     }
 

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]