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(':fints4javaBankingClient')
implementation project(':hbci4jBankingClient')
implementation project(':BankingPersistenceJson')

View File

@ -1,13 +1,12 @@
package net.dankito.banking.javafx.dialogs.mainwindow
import javafx.scene.control.SplitPane
import net.dankito.banking.fints4javaBankingClientCreator
import net.dankito.banking.hbci4jBankingClientCreator
import net.dankito.banking.persistence.BankingPersistenceJson
import net.dankito.banking.ui.javafx.RouterJavaFx
import net.dankito.banking.ui.javafx.controls.AccountTransactionsView
import net.dankito.banking.ui.javafx.controls.AccountsView
import net.dankito.banking.ui.javafx.dialogs.mainwindow.controls.MainMenuBar
import net.dankito.banking.ui.javafx.util.Base64ServiceJava8
import net.dankito.banking.ui.presenter.MainWindowPresenter
import tornadofx.*
import tornadofx.FX.Companion.messages
@ -18,7 +17,8 @@ class MainWindow : View(messages["application.title"]) {
private val dataFolder = File("data", "accounts")
private val presenter = MainWindowPresenter(fints4javaBankingClientCreator(), dataFolder, BankingPersistenceJson(File(dataFolder, "accounts.json")), Base64ServiceJava8(), RouterJavaFx())
// private val presenter = MainWindowPresenter(fints4javaBankingClientCreator(OkHttpWebClient(), Base64ServiceJava8()), dataFolder, BankingPersistenceJson(File(dataFolder, "accounts.json")), RouterJavaFx())
private val presenter = MainWindowPresenter(hbci4jBankingClientCreator(), dataFolder, BankingPersistenceJson(File(dataFolder, "accounts.json")), RouterJavaFx())

View File

@ -1,20 +1,19 @@
package net.dankito.banking.ui
import net.dankito.banking.util.IBase64Service
import net.dankito.fints.model.BankInfo
import net.dankito.utils.IThreadPool
import net.dankito.utils.web.client.IWebClient
import java.io.File
interface IBankingClientCreator {
fun createClient(bankInfo: BankInfo, // TODO: create own value object to get rid off fints4java dependency
customerId: String,
pin: String,
webClient: IWebClient, // TODO: wrap away JavaUtils IWebClient
base64Service: IBase64Service,
threadPool: IThreadPool, // TODO: wrap away JavaUtils IThreadPool
callback: BankingClientCallback
fun createClient(
bankInfo: BankInfo, // TODO: create own value object to get rid off fints4java dependency
customerId: String,
pin: String,
dataFolder: File,
threadPool: IThreadPool, // TODO: wrap away JavaUtils IWebClient
callback: BankingClientCallback
): IBankingClient
}

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.TanChallenge
import net.dankito.banking.ui.model.tan.TanGeneratorTanMedium
import net.dankito.banking.util.IBase64Service
import net.dankito.fints.banks.BankFinder
import net.dankito.fints.model.BankInfo
import net.dankito.utils.IThreadPool
import net.dankito.utils.ThreadPool
import net.dankito.utils.extensions.ofMaxLength
import net.dankito.utils.web.client.IWebClient
import net.dankito.utils.web.client.OkHttpWebClient
import org.slf4j.LoggerFactory
import java.io.File
import java.math.BigDecimal
@ -35,9 +32,7 @@ open class MainWindowPresenter(
protected val bankingClientCreator: IBankingClientCreator,
protected val dataFolder: File,
protected val persister: IBankingPersistence,
protected val base64Service: IBase64Service,
protected val router: IRouter,
protected val webClient: IWebClient = OkHttpWebClient(),
protected val threadPool: IThreadPool = ThreadPool()
) {
@ -107,7 +102,7 @@ open class MainWindowPresenter(
val bankInfo = BankInfo(bank.name, bank.bankCode, bank.bic, "", "", "", bank.finTsServerAddress, "FinTS V3.0", null)
val newClient = bankingClientCreator.createClient(bankInfo, account.customerId, account.password,
dataFolder, webClient, base64Service, threadPool, callback)
dataFolder, threadPool, callback)
try {
newClient.restoreData()
@ -135,7 +130,7 @@ open class MainWindowPresenter(
// TODO: move BankInfo out of fints4javaLib
open fun addAccountAsync(bankInfo: BankInfo, customerId: String, pin: String, callback: (AddAccountResponse) -> Unit) {
val newClient = bankingClientCreator.createClient(bankInfo, customerId, pin, dataFolder, webClient, base64Service, threadPool, this.callback)
val newClient = bankingClientCreator.createClient(bankInfo, customerId, pin, dataFolder, threadPool, this.callback)
newClient.addAccountAsync { response ->
val account = response.account

View File

@ -1,4 +1,3 @@
// TODO: move to versions.gradle
ext {
appVersionName = '0.1.0-SNAPSHOT'
@ -9,6 +8,10 @@ ext {
javaUtilsVersion = '1.0.9'
hbci4jVersion = '3.1.37'
androidUtilsVersion = '1.1.0'
clansFloatingActionButtonVersion = '1.6.4'

View File

@ -21,6 +21,7 @@ import net.dankito.banking.fints4javaBankingClientCreator
import net.dankito.banking.persistence.BankingPersistenceJson
import net.dankito.banking.ui.model.Account
import net.dankito.banking.ui.presenter.MainWindowPresenter
import net.dankito.utils.web.client.OkHttpWebClient
import org.slf4j.LoggerFactory
import java.io.File
@ -47,8 +48,8 @@ class MainActivity : AppCompatActivity() {
val dataFolder = File(this.filesDir, "data/accounts")
presenter = MainWindowPresenter(fints4javaBankingClientCreator(), dataFolder,
BankingPersistenceJson(File(dataFolder, "accounts.json")), Base64ServiceAndroid(), RouterAndroid(this))
presenter = MainWindowPresenter(fints4javaBankingClientCreator(OkHttpWebClient(), Base64ServiceAndroid()), dataFolder,
BankingPersistenceJson(File(dataFolder, "accounts.json")), RouterAndroid(this))
initUi()
}

View File

@ -8,24 +8,24 @@ import net.dankito.banking.util.UiCommonBase64ServiceWrapper
import net.dankito.fints.model.BankInfo
import net.dankito.utils.IThreadPool
import net.dankito.utils.web.client.IWebClient
import net.dankito.utils.web.client.OkHttpWebClient
import java.io.File
open class fints4javaBankingClientCreator : IBankingClientCreator {
open class fints4javaBankingClientCreator(
protected val webClient: IWebClient,
protected val base64Service: IBase64Service
) : IBankingClientCreator {
override fun createClient(
bankInfo: BankInfo,
customerId: String,
pin: String,
dataFolder: File,
webClient: IWebClient,
base64Service: IBase64Service,
threadPool: IThreadPool,
callback: BankingClientCallback
): IBankingClient {
return fints4javaBankingClient(bankInfo, customerId, pin, dataFolder, OkHttpWebClient(), UiCommonBase64ServiceWrapper(base64Service), threadPool, callback)
return fints4javaBankingClient(bankInfo, customerId, pin, dataFolder, webClient, UiCommonBase64ServiceWrapper(base64Service), threadPool, callback)
}
}

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