From cfcad3f5e096b52e819b3194d76f597ab2ea2eef Mon Sep 17 00:00:00 2001 From: dankl Date: Sun, 26 Jan 2020 12:23:02 +0100 Subject: [PATCH] Implemented hbci4jBankingClient --- BankingJavaFxApp/build.gradle | 1 + .../javafx/dialogs/mainwindow/MainWindow.kt | 6 +- .../banking/ui/IBankingClientCreator.kt | 17 +- .../ui/presenter/MainWindowPresenter.kt | 9 +- build.gradle | 5 +- .../fints4java/android/MainActivity.kt | 5 +- .../banking/fints4javaBankingClientCreator.kt | 10 +- hbci4jBankingClient/build.gradle | 23 ++ .../net/dankito/banking/HbciCallback.kt | 216 +++++++++++ .../dankito/banking/hbci4jBankingClient.kt | 367 ++++++++++++++++++ .../banking/hbci4jBankingClientCreator.kt | 25 ++ .../banking/model/AccountCredentials.kt | 8 + .../dankito/banking/model/ConnectResult.kt | 12 + .../banking/util/AccountTransactionMapper.kt | 162 ++++++++ .../dankito/banking/util/hbci4jModelMapper.kt | 106 +++++ 15 files changed, 945 insertions(+), 27 deletions(-) create mode 100644 hbci4jBankingClient/build.gradle create mode 100644 hbci4jBankingClient/src/main/kotlin/net/dankito/banking/HbciCallback.kt create mode 100644 hbci4jBankingClient/src/main/kotlin/net/dankito/banking/hbci4jBankingClient.kt create mode 100644 hbci4jBankingClient/src/main/kotlin/net/dankito/banking/hbci4jBankingClientCreator.kt create mode 100644 hbci4jBankingClient/src/main/kotlin/net/dankito/banking/model/AccountCredentials.kt create mode 100755 hbci4jBankingClient/src/main/kotlin/net/dankito/banking/model/ConnectResult.kt create mode 100644 hbci4jBankingClient/src/main/kotlin/net/dankito/banking/util/AccountTransactionMapper.kt create mode 100644 hbci4jBankingClient/src/main/kotlin/net/dankito/banking/util/hbci4jModelMapper.kt diff --git a/BankingJavaFxApp/build.gradle b/BankingJavaFxApp/build.gradle index e084660d..7dabcf33 100644 --- a/BankingJavaFxApp/build.gradle +++ b/BankingJavaFxApp/build.gradle @@ -22,6 +22,7 @@ dependencies { implementation project(':BankingJavaFxControls') implementation project(':fints4javaBankingClient') + implementation project(':hbci4jBankingClient') implementation project(':BankingPersistenceJson') diff --git a/BankingJavaFxApp/src/main/kotlin/net/dankito/banking/ui/javafx/dialogs/mainwindow/MainWindow.kt b/BankingJavaFxApp/src/main/kotlin/net/dankito/banking/ui/javafx/dialogs/mainwindow/MainWindow.kt index 5df907c4..f44170a9 100755 --- a/BankingJavaFxApp/src/main/kotlin/net/dankito/banking/ui/javafx/dialogs/mainwindow/MainWindow.kt +++ b/BankingJavaFxApp/src/main/kotlin/net/dankito/banking/ui/javafx/dialogs/mainwindow/MainWindow.kt @@ -1,13 +1,12 @@ package net.dankito.banking.javafx.dialogs.mainwindow import javafx.scene.control.SplitPane -import net.dankito.banking.fints4javaBankingClientCreator +import net.dankito.banking.hbci4jBankingClientCreator import net.dankito.banking.persistence.BankingPersistenceJson import net.dankito.banking.ui.javafx.RouterJavaFx import net.dankito.banking.ui.javafx.controls.AccountTransactionsView import net.dankito.banking.ui.javafx.controls.AccountsView import net.dankito.banking.ui.javafx.dialogs.mainwindow.controls.MainMenuBar -import net.dankito.banking.ui.javafx.util.Base64ServiceJava8 import net.dankito.banking.ui.presenter.MainWindowPresenter import tornadofx.* import tornadofx.FX.Companion.messages @@ -18,7 +17,8 @@ class MainWindow : View(messages["application.title"]) { private val dataFolder = File("data", "accounts") - private val presenter = MainWindowPresenter(fints4javaBankingClientCreator(), dataFolder, BankingPersistenceJson(File(dataFolder, "accounts.json")), Base64ServiceJava8(), RouterJavaFx()) +// private val presenter = MainWindowPresenter(fints4javaBankingClientCreator(OkHttpWebClient(), Base64ServiceJava8()), dataFolder, BankingPersistenceJson(File(dataFolder, "accounts.json")), RouterJavaFx()) + private val presenter = MainWindowPresenter(hbci4jBankingClientCreator(), dataFolder, BankingPersistenceJson(File(dataFolder, "accounts.json")), RouterJavaFx()) diff --git a/BankingUiCommon/src/main/java/net/dankito/banking/ui/IBankingClientCreator.kt b/BankingUiCommon/src/main/java/net/dankito/banking/ui/IBankingClientCreator.kt index 351f7718..f707ee78 100644 --- a/BankingUiCommon/src/main/java/net/dankito/banking/ui/IBankingClientCreator.kt +++ b/BankingUiCommon/src/main/java/net/dankito/banking/ui/IBankingClientCreator.kt @@ -1,20 +1,19 @@ package net.dankito.banking.ui -import net.dankito.banking.util.IBase64Service import net.dankito.fints.model.BankInfo import net.dankito.utils.IThreadPool -import net.dankito.utils.web.client.IWebClient +import java.io.File interface IBankingClientCreator { - fun createClient(bankInfo: BankInfo, // TODO: create own value object to get rid off fints4java dependency - customerId: String, - pin: String, - webClient: IWebClient, // TODO: wrap away JavaUtils IWebClient - base64Service: IBase64Service, - threadPool: IThreadPool, // TODO: wrap away JavaUtils IThreadPool - callback: BankingClientCallback + fun createClient( + bankInfo: BankInfo, // TODO: create own value object to get rid off fints4java dependency + customerId: String, + pin: String, + dataFolder: File, + threadPool: IThreadPool, // TODO: wrap away JavaUtils IWebClient + callback: BankingClientCallback ): IBankingClient } \ No newline at end of file diff --git a/BankingUiCommon/src/main/java/net/dankito/banking/ui/presenter/MainWindowPresenter.kt b/BankingUiCommon/src/main/java/net/dankito/banking/ui/presenter/MainWindowPresenter.kt index 9482fe92..3e84917b 100644 --- a/BankingUiCommon/src/main/java/net/dankito/banking/ui/presenter/MainWindowPresenter.kt +++ b/BankingUiCommon/src/main/java/net/dankito/banking/ui/presenter/MainWindowPresenter.kt @@ -16,14 +16,11 @@ import net.dankito.banking.ui.model.tan.EnterTanGeneratorAtcResult import net.dankito.banking.ui.model.tan.EnterTanResult import net.dankito.banking.ui.model.tan.TanChallenge import net.dankito.banking.ui.model.tan.TanGeneratorTanMedium -import net.dankito.banking.util.IBase64Service import net.dankito.fints.banks.BankFinder import net.dankito.fints.model.BankInfo import net.dankito.utils.IThreadPool import net.dankito.utils.ThreadPool import net.dankito.utils.extensions.ofMaxLength -import net.dankito.utils.web.client.IWebClient -import net.dankito.utils.web.client.OkHttpWebClient import org.slf4j.LoggerFactory import java.io.File import java.math.BigDecimal @@ -35,9 +32,7 @@ open class MainWindowPresenter( protected val bankingClientCreator: IBankingClientCreator, protected val dataFolder: File, protected val persister: IBankingPersistence, - protected val base64Service: IBase64Service, protected val router: IRouter, - protected val webClient: IWebClient = OkHttpWebClient(), protected val threadPool: IThreadPool = ThreadPool() ) { @@ -107,7 +102,7 @@ open class MainWindowPresenter( val bankInfo = BankInfo(bank.name, bank.bankCode, bank.bic, "", "", "", bank.finTsServerAddress, "FinTS V3.0", null) val newClient = bankingClientCreator.createClient(bankInfo, account.customerId, account.password, - dataFolder, webClient, base64Service, threadPool, callback) + dataFolder, threadPool, callback) try { newClient.restoreData() @@ -135,7 +130,7 @@ open class MainWindowPresenter( // TODO: move BankInfo out of fints4javaLib open fun addAccountAsync(bankInfo: BankInfo, customerId: String, pin: String, callback: (AddAccountResponse) -> Unit) { - val newClient = bankingClientCreator.createClient(bankInfo, customerId, pin, dataFolder, webClient, base64Service, threadPool, this.callback) + val newClient = bankingClientCreator.createClient(bankInfo, customerId, pin, dataFolder, threadPool, this.callback) newClient.addAccountAsync { response -> val account = response.account diff --git a/build.gradle b/build.gradle index a4f7ad87..4b19dc16 100644 --- a/build.gradle +++ b/build.gradle @@ -1,4 +1,3 @@ - // TODO: move to versions.gradle ext { appVersionName = '0.1.0-SNAPSHOT' @@ -9,6 +8,10 @@ ext { javaUtilsVersion = '1.0.9' + + hbci4jVersion = '3.1.37' + + androidUtilsVersion = '1.1.0' clansFloatingActionButtonVersion = '1.6.4' diff --git a/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/MainActivity.kt b/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/MainActivity.kt index 15976a96..11fc2588 100644 --- a/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/MainActivity.kt +++ b/fints4javaAndroidApp/src/main/java/net/dankito/banking/fints4java/android/MainActivity.kt @@ -21,6 +21,7 @@ import net.dankito.banking.fints4javaBankingClientCreator import net.dankito.banking.persistence.BankingPersistenceJson import net.dankito.banking.ui.model.Account import net.dankito.banking.ui.presenter.MainWindowPresenter +import net.dankito.utils.web.client.OkHttpWebClient import org.slf4j.LoggerFactory import java.io.File @@ -47,8 +48,8 @@ class MainActivity : AppCompatActivity() { val dataFolder = File(this.filesDir, "data/accounts") - presenter = MainWindowPresenter(fints4javaBankingClientCreator(), dataFolder, - BankingPersistenceJson(File(dataFolder, "accounts.json")), Base64ServiceAndroid(), RouterAndroid(this)) + presenter = MainWindowPresenter(fints4javaBankingClientCreator(OkHttpWebClient(), Base64ServiceAndroid()), dataFolder, + BankingPersistenceJson(File(dataFolder, "accounts.json")), RouterAndroid(this)) initUi() } diff --git a/fints4javaBankingClient/src/main/kotlin/net/dankito/banking/fints4javaBankingClientCreator.kt b/fints4javaBankingClient/src/main/kotlin/net/dankito/banking/fints4javaBankingClientCreator.kt index 6698733a..5702083b 100644 --- a/fints4javaBankingClient/src/main/kotlin/net/dankito/banking/fints4javaBankingClientCreator.kt +++ b/fints4javaBankingClient/src/main/kotlin/net/dankito/banking/fints4javaBankingClientCreator.kt @@ -8,24 +8,24 @@ import net.dankito.banking.util.UiCommonBase64ServiceWrapper import net.dankito.fints.model.BankInfo import net.dankito.utils.IThreadPool import net.dankito.utils.web.client.IWebClient -import net.dankito.utils.web.client.OkHttpWebClient import java.io.File -open class fints4javaBankingClientCreator : IBankingClientCreator { +open class fints4javaBankingClientCreator( + protected val webClient: IWebClient, + protected val base64Service: IBase64Service +) : IBankingClientCreator { override fun createClient( bankInfo: BankInfo, customerId: String, pin: String, dataFolder: File, - webClient: IWebClient, - base64Service: IBase64Service, threadPool: IThreadPool, callback: BankingClientCallback ): IBankingClient { - return fints4javaBankingClient(bankInfo, customerId, pin, dataFolder, OkHttpWebClient(), UiCommonBase64ServiceWrapper(base64Service), threadPool, callback) + return fints4javaBankingClient(bankInfo, customerId, pin, dataFolder, webClient, UiCommonBase64ServiceWrapper(base64Service), threadPool, callback) } } \ No newline at end of file diff --git a/hbci4jBankingClient/build.gradle b/hbci4jBankingClient/build.gradle new file mode 100644 index 00000000..550011c8 --- /dev/null +++ b/hbci4jBankingClient/build.gradle @@ -0,0 +1,23 @@ +apply plugin: 'java-library' +apply plugin: 'kotlin' + + +sourceCompatibility = "1.8" +targetCompatibility = "1.8" + +compileKotlin { + kotlinOptions.jvmTarget = "1.8" +} +compileTestKotlin { + kotlinOptions.jvmTarget = "1.8" +} + + +dependencies { + api project(':BankingUiCommon') + implementation "com.github.hbci4j:hbci4j-core:$hbci4jVersion" + + + testImplementation "junit:junit:$junitVersion" + testImplementation "org.assertj:assertj-core:$assertJVersion" +} \ No newline at end of file diff --git a/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/HbciCallback.kt b/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/HbciCallback.kt new file mode 100644 index 00000000..bd7e8cf4 --- /dev/null +++ b/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/HbciCallback.kt @@ -0,0 +1,216 @@ +package net.dankito.banking + +import net.dankito.banking.model.AccountCredentials +import net.dankito.banking.ui.BankingClientCallback +import net.dankito.banking.ui.model.Account +import net.dankito.banking.ui.model.tan.FlickerCodeTanChallenge +import net.dankito.banking.ui.model.tan.ImageTanChallenge +import net.dankito.banking.ui.model.tan.TanChallenge +import net.dankito.banking.ui.model.tan.TanImage +import net.dankito.banking.util.hbci4jModelMapper +import org.kapott.hbci.callback.AbstractHBCICallback +import org.kapott.hbci.callback.HBCICallback +import org.kapott.hbci.manager.HBCIUtils +import org.kapott.hbci.manager.MatrixCode +import org.kapott.hbci.manager.QRCode +import org.kapott.hbci.passport.HBCIPassport +import org.slf4j.LoggerFactory +import java.util.* + + +/** + * Ueber diesen Callback kommuniziert HBCI4Java mit dem Benutzer und fragt die benoetigten + * Informationen wie Benutzerkennung, PIN usw. ab. + */ +open class HbciCallback( + protected val credentials: AccountCredentials, + protected val account: Account, + protected val mapper: hbci4jModelMapper, + protected val callback: BankingClientCallback +) : AbstractHBCICallback() { + + companion object { + private val log = LoggerFactory.getLogger(HbciCallback::class.java) + } + + + /** + * @see org.kapott.hbci.callback.HBCICallback.log + */ + override fun log(msg: String, level: Int, date: Date, trace: StackTraceElement) { + // Ausgabe von Log-Meldungen bei Bedarf + when (level) { + HBCIUtils.LOG_ERR -> log.error(msg) + HBCIUtils.LOG_WARN -> log.warn(msg) + HBCIUtils.LOG_INFO-> log.info(msg) + HBCIUtils.LOG_DEBUG, HBCIUtils.LOG_DEBUG2 -> log.debug(msg) + else -> log.trace(msg) + } + } + + /** + * @see org.kapott.hbci.callback.HBCICallback.callback + */ + override fun callback(passport: HBCIPassport, reason: Int, msg: String, datatype: Int, retData: StringBuffer) { + log.info("Callback: [$reason] $msg ($retData)") // TODO: remove again + + // Diese Funktion ist wichtig. Ueber die fragt HBCI4Java die benoetigten Daten von uns ab. + when (reason) { + // Mit dem Passwort verschluesselt HBCI4Java die Passport-Datei. + // Wir nehmen hier der Einfachheit halber direkt die PIN. In der Praxis + // sollte hier aber ein staerkeres Passwort genutzt werden. + // Die Ergebnis-Daten muessen in dem StringBuffer "retData" platziert werden. + // if you like or need to change your pin, return your old one for NEED_PASSPHRASE_LOAD and your new + // one for NEED_PASSPHRASE_SAVE + HBCICallback.NEED_PASSPHRASE_LOAD, HBCICallback.NEED_PASSPHRASE_SAVE -> retData.replace(0, retData.length, credentials.password) + + + /* Customer (authentication) data */ + + // BLZ wird benoetigt + HBCICallback.NEED_BLZ -> retData.replace(0, retData.length, credentials.bankCode) + + // Die Benutzerkennung + HBCICallback.NEED_USERID -> retData.replace(0, retData.length, credentials.customerId) + + // Die Kundenkennung. Meist identisch mit der Benutzerkennung. + // Bei manchen Banken kann man die auch leer lassen + HBCICallback.NEED_CUSTOMERID -> retData.replace(0, retData.length, credentials.customerId) + + // PIN wird benoetigt + HBCICallback.NEED_PT_PIN -> retData.replace(0, retData.length, credentials.password) + + + /* TAN */ + + // ADDED: Auswaehlen welches PinTan Verfahren verwendet werden soll + HBCICallback.NEED_PT_SECMECH -> selectTanProcedure(retData.toString())?.let { selectedTanProcedure -> + account.selectedTanProcedure = selectedTanProcedure + retData.replace(0, retData.length, selectedTanProcedure.bankInternalProcedureCode) + } + + // chipTan or simple TAN request (iTAN, smsTAN, ...) + HBCICallback.NEED_PT_TAN -> { + getTanFromUser(account, msg, retData.toString())?.let { enteredTan -> + retData.replace(0, retData.length, enteredTan) + } + } + + // chipTAN-QR + HBCICallback.NEED_PT_QRTAN -> { // use class QRCode to display QR code + val qrData = retData.toString() + val qrCode = QRCode(qrData, msg) + val enterTanResult = callback.enterTan(account, ImageTanChallenge(TanImage(qrCode.mimetype, qrCode.image), msg, account.selectedTanProcedure!!)) + enterTanResult.enteredTan?.let { enteredTan -> + retData.replace(0, retData.length, enteredTan) + } + } + + // photoTan + HBCICallback.NEED_PT_PHOTOTAN -> { // use class MatrixCode to display photo + val matrixCode = MatrixCode(retData.toString()) + val enterTanResult = callback.enterTan(account, ImageTanChallenge(TanImage(matrixCode.mimetype, matrixCode.image), msg, account.selectedTanProcedure!!)) + enterTanResult.enteredTan?.let { enteredTan -> + retData.replace(0, retData.length, enteredTan) + } + } + + // smsTan: Select cell phone to which SMS should be send + HBCICallback.NEED_PT_TANMEDIA -> { + log.info("TODO: select cell phone: $msg ($retData)") + } + + // wrong pin entered -> inform user + HBCICallback.WRONG_PIN -> { + log.info("TODO: user entered wrong pin: $msg ($retData)") + } + + // UserId changed -> inform user + HBCICallback.USERID_CHANGED -> { // im Parameter retData stehen die neuen Daten im Format UserID|CustomerID drin + log.info("TODO: UserId changed: $msg ($retData)") + } + + // user entered wrong Banleitzahl or Kontonummer -> inform user + HBCICallback.HAVE_CRC_ERROR -> { // retData contains wrong values in form "BLZ|KONTONUMMER". Set correct ones in the same form in retData + log.info("TODO: wrong Banleitzahl or Kontonummer entered: $msg ($retData)") + } + + // user entered wrong IBAN -> inform user + HBCICallback.HAVE_IBAN_ERROR -> { // retData contains wrong IBAN. Set correct IBAN in retData + log.info("TODO: wrong IBAN entered: $msg ($retData)") + } + + // message from bank to user. should get displayed to user + HBCICallback.HAVE_INST_MSG -> { + // TODO: inform user + log.error("TODO: inform user, received a message from bank: $msg\n$retData") + } + + // Manche Fehlermeldungen werden hier ausgegeben + HBCICallback.HAVE_ERROR -> { // to ignore error set an empty String in retData + // TODO: inform user + log.error("TODO: inform user, error occurred: $msg\n$retData") + } + + else -> { // Wir brauchen nicht alle der Callbacks + } + } + } + + /** + * @see org.kapott.hbci.callback.HBCICallback.status + */ + override fun status(passport: HBCIPassport, statusTag: Int, o: Array?) { + // So aehnlich wie log(String,int,Date,StackTraceElement) jedoch fuer Status-Meldungen. + val param = if (o == null) "" else o.joinToString() + + when (statusTag) { + HBCICallback.STATUS_MSG_RAW_SEND -> log.debug("Sending message:\n$param") + HBCICallback.STATUS_MSG_RAW_RECV -> log.debug("Received message:\n$param") +// else -> log.debug("New status [$statusTag]: $param") + } + } + + + open fun getTanFromUser(account: Account, messageToShowToUser: String, challengeHHD_UC: String): String? { + // Wenn per "retData" Daten uebergeben wurden, dann enthalten diese + // den fuer chipTAN optisch zu verwendenden Flickercode. + // Falls nicht, ist es eine TAN-Abfrage, fuer die keine weiteren + // Parameter benoetigt werden (z.B. smsTAN, iTAN oder aehnliches) + + // Die Variable "msg" aus der Methoden-Signatur enthaelt uebrigens + // den bankspezifischen Text mit den Instruktionen fuer den User. + // Der Text aus "msg" sollte daher im Dialog dem User angezeigt + // werden. + + val enterTanResult = if (challengeHHD_UC.isNullOrEmpty()) { + callback.enterTan(account, TanChallenge(messageToShowToUser, account.selectedTanProcedure!!)) + } + else { + // for Sparkasse messageToShowToUser started with "chipTAN optisch\nTAN-Nummer\n\n" + val usefulMessage = messageToShowToUser.split("\n").last().trim() + +// val parsedDataSet = FlickerCode(challengeHHD_UC).render() + callback.enterTan(account, FlickerCodeTanChallenge(net.dankito.banking.ui.model.tan.FlickerCode("", challengeHHD_UC), usefulMessage, account.selectedTanProcedure!!)) + } + + return enterTanResult.enteredTan + } + + + + open fun selectTanProcedure(supportedTanProceduresString: String): net.dankito.banking.ui.model.tan.TanProcedure? { + val supportedTanProcedures = mapper.mapTanProcedures(supportedTanProceduresString) + + account.supportedTanProcedures = supportedTanProcedures + + if (supportedTanProcedures.isNotEmpty()) { + // select any procedure, user then can select her preferred one in EnterTanDialog; try not to select 'chipTAN manuell' + return supportedTanProcedures.firstOrNull { it.displayName.contains("manuell", true) == false } + ?: supportedTanProcedures.firstOrNull() + } + + return null + } + +} \ No newline at end of file diff --git a/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/hbci4jBankingClient.kt b/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/hbci4jBankingClient.kt new file mode 100644 index 00000000..d2971cfe --- /dev/null +++ b/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/hbci4jBankingClient.kt @@ -0,0 +1,367 @@ +package net.dankito.banking + +import net.dankito.banking.model.AccountCredentials +import net.dankito.banking.model.ConnectResult +import net.dankito.banking.ui.BankingClientCallback +import net.dankito.banking.ui.IBankingClient +import net.dankito.banking.ui.model.Account +import net.dankito.banking.ui.model.AccountTransaction +import net.dankito.banking.ui.model.Bank +import net.dankito.banking.ui.model.BankAccount +import net.dankito.banking.ui.model.parameters.GetTransactionsParameter +import net.dankito.banking.ui.model.parameters.TransferMoneyData +import net.dankito.banking.ui.model.responses.AddAccountResponse +import net.dankito.banking.ui.model.responses.BankingClientResponse +import net.dankito.banking.ui.model.responses.GetTransactionsResponse +import net.dankito.banking.util.AccountTransactionMapper +import net.dankito.banking.util.hbci4jModelMapper +import net.dankito.fints.model.BankInfo +import net.dankito.utils.IThreadPool +import net.dankito.utils.ThreadPool +import org.kapott.hbci.GV.HBCIJob +import org.kapott.hbci.GV_Result.GVRKUms +import org.kapott.hbci.GV_Result.GVRSaldoReq +import org.kapott.hbci.manager.HBCIHandler +import org.kapott.hbci.manager.HBCIUtils +import org.kapott.hbci.manager.HBCIVersion +import org.kapott.hbci.passport.AbstractHBCIPassport +import org.kapott.hbci.passport.HBCIPassport +import org.kapott.hbci.status.HBCIExecStatus +import org.kapott.hbci.structures.Value +import org.slf4j.LoggerFactory +import java.io.File +import java.math.BigDecimal +import java.text.SimpleDateFormat +import java.util.* + + +open class hbci4jBankingClient( + bankInfo: BankInfo, + customerId: String, + pin: String, + protected val dataFolder: File, + protected val threadPool: IThreadPool = ThreadPool(), + protected val callback: BankingClientCallback +) : IBankingClient { + + companion object { + // the date format is hard coded in HBCIUtils.string2DateISO() + val HbciLibDateFormat = SimpleDateFormat("yyyy-MM-dd") + + const val NinetyDaysInMilliseconds = 90 * 24 * 60 * 60 * 1000L + + private val log = LoggerFactory.getLogger(hbci4jBankingClient::class.java) + } + + + protected val credentials = AccountCredentials(bankInfo.bankCode, customerId, pin) + + protected var bank = Bank(bankInfo.name, bankInfo.bankCode, bankInfo.bic, bankInfo.pinTanAddress ?: "") + + protected var account = Account(bank, customerId, pin, "") + + + protected val mapper = hbci4jModelMapper() + + protected val accountTransactionMapper = AccountTransactionMapper() + + + + override fun addAccountAsync(callback: (AddAccountResponse) -> Unit) { + threadPool.runAsync { + callback(addAccount()) + } + } + + open fun addAccount(): AddAccountResponse { + val connection = connect() + closeConnection(connection) + + if(connection.successful) { + connection.passport?.let { passport -> + val accounts = passport.accounts + if (accounts == null || accounts.size == 0) { + log.error("Keine Konten ermittelbar") + return AddAccountResponse(false, "Keine Konten ermittelbar", account) // TODO: translate + } + + this.account.bankAccounts = mapper.mapBankAccounts(account, accounts, passport) + + val transactionsOfLast90DaysResponse = tryToRetrieveAccountTransactionsForAddedAccounts(account) + + return AddAccountResponse(true, null, account, transactionsOfLast90DaysResponse.isSuccessful, + transactionsOfLast90DaysResponse.bookedTransactions, transactionsOfLast90DaysResponse.unbookedTransactions, + transactionsOfLast90DaysResponse.balances) + } + } + + return AddAccountResponse(false, null, account, error = connection.error) + } + + protected open fun tryToRetrieveAccountTransactionsForAddedAccounts(account: Account): GetTransactionsResponse { + val transactionsOfLast90DaysResponses = mutableListOf() + val balances = mutableMapOf() + val bookedTransactions = mutableMapOf>() + val unbookedTransactions = mutableMapOf>() + + account.bankAccounts.forEach { bankAccount -> + if (bankAccount.supportsRetrievingAccountTransactions) { + val response = getTransactionsOfLast90Days(bankAccount) + transactionsOfLast90DaysResponses.add(response) + + response.bookedTransactions[bankAccount]?.let { bookedTransactions.put(bankAccount, it) } + response.unbookedTransactions[bankAccount]?.let { unbookedTransactions.put(bankAccount, it) } + response.balances[bankAccount]?.let { balances.put(bankAccount, it) } + } + } + + val supportsRetrievingTransactionsOfLast90DaysWithoutTan = transactionsOfLast90DaysResponses.firstOrNull { it.isSuccessful } != null + + return GetTransactionsResponse(supportsRetrievingTransactionsOfLast90DaysWithoutTan, null, bookedTransactions, unbookedTransactions, balances) + } + + + /** + * According to PSD2 for the accounting entries of the last 90 days the two-factor authorization does not have to + * be applied. It depends on the bank if they request a second factor or not. + * + * So we simply try to retrieve at accounting entries of the last 90 days and see if a second factor is required + * or not. + */ + open fun getTransactionsOfLast90DaysAsync(bankAccount: BankAccount, callback: (GetTransactionsResponse) -> Unit) { + threadPool.runAsync { + callback(getTransactionsOfLast90Days(bankAccount)) + } + } + + /** + * According to PSD2 for the accounting entries of the last 90 days the two-factor authorization does not have to + * be applied. It depends on the bank if they request a second factor or not. + * + * So we simply try to retrieve at accounting entries of the last 90 days and see if a second factor is required + * or not. + */ + open fun getTransactionsOfLast90Days(bankAccount: BankAccount): GetTransactionsResponse { + val ninetyDaysAgo = Date(Date().time - NinetyDaysInMilliseconds) + + return getTransactions(bankAccount, GetTransactionsParameter(bankAccount.supportsRetrievingBalance, ninetyDaysAgo)) + } + + override fun getTransactionsAsync(bankAccount: BankAccount, parameter: GetTransactionsParameter, callback: (GetTransactionsResponse) -> Unit) { + threadPool.runAsync { + callback(getTransactions(bankAccount, parameter)) + } + } + + protected open fun getTransactions(bankAccount: BankAccount, parameter: GetTransactionsParameter): GetTransactionsResponse { + val connection = connect() + + connection.handle?.let { handle -> + try { + val (nullableBalanceJob, accountTransactionsJob, status) = executeJobsForGetAccountingEntries(handle, bankAccount, parameter) + + // Pruefen, ob die Kommunikation mit der Bank grundsaetzlich geklappt hat + if (!status.isOK) { + log.error("Could not connect to bank ${credentials.bankCode} ${status.toString()}: ${status.errorString}") + return GetTransactionsResponse(false, null, error = Exception("Could not connect to bank ${credentials.bankCode}: ${status.toString()}")) + } + + // Auswertung des Saldo-Abrufs. + var balance = BigDecimal.ZERO + if (parameter.alsoRetrieveBalance && nullableBalanceJob != null) { + val balanceResult = nullableBalanceJob.jobResult as GVRSaldoReq + if(balanceResult.isOK == false) { + log.error("Could not fetch balance of bank account $bankAccount: $balanceResult", balanceResult.getJobStatus().exceptions) + return GetTransactionsResponse(false, null, error = Exception("Could not fetch balance of bank account $bankAccount: $balanceResult")) + } + + balance = balanceResult.entries[0].ready.value.bigDecimalValue + } + + + // Das Ergebnis des Jobs koennen wir auf "GVRKUms" casten. Jobs des Typs "KUmsAll" + // liefern immer diesen Typ. + val result = accountTransactionsJob.jobResult as GVRKUms + + // Pruefen, ob der Abruf der Umsaetze geklappt hat + if (result.isOK == false) { + log.error("Could not get fetch account transactions of bank account $bankAccount: $result", result.getJobStatus().exceptions) + return GetTransactionsResponse(false, null, error = Exception("Could not fetch account transactions of bank account $bankAccount: $result")) + } + + return GetTransactionsResponse(true, null, mapOf(bankAccount to accountTransactionMapper.mapAccountTransactions(bankAccount, result)), + mapOf(), mapOf(bankAccount to balance)) + } + catch(e: Exception) { + log.error("Could not get accounting details for bank ${credentials.bankCode}", e) + return GetTransactionsResponse(false, null, error = e) + } + finally { + closeConnection(connection) + } + } + + closeConnection(connection) + + return GetTransactionsResponse(false, null, error = connection.error) + } + + protected open fun executeJobsForGetAccountingEntries(handle: HBCIHandler, bankAccount: BankAccount, parameter: GetTransactionsParameter): Triple { + val konto = mapper.mapToKonto(bank, bankAccount) + + // 1. Auftrag fuer das Abrufen des Saldos erzeugen + var balanceJob: HBCIJob? = null + if (parameter.alsoRetrieveBalance) { + val createdBalanceJob = handle.newJob("SaldoReq") + createdBalanceJob.setParam("my", konto) // festlegen, welches Konto abgefragt werden soll. + createdBalanceJob.addToQueue() // Zur Liste der auszufuehrenden Auftraege hinzufuegen + + balanceJob = createdBalanceJob + } + // 2. Auftrag fuer das Abrufen der Umsaetze erzeugen + val accountTransactionsJob = handle.newJob("KUmsAll") + accountTransactionsJob.setParam("my", konto) // festlegen, welches Konto abgefragt werden soll. + // evtl. Datum setzen, ab welchem die Auszüge geholt werden sollen + parameter.fromDate?.let { + accountTransactionsJob.setParam("startdate", HbciLibDateFormat.format(it)) + } + accountTransactionsJob.addToQueue() // Zur Liste der auszufuehrenden Auftraege hinzufuegen + + // Hier koennen jetzt noch weitere Auftraege fuer diesen Bankzugang hinzugefuegt + // werden. Z.Bsp. Ueberweisungen. + + // Alle Auftraege aus der Liste ausfuehren. + val status = handle.execute() + + return Triple(balanceJob, accountTransactionsJob, status) + } + + + override fun transferMoneyAsync(data: TransferMoneyData, bankAccount: BankAccount, callback: (BankingClientResponse) -> Unit) { + threadPool.runAsync { + callback(transferMoney(data, bankAccount)) + } + } + + open fun transferMoney(data: TransferMoneyData, bankAccount: BankAccount): BankingClientResponse { + val connection = connect() + + connection.handle?.let { handle -> + try { + createTransferCashJob(handle, data, bankAccount) + + val status = handle.execute() + + return BankingClientResponse(status.isOK, status.toString()) + } catch(e: Exception) { + log.error("Could not transfer cash for account $bankAccount" , e) + return BankingClientResponse(false, e.localizedMessage, e) + } + finally { + closeConnection(connection) + } + } + + return BankingClientResponse(false, "Could not connect", connection.error) + } + + protected open fun createTransferCashJob(handle: HBCIHandler, data: TransferMoneyData, bankAccount: BankAccount) { + val transferCashJob = handle.newJob("UebSEPA") + + val source = mapper.mapToKonto(bank, bankAccount) + val destination = mapper.mapToKonto(data) + val amount = Value(data.amount, "EUR") + + transferCashJob.setParam("src", source) + transferCashJob.setParam("dst", destination) + transferCashJob.setParam("btg", amount) + transferCashJob.setParam("usage", data.usage) + + transferCashJob.addToQueue() + } + + + override fun restoreData() { + // nothing to do for hbci4j + } + + + protected open fun connect(): ConnectResult { + return connect(credentials, HBCIVersion.HBCI_300) + } + + protected open fun connect(credentials: AccountCredentials, version: HBCIVersion): ConnectResult { + // HBCI4Java initialisieren + // In "props" koennen optional Kernel-Parameter abgelegt werden, die in der Klasse + // org.kapott.hbci.manager.HBCIUtils (oben im Javadoc) beschrieben sind. + val props = Properties() + HBCIUtils.init(props, HbciCallback(credentials, account, mapper, callback)) + + // In der Passport-Datei speichert HBCI4Java die Daten des Bankzugangs (Bankparameterdaten, Benutzer-Parameter, etc.). + // Die Datei kann problemlos geloescht werden. Sie wird beim naechsten mal automatisch neu erzeugt, + // wenn der Parameter "client.passport.PinTan.init" den Wert "1" hat (siehe unten). + // Wir speichern die Datei der Einfachheit halber im aktuellen Verzeichnis. + dataFolder.mkdirs() + val passportFile = File(dataFolder,"passport.dat") + + // Wir setzen die Kernel-Parameter zur Laufzeit. Wir koennten sie alternativ + // auch oben in "props" setzen. + HBCIUtils.setParam("client.passport.default", "PinTan") // Legt als Verfahren PIN/TAN fest. + HBCIUtils.setParam("client.passport.PinTan.filename", passportFile.absolutePath) + HBCIUtils.setParam("client.passport.PinTan.init", "1") + + var handle: HBCIHandler? = null + var passport: HBCIPassport? = null + + try { + // Erzeugen des Passport-Objektes. + passport = AbstractHBCIPassport.getInstance() + + // Konfigurieren des Passport-Objektes. + // Das kann alternativ auch alles ueber den Callback unten geschehen + + // Das Land. + passport.country = "DE" + + // Server-Adresse angeben. Koennen wir entweder manuell eintragen oder direkt von HBCI4Java ermitteln lassen + val info = HBCIUtils.getBankInfo(credentials.bankCode) + passport.host = info.pinTanAddress + + // TCP-Port des Servers. Bei PIN/TAN immer 443, da das ja ueber HTTPS laeuft. + passport.port = 443 + + // Art der Nachrichten-Codierung. Bei Chipkarte/Schluesseldatei wird + // "None" verwendet. Bei PIN/TAN kommt "Base64" zum Einsatz. + passport.filterType = "Base64" + + // Verbindung zum Server aufbauen + handle = HBCIHandler(version.getId(), passport) + + + } + catch(e: Exception) { + log.error("Could not connect to bank ${credentials.bankCode}", e) + closeConnection(handle, passport) + + return ConnectResult(false, e) + } + + return ConnectResult(true, null, handle, passport) + } + + protected open fun closeConnection(connection: ConnectResult) { + closeConnection(connection.handle, connection.passport) + } + + protected open fun closeConnection(handle: HBCIHandler?, passport: HBCIPassport?) { + // Sicherstellen, dass sowohl Passport als auch Handle nach Beendigung geschlossen werden. + try { + handle?.close() + + passport?.close() + + HBCIUtils.doneThread() // i hate static variables, here's one of the reasons why: Old callbacks and therefore credentials get stored in static variables and therefor always the first entered credentials have been used + } catch(e: Exception) { log.error("Could not close connection", e) } + } + +} \ No newline at end of file diff --git a/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/hbci4jBankingClientCreator.kt b/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/hbci4jBankingClientCreator.kt new file mode 100644 index 00000000..00242b2e --- /dev/null +++ b/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/hbci4jBankingClientCreator.kt @@ -0,0 +1,25 @@ +package net.dankito.banking + +import net.dankito.banking.ui.BankingClientCallback +import net.dankito.banking.ui.IBankingClient +import net.dankito.banking.ui.IBankingClientCreator +import net.dankito.fints.model.BankInfo +import net.dankito.utils.IThreadPool +import java.io.File + + +open class hbci4jBankingClientCreator : IBankingClientCreator { + + override fun createClient( + bankInfo: BankInfo, + customerId: String, + pin: String, + dataFolder: File, + threadPool: IThreadPool, + callback: BankingClientCallback + ): IBankingClient { + + return hbci4jBankingClient(bankInfo, customerId, pin, dataFolder, threadPool, callback) + } + +} \ No newline at end of file diff --git a/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/model/AccountCredentials.kt b/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/model/AccountCredentials.kt new file mode 100644 index 00000000..5e1f1d26 --- /dev/null +++ b/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/model/AccountCredentials.kt @@ -0,0 +1,8 @@ +package net.dankito.banking.model + + +open class AccountCredentials( + val bankCode: String, + val customerId: String, + var password: String +) \ No newline at end of file diff --git a/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/model/ConnectResult.kt b/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/model/ConnectResult.kt new file mode 100755 index 00000000..6b7d1688 --- /dev/null +++ b/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/model/ConnectResult.kt @@ -0,0 +1,12 @@ +package net.dankito.banking.model + +import org.kapott.hbci.manager.HBCIHandler +import org.kapott.hbci.passport.HBCIPassport + + +open class ConnectResult( + val successful: Boolean, + val error: Exception? = null, + val handle: HBCIHandler? = null, + val passport: HBCIPassport? = null +) \ No newline at end of file diff --git a/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/util/AccountTransactionMapper.kt b/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/util/AccountTransactionMapper.kt new file mode 100644 index 00000000..3c824493 --- /dev/null +++ b/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/util/AccountTransactionMapper.kt @@ -0,0 +1,162 @@ +package net.dankito.banking.util + +import net.dankito.banking.ui.model.AccountTransaction +import net.dankito.banking.ui.model.BankAccount +import org.kapott.hbci.GV_Result.GVRKUms +import org.slf4j.LoggerFactory +import java.math.BigDecimal +import java.text.SimpleDateFormat + + +open class AccountTransactionMapper { + + companion object { + protected val DateStartString = "DATUM " + protected val DateEndString = " UHR" + + protected val DateTimeFormat = SimpleDateFormat("dd.MM.yyyy,HH.mm") + + protected val DateFormat = SimpleDateFormat("dd.MM.yyyy,") + + private val log = LoggerFactory.getLogger(AccountTransactionMapper::class.java) + } + + + open fun mapAccountTransactions(bankAccount: BankAccount, result: GVRKUms): List { + val entries = ArrayList() + + result.flatData.forEach { transaction -> + entries.add(mapAccountingEntry(bankAccount, transaction)) + } + + log.debug("Retrieved ${result.flatData.size} accounting entries") + + return entries.sortedByDescending { it.bookingDate } + } + + protected open fun mapAccountingEntry(bankAccount: BankAccount, transaction: GVRKUms.UmsLine): AccountTransaction { + + val result = AccountTransaction(BigDecimal.valueOf(transaction.value.longValue).divide(BigDecimal.valueOf(100)), transaction.bdate, transaction.usage.joinToString(""), + if (transaction.other.name2.isNullOrBlank() == false) transaction.other.name + " " + transaction.other.name2 else transaction.other.name, + if (transaction.other.bic != null) transaction.other.bic else transaction.other.blz, + if (transaction.other.iban != null) transaction.other.iban else transaction.other.number, + transaction.text, BigDecimal.valueOf(transaction.saldo.value.longValue), transaction.value.curr, bankAccount) + +// mapUsage(transaction, result) + + return result + } + + /** + * From https://sites.google.com/a/crem-solutions.de/doku/version-2012-neu/buchhaltung/03-zahlungsverkehr/05-e-banking/technische-beschreibung-der-mt940-sta-datei: + * + * Weitere 4 Verwendungszwecke können zu den Feldschlüsseln 60 bis 63 eingestellt werden. + * Jeder Bezeichner [z.B. EREF+] muss am Anfang eines Subfeldes [z. B. ?21] stehen. + * Bei Längenüberschreitung wird im nachfolgenden Subfeld ohne Wiederholung des Bezeichners fortgesetzt. Bei Wechsel des Bezeichners ist ein neues Subfeld zu beginnen. + * Belegung in der nachfolgenden Reihenfolge, wenn vorhanden: + * EREF+[ Ende-zu-Ende Referenz ] (DD-AT10; CT-AT41 - Angabe verpflichtend; NOTPROVIDED wird nicht eingestellt.) + * KREF+[Kundenreferenz] + * MREF+[Mandatsreferenz] (DD-AT01 - Angabe verpflichtend) + * CRED+[Creditor Identifier] (DD-AT02 - Angabe verpflichtend bei SEPA-Lastschriften, nicht jedoch bei SEPA-Rücklastschriften) + * DEBT+[Originators Identification Code](CT-AT10- Angabe verpflichtend,) + * Entweder CRED oder DEBT + * + * optional zusätzlich zur Einstellung in Feld 61, Subfeld 9: + * + * COAM+ [Compensation Amount / Summe aus Auslagenersatz und Bearbeitungsprovision bei einer nationalen Rücklastschrift sowie optionalem Zinsausgleich.] + * OAMT+[Original Amount] Betrag der ursprünglichen Lastschrift + * + * SVWZ+[SEPA-Verwendungszweck] (DD-AT22; CT-AT05 -Angabe verpflichtend, nicht jedoch bei R-Transaktionen) + * ABWA+[Abweichender Überweisender] (CT-AT08) / Abweichender Zahlungsempfänger (DD-AT38) ] (optional) + * ABWE+[Abweichender Zahlungsemp-fänger (CT-AT28) / Abweichender Zahlungspflichtiger ((DD-AT15)] (optional) + */ +// protected open fun mapUsage(buchung: GVRKUms.UmsLine, entry: AccountingEntry) { +// var lastUsageLineType = UsageLineType.ContinuationFromLastLine +// var typeValue = "" +// +// buchung.usage.forEach { line -> +// val (type, adjustedString) = getUsageLineType(line, entry) +// +// if (type == UsageLineType.ContinuationFromLastLine) { +// typeValue += (if(adjustedString[0].isUpperCase()) " " else "") + adjustedString +// } +// else if (lastUsageLineType != type) { +// if (lastUsageLineType != UsageLineType.ContinuationFromLastLine) { +// setUsageLineValue(entry, lastUsageLineType, typeValue) +// } +// +// typeValue = adjustedString +// lastUsageLineType = type +// } +// +// tryToParseBookingDateFromUsageLine(entry, adjustedString, typeValue) +// } +// +// if(lastUsageLineType != UsageLineType.ContinuationFromLastLine) { +// setUsageLineValue(entry, lastUsageLineType, typeValue) +// } +// } +// +// protected open fun setUsageLineValue(entry: AccountingEntry, lastUsageLineType: UsageLineType, typeValue: String) { +// entry.parsedUsages.add(typeValue) +// +// when (lastUsageLineType) { +// UsageLineType.EREF -> entry.endToEndReference = typeValue +// UsageLineType.KREF -> entry.kundenreferenz = typeValue +// UsageLineType.MREF -> entry.mandatsreferenz = typeValue +// UsageLineType.CRED -> entry.creditorIdentifier = typeValue +// UsageLineType.DEBT -> entry.originatorsIdentificationCode = typeValue +// UsageLineType.COAM -> entry.compensationAmount = typeValue +// UsageLineType.OAMT -> entry.originalAmount = typeValue +// UsageLineType.SVWZ -> entry.sepaVerwendungszweck = typeValue +// UsageLineType.ABWA -> entry.abweichenderAuftraggeber = typeValue +// UsageLineType.ABWE -> entry.abweichenderZahlungsempfaenger = typeValue +// UsageLineType.NoSpecialType -> entry.usageWithNoSpecialType = typeValue +// } +// } +// +// protected open fun getUsageLineType(line: String, entry: AccountingEntry): Pair { +// return when { +// line.startsWith("EREF+") -> Pair(UsageLineType.EREF, line.substring(5)) +// line.startsWith("KREF+") -> Pair(UsageLineType.KREF, line.substring(5)) +// line.startsWith("MREF+") -> Pair(UsageLineType.MREF, line.substring(5)) +// line.startsWith("CRED+") -> Pair(UsageLineType.CRED, line.substring(5)) +// line.startsWith("DEBT+") -> Pair(UsageLineType.DEBT, line.substring(5)) +// line.startsWith("COAM+") -> Pair(UsageLineType.COAM, line.substring(5)) +// line.startsWith("OAMT+") -> Pair(UsageLineType.OAMT, line.substring(5)) +// line.startsWith("SVWZ+") -> Pair(UsageLineType.SVWZ, line.substring(5)) +// line.startsWith("ABWA+") -> Pair(UsageLineType.ABWA, line.substring(5)) +// line.startsWith("ABWE+") -> Pair(UsageLineType.ABWE, line.substring(5)) +// entry.usage.startsWith(line) -> Pair(UsageLineType.NoSpecialType, line) +// else -> Pair(UsageLineType.ContinuationFromLastLine, line) +// } +// } +// +// protected open fun tryToParseBookingDateFromUsageLine(entry: AccountingEntry, currentLine: String, typeLine: String) { +// if (currentLine.startsWith(DateStartString)) { +// tryToParseBookingDateFromUsageLine(entry, currentLine) +// } +// else if (typeLine.startsWith(DateStartString)) { +// tryToParseBookingDateFromUsageLine(entry, typeLine) +// } +// } +// +// protected open fun tryToParseBookingDateFromUsageLine(entry: AccountingEntry, line: String) { +// var dateString = line.replace(DateStartString, "") +// val index = dateString.indexOf(DateEndString) +// if (index > 0) { +// dateString = dateString.substring(0, index) +// } +// +// try { +// entry.bookingDate = DateTimeFormat.parse(dateString) +// } catch (e: Exception) { +// try { +// entry.bookingDate = DateFormat.parse(dateString) +// } catch (secondException: Exception) { +// log.debug("Could not parse '$dateString' from '$line' to a Date", e) +// } +// } +// } + +} \ No newline at end of file diff --git a/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/util/hbci4jModelMapper.kt b/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/util/hbci4jModelMapper.kt new file mode 100644 index 00000000..84638f48 --- /dev/null +++ b/hbci4jBankingClient/src/main/kotlin/net/dankito/banking/util/hbci4jModelMapper.kt @@ -0,0 +1,106 @@ +package net.dankito.banking.util + +import net.dankito.banking.ui.model.Account +import net.dankito.banking.ui.model.Bank +import net.dankito.banking.ui.model.BankAccount +import net.dankito.banking.ui.model.BankAccountType +import net.dankito.banking.ui.model.parameters.TransferMoneyData +import net.dankito.banking.ui.model.tan.TanProcedureType +import org.kapott.hbci.passport.HBCIPassport +import org.kapott.hbci.structures.Konto +import java.math.BigDecimal + + +open class hbci4jModelMapper { + + open fun mapToKonto(bank: Bank, bankAccount: BankAccount): Konto { + val konto = Konto("DE", bank.bankCode, bankAccount.identifier, bankAccount.subAccountNumber) + + konto.name = bank.name + konto.iban = bankAccount.iban + konto.bic = bank.bic + + return konto + } + + open fun mapToKonto(data: TransferMoneyData): Konto { + return mapToKonto(data.creditorName, data.creditorIban, data.creditorBic) + } + + open fun mapToKonto(accountHolderName: String, iban: String, bic: String): Konto { + val konto = Konto() + + konto.name = accountHolderName + konto.iban = iban + konto.bic = bic + + return konto + } + + + open fun mapBankAccounts(account: Account, bankAccounts: Array, passport: HBCIPassport): List { + return bankAccounts.map { bankAccount -> + val iban = if (bankAccount.iban.isNullOrBlank() == false) bankAccount.iban else passport.upd.getProperty("KInfo.iban") ?: "" + + BankAccount(account, bankAccount.number, + if (bankAccount.name2.isNullOrBlank() == false) bankAccount.name + " " + bankAccount.name2 else bankAccount.name, + iban, bankAccount.subnumber, BigDecimal.ZERO, bankAccount.curr, mapBankAccountType(bankAccount), + bankAccount.allowedGVs.contains("HKKAZ"), bankAccount.allowedGVs.contains("HKSAL"), bankAccount.allowedGVs.contains("HKCCS")) + } + } + + open fun mapBankAccountType(bankAccount: Konto): BankAccountType { + val type = bankAccount.acctype + + return when { + type.length == 1 -> BankAccountType.Girokonto + type.startsWith("1") -> BankAccountType.Sparkonto + type.startsWith("2") -> BankAccountType.Festgeldkonto + type.startsWith("3") -> BankAccountType.Wertpapierdepot + type.startsWith("4") -> BankAccountType.Darlehenskonto + type.startsWith("5") -> BankAccountType.Kreditkartenkonto + type.startsWith("6") -> BankAccountType.FondsDepot + type.startsWith("7") -> BankAccountType.Bausparvertrag + type.startsWith("8") -> BankAccountType.Versicherungsvertrag + type.startsWith("9") -> BankAccountType.Sonstige + else -> BankAccountType.Sonstige + } + } + + + open fun mapTanProcedures(tanProceduresString: String): List { + return tanProceduresString.split('|') + .map { mapTanProcedure(it) } + .filterNotNull() + } + + open fun mapTanProcedure(tanProcedureString: String): net.dankito.banking.ui.model.tan.TanProcedure? { + val parts = tanProcedureString.split(':') + + if (parts.size > 1) { + val code = parts[0] + val displayName = parts[1] + val displayNameLowerCase = displayName.toLowerCase() + + return when { + displayNameLowerCase.contains("chiptan") -> { + if (displayNameLowerCase.contains("qr")) { + net.dankito.banking.ui.model.tan.TanProcedure(displayName, TanProcedureType.ChipTanQrCode, code) + } + else { + net.dankito.banking.ui.model.tan.TanProcedure(displayName, TanProcedureType.ChipTanOptisch, code) + } + } + + displayNameLowerCase.contains("sms") -> net.dankito.banking.ui.model.tan.TanProcedure(displayName, TanProcedureType.SmsTan, code) + displayNameLowerCase.contains("push") -> net.dankito.banking.ui.model.tan.TanProcedure(displayName, TanProcedureType.PushTan, code) + + // we filter out iTAN and Einschritt-Verfahren as they are not permitted anymore according to PSD2 + else -> null + } + } + + return null + } + +} \ No newline at end of file