Implemented SEPA bank transfer

This commit is contained in:
dankl 2019-10-12 20:15:45 +02:00 committed by dankito
parent 975a84cded
commit 304b3ba9d6
11 changed files with 423 additions and 17 deletions

View File

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Mit XMLSpy v2008 rel. 2 sp2 (http://www.altova.com) im Januar 2016 von der SIZ GmbH (Wenzel) bearbeitet -->
<!-- Änderungen: NUR NAMESPACE -->
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.001.03" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:iso:std:iso:20022:tech:xsd:pain.001.001.03 pain.001.001.03.xsd">
<!-- Customer Credit Transfer Initiation: Überweisungsauftrag durch den Kunden -->
<CstmrCdtTrfInitn>
<!-- Group Header: Kenndaten, die für alle Transaktionen innerhalb der SEPA-Nachricht gelten -->
<GrpHdr>
<!-- MessageIdentification: Punkt-zu-Punkt-Referenz der anweisenden Partei für die folgende Partei in der Nachrichten-Kette, um die Nachricht (Datei) eindeutig zu identifizieren. -->
<!-- Die <MsgID> in Kombination mit der Kunden-ID oder der Auftraggeber-IBAN kann als Kriterium für die Verhinderung einer Doppelverarbeitung bei versehentlich doppelt eingereichten Dateien dienen und muss somit für jede neue pain-Nachricht einen neuen Wert enthalten. -->
<MsgId>Message-ID-4711</MsgId>
<!-- CreationDateTime: Datum und Zeit, wann die ZV-Nachricht durch die anweisende Partei erzeugt wurde. ISODateTime -->
<CreDtTm>2010-11-11T09:30:47.000Z</CreDtTm>
<!-- NumberOfTransactions: Anzahl der einzelnen Transaktionen innerhalb der gesamten Nachricht. Max15NumericText -->
<NbOfTxs>2</NbOfTxs>
<!-- ControlSum: Summe der Beträge aller Einzeltransaktionen in der gesamten Nachricht. DecimalNumber. Es sind maximal zwei Nachkommastellen zulässig. -->
<CtrlSum>6655.86</CtrlSum>
<!-- InitiatingParty: Informationen über die Partei, welche die Zahlung anweist, das heißt der Zahler (Auftraggeber) oder eine Partei, welche im Auftrag des Zahlers handelt. -->
<!-- Belegung ist auch abweichend von Debtor zugelassen. Empfehlung: Nur das Unterelement Name sollte verwendet werden. -->
<InitgPty>
<!-- Name. Max70Text. Name ist begrenzt auf 70 Zeichen. -->
<Nm>Initiator Name</Nm>
<!-- Identification (<Id>): Es wird empfohlen, diese Feldgruppe nicht zu verwenden. -->
</InitgPty>
</GrpHdr>
<!-- PaymentInformation -->
<PmtInf>
<!-- PaymentInformationIdentification: Referenz zur eindeutigen Identifizierung des Sammlers. RestrictedIdentificationSEPA1 -->
<PmtInfId>Payment-Information-ID-4711</PmtInfId>
<!-- PaymentMethod: Zahlungsinstrument, z. B. Überweisung. PaymentMethodSCTCode. Enthält die Konstante TRF -->
<PmtMtd>TRF</PmtMtd>
<!-- BatchBooking ([0..1]): Indikator, der aussagt, ob es sich um eine Sammelbuchung (true) oder eine Einzelbuchung handelt (false) -->
<!-- Nur wenn eine entsprechende Vereinbarung für Einzelbuchungen mit dem Kunden vorliegt, wird im Falle von Belegung mit false, jede Transaktion einzeln auf dem Kontoauszug des Zahlers (Auftraggebers) dargestellt. Andernfalls immer Sammelbuchung (Default/pre-agreed: true). -->
<BtchBookg>true</BtchBookg>
<!-- NumberOfTransactions: Anzahl der einzelnen Transaktionen innerhalb des Payment Information Blocks. Max15NumericText -->
<NbOfTxs>2</NbOfTxs>
<!-- ControlSum: Summe der Beträge aller Einzeltransaktionen innerhalb des Payment Information Blocks. DecimalNumber. Es sind maximal zwei Nachkommastellen zulässig. -->
<CtrlSum>6655.86</CtrlSum>
<!-- PaymentTypeInformation ([0..1]): Transaktionstyp. PaymentTypeInformationSCT1. -->
<!-- Es wird empfohlen, <PmtTpInf> hier und nicht auf Einzeltransaktionsebene zu belegen. Zudem ist eine Belegung der Elementgruppe auf beiden Ebenen gleichzeitig nicht zulässig. -->
<PmtTpInf>
<!-- ServiceLevel: Vereinbarung oder Regel, nach der die Transaktion verarbeitet werden sollte. ServiceLevelSEPA -->
<SvcLvl>
<!-- Code: Code einer vorvereinbarten Serviceleistung zwischen den Parteien. ExternalServiceLevel1Code -->
<!-- Einziger zugelassener Wert aus der externen ISO20022-Codeliste ist SEPA. -->
<Cd>SEPA</Cd>
</SvcLvl>
</PmtTpInf>
<!-- RequestedExecutionDate: Ausführungstermin. ISODate -->
<!-- Vom Kunden gewünschter Ausführungstermin. Fällt der angegebene Termin auf keinen TARGET-Geschäftstag, so ist die Bank berechtigt, den folgenden TARGET-Geschäftstag als Ausführungstag anzugeben. Geht der Datensatz erst nach der von der Bank angegebenen Cut-Off-Zeit ein, so gilt der Auftrag erst am folgenden Geschäftstag als zugegangen. Banken sind nicht verpflichtet, Auftragsdaten zu verarbeiten, die mehr als 15 Kalendertage VOR dem Ausführungsdatum eingeliefert wurden. -->
<ReqdExctnDt>2010-11-25</ReqdExctnDt>
<!-- Debtor -->
<Dbtr>
<!-- Max70Text. Name ist auf 70 Zeichen begrenzt. -->
<Nm>Debtor Name</Nm>
</Dbtr>
<!-- DebtorAccount: Konto des Zahlers (Auftraggebers). CashAccountSEPA1 -->
<DbtrAcct>
<!-- Identification: Identifikation des Kontos. AccountIdentificationSEPA -->
<Id>
<!-- International Bank Account Number (IBAN). IBAN2007Identifier. Diese kann maximal 34 Stellen lang sein. -->
<IBAN>DE87200500001234567890</IBAN>
</Id>
</DbtrAcct>
<!-- DebtorAgent: Kreditinstitut des Zahlers (Auftraggebers). BranchAndFinancialInstitutionIdentificationSEPA3 -->
<DbtrAgt>
<!-- FinancialInstitutionIdentification: eindeutige Identifikation eines Kreditinstituts. FinancialInstituteIdentificationSEPA3 -->
<FinInstnId>
<!-- Business Identifier Code (SWIFT-Code). BICIdentifier. Dieser kann 8 oder 11 Stellen lang sein. -->
<BIC>BANKDEFFXXX</BIC>
</FinInstnId>
</DbtrAgt>
<!-- ChargeBearer ([0..1]): Entgeltverrechnung; Code, der bedeutet, dass bestimmte Regeln Anwendung finden. ChargeBearerTypeSEPACode -->
<!-- Es wird empfohlen, <ChrgBr> hier und nicht auf Einzeltransaktionsebene zu belegen. Zudem ist eine Belegung auf beiden Ebenen gleichzeitig nicht zulässig. Falls belegt, enthält es die Konstante SLEV -->
<ChrgBr>SLEV</ChrgBr>
<!-- CreditTransferTransactionInformation ([1..n]): Einzeltransaktion -->
<CdtTrfTxInf>
<!-- PaymentIdentification: Referenzierung dieser Transaktion. PaymentIdentificationSEPA -->
<PmtId>
<!-- EndToEndIdentification: eindeutige Referenz des Zahlers (Auftraggebers). Diese Referenz wird unverändert durch die gesamte Kette bis zum Zahlungsempfänger geleitet (Ende-zu-Ende-Referenz). -->
<!-- Es wird empfohlen, jede Überweisung mit einer eindeutigen Referenz zu belegen. Ist keine Referenz vorhanden muss die Konstante NOTPROVIDED benutzt werden. -->
<EndToEndId>OriginatorID1234</EndToEndId>
</PmtId>
<!-- Amount: Betrag. AmountTypeSEPA -->
<Amt>
<!-- InstructedAmount: beauftragterBetrag. ActiveOrHistoricCurrencyAndAmountSEPA -->
<!-- Ist mit einem Geldbetrag zu belegen, das Dezimaltrennzeichen ist ein Punkt. -->
<InstdAmt Ccy="EUR">6543.14</InstdAmt>
</Amt>
<!-- CreditorAgent ([0..1]): Kreditinstitut des Zahlungsempfängers. BranchAndFinancialInstitutionIdentificationSEPA1 -->
<CdtrAgt>
<!-- FinancialInstitutionIdentification: eindeutige Identifikation eines Kreditinstituts. FinancialInstitutionIdentificationSEPA1 -->
<FinInstnId>
<!-- BusinessCode Identifier (SWIFT-Code gemäß ISO 9362) -->
<!-- Diese Angabe kann vom ZDL bei Zahlungen außerhalb EU/EWR verlangt werden. Der BIC kann 8 oder 11 Stellen lang sein. -->
<BIC>SPUEDE2UXXX</BIC>
</FinInstnId>
</CdtrAgt>
<!-- Creditor -->
<Cdtr>
<!-- Max70Text. Name ist begrenzt auf 70 Zeichen. -->
<Nm>Creditor Name</Nm>
</Cdtr>
<!-- CreditorAccount: Konto des Zahlungsempfängers. CashAccountSEPA2 -->
<CdtrAcct>
<Id>
<IBAN>DE21500500009876543210</IBAN>
</Id>
</CdtrAcct>
<!-- RemittanceInformation ([0..1]): Verwendungszweck. Es wird entweder Unstructured oder Structured, belegt, jedoch nicht beide Structured sollte nur in Absprache mit dem Zahlungsempfänger belegt werden. -->
<RmtInf>
<!-- Unstructured: unstrukturierter Verwendungszweck. Max140Text. -->
<!-- Es wird empfohlen, den unstrukturierten Verwendungszweck zu verwenden. In bilateraler Abstimmung zwischen Zahlungsempfänger und Zahler (Auftraggeber) kann der unstrukturierte Verwendungszweck strukturierte Informationen enthalten. -->
<Ustrd>Unstructured Remittance Information</Ustrd>
</RmtInf>
</CdtTrfTxInf>
<!-- Zweite Überweisung, Parameter wie oben -->
<CdtTrfTxInf>
<PmtId>
<EndToEndId>OriginatorID1235</EndToEndId>
</PmtId>
<Amt>
<InstdAmt Ccy="EUR">112.72</InstdAmt>
</Amt>
<CdtrAgt>
<FinInstnId>
<BIC>SPUEDE2UXXX</BIC>
</FinInstnId>
</CdtrAgt>
<Cdtr>
<Nm>Other Creditor Name</Nm>
</Cdtr>
<CdtrAcct>
<Id>
<IBAN>DE21500500001234567897</IBAN>
</Id>
</CdtrAcct>
<RmtInf>
<Ustrd>Unstructured Remittance Information</Ustrd>
</RmtInf>
</CdtTrfTxInf>
</PmtInf>
</CstmrCdtTrfInitn>
</Document>

View File

@ -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)

View File

@ -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<Segment>): String {

View File

@ -15,6 +15,8 @@ enum class CustomerSegmentId(override val id: String) : ISegmentId {
Balance("HKSAL"),
AccountTransactionsMt940("HKKAZ")
AccountTransactionsMt940("HKKAZ"),
SepaBankTransfer("HKCCS")
}

View File

@ -0,0 +1,8 @@
package net.dankito.fints.messages.segmente.implementierte.sepa
interface ISepaMessageCreator {
fun createXmlFile(filename: String, replacementStrings: Map<String, String>): String
}

View File

@ -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 <ReqdExctnDt> ist der 1999-01-01 einzustellen.
*/
const val RequestedExecutionDateValueForNotScheduledTransfers = "1999-01-01"
}
}

View File

@ -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, String>): 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)
}
}

View File

@ -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<String, String>,
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)

View File

@ -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
)

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.001.03" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:iso:std:iso:20022:tech:xsd:pain.001.001.03 pain.001.001.03.xsd"><CstmrCdtTrfInitn><GrpHdr><MsgId>$MessageId$</MsgId><CreDtTm>$CreationDateTime$</CreDtTm><NbOfTxs>$NumberOfTransactions$</NbOfTxs><CtrlSum>$Amount$</CtrlSum><InitgPty><Nm>$DebitorName$</Nm></InitgPty></GrpHdr><PmtInf><PmtInfId>$PaymentInformationId$</PmtInfId><PmtMtd>TRF</PmtMtd><NbOfTxs>$NumberOfTransactions$</NbOfTxs><CtrlSum>$Amount$</CtrlSum><PmtTpInf><SvcLvl><Cd>SEPA</Cd></SvcLvl></PmtTpInf><ReqdExctnDt>$RequestedExecutionDate$</ReqdExctnDt><Dbtr><Nm>$DebitorName$</Nm></Dbtr><DbtrAcct><Id><IBAN>$DebitorIban$</IBAN></Id></DbtrAcct><DbtrAgt><FinInstnId><BIC>$DebitorBic$</BIC></FinInstnId></DbtrAgt><ChrgBr>SLEV</ChrgBr><CdtTrfTxInf><PmtId><EndToEndId>NOTPROVIDED</EndToEndId></PmtId><Amt><InstdAmt Ccy="EUR">$Amount$</InstdAmt></Amt><CdtrAgt><FinInstnId><BIC>$CreditorBic$</BIC></FinInstnId></CdtrAgt><Cdtr><Nm>$CreditorName$</Nm></Cdtr><CdtrAcct><Id><IBAN>$CreditorIban$</IBAN></Id></CdtrAcct><RmtInf><Ustrd>$Usage$</Ustrd></RmtInf></CdtTrfTxInf></PmtInf></CstmrCdtTrfInitn></Document>

View File

@ -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)
}
}