libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit de7e5a9726a29a39b801cf0a2b1e708c36f7f2aa
parent 1e3e092725d11d8c1e1f76a6fec421606884dc72
Author: MS <ms@taler.net>
Date:   Tue, 14 Nov 2023 09:25:26 +0100

nexus fetch: implementing HAC.

Diffstat:
Mnexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt | 42+++++++++++++++++++++++++++++++++---------
Mnexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt | 23+++++++++++++++++++++++
Mnexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt | 9+++++----
Mnexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt | 18++++++++++++++++++
4 files changed, 79 insertions(+), 13 deletions(-)

diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt @@ -47,9 +47,9 @@ data class FetchContext( */ val whichDocument: SupportedDocument, /** - * EBICS version. + * EBICS version. For the HAC message type, version gets switched to EBICS 2. */ - val ebicsVersion: EbicsVersion = EbicsVersion.three, + var ebicsVersion: EbicsVersion = EbicsVersion.three, /** * Start date of the returned documents. Only * used in --transient mode. @@ -118,8 +118,13 @@ private suspend inline fun downloadHelper( * * @param cfg config handle. * @param content ZIP bytes from the server. + * @param nonZip only true when downloading via HAC (EBICS 2) */ -fun maybeLogFile(cfg: EbicsSetupConfig, content: ByteArray) { +fun maybeLogFile( + cfg: EbicsSetupConfig, + content: ByteArray, + nonZip: Boolean = false +) { // Main dir. val maybeLogDir = cfg.config.lookupString( "nexus-fetch", @@ -133,15 +138,23 @@ fun maybeLogFile(cfg: EbicsSetupConfig, content: ByteArray) { // Creating the combined dir. val dirs = Path.of(maybeLogDir, subDir) doOrFail { dirs.createDirectories() } - // Write each ZIP entry in the combined dir. - content.unzipForEach { fileName, xmlContent -> - val f = File(dirs.toString(), "${now.toDbMicros()}_$fileName") - // Rare: cannot download the same file twice in the same microsecond. + fun maybeWrite(f: File, xml: String) { if (f.exists()) { logger.error("Log file exists already at: ${f.path}") exitProcess(1) } - doOrFail { f.writeText(xmlContent) } + doOrFail { f.writeText(xml) } + } + if (nonZip) { + val f = File(dirs.toString(), "${now.toDbMicros()}_HAC_response.pain.002.xml") + maybeWrite(f, content.toString(Charsets.UTF_8)) + return + } + // Write each ZIP entry in the combined dir. + content.unzipForEach { fileName, xmlContent -> + val f = File(dirs.toString(), "${now.toDbMicros()}_$fileName") + // Rare: cannot download the same file twice in the same microsecond. + maybeWrite(f, xmlContent) } } @@ -174,7 +187,11 @@ private suspend fun fetchDocuments( logger.debug("Fetching documents from timestamp: $lastExecutionTime") val maybeContent = downloadHelper(ctx, lastExecutionTime) ?: exitProcess(1) // client is wrong, failing. if (maybeContent.isEmpty()) return - maybeLogFile(ctx.cfg, maybeContent) + maybeLogFile( + ctx.cfg, + maybeContent, + nonZip = ctx.whichDocument == SupportedDocument.PAIN_002_LOGS + ) } class EbicsFetch: CliktCommand("Fetches bank records. Defaults to camt.054 notifications") { @@ -200,6 +217,10 @@ class EbicsFetch: CliktCommand("Fetches bank records. Defaults to camt.054 noti help = "Downloads only camt.052 intraday reports" ).flag(default = false) + private val onlyLogs by option( + help = "Downloads only EBICS activity logs via pain.002, only available to --transient mode. Config needs log directory" + ).flag(default = false) + private val pinnedStart by option( help = "constant YYYY-MM-DD date for the earliest document to download " + "(only consumed in --transient mode). The latest document is always" + @@ -236,6 +257,7 @@ class EbicsFetch: CliktCommand("Fetches bank records. Defaults to camt.054 noti if (onlyAck) whichDoc = SupportedDocument.PAIN_002 if (onlyReports) whichDoc = SupportedDocument.CAMT_052 if (onlyStatements) whichDoc = SupportedDocument.CAMT_053 + if (onlyLogs) whichDoc = SupportedDocument.PAIN_002_LOGS val ctx = FetchContext( cfg, @@ -256,6 +278,8 @@ class EbicsFetch: CliktCommand("Fetches bank records. Defaults to camt.054 noti } } else null ctx.pinnedStart = pinnedStartArg + if (whichDoc == SupportedDocument.PAIN_002_LOGS) + ctx.ebicsVersion = EbicsVersion.two runBlocking { fetchDocuments(db, ctx) } diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt @@ -384,6 +384,28 @@ private fun prepNotificationRequest2( } /** + * Prepares an EBICS 2 request to get logs from the bank about any + * uploaded or downloaded document. + * + * @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 prepLogsRequest2( + startDate: Instant? = null, + endDate: Instant? = null +): Ebics2Request { + val maybeDateRange = if (startDate != null) EbicsDateRange(startDate, endDate ?: Instant.now()) else null + return Ebics2Request( + messageType = "HAC", + orderParams = EbicsStandardOrderParams(dateRange = maybeDateRange) + ) +} + +/** * Abstracts EBICS 2 request creation of a download init phase. * * @param whichDoc type of wanted document. @@ -402,4 +424,5 @@ fun prepEbics2Document( SupportedDocument.CAMT_052 -> prepReportRequest2(startDate) SupportedDocument.CAMT_053 -> prepStatementRequest2(startDate) SupportedDocument.CAMT_054 -> prepNotificationRequest2(startDate) + SupportedDocument.PAIN_002_LOGS -> prepLogsRequest2(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 @@ -272,7 +272,7 @@ private fun getEbics3DateRange( * * @return [Ebics3Request.OrderDetails.BTOrderParams] */ -private fun prepNotificationRequest3( +fun prepNotificationRequest3( startDate: Instant? = null, endDate: Instant? = null, isAppendix: Boolean @@ -309,7 +309,7 @@ private fun prepNotificationRequest3( * * @return [Ebics3Request.OrderDetails.BTOrderParams] */ -private fun prepAckRequest3( +fun prepAckRequest3( startDate: Instant? = null, endDate: Instant? = null ): Ebics3Request.OrderDetails.BTOrderParams { @@ -342,7 +342,7 @@ private fun prepAckRequest3( * * @return [Ebics3Request.OrderDetails.BTOrderParams] */ -private fun prepStatementRequest3( +fun prepStatementRequest3( startDate: Instant? = null, endDate: Instant? = null ): Ebics3Request.OrderDetails.BTOrderParams { @@ -375,7 +375,7 @@ private fun prepStatementRequest3( * * @return [Ebics3Request.OrderDetails.BTOrderParams] */ -private fun prepReportRequest3( +fun prepReportRequest3( startDate: Instant? = null, endDate: Instant? = null ): Ebics3Request.OrderDetails.BTOrderParams { @@ -417,4 +417,5 @@ fun prepEbics3Document( SupportedDocument.CAMT_052 -> prepReportRequest3(startDate) SupportedDocument.CAMT_053 -> prepStatementRequest3(startDate) SupportedDocument.CAMT_054 -> prepReportRequest3(startDate) + SupportedDocument.PAIN_002_LOGS -> throw Exception("HAC (--only-logs) not available in EBICS 3") } \ 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 @@ -65,9 +65,27 @@ enum class EbicsVersion { two, three } * Which documents can be downloaded via EBICS. */ enum class SupportedDocument { + /** + * Payment acknowledgement. + */ PAIN_002, + /** + * From an HAC request. Informs about any + * download/upload activity, including wrong + * documents. + */ + PAIN_002_LOGS, + /** + * Account statements. + */ CAMT_053, + /** + * Account intraday reports. + */ CAMT_052, + /** + * Account notifications. + */ CAMT_054 }