Implemented hbci4jBankingClient

This commit is contained in:
dankl 2020-01-26 12:23:02 +01:00 committed by dankito
parent 5b5173132f
commit cfcad3f5e0
15 changed files with 945 additions and 27 deletions

View File

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

View File

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

View File

@ -1,20 +1,19 @@
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(
customerId: String, bankInfo: BankInfo, // TODO: create own value object to get rid off fints4java dependency
pin: String, customerId: String,
webClient: IWebClient, // TODO: wrap away JavaUtils IWebClient pin: String,
base64Service: IBase64Service, dataFolder: File,
threadPool: IThreadPool, // TODO: wrap away JavaUtils IThreadPool threadPool: IThreadPool, // TODO: wrap away JavaUtils IWebClient
callback: BankingClientCallback callback: BankingClientCallback
): IBankingClient ): IBankingClient
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
package net.dankito.banking.model
open class AccountCredentials(
val bankCode: String,
val customerId: String,
var password: String
)

View File

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

View File

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

View File

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