[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.
- [libeufin] branch master updated: nexus fetch, gnunet, 2023/11/09
- [libeufin] branch master updated: nexus fetch, gnunet, 2023/11/09
- [libeufin] branch master updated: nexus fetch, gnunet, 2023/11/10
- [libeufin] branch master updated: nexus fetch,
gnunet <=
- [libeufin] branch master updated: nexus fetch, gnunet, 2023/11/15
- [libeufin] branch master updated: nexus fetch, gnunet, 2023/11/15
- [libeufin] branch master updated: nexus fetch, gnunet, 2023/11/20
- [libeufin] branch master updated: nexus fetch, gnunet, 2023/11/21