From 304b3ba9d66c48de77dcd0081126395f2793d7a0 Mon Sep 17 00:00:00 2001 From: dankl Date: Sat, 12 Oct 2019 20:15:45 +0200 Subject: [PATCH] Implemented SEPA bank transfer --- docs/pain.001.001.03_with_comments.xml | 170 ++++++++++++++++++ .../kotlin/net/dankito/fints/FinTsClient.kt | 44 +++-- .../dankito/fints/messages/MessageBuilder.kt | 15 +- .../messages/segmente/id/CustomerSegmentId.kt | 4 +- .../sepa/ISepaMessageCreator.kt | 8 + .../sepa/SepaEinzelueberweisung.kt | 44 +++++ .../implementierte/sepa/SepaMessageCreator.kt | 75 ++++++++ .../implementierte/sepa/SepaSegment.kt | 28 +++ .../dankito/fints/model/BankTransferData.kt | 12 ++ .../main/resources/sepa/pain.001.001.03.xml | 1 + .../sepa/SepaEinzelueberweisungTest.kt | 39 ++++ 11 files changed, 423 insertions(+), 17 deletions(-) create mode 100644 docs/pain.001.001.03_with_comments.xml create mode 100644 fints4javaLib/src/main/kotlin/net/dankito/fints/messages/segmente/implementierte/sepa/ISepaMessageCreator.kt create mode 100644 fints4javaLib/src/main/kotlin/net/dankito/fints/messages/segmente/implementierte/sepa/SepaEinzelueberweisung.kt create mode 100644 fints4javaLib/src/main/kotlin/net/dankito/fints/messages/segmente/implementierte/sepa/SepaMessageCreator.kt create mode 100644 fints4javaLib/src/main/kotlin/net/dankito/fints/messages/segmente/implementierte/sepa/SepaSegment.kt create mode 100644 fints4javaLib/src/main/kotlin/net/dankito/fints/model/BankTransferData.kt create mode 100644 fints4javaLib/src/main/resources/sepa/pain.001.001.03.xml create mode 100644 fints4javaLib/src/test/kotlin/net/dankito/fints/messages/segmente/implementierte/sepa/SepaEinzelueberweisungTest.kt diff --git a/docs/pain.001.001.03_with_comments.xml b/docs/pain.001.001.03_with_comments.xml new file mode 100644 index 00000000..cbd58331 --- /dev/null +++ b/docs/pain.001.001.03_with_comments.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + Message-ID-4711 + + + 2010-11-11T09:30:47.000Z + + + 2 + + + 6655.86 + + + + + + Initiator Name + + + + + + + + + + Payment-Information-ID-4711 + + + TRF + + + + true + + + 2 + + + 6655.86 + + + + + + + + + SEPA + + + + + + 2010-11-25 + + + + + Debtor Name + + + + + + + + DE87200500001234567890 + + + + + + + + + BANKDEFFXXX + + + + + + SLEV + + + + + + + + + OriginatorID1234 + + + + + + + 6543.14 + + + + + + + + + SPUEDE2UXXX + + + + + + + Creditor Name + + + + + + DE21500500009876543210 + + + + + + + + Unstructured Remittance Information + + + + + + + OriginatorID1235 + + + 112.72 + + + + SPUEDE2UXXX + + + + Other Creditor Name + + + + DE21500500001234567897 + + + + Unstructured Remittance Information + + + + + diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/FinTsClient.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/FinTsClient.kt index 126c7008..d24f4b0c 100644 --- a/fints4javaLib/src/main/kotlin/net/dankito/fints/FinTsClient.kt +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/FinTsClient.kt @@ -3,10 +3,7 @@ package net.dankito.fints import net.dankito.fints.messages.MessageBuilder import net.dankito.fints.messages.datenelemente.implementierte.Dialogsprache import net.dankito.fints.messages.datenelemente.implementierte.KundensystemStatusWerte -import net.dankito.fints.model.BankData -import net.dankito.fints.model.CustomerData -import net.dankito.fints.model.DialogData -import net.dankito.fints.model.ProductData +import net.dankito.fints.model.* import net.dankito.fints.response.InstituteSegmentId import net.dankito.fints.response.Response import net.dankito.fints.response.ResponseParser @@ -92,14 +89,6 @@ open class FinTsClient( return response } - protected open fun closeDialog(bank: BankData, customer: CustomerData, dialogData: DialogData) { - dialogData.increaseMessageNumber() - - val dialogEndRequestBody = messageBuilder.createDialogEndMessage(bank, customer, dialogData) - - getResponseForMessage(dialogEndRequestBody, bank) - } - open fun getTransactions(bank: BankData, customer: CustomerData, product: ProductData): Response { val dialogData = DialogData() @@ -134,6 +123,37 @@ open class FinTsClient( } + open fun doBankTransfer(bankTransferData: BankTransferData, bank: BankData, customer: CustomerData, product: ProductData): Response { + val dialogData = DialogData() + + val initDialogResponse = initDialog(bank, customer, product, dialogData) + + if (initDialogResponse.successful == false) { + return initDialogResponse + } + + + dialogData.increaseMessageNumber() + + val requestBody = messageBuilder.createBankTransferMessage(bankTransferData, bank, customer, dialogData) + + val response = getAndHandleResponseForMessage(requestBody, bank) + + closeDialog(bank, customer, dialogData) + + return response + } + + + protected open fun closeDialog(bank: BankData, customer: CustomerData, dialogData: DialogData) { + dialogData.increaseMessageNumber() + + val dialogEndRequestBody = messageBuilder.createDialogEndMessage(bank, customer, dialogData) + + getResponseForMessage(dialogEndRequestBody, bank) + } + + protected open fun getAndHandleResponseForMessage(requestBody: String, bank: BankData): Response { val webResponse = getResponseForMessage(requestBody, bank) diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/messages/MessageBuilder.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/messages/MessageBuilder.kt index e0645dc8..b30c33fc 100644 --- a/fints4javaLib/src/main/kotlin/net/dankito/fints/messages/MessageBuilder.kt +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/messages/MessageBuilder.kt @@ -8,12 +8,10 @@ import net.dankito.fints.messages.segmente.SegmentNumberGenerator import net.dankito.fints.messages.segmente.Synchronisierung import net.dankito.fints.messages.segmente.id.CustomerSegmentId import net.dankito.fints.messages.segmente.implementierte.* +import net.dankito.fints.messages.segmente.implementierte.sepa.SepaEinzelueberweisung import net.dankito.fints.messages.segmente.implementierte.umsaetze.KontoumsaetzeZeitraumMt940Version5 -import net.dankito.fints.model.BankData -import net.dankito.fints.model.CustomerData -import net.dankito.fints.model.DialogData -import net.dankito.fints.model.ProductData import net.dankito.fints.messages.segmente.implementierte.umsaetze.Saldenabfrage +import net.dankito.fints.model.* import net.dankito.fints.util.FinTsUtils import java.util.concurrent.ThreadLocalRandom @@ -109,6 +107,15 @@ open class MessageBuilder(protected val generator: ISegmentNumberGenerator = Seg } + open fun createBankTransferMessage(bankTransferData: BankTransferData, bank: BankData, customer: CustomerData, dialogData: DialogData): String { + + return createSignedMessage(bank, customer, dialogData, listOf( + SepaEinzelueberweisung(generator.resetSegmentNumber(2), customer, bank.bic!!, bankTransferData), + ZweiSchrittTanEinreichung(generator.getNextSegmentNumber(), TanProcess.TanProcess4, CustomerSegmentId.SepaBankTransfer) + )) + } + + open fun createSignedMessage(bank: BankData, customer: CustomerData, dialogData: DialogData, payloadSegments: List): String { diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/messages/segmente/id/CustomerSegmentId.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/messages/segmente/id/CustomerSegmentId.kt index 6e626db8..125cad2c 100644 --- a/fints4javaLib/src/main/kotlin/net/dankito/fints/messages/segmente/id/CustomerSegmentId.kt +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/messages/segmente/id/CustomerSegmentId.kt @@ -15,6 +15,8 @@ enum class CustomerSegmentId(override val id: String) : ISegmentId { Balance("HKSAL"), - AccountTransactionsMt940("HKKAZ") + AccountTransactionsMt940("HKKAZ"), + + SepaBankTransfer("HKCCS") } \ No newline at end of file diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/messages/segmente/implementierte/sepa/ISepaMessageCreator.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/messages/segmente/implementierte/sepa/ISepaMessageCreator.kt new file mode 100644 index 00000000..0805157d --- /dev/null +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/messages/segmente/implementierte/sepa/ISepaMessageCreator.kt @@ -0,0 +1,8 @@ +package net.dankito.fints.messages.segmente.implementierte.sepa + + +interface ISepaMessageCreator { + + fun createXmlFile(filename: String, replacementStrings: Map): String + +} \ No newline at end of file diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/messages/segmente/implementierte/sepa/SepaEinzelueberweisung.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/messages/segmente/implementierte/sepa/SepaEinzelueberweisung.kt new file mode 100644 index 00000000..143e74f0 --- /dev/null +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/messages/segmente/implementierte/sepa/SepaEinzelueberweisung.kt @@ -0,0 +1,44 @@ +package net.dankito.fints.messages.segmente.implementierte.sepa + +import net.dankito.fints.messages.segmente.id.CustomerSegmentId +import net.dankito.fints.model.BankTransferData +import net.dankito.fints.model.CustomerData + + +open class SepaEinzelueberweisung( + segmentNumber: Int, + debitor: CustomerData, + debitorBic: String, + data: BankTransferData, + messageCreator: ISepaMessageCreator = SepaMessageCreator() +) + : SepaSegment( + segmentNumber, + CustomerSegmentId.SepaBankTransfer, + 1, + "urn?:iso?:std?:iso?:20022?:tech?:xsd?:pain.001.001.03", // TODO: read from HISPAS + "pain.001.001.03.xml", + data.creditorIban, + data.creditorBic, + mapOf( + SepaMessageCreator.NumberOfTransactionsKey to "1", // TODO: may someday support more then one transaction per file + "DebitorName" to debitor.name, + "DebitorIban" to debitor.iban!!, + "DebitorBic" to debitorBic, + "CreditorName" to data.creditorName, + "CreditorIban" to data.creditorIban, + "CreditorBic" to data.creditorBic, + "Amount" to data.amount.toString(), + "Usage" to data.usage, + "RequestedExecutionDate" to RequestedExecutionDateValueForNotScheduledTransfers + ), + messageCreator +) { + + companion object { + /** + * In das Mussfeld RequestedExecutionDate ist der 1999-01-01 einzustellen. + */ + const val RequestedExecutionDateValueForNotScheduledTransfers = "1999-01-01" + } +} \ No newline at end of file diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/messages/segmente/implementierte/sepa/SepaMessageCreator.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/messages/segmente/implementierte/sepa/SepaMessageCreator.kt new file mode 100644 index 00000000..0c0afd58 --- /dev/null +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/messages/segmente/implementierte/sepa/SepaMessageCreator.kt @@ -0,0 +1,75 @@ +package net.dankito.fints.messages.segmente.implementierte.sepa + +import net.dankito.fints.messages.datenelemente.implementierte.sepa.SepaMessage +import org.slf4j.LoggerFactory +import java.io.File +import java.text.SimpleDateFormat +import java.util.* + + +/** + * It may sounds like beginners programming loading a XML file and doing string replacements to set actual values. + * And yes I know how to use xjc :). + * + * But there's some reason behind it: + * - Serializing to XML is always a bit problematic on Android. + * - I don't need another dependency. + * - And it should be a little bit faster (even though not much :) ). + */ +open class SepaMessageCreator : ISepaMessageCreator { + + companion object { + const val MessageIdKey = "MessageId" + + const val CreationDateTimeKey = "CreationDateTime" + + const val PaymentInformationIdKey = "PaymentInformationId" + + const val NumberOfTransactionsKey = "NumberOfTransactions" + + val IsoDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ") + + private val log = LoggerFactory.getLogger(SepaMessageCreator::class.java) + } + + + override fun createXmlFile(filename: String, replacementStrings: Map): String { + var xmlFile = loadXmlFile(filename) + + val now = Date() + val nowInIsoDate = IsoDateFormat.format(now) + + if (replacementStrings.containsKey(MessageIdKey) == false) { + xmlFile = replacePlaceholderWithValue(xmlFile, MessageIdKey, nowInIsoDate) + } + if (replacementStrings.containsKey(CreationDateTimeKey) == false) { + xmlFile = replacePlaceholderWithValue(xmlFile, CreationDateTimeKey, nowInIsoDate) + } + if (replacementStrings.containsKey(PaymentInformationIdKey) == false) { + xmlFile = replacePlaceholderWithValue(xmlFile, PaymentInformationIdKey, nowInIsoDate) + } + + replacementStrings.forEach { entry -> + xmlFile = replacePlaceholderWithValue(xmlFile, entry.key, entry.value) + } + + return xmlFile + } + + protected open fun loadXmlFile(filename: String): String { + val filePath = "sepa/" + filename + + SepaMessage::class.java.classLoader.getResourceAsStream(filePath)?.use { inputStream -> + return inputStream.bufferedReader().readText() + } + + log.error("Could not load SEPA file from path ${File(filePath).absolutePath}") // TODO: how to inform user? + + return "" + } + + protected open fun replacePlaceholderWithValue(xmlFile: String, key: String, value: String): String { + return xmlFile.replace("$$key$", value) + } + +} \ No newline at end of file diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/messages/segmente/implementierte/sepa/SepaSegment.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/messages/segmente/implementierte/sepa/SepaSegment.kt new file mode 100644 index 00000000..75f4fe30 --- /dev/null +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/messages/segmente/implementierte/sepa/SepaSegment.kt @@ -0,0 +1,28 @@ +package net.dankito.fints.messages.segmente.implementierte.sepa + +import net.dankito.fints.messages.Existenzstatus +import net.dankito.fints.messages.datenelemente.basisformate.AlphanumerischesDatenelement +import net.dankito.fints.messages.datenelemente.implementierte.sepa.SepaMessage +import net.dankito.fints.messages.datenelementgruppen.implementierte.Segmentkopf +import net.dankito.fints.messages.datenelementgruppen.implementierte.account.KontoverbindungInternational +import net.dankito.fints.messages.segmente.Segment +import net.dankito.fints.messages.segmente.id.ISegmentId + + +open class SepaSegment( + segmentNumber: Int, + segmentId: ISegmentId, + segmentVersion: Int, + sepaDescriptorUrn: String, + sepaFileName: String, + iban: String, + bic: String, + replacementStrings: Map, + messageCreator: ISepaMessageCreator = SepaMessageCreator() +) + : Segment(listOf( + Segmentkopf(segmentId, segmentVersion, segmentNumber), + KontoverbindungInternational(iban, bic, null), + object : AlphanumerischesDatenelement(sepaDescriptorUrn, Existenzstatus.Mandatory, 256) { }, + SepaMessage(sepaFileName, replacementStrings, messageCreator) +), Existenzstatus.Mandatory) \ No newline at end of file diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/model/BankTransferData.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/model/BankTransferData.kt new file mode 100644 index 00000000..9bda9c46 --- /dev/null +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/model/BankTransferData.kt @@ -0,0 +1,12 @@ +package net.dankito.fints.model + +import java.math.BigDecimal + + +open class BankTransferData( + val creditorName: String, + val creditorIban: String, + val creditorBic: String, + val amount: BigDecimal, + val usage: String +) \ No newline at end of file diff --git a/fints4javaLib/src/main/resources/sepa/pain.001.001.03.xml b/fints4javaLib/src/main/resources/sepa/pain.001.001.03.xml new file mode 100644 index 00000000..4a57d09f --- /dev/null +++ b/fints4javaLib/src/main/resources/sepa/pain.001.001.03.xml @@ -0,0 +1 @@ +$MessageId$$CreationDateTime$$NumberOfTransactions$$Amount$$DebitorName$$PaymentInformationId$TRF$NumberOfTransactions$$Amount$SEPA$RequestedExecutionDate$$DebitorName$$DebitorIban$$DebitorBic$SLEVNOTPROVIDED$Amount$$CreditorBic$$CreditorName$$CreditorIban$$Usage$ \ No newline at end of file diff --git a/fints4javaLib/src/test/kotlin/net/dankito/fints/messages/segmente/implementierte/sepa/SepaEinzelueberweisungTest.kt b/fints4javaLib/src/test/kotlin/net/dankito/fints/messages/segmente/implementierte/sepa/SepaEinzelueberweisungTest.kt new file mode 100644 index 00000000..247e5e51 --- /dev/null +++ b/fints4javaLib/src/test/kotlin/net/dankito/fints/messages/segmente/implementierte/sepa/SepaEinzelueberweisungTest.kt @@ -0,0 +1,39 @@ +package net.dankito.fints.messages.segmente.implementierte.sepa + +import net.dankito.fints.model.BankTransferData +import net.dankito.fints.model.CustomerData +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class SepaEinzelueberweisungTest { + + @Test + fun format() { + + // given + val segmentNumber = 7 + val debitorName = "Nelson Mandela" + val debitorIban = "ZA123456780987654321" + val debitorBic = "ABCDZAEFXXX" + val creditorName = "Mahatma Gandhi" + val creditorIban = "IN123456780987654321" + val creditorBic = "ABCDINEFXXX" + val amount = 1234.56.toBigDecimal() + val usage = "What should Mahatma Gandhi want with money?" + + val underTest = SepaEinzelueberweisung(segmentNumber, + CustomerData("", "", "", debitorName, debitorIban), + debitorBic, + BankTransferData(creditorName, creditorIban, creditorBic, amount, usage) + ) + + + // when + val result = underTest.format() + + + // then + assertThat(result).contains(debitorName, debitorIban, debitorBic, creditorName, creditorIban, creditorBic, + amount.toString(), usage) + } +} \ No newline at end of file