libeufin

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

commit ce91844da940b21187b13b5db08b1a19b4e536a5
parent 8487b5351280a80d721a46ee8ce30cfd29de7bec
Author: MS <ms@taler.net>
Date:   Wed, 25 Oct 2023 15:26:50 +0200

nexus: drafting pain.001 generation

Diffstat:
Dnexus/src/main/kotlin/tech/libeufin/nexus/Ebics3.kt | 91-------------------------------------------------------------------------------
Anexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt | 149+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rnexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics.kt -> nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt | 0
4 files changed, 243 insertions(+), 91 deletions(-)

diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Ebics3.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Ebics3.kt @@ -1,90 +0,0 @@ -package tech.libeufin.nexus - -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.toHexString -import java.math.BigInteger -import java.util.* -import javax.xml.datatype.DatatypeFactory - -/** - * Creates the EBICS 3 document for the init phase of an upload - * transaction. - * - * @param cfg configuration handle. - * @param preparedUploadData business payload to send. - * @param bankkeys bank public keys. - * @param clientKeys client private keys. - * @param orderService EBICS 3 document defining the request type - * @return raw XML of the EBICS 3 init phase. - */ -fun createEbics3RequestForUploadInitialization( - cfg: EbicsSetupConfig, - preparedUploadData: PreparedUploadData, - bankkeys: BankPublicKeysFile, - clientKeys: ClientPrivateKeysFile, - orderService: Ebics3Request.OrderDetails.Service -): String { - val nonce = getNonce(128) - val req = Ebics3Request.createForUploadInitializationPhase( - preparedUploadData.transactionKey, - preparedUploadData.userSignatureDataEncrypted, - preparedUploadData.dataDigest, - cfg.ebicsHostId, - nonce, - cfg.ebicsPartnerId, - cfg.ebicsUserId, - DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar()), - bankkeys.bank_authentication_public_key, - bankkeys.bank_encryption_public_key, - BigInteger.ONE, - orderService - ) - val doc = XMLUtil.convertJaxbToDocument( - req, - withSchemaLocation = "urn:org:ebics:H005 ebics_request_H005.xsd" - ) - tech.libeufin.util.logger.debug("Created EBICS 3 document for upload initialization," + - " nonce: ${nonce.toHexString()}") - XMLUtil.signEbicsDocument( - doc, - clientKeys.authentication_private_key, - withEbics3 = true - ) - return XMLUtil.convertDomToString(doc) -} - -/** - * Crafts one EBICS 3 request for the upload transfer phase. Currently - * only 1-chunk payloads are supported. - * - * @param cfg configuration handle. - * @param clientKeys client private keys. - * @param transactionId EBICS transaction ID obtained from an init phase. - * @param uploadData business content to upload. - * - * @return raw XML document. - */ -fun createEbics3RequestForUploadTransferPhase( - cfg: EbicsSetupConfig, - clientKeys: ClientPrivateKeysFile, - transactionId: String, - uploadData: PreparedUploadData -): String { - val chunkIndex = 1 // only 1-chunk communication currently supported. - val req = Ebics3Request.createForUploadTransferPhase( - cfg.ebicsHostId, - transactionId, - BigInteger.valueOf(chunkIndex.toLong()), - uploadData.encryptedPayloadChunks[chunkIndex] - ) - val doc = XMLUtil.convertJaxbToDocument(req) - XMLUtil.signEbicsDocument( - doc, - clientKeys.authentication_private_key, - withEbics3 = true - ) - return XMLUtil.convertDomToString(doc) -} -\ No newline at end of file diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt @@ -0,0 +1,148 @@ +package tech.libeufin.nexus + +import tech.libeufin.util.IbanPayto +import tech.libeufin.util.constructXml +import tech.libeufin.util.parsePayto +import java.time.Instant +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter + + +data class Pain001Namespaces( + val fullNamespace: String, + val xsdFilename: String +) + +/** + * Create a pain.001 document. It requires the debtor BIC. + * + * @param requestUid UID of this request, helps to make this request idempotent. + * @param debtorMetadataFile bank account information of the EBICS subscriber that + * sends this request. + * @param amount amount to pay. + * @param wireTransferSubject wire transfer subject. + * @param creditAccount payment receiver. + * @return raw pain.001 XML, or throws if the debtor BIC is not found. + */ +fun createPain001( + requestUid: String, + debtorMetadataFile: BankAccountMetadataFile, + amount: TalerAmount, + wireTransferSubject: String, + creditAccount: IbanPayto +): String { + val namespace = Pain001Namespaces( + fullNamespace = "urn:iso:std:iso:20022:tech:xsd:pain.001.001.09", + xsdFilename = "pain.001.001.09.ch.03.xsd" + ) + val creationTimestamp = Instant.now() + val amountWithoutCurrency: String = amount.toString().split(":").run { + if (this.size != 2) throw Exception("Invalid stringified amount: $amount") + return@run this[1] + } + if (debtorMetadataFile.bank_code == null) + throw Exception("Need debtor BIC, but not found in the debtor account metadata file.") + + return constructXml(indent = true) { + root("Document") { + attribute("xmlns", namespace.fullNamespace) + attribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") + attribute( + "xsi:schemaLocation", + "${namespace.fullNamespace} ${namespace.xsdFilename}" + ) + element("CstmrCdtTrfInitn") { + element("GrpHdr") { + element("MsgId") { + text(requestUid) + } + element("CreDtTm") { + val dateFormatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME + val zoned = ZonedDateTime.ofInstant( + creationTimestamp, + ZoneId.systemDefault() // FIXME: should this be UTC? + ) + text(dateFormatter.format(zoned)) + } + element("NbOfTxs") { + text("1") + } + element("CtrlSum") { + text(amount.toString()) + } + element("InitgPty/Nm") { // optional + text(debtorMetadataFile.account_holder_name) + } + } + element("PmtInf") { + element("PmtInfId") { + text("NOT GIVEN") + } + element("PmtMtd") { + text("TRF") + } + element("BtchBookg") { + text("true") + } + element("NbOfTxs") { + text("1") + } + element("CtrlSum") { + text(amount.toString()) + } + element("PmtTpInf/SvcLvl/Cd") { + text("SDVA") + } + element("ReqdExctnDt") { + text(DateTimeFormatter.ISO_DATE.format(creationTimestamp)) + } + element("Dbtr/Nm") { // optional + text(debtorMetadataFile.account_holder_name) + } + element("DbtrAcct/Id/IBAN") { + text(debtorMetadataFile.account_holder_iban) + } + element("DbtrAgt/FinInstnId") { + element("BICFI") { + text(debtorMetadataFile.bank_code) + } + } + element("ChrgBr") { + text("SLEV") + } + element("CdtTrfTxInf") { + element("PmtId") { + element("InstrId") { text("NOT PROVIDED") } + element("EndToEndId") { text("NOT PROVIDED") } + } + element("Amt/InstdAmt") { + attribute("Ccy", amount.currency) + text(amountWithoutCurrency) + } + creditAccount.bic.apply { + if (this != null) + element("CdtrAgt/FinInstnId") { + element("BICFI") { + text(this@apply) + } + } + } + creditAccount.receiverName.apply { + if (this != null) + element("Cdtr/Nm") { + text(this@apply) + } + } + element("CdtrAcct/Id/IBAN") { + text(creditAccount.iban) + } + element("RmtInf/Ustrd") { + text(wireTransferSubject) + } + } + } + } + } + } +} +\ 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 @@ -0,0 +1,93 @@ +package tech.libeufin.nexus.ebics + +import tech.libeufin.nexus.BankPublicKeysFile +import tech.libeufin.nexus.ClientPrivateKeysFile +import tech.libeufin.nexus.EbicsSetupConfig +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.toHexString +import java.math.BigInteger +import java.util.* +import javax.xml.datatype.DatatypeFactory + +/** + * Creates the EBICS 3 document for the init phase of an upload + * transaction. + * + * @param cfg configuration handle. + * @param preparedUploadData business payload to send. + * @param bankkeys bank public keys. + * @param clientKeys client private keys. + * @param orderService EBICS 3 document defining the request type + * @return raw XML of the EBICS 3 init phase. + */ +fun createEbics3RequestForUploadInitialization( + cfg: EbicsSetupConfig, + preparedUploadData: PreparedUploadData, + bankkeys: BankPublicKeysFile, + clientKeys: ClientPrivateKeysFile, + orderService: Ebics3Request.OrderDetails.Service +): String { + val nonce = getNonce(128) + val req = Ebics3Request.createForUploadInitializationPhase( + preparedUploadData.transactionKey, + preparedUploadData.userSignatureDataEncrypted, + preparedUploadData.dataDigest, + cfg.ebicsHostId, + nonce, + cfg.ebicsPartnerId, + cfg.ebicsUserId, + DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar()), + bankkeys.bank_authentication_public_key, + bankkeys.bank_encryption_public_key, + BigInteger.ONE, + orderService + ) + val doc = XMLUtil.convertJaxbToDocument( + req, + withSchemaLocation = "urn:org:ebics:H005 ebics_request_H005.xsd" + ) + tech.libeufin.util.logger.debug("Created EBICS 3 document for upload initialization," + + " nonce: ${nonce.toHexString()}") + XMLUtil.signEbicsDocument( + doc, + clientKeys.authentication_private_key, + withEbics3 = true + ) + return XMLUtil.convertDomToString(doc) +} + +/** + * Crafts one EBICS 3 request for the upload transfer phase. Currently + * only 1-chunk payloads are supported. + * + * @param cfg configuration handle. + * @param clientKeys client private keys. + * @param transactionId EBICS transaction ID obtained from an init phase. + * @param uploadData business content to upload. + * + * @return raw XML document. + */ +fun createEbics3RequestForUploadTransferPhase( + cfg: EbicsSetupConfig, + clientKeys: ClientPrivateKeysFile, + transactionId: String, + uploadData: PreparedUploadData +): String { + val chunkIndex = 1 // only 1-chunk communication currently supported. + val req = Ebics3Request.createForUploadTransferPhase( + cfg.ebicsHostId, + transactionId, + BigInteger.valueOf(chunkIndex.toLong()), + uploadData.encryptedPayloadChunks[chunkIndex] + ) + val doc = XMLUtil.convertJaxbToDocument(req) + XMLUtil.signEbicsDocument( + doc, + clientKeys.authentication_private_key, + withEbics3 = true + ) + return XMLUtil.convertDomToString(doc) +} +\ No newline at end of file diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt