[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.
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [libeufin] branch master updated: nexus fetch: crafting camt.053 & pain.002 requests,
gnunet <=