Implemented hbci4jBankingClient
This commit is contained in:
parent
5b5173132f
commit
cfcad3f5e0
|
@ -22,6 +22,7 @@ dependencies {
|
||||||
implementation project(':BankingJavaFxControls')
|
implementation project(':BankingJavaFxControls')
|
||||||
|
|
||||||
implementation project(':fints4javaBankingClient')
|
implementation project(':fints4javaBankingClient')
|
||||||
|
implementation project(':hbci4jBankingClient')
|
||||||
|
|
||||||
implementation project(':BankingPersistenceJson')
|
implementation project(':BankingPersistenceJson')
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
package net.dankito.banking.javafx.dialogs.mainwindow
|
package net.dankito.banking.javafx.dialogs.mainwindow
|
||||||
|
|
||||||
import javafx.scene.control.SplitPane
|
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.persistence.BankingPersistenceJson
|
||||||
import net.dankito.banking.ui.javafx.RouterJavaFx
|
import net.dankito.banking.ui.javafx.RouterJavaFx
|
||||||
import net.dankito.banking.ui.javafx.controls.AccountTransactionsView
|
import net.dankito.banking.ui.javafx.controls.AccountTransactionsView
|
||||||
import net.dankito.banking.ui.javafx.controls.AccountsView
|
import net.dankito.banking.ui.javafx.controls.AccountsView
|
||||||
import net.dankito.banking.ui.javafx.dialogs.mainwindow.controls.MainMenuBar
|
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 net.dankito.banking.ui.presenter.MainWindowPresenter
|
||||||
import tornadofx.*
|
import tornadofx.*
|
||||||
import tornadofx.FX.Companion.messages
|
import tornadofx.FX.Companion.messages
|
||||||
|
@ -18,7 +17,8 @@ class MainWindow : View(messages["application.title"]) {
|
||||||
|
|
||||||
private val dataFolder = File("data", "accounts")
|
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())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,18 @@
|
||||||
package net.dankito.banking.ui
|
package net.dankito.banking.ui
|
||||||
|
|
||||||
import net.dankito.banking.util.IBase64Service
|
|
||||||
import net.dankito.fints.model.BankInfo
|
import net.dankito.fints.model.BankInfo
|
||||||
import net.dankito.utils.IThreadPool
|
import net.dankito.utils.IThreadPool
|
||||||
import net.dankito.utils.web.client.IWebClient
|
import java.io.File
|
||||||
|
|
||||||
|
|
||||||
interface IBankingClientCreator {
|
interface IBankingClientCreator {
|
||||||
|
|
||||||
fun createClient(bankInfo: BankInfo, // TODO: create own value object to get rid off fints4java dependency
|
fun createClient(
|
||||||
|
bankInfo: BankInfo, // TODO: create own value object to get rid off fints4java dependency
|
||||||
customerId: String,
|
customerId: String,
|
||||||
pin: String,
|
pin: String,
|
||||||
webClient: IWebClient, // TODO: wrap away JavaUtils IWebClient
|
dataFolder: File,
|
||||||
base64Service: IBase64Service,
|
threadPool: IThreadPool, // TODO: wrap away JavaUtils IWebClient
|
||||||
threadPool: IThreadPool, // TODO: wrap away JavaUtils IThreadPool
|
|
||||||
callback: BankingClientCallback
|
callback: BankingClientCallback
|
||||||
): IBankingClient
|
): IBankingClient
|
||||||
|
|
||||||
|
|
|
@ -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.EnterTanResult
|
||||||
import net.dankito.banking.ui.model.tan.TanChallenge
|
import net.dankito.banking.ui.model.tan.TanChallenge
|
||||||
import net.dankito.banking.ui.model.tan.TanGeneratorTanMedium
|
import net.dankito.banking.ui.model.tan.TanGeneratorTanMedium
|
||||||
import net.dankito.banking.util.IBase64Service
|
|
||||||
import net.dankito.fints.banks.BankFinder
|
import net.dankito.fints.banks.BankFinder
|
||||||
import net.dankito.fints.model.BankInfo
|
import net.dankito.fints.model.BankInfo
|
||||||
import net.dankito.utils.IThreadPool
|
import net.dankito.utils.IThreadPool
|
||||||
import net.dankito.utils.ThreadPool
|
import net.dankito.utils.ThreadPool
|
||||||
import net.dankito.utils.extensions.ofMaxLength
|
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 org.slf4j.LoggerFactory
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
@ -35,9 +32,7 @@ open class MainWindowPresenter(
|
||||||
protected val bankingClientCreator: IBankingClientCreator,
|
protected val bankingClientCreator: IBankingClientCreator,
|
||||||
protected val dataFolder: File,
|
protected val dataFolder: File,
|
||||||
protected val persister: IBankingPersistence,
|
protected val persister: IBankingPersistence,
|
||||||
protected val base64Service: IBase64Service,
|
|
||||||
protected val router: IRouter,
|
protected val router: IRouter,
|
||||||
protected val webClient: IWebClient = OkHttpWebClient(),
|
|
||||||
protected val threadPool: IThreadPool = ThreadPool()
|
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 bankInfo = BankInfo(bank.name, bank.bankCode, bank.bic, "", "", "", bank.finTsServerAddress, "FinTS V3.0", null)
|
||||||
|
|
||||||
val newClient = bankingClientCreator.createClient(bankInfo, account.customerId, account.password,
|
val newClient = bankingClientCreator.createClient(bankInfo, account.customerId, account.password,
|
||||||
dataFolder, webClient, base64Service, threadPool, callback)
|
dataFolder, threadPool, callback)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
newClient.restoreData()
|
newClient.restoreData()
|
||||||
|
@ -135,7 +130,7 @@ open class MainWindowPresenter(
|
||||||
// TODO: move BankInfo out of fints4javaLib
|
// TODO: move BankInfo out of fints4javaLib
|
||||||
open fun addAccountAsync(bankInfo: BankInfo, customerId: String, pin: String, callback: (AddAccountResponse) -> Unit) {
|
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 ->
|
newClient.addAccountAsync { response ->
|
||||||
val account = response.account
|
val account = response.account
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
// TODO: move to versions.gradle
|
// TODO: move to versions.gradle
|
||||||
ext {
|
ext {
|
||||||
appVersionName = '0.1.0-SNAPSHOT'
|
appVersionName = '0.1.0-SNAPSHOT'
|
||||||
|
@ -9,6 +8,10 @@ ext {
|
||||||
|
|
||||||
javaUtilsVersion = '1.0.9'
|
javaUtilsVersion = '1.0.9'
|
||||||
|
|
||||||
|
|
||||||
|
hbci4jVersion = '3.1.37'
|
||||||
|
|
||||||
|
|
||||||
androidUtilsVersion = '1.1.0'
|
androidUtilsVersion = '1.1.0'
|
||||||
|
|
||||||
clansFloatingActionButtonVersion = '1.6.4'
|
clansFloatingActionButtonVersion = '1.6.4'
|
||||||
|
|
|
@ -21,6 +21,7 @@ import net.dankito.banking.fints4javaBankingClientCreator
|
||||||
import net.dankito.banking.persistence.BankingPersistenceJson
|
import net.dankito.banking.persistence.BankingPersistenceJson
|
||||||
import net.dankito.banking.ui.model.Account
|
import net.dankito.banking.ui.model.Account
|
||||||
import net.dankito.banking.ui.presenter.MainWindowPresenter
|
import net.dankito.banking.ui.presenter.MainWindowPresenter
|
||||||
|
import net.dankito.utils.web.client.OkHttpWebClient
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
@ -47,8 +48,8 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
val dataFolder = File(this.filesDir, "data/accounts")
|
val dataFolder = File(this.filesDir, "data/accounts")
|
||||||
|
|
||||||
presenter = MainWindowPresenter(fints4javaBankingClientCreator(), dataFolder,
|
presenter = MainWindowPresenter(fints4javaBankingClientCreator(OkHttpWebClient(), Base64ServiceAndroid()), dataFolder,
|
||||||
BankingPersistenceJson(File(dataFolder, "accounts.json")), Base64ServiceAndroid(), RouterAndroid(this))
|
BankingPersistenceJson(File(dataFolder, "accounts.json")), RouterAndroid(this))
|
||||||
|
|
||||||
initUi()
|
initUi()
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,24 +8,24 @@ import net.dankito.banking.util.UiCommonBase64ServiceWrapper
|
||||||
import net.dankito.fints.model.BankInfo
|
import net.dankito.fints.model.BankInfo
|
||||||
import net.dankito.utils.IThreadPool
|
import net.dankito.utils.IThreadPool
|
||||||
import net.dankito.utils.web.client.IWebClient
|
import net.dankito.utils.web.client.IWebClient
|
||||||
import net.dankito.utils.web.client.OkHttpWebClient
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
|
||||||
open class fints4javaBankingClientCreator : IBankingClientCreator {
|
open class fints4javaBankingClientCreator(
|
||||||
|
protected val webClient: IWebClient,
|
||||||
|
protected val base64Service: IBase64Service
|
||||||
|
) : IBankingClientCreator {
|
||||||
|
|
||||||
override fun createClient(
|
override fun createClient(
|
||||||
bankInfo: BankInfo,
|
bankInfo: BankInfo,
|
||||||
customerId: String,
|
customerId: String,
|
||||||
pin: String,
|
pin: String,
|
||||||
dataFolder: File,
|
dataFolder: File,
|
||||||
webClient: IWebClient,
|
|
||||||
base64Service: IBase64Service,
|
|
||||||
threadPool: IThreadPool,
|
threadPool: IThreadPool,
|
||||||
callback: BankingClientCallback
|
callback: BankingClientCallback
|
||||||
): IBankingClient {
|
): IBankingClient {
|
||||||
|
|
||||||
return fints4javaBankingClient(bankInfo, customerId, pin, dataFolder, OkHttpWebClient(), UiCommonBase64ServiceWrapper(base64Service), threadPool, callback)
|
return fints4javaBankingClient(bankInfo, customerId, pin, dataFolder, webClient, UiCommonBase64ServiceWrapper(base64Service), threadPool, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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<Any>?) {
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<GetTransactionsResponse>()
|
||||||
|
val balances = mutableMapOf<BankAccount, BigDecimal>()
|
||||||
|
val bookedTransactions = mutableMapOf<BankAccount, List<AccountTransaction>>()
|
||||||
|
val unbookedTransactions = mutableMapOf<BankAccount, List<Any>>()
|
||||||
|
|
||||||
|
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<HBCIJob?, HBCIJob, HBCIExecStatus> {
|
||||||
|
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) }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package net.dankito.banking.model
|
||||||
|
|
||||||
|
|
||||||
|
open class AccountCredentials(
|
||||||
|
val bankCode: String,
|
||||||
|
val customerId: String,
|
||||||
|
var password: String
|
||||||
|
)
|
|
@ -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
|
||||||
|
)
|
|
@ -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<AccountTransaction> {
|
||||||
|
val entries = ArrayList<AccountTransaction>()
|
||||||
|
|
||||||
|
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<UsageLineType, String> {
|
||||||
|
// 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)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
}
|
|
@ -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<out Konto>, passport: HBCIPassport): List<BankAccount> {
|
||||||
|
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<net.dankito.banking.ui.model.tan.TanProcedure> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue