gnunet-svn
[Top][All Lists]
Advanced

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

[libeufin] branch master updated: nexus fetch


From: gnunet
Subject: [libeufin] branch master updated: nexus fetch
Date: Sat, 11 Nov 2023 11:32:49 +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 a335eb9c nexus fetch
a335eb9c is described below

commit a335eb9c62046978bb4c442ffbc801e598f603b9
Author: MS <ms@taler.net>
AuthorDate: Sat Nov 11 11:31:21 2023 +0100

    nexus fetch
    
    wiring the EBICS 2 support into the logic, not
    triggering it yet.
---
 .../main/kotlin/tech/libeufin/nexus/EbicsFetch.kt  | 361 +++++----------------
 .../kotlin/tech/libeufin/nexus/ebics/Ebics2.kt     | 120 ++++++-
 .../kotlin/tech/libeufin/nexus/ebics/Ebics3.kt     | 191 ++++++++++-
 .../tech/libeufin/nexus/ebics/EbicsCommon.kt       |  40 +++
 nexus/src/test/kotlin/PostFinance.kt               |   8 +-
 util/src/main/kotlin/Ebics.kt                      |  13 +-
 util/src/main/kotlin/ebics_h005/Ebics3Request.kt   |   2 -
 7 files changed, 437 insertions(+), 298 deletions(-)

diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
index b627ec17..46a23045 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
@@ -7,9 +7,8 @@ import io.ktor.client.*
 import kotlinx.coroutines.runBlocking
 import org.apache.commons.compress.archivers.zip.ZipFile
 import org.apache.commons.compress.utils.SeekableInMemoryByteChannel
-import tech.libeufin.nexus.ebics.EbicsSideException
-import tech.libeufin.nexus.ebics.createEbics3DownloadInitialization
-import tech.libeufin.nexus.ebics.doEbicsDownload
+import tech.libeufin.nexus.ebics.*
+import tech.libeufin.util.EbicsOrderParams
 import tech.libeufin.util.ebics_h005.Ebics3Request
 import tech.libeufin.util.getXmlDate
 import tech.libeufin.util.toDbMicros
@@ -20,224 +19,85 @@ import java.time.LocalDate
 import java.time.ZoneId
 import kotlin.concurrent.fixedRateTimer
 import kotlin.io.path.createDirectories
+import kotlin.reflect.typeOf
 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.
- *
- * @param startDate inclusive starting date for the returned banking events.
- * @param endDate inclusive ending date for the returned banking events.
- * @return [Ebics3Request.DateRange]
+ * Necessary data to perform a download.
  */
-fun getEbics3DateRange(
-    startDate: Instant,
-    endDate: Instant
-): Ebics3Request.DateRange {
-    return Ebics3Request.DateRange().apply {
-        start = getXmlDate(startDate)
-        end = getXmlDate(endDate)
-    }
-}
-
-/**
- * Prepares the request for a camt.054 notification from the bank.
- * Notifications inform the subscriber that some new events occurred
- * on their account.  One main difference with reports/statements is
- * that notifications - according to the ISO20022 documentation - do
- * NOT contain any balance.
- *
- * @param startDate inclusive starting date for the returned notification(s).
- * @param endDate inclusive ending date for the returned notification(s).  
NOTE:
- *        if startDate is NOT null and endDate IS null, endDate gets defaulted
- *        to the current UTC time.
- * @param isAppendix if true, the responded camt.054 will be an appendix of
- *        another camt.053 document, not therefore strictly acting as a 
notification.
- *        For example, camt.053 may omit wire transfer subjects and its related
- *        camt.054 appendix would instead contain those.
- *
- * @return [Ebics3Request.OrderDetails.BTOrderParams]
- */
-fun prepNotificationRequest(
-    startDate: Instant? = null,
-    endDate: Instant? = null,
-    isAppendix: Boolean
-): Ebics3Request.OrderDetails.BTOrderParams {
-    val service = Ebics3Request.OrderDetails.Service().apply {
-        serviceName = "REP"
-        scope = "CH"
-        container = Ebics3Request.OrderDetails.Service.Container().apply {
-            containerType = "ZIP"
-        }
-        messageName = Ebics3Request.OrderDetails.Service.MessageName().apply {
-            value = "camt.054"
-            version = "08"
-        }
-        if (!isAppendix)
-            serviceOption = "XDCI"
-    }
-    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 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 = "10"
-        }
-    }
-    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.
- *
- * @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 prepReportRequest(
-    startDate: Instant? = null,
-    endDate: Instant? = null
-): Ebics3Request.OrderDetails.BTOrderParams {
-    val service = Ebics3Request.OrderDetails.Service().apply {
-        serviceName = "STM"
-        scope = "CH"
-        container = Ebics3Request.OrderDetails.Service.Container().apply {
-            containerType = "ZIP"
-        }
-        messageName = Ebics3Request.OrderDetails.Service.MessageName().apply {
-            value = "camt.052"
-            version = "08"
-        }
-    }
-    return Ebics3Request.OrderDetails.BTOrderParams().apply {
-            this.service = service
-            this.dateRange = if (startDate != null)
-                getEbics3DateRange(startDate, endDate ?: Instant.now())
-            else null
-    }
-}
+data class FetchContext(
+    /**
+     * Config handle.
+     */
+    val cfg: EbicsSetupConfig,
+    /**
+     * HTTP client handle to reach the bank
+     */
+    val httpClient: HttpClient,
+    /**
+     * EBICS subscriber private keys.
+     */
+    val clientKeys: ClientPrivateKeysFile,
+    /**
+     * Bank public keys.
+     */
+    val bankKeys: BankPublicKeysFile,
+    /**
+     * Type of document to download.
+     */
+    val whichDocument: SupportedDocument,
+    /**
+     * EBICS version.
+     */
+    val ebicsVersion: EbicsVersion = EbicsVersion.three,
+    /**
+     * Start date of the returned documents.  Only
+     * used in --transient mode.
+     */
+    var pinnedStart: Instant? = null
+)
 
 /**
  * Downloads content via EBICS, according to the order params passed
  * by the caller.
  *
- * @param cfg configuration handle.
- * @param bankKeys bank public keys.
- * @param clientKeys EBICS subscriber private keys.
- * @param httpClient handle to the HTTP layer.
+ * @param T [Ebics2Request] for EBICS 2 or 
[Ebics3Request.OrderDetails.BTOrderParams] for EBICS 3
+ * @param ctx [FetchContext]
  * @param req contains the instructions for the download, namely
  *            which document is going to be downloaded from the bank.
  * @return the [ByteArray] payload.  On an empty response, the array
  *         length is zero.  It returns null, if the bank assigned an
  *         error to the EBICS transaction.
  */
-private suspend fun downloadHelper(
-    cfg: EbicsSetupConfig,
-    bankKeys: BankPublicKeysFile,
-    clientKeys: ClientPrivateKeysFile,
-    httpClient: HttpClient,
-    req: Ebics3Request.OrderDetails.BTOrderParams
+private suspend inline fun downloadHelper(
+    ctx: FetchContext,
+    lastExecutionTime: Instant? = null
 ): ByteArray? {
-    val initXml = createEbics3DownloadInitialization(
-        cfg,
-        bankKeys,
-        clientKeys,
-        orderParams = req
-    )
+    val initXml = if (ctx.ebicsVersion == EbicsVersion.three) {
+        createEbics3DownloadInitialization(
+            ctx.cfg,
+            ctx.bankKeys,
+            ctx.clientKeys,
+            prepEbics3Document(ctx.whichDocument, lastExecutionTime)
+        )
+    } else {
+        val ebics2Req = prepEbics2Document(ctx.whichDocument, 
lastExecutionTime)
+        createEbics25DownloadInit(
+            ctx.cfg,
+            ctx.clientKeys,
+            ctx.bankKeys,
+            ebics2Req.messageType,
+            ebics2Req.orderParams
+        )
+    }
     try {
         return doEbicsDownload(
-            httpClient,
-            cfg,
-            clientKeys,
-            bankKeys,
+            ctx.httpClient,
+            ctx.cfg,
+            ctx.clientKeys,
+            ctx.bankKeys,
             initXml,
-            isEbics3 = true,
+            isEbics3 = ctx.ebicsVersion == EbicsVersion.three,
             tolerateEmptyResult = true
         )
     } catch (e: EbicsSideException) {
@@ -298,65 +158,25 @@ fun maybeLogFile(cfg: EbicsSetupConfig, content: 
ByteArray) {
  * What this function does NOT do (now): linking documents between
  * different camt.05x formats and/or pain.002 acknowledgements.
  *
- * @param cfg config handle.
  * @param db database connection
- * @param httpClient HTTP client handle to reach the bank
- * @param clientKeys EBICS subscriber private keys.
- * @param bankKeys bank public keys.
+ * @param ctx [FetchContext]
  * @param pinnedStart explicit start date for the downloaded documents.
  *        This parameter makes the last incoming transaction timestamp in
  *        the database IGNORED.  Only useful when running in --transient
  *        mode to download past documents / debug.
  */
 private suspend fun fetchDocuments(
-    cfg: EbicsSetupConfig,
     db: Database,
-    httpClient: HttpClient,
-    clientKeys: ClientPrivateKeysFile,
-    bankKeys: BankPublicKeysFile,
-    whichDocument: SupportedDocument = SupportedDocument.CAMT_054,
-    pinnedStart: Instant? = null
+    ctx: FetchContext
 ) {
     // maybe get last execution_date.
-    val lastExecutionTime: Instant? = pinnedStart ?: 
db.incomingPaymentLastExecTime()
+    val lastExecutionTime: Instant? = ctx.pinnedStart ?: 
db.incomingPaymentLastExecTime()
     logger.debug("Fetching documents from timestamp: $lastExecutionTime")
-    val req = when(whichDocument) {
-        SupportedDocument.PAIN_002 -> prepAckRequest(lastExecutionTime)
-        SupportedDocument.CAMT_052 -> prepReportRequest(lastExecutionTime)
-        SupportedDocument.CAMT_053 -> prepStatementRequest(lastExecutionTime)
-        SupportedDocument.CAMT_054 -> 
prepNotificationRequest(lastExecutionTime, isAppendix = true)
-    }
-    val maybeContent = downloadHelper(
-        cfg,
-        bankKeys,
-        clientKeys,
-        httpClient,
-        req
-    ) ?: exitProcess(1) // client is wrong, failing.
-
+    val maybeContent = downloadHelper(ctx, lastExecutionTime) ?: 
exitProcess(1) // client is wrong, failing.
     if (maybeContent.isEmpty()) return
-    maybeLogFile(cfg, maybeContent)
+    maybeLogFile(ctx.cfg, maybeContent)
 }
 
-/**
- * Turns a YYYY-MM-DD date string into Instant.  Used
- * to parse the --pinned-start CLI options.  Fails the
- * process, if the input is invalid.
- *
- * @param dashedDate pinned start command line option.
- * @return [Instant]
- */
-fun parseDashedDate(dashedDate: String): Instant =
-    doOrFail {
-        LocalDate.parse(dashedDate).atStartOfDay(ZoneId.of("UTC")).toInstant()
-    }
-
-enum class SupportedDocument {
-    PAIN_002,
-    CAMT_053,
-    CAMT_052,
-    CAMT_054
-}
 class EbicsFetch: CliktCommand("Fetches bank records.  Defaults to camt.054 
notifications") {
     private val configFile by option(
         "--config", "-c",
@@ -411,30 +231,33 @@ class EbicsFetch: CliktCommand("Fetches bank records.  
Defaults to camt.054 noti
             logger.error("Client private keys not found at: 
${cfg.clientPrivateKeysFilename}")
             exitProcess(1)
         }
-        val httpClient = HttpClient()
-
+        // Deciding what to download.
         var whichDoc = SupportedDocument.CAMT_054
         if (onlyAck) whichDoc = SupportedDocument.PAIN_002
         if (onlyReports) whichDoc = SupportedDocument.CAMT_052
         if (onlyStatements) whichDoc = SupportedDocument.CAMT_053
 
+        val ctx = FetchContext(
+            cfg,
+            HttpClient(),
+            clientKeys,
+            bankKeys,
+            whichDoc
+        )
+
         if (transient) {
             logger.info("Transient mode: fetching once and returning.")
             val pinnedStartVal = pinnedStart
             val pinnedStartArg = if (pinnedStartVal != null) {
                 logger.debug("Pinning start date to: $pinnedStartVal")
-                parseDashedDate(pinnedStartVal)
+                doOrFail {
+                    // Converting YYYY-MM-DD to Instant.
+                    
LocalDate.parse(pinnedStartVal).atStartOfDay(ZoneId.of("UTC")).toInstant()
+                }
             } else null
+            ctx.pinnedStart = pinnedStartArg
             runBlocking {
-                fetchDocuments(
-                    cfg,
-                    db,
-                    httpClient,
-                    clientKeys,
-                    bankKeys,
-                    whichDoc,
-                    pinnedStartArg
-                )
+                fetchDocuments(db, ctx)
             }
             return
         }
@@ -447,14 +270,7 @@ class EbicsFetch: CliktCommand("Fetches bank records.  
Defaults to camt.054 noti
         if (frequency.inSeconds == 0) {
             logger.warn("Long-polling not implemented, running therefore in 
transient mode")
             runBlocking {
-                fetchDocuments(
-                    cfg,
-                    db,
-                    httpClient,
-                    clientKeys,
-                    bankKeys,
-                    whichDoc
-                )
+                fetchDocuments(db, ctx)
             }
             return
         }
@@ -463,16 +279,9 @@ class EbicsFetch: CliktCommand("Fetches bank records.  
Defaults to camt.054 noti
             period = (frequency.inSeconds * 1000).toLong(),
             action = {
                 runBlocking {
-                    fetchDocuments(
-                        cfg,
-                        db,
-                        httpClient,
-                        clientKeys,
-                        bankKeys,
-                        whichDoc
-                    )
+                    fetchDocuments(db, ctx)
                 }
             }
         )
     }
-}
\ No newline at end of file
+}
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 34cf5c5c..91ec9287 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt
@@ -30,7 +30,9 @@ import tech.libeufin.nexus.ClientPrivateKeysFile
 import tech.libeufin.nexus.EbicsSetupConfig
 import tech.libeufin.util.*
 import tech.libeufin.util.ebics_h004.*
+import tech.libeufin.util.ebics_h005.Ebics3Request
 import java.security.interfaces.RSAPrivateCrtKey
+import java.time.Instant
 import java.time.ZoneId
 import java.util.*
 import javax.xml.datatype.DatatypeFactory
@@ -161,7 +163,7 @@ fun createEbics25DownloadReceiptPhase(
  *
  * @param cfg configuration handle.
  * @param clientKeys user EBICS private keys.
- * @param segNumber which segment we ask to the bank.
+ * @param segNumber which segment we ask the bank.
  * @param totalSegments how many segments compose the whole EBICS transaction.
  * @param transactionId ID of the EBICS transaction that transports all the 
segments.
  * @return raw XML string of the request.
@@ -286,4 +288,118 @@ fun generateHpbMessage(cfg: EbicsSetupConfig, clientKeys: 
ClientPrivateKeysFile)
     val doc = XMLUtil.convertJaxbToDocument(hpbRequest)
     XMLUtil.signEbicsDocument(doc, clientKeys.authentication_private_key)
     return XMLUtil.convertDomToString(doc)
-}
\ No newline at end of file
+}
+
+/**
+ * Collects message type and date range of an EBICS 2 request.
+ */
+data class Ebics2Request(
+    val messageType: String,
+    val orderParams: EbicsOrderParams
+)
+
+/**
+ * Prepares an EBICS 2 request to get pain.002 acknowledgements
+ * about submitted pain.001 documents.
+ *
+ * @param startDate earliest timestamp of the returned document(s).  If
+ *        null, it defaults to download the unseen documents.
+ * @param endDate latest timestamp of the returned document(s).  If
+ *        null, it defaults to the current time.
+ * @return [Ebics2Request] object to be first converted in XML and
+ *         then be passed to the EBICS downloader.
+ */
+private fun prepAckRequest2(
+    startDate: Instant? = null,
+    endDate: Instant? = null
+): Ebics2Request {
+    val maybeDateRange = if (startDate != null) EbicsDateRange(startDate, 
endDate ?: Instant.now()) else null
+    return Ebics2Request(
+        messageType = "Z01",
+        orderParams = EbicsStandardOrderParams(dateRange = maybeDateRange)
+    )
+}
+
+/**
+ * Prepares an EBICS 2 request to get intraday camt.052 reports.
+ *
+ * @param startDate earliest timestamp of the returned document(s).  If
+ *        null, it defaults to download the unseen documents.
+ * @param endDate latest timestamp of the returned document(s).  If
+ *        null, it defaults to the current time.
+ * @return [Ebics2Request] object to be first converted in XML and
+ *         then be passed to the EBICS downloader.
+ */
+private fun prepReportRequest2(
+    startDate: Instant? = null,
+    endDate: Instant? = null
+): Ebics2Request {
+    val maybeDateRange = if (startDate != null) EbicsDateRange(startDate, 
endDate ?: Instant.now()) else null
+    return Ebics2Request(
+        messageType = "Z52",
+        orderParams = EbicsStandardOrderParams(dateRange = maybeDateRange)
+    )
+}
+
+/**
+ * Prepares an EBICS 2 request to get daily camt.053 statements.
+ *
+ * @param startDate earliest timestamp of the returned document(s).  If
+ *        null, it defaults to download the unseen documents.
+ * @param endDate latest timestamp of the returned document(s).  If
+ *        null, it defaults to the current time.
+ * @return [Ebics2Request] object to be first converted in XML and
+ *         then be passed to the EBICS downloader.
+ */
+private fun prepStatementRequest2(
+    startDate: Instant? = null,
+    endDate: Instant? = null
+): Ebics2Request {
+    val maybeDateRange = if (startDate != null) EbicsDateRange(startDate, 
endDate ?: Instant.now()) else null
+    return Ebics2Request(
+        messageType = "Z53",
+        orderParams = EbicsStandardOrderParams(dateRange = maybeDateRange)
+    )
+}
+
+/**
+ * Prepares an EBICS 2 request to get camt.054 notifications.
+ *
+ * @param startDate earliest timestamp of the returned document(s).  If
+ *        null, it defaults to download the unseen documents.
+ * @param endDate latest timestamp of the returned document(s).  If
+ *        null, it defaults to the current time.
+ * @return [Ebics2Request] object to be first converted in XML and
+ *         then be passed to the EBICS downloader.
+ */
+private fun prepNotificationRequest2(
+    startDate: Instant? = null,
+    endDate: Instant? = null
+): Ebics2Request {
+    val maybeDateRange = if (startDate != null) EbicsDateRange(startDate, 
endDate ?: Instant.now()) else null
+    return Ebics2Request(
+        messageType = "Z54", // ZS2 is the non-appendix type
+        orderParams = EbicsStandardOrderParams(dateRange = maybeDateRange)
+    )
+}
+
+/**
+ * Abstracts EBICS 2 request creation of a download init phase.
+ *
+ * @param whichDoc type of wanted document.
+ * @param startDate earliest timestamp of the document(s) to download.
+ *                  If null, it gets the unseen documents.  If defined,
+ *                  the latest timestamp defaults to the current time.
+ * @return [Ebics2Request] to be converted to XML string and passed to
+ *         the EBICS downloader.
+ */
+fun prepEbics2Document(
+    whichDoc: SupportedDocument,
+    startDate: Instant? = null
+): Ebics2Request =
+    when(whichDoc) {
+        SupportedDocument.PAIN_002 -> prepAckRequest2(startDate)
+        SupportedDocument.CAMT_052 -> prepReportRequest2(startDate)
+        SupportedDocument.CAMT_053 -> prepStatementRequest2(startDate)
+        SupportedDocument.CAMT_054 -> prepNotificationRequest2(startDate)
+    }
\ No newline at end of file
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 995483a3..8d56a5ed 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt
@@ -9,13 +9,15 @@ import tech.libeufin.util.PreparedUploadData
 import tech.libeufin.util.XMLUtil
 import tech.libeufin.util.ebics_h005.Ebics3Request
 import tech.libeufin.util.getNonce
+import tech.libeufin.util.getXmlDate
 import java.math.BigInteger
+import java.time.Instant
 import java.util.*
 import javax.xml.datatype.DatatypeFactory
 
 /**
- * Crafts an EBICS request for the receipt phase of a
- * download transaction.
+ * Crafts an EBICS request for the receipt phase of a download
+ * transaction.
  *
  * @param cfg config handle
  * @param clientKeys subscriber private keys.
@@ -232,4 +234,187 @@ suspend fun submitPain001(
             " EBICS technical code is: ${maybeUploaded.technicalReturnCode}," +
             " bank technical return code is: ${maybeUploaded.bankReturnCode}"
     )
-}
\ No newline at end of file
+}
+
+/**
+ * Crafts a date range object, when the caller needs a time range.
+ *
+ * @param startDate inclusive starting date for the returned banking events.
+ * @param endDate inclusive ending date for the returned banking events.
+ * @return [Ebics3Request.DateRange]
+ */
+private fun getEbics3DateRange(
+    startDate: Instant,
+    endDate: Instant
+): Ebics3Request.DateRange {
+    return Ebics3Request.DateRange().apply {
+        start = getXmlDate(startDate)
+        end = getXmlDate(endDate)
+    }
+}
+
+/**
+ * Prepares the request for a camt.054 notification from the bank,
+ * via EBICS 3.
+ * Notifications inform the subscriber that some new events occurred
+ * on their account.  One main difference with reports/statements is
+ * that notifications - according to the ISO20022 documentation - do
+ * NOT contain any balance.
+ *
+ * @param startDate inclusive starting date for the returned notification(s).
+ * @param endDate inclusive ending date for the returned notification(s).  
NOTE:
+ *        if startDate is NOT null and endDate IS null, endDate gets defaulted
+ *        to the current UTC time.
+ * @param isAppendix if true, the responded camt.054 will be an appendix of
+ *        another camt.053 document, not therefore strictly acting as a 
notification.
+ *        For example, camt.053 may omit wire transfer subjects and its related
+ *        camt.054 appendix would instead contain those.
+ *
+ * @return [Ebics3Request.OrderDetails.BTOrderParams]
+ */
+private fun prepNotificationRequest3(
+    startDate: Instant? = null,
+    endDate: Instant? = null,
+    isAppendix: Boolean
+): Ebics3Request.OrderDetails.BTOrderParams {
+    val service = Ebics3Request.OrderDetails.Service().apply {
+        serviceName = "REP"
+        scope = "CH"
+        container = Ebics3Request.OrderDetails.Service.Container().apply {
+            containerType = "ZIP"
+        }
+        messageName = Ebics3Request.OrderDetails.Service.MessageName().apply {
+            value = "camt.054"
+            version = "08"
+        }
+        if (!isAppendix)
+            serviceOption = "XDCI"
+    }
+    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 pain.002 acknowledgement from the bank, via
+ * EBICS 3.
+ *
+ * @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]
+ */
+private fun prepAckRequest3(
+    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 = "10"
+        }
+    }
+    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) via EBICS 3.
+ *
+ * @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]
+ */
+private fun prepStatementRequest3(
+    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 via EBICS 3.
+ *
+ * @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]
+ */
+private fun prepReportRequest3(
+    startDate: Instant? = null,
+    endDate: Instant? = null
+): Ebics3Request.OrderDetails.BTOrderParams {
+    val service = Ebics3Request.OrderDetails.Service().apply {
+        serviceName = "STM"
+        scope = "CH"
+        container = Ebics3Request.OrderDetails.Service.Container().apply {
+            containerType = "ZIP"
+        }
+        messageName = Ebics3Request.OrderDetails.Service.MessageName().apply {
+            value = "camt.052"
+            version = "08"
+        }
+    }
+    return Ebics3Request.OrderDetails.BTOrderParams().apply {
+        this.service = service
+        this.dateRange = if (startDate != null)
+            getEbics3DateRange(startDate, endDate ?: Instant.now())
+        else null
+    }
+}
+
+/**
+ * Abstracts EBICS 3 request creation of a download init phase.
+ *
+ * @param whichDoc type of wanted document.
+ * @param startDate earliest timestamp of the document(s) to download.
+ *                  If null, it gets the unseen documents.  If defined,
+ *                  the latest timestamp defaults to the current time.
+ * @return [Ebics2Request] to be converted to XML string and passed to
+ *         the EBICS downloader.
+ */
+fun prepEbics3Document(
+    whichDoc: SupportedDocument,
+    startDate: Instant? = null
+): Ebics3Request.OrderDetails.BTOrderParams =
+    when(whichDoc) {
+        SupportedDocument.PAIN_002 -> prepAckRequest3(startDate)
+        SupportedDocument.CAMT_052 -> prepReportRequest3(startDate)
+        SupportedDocument.CAMT_053 -> prepStatementRequest3(startDate)
+        SupportedDocument.CAMT_054 -> prepReportRequest3(startDate)
+    }
\ No newline at end of file
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 0d788e62..c40d0099 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt
@@ -42,17 +42,57 @@ import io.ktor.client.plugins.*
 import io.ktor.client.request.*
 import io.ktor.client.statement.*
 import io.ktor.http.*
+import org.apache.commons.compress.archivers.zip.ZipFile
+import org.apache.commons.compress.utils.SeekableInMemoryByteChannel
 import tech.libeufin.nexus.*
 import tech.libeufin.util.*
 import tech.libeufin.util.ebics_h005.Ebics3Request
 import tech.libeufin.util.logger
 import java.io.ByteArrayOutputStream
 import java.security.interfaces.RSAPrivateCrtKey
+import java.time.Instant
 import java.time.LocalDateTime
 import java.time.format.DateTimeFormatter
 import java.util.*
 import java.util.zip.DeflaterInputStream
 
+/**
+ * Available EBICS versions.
+ */
+enum class EbicsVersion { two, three }
+
+/**
+ * Which documents can be downloaded via EBICS.
+ */
+enum class SupportedDocument {
+    PAIN_002,
+    CAMT_053,
+    CAMT_052,
+    CAMT_054
+}
+
+
+/**
+ * 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()) {
+        tech.libeufin.nexus.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()
+}
+
 /**
  * Decrypts and decompresses the business payload that was
  * transported within an EBICS message from the bank
diff --git a/nexus/src/test/kotlin/PostFinance.kt 
b/nexus/src/test/kotlin/PostFinance.kt
index 80c76362..c2ecdd55 100644
--- a/nexus/src/test/kotlin/PostFinance.kt
+++ b/nexus/src/test/kotlin/PostFinance.kt
@@ -29,7 +29,7 @@ class Iso20022 {
 
     @Test // asks a pain.002, links with pain.001's MsgId
     fun getAck() {
-        download(prepAckRequest(startDate = yesterday)
+        download(prepAckRequest3(startDate = yesterday)
         )?.unzipForEach { name, content ->
             println(name)
             println(content)
@@ -42,7 +42,7 @@ class Iso20022 {
      */
     @Test
     fun getStatement() {
-        val inflatedBytes = download(prepStatementRequest())
+        val inflatedBytes = download(prepStatementRequest3())
         inflatedBytes?.unzipForEach { name, content ->
             println(name)
             println(content)
@@ -52,7 +52,7 @@ class Iso20022 {
     @Test
     fun getNotification() {
         val inflatedBytes = download(
-            prepNotificationRequest(
+            prepNotificationRequest3(
                 // startDate = yesterday,
                 isAppendix = true
             )
@@ -68,7 +68,7 @@ class Iso20022 {
      */
     @Test
     fun getReport() {
-        download(prepReportRequest(yesterday))?.unzipForEach { name, content ->
+        download(prepReportRequest3(yesterday))?.unzipForEach { name, content 
->
             println(name)
             println(content)
         }
diff --git a/util/src/main/kotlin/Ebics.kt b/util/src/main/kotlin/Ebics.kt
index 558eadba..4236eded 100644
--- a/util/src/main/kotlin/Ebics.kt
+++ b/util/src/main/kotlin/Ebics.kt
@@ -59,8 +59,8 @@ data class EbicsProtocolError(
 ) : Exception(reason)
 
 data class EbicsDateRange(
-    val start: ZonedDateTime,
-    val end: ZonedDateTime
+    val start: Instant,
+    val end: Instant
 )
 
 sealed class EbicsOrderParams
@@ -152,15 +152,6 @@ fun makeOrderParams(orderParams: EbicsOrderParams): 
EbicsRequest.OrderParams {
     }
 }
 
-fun makeEbics3DateRange(ebicsDateRange: EbicsDateRange?): 
Ebics3Request.DateRange? {
-    return if (ebicsDateRange != null)
-        return Ebics3Request.DateRange().apply {
-            this.start = getXmlDate(ebicsDateRange.start)
-            this.end = getXmlDate(ebicsDateRange.end)
-        }
-    else null
-}
-
 fun signOrder(
     orderBlob: ByteArray,
     signKey: RSAPrivateCrtKey,
diff --git a/util/src/main/kotlin/ebics_h005/Ebics3Request.kt 
b/util/src/main/kotlin/ebics_h005/Ebics3Request.kt
index b1bd7eb3..5931c17e 100644
--- a/util/src/main/kotlin/ebics_h005/Ebics3Request.kt
+++ b/util/src/main/kotlin/ebics_h005/Ebics3Request.kt
@@ -2,8 +2,6 @@ package tech.libeufin.util.ebics_h005
 
 import org.apache.xml.security.binding.xmldsig.SignatureType
 import tech.libeufin.util.CryptoUtil
-import tech.libeufin.util.EbicsStandardOrderParams
-import tech.libeufin.util.makeEbics3DateRange
 import java.math.BigInteger
 import java.security.interfaces.RSAPublicKey
 import java.util.*

-- 
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]