2020-01-26 11:23:02 +00:00
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
2020-05-16 20:51:51 +00:00
import net.dankito.banking.ui.model.*
2020-09-11 10:25:05 +00:00
import net.dankito.banking.ui.model.mapper.IModelCreator
2020-01-26 11:23:02 +00:00
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
2020-07-10 12:03:08 +00:00
import net.dankito.banking.util.*
2020-01-26 11:23:02 +00:00
import net.dankito.utils.ThreadPool
2020-08-16 21:55:24 +00:00
import net.dankito.utils.multiplatform.*
2020-07-12 10:14:56 +00:00
import net.dankito.utils.multiplatform.Date
2020-01-26 11:23:02 +00:00
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.text.SimpleDateFormat
import java.util.*
open class hbci4jBankingClient (
2020-09-22 04:06:11 +00:00
protected val bank : TypedBankData ,
2020-09-11 10:25:05 +00:00
modelCreator : IModelCreator ,
2020-01-26 11:23:02 +00:00
protected val dataFolder : File ,
2020-07-10 12:03:08 +00:00
protected val asyncRunner : IAsyncRunner = ThreadPoolAsyncRunner ( ThreadPool ( ) ) ,
2020-01-26 11:23:02 +00:00
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 )
}
2020-09-22 04:06:11 +00:00
protected val credentials = AccountCredentials ( bank )
2020-01-26 11:23:02 +00:00
2020-09-11 10:25:05 +00:00
protected val mapper = hbci4jModelMapper ( modelCreator )
2020-01-26 11:23:02 +00:00
2020-09-11 10:25:05 +00:00
protected val accountTransactionMapper = AccountTransactionMapper ( modelCreator )
2020-01-26 11:23:02 +00:00
2020-05-16 20:51:51 +00:00
override val messageLogWithoutSensitiveData : List < MessageLogEntry > = listOf ( ) // TODO: implement
2020-01-26 11:23:02 +00:00
override fun addAccountAsync ( callback : ( AddAccountResponse ) -> Unit ) {
2020-07-10 12:03:08 +00:00
asyncRunner . runAsync {
2020-01-26 11:23:02 +00:00
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 " )
2020-09-22 04:06:11 +00:00
return AddAccountResponse ( bank , " Keine Konten ermittelbar " ) // TODO: translate
2020-01-26 11:23:02 +00:00
}
2020-09-22 04:06:11 +00:00
this . bank . accounts = mapper . mapAccounts ( bank , accounts , passport )
2020-01-26 11:23:02 +00:00
2020-09-22 04:06:11 +00:00
return tryToRetrieveAccountTransactionsForAddedAccounts ( bank )
2020-01-26 11:23:02 +00:00
}
}
2020-09-22 04:06:11 +00:00
return AddAccountResponse ( bank , connection . error ?. getInnerExceptionMessage ( ) ?: " Could not connect " )
2020-01-26 11:23:02 +00:00
}
2020-09-22 04:06:11 +00:00
protected open fun tryToRetrieveAccountTransactionsForAddedAccounts ( bank : TypedBankData ) : AddAccountResponse {
2020-09-19 00:35:57 +00:00
var userCancelledAction = false
2020-09-22 04:06:11 +00:00
val retrievedData = bank . accounts . map { account ->
2020-09-19 00:35:57 +00:00
if ( account . supportsRetrievingAccountTransactions ) {
val response = getTransactionsOfLast90Days ( account )
2020-09-19 00:50:23 +00:00
2020-09-19 00:35:57 +00:00
if ( response . userCancelledAction ) {
userCancelledAction = true
}
response . retrievedData . first ( )
}
else {
2020-09-30 00:46:07 +00:00
RetrievedAccountData . unsuccessful ( account )
2020-01-26 11:23:02 +00:00
}
}
2020-09-30 00:46:07 +00:00
return AddAccountResponse ( bank , retrievedData , null , false , userCancelledAction )
2020-01-26 11:23:02 +00:00
}
/ * *
2020-09-22 04:06:11 +00:00
* According to PSD2 for the account transactions of the last 90 days the two - factor authorization does not have to
2020-01-26 11:23:02 +00:00
* be applied . It depends on the bank if they request a second factor or not .
*
2020-09-22 04:06:11 +00:00
* So we simply try to retrieve at account transactions of the last 90 days and see if a second factor is required
2020-01-26 11:23:02 +00:00
* or not .
* /
2020-09-22 04:06:11 +00:00
open fun getTransactionsOfLast90DaysAsync ( account : TypedBankAccount , callback : ( GetTransactionsResponse ) -> Unit ) {
2020-07-10 12:03:08 +00:00
asyncRunner . runAsync {
2020-09-22 04:06:11 +00:00
callback ( getTransactionsOfLast90Days ( account ) )
2020-01-26 11:23:02 +00:00
}
}
/ * *
2020-09-22 04:06:11 +00:00
* According to PSD2 for the account transactions of the last 90 days the two - factor authorization does not have to
2020-01-26 11:23:02 +00:00
* be applied . It depends on the bank if they request a second factor or not .
*
2020-09-22 04:06:11 +00:00
* So we simply try to retrieve at account transactions of the last 90 days and see if a second factor is required
2020-01-26 11:23:02 +00:00
* or not .
* /
2020-09-22 04:06:11 +00:00
open fun getTransactionsOfLast90Days ( account : TypedBankAccount ) : GetTransactionsResponse {
2020-09-22 01:59:59 +00:00
val ninetyDaysAgo = Date ( Date . today . time - NinetyDaysInMilliseconds )
2020-01-26 11:23:02 +00:00
2020-09-22 04:06:11 +00:00
return getTransactions ( GetTransactionsParameter ( account , account . supportsRetrievingBalance , ninetyDaysAgo ) ) // TODO: implement abortIfTanIsRequired
2020-01-26 11:23:02 +00:00
}
2020-09-19 02:05:34 +00:00
override fun getTransactionsAsync ( parameter : GetTransactionsParameter , callback : ( GetTransactionsResponse ) -> Unit ) {
2020-07-10 12:03:08 +00:00
asyncRunner . runAsync {
2020-09-19 02:05:34 +00:00
callback ( getTransactions ( parameter ) )
2020-01-26 11:23:02 +00:00
}
}
2020-09-19 02:05:34 +00:00
protected open fun getTransactions ( parameter : GetTransactionsParameter ) : GetTransactionsResponse {
2020-01-26 11:23:02 +00:00
val connection = connect ( )
2020-09-19 02:05:34 +00:00
val account = parameter . account
2020-01-26 11:23:02 +00:00
connection . handle ?. let { handle ->
try {
2020-09-22 04:06:11 +00:00
val ( nullableBalanceJob , accountTransactionsJob , status ) = executeJobsForGetAccountTransactions ( handle , parameter )
2020-01-26 11:23:02 +00:00
// Pruefen, ob die Kommunikation mit der Bank grundsaetzlich geklappt hat
if ( ! status . isOK ) {
2020-09-19 00:35:57 +00:00
log . error ( " Could not connect to bank ${credentials.bankCode} $status : ${status.errorString} " )
2020-09-19 01:15:56 +00:00
return GetTransactionsResponse ( account , " Could not connect to bank ${credentials.bankCode} : $status " )
2020-01-26 11:23:02 +00:00
}
// Auswertung des Saldo-Abrufs.
2020-07-12 10:14:56 +00:00
var balance = BigDecimal . Zero
2020-01-26 11:23:02 +00:00
if ( parameter . alsoRetrieveBalance && nullableBalanceJob != null ) {
val balanceResult = nullableBalanceJob . jobResult as GVRSaldoReq
if ( balanceResult . isOK == false ) {
2020-09-19 01:15:56 +00:00
log . error ( " Could not fetch balance of bank account $account : $balanceResult " , balanceResult . getJobStatus ( ) . exceptions )
return GetTransactionsResponse ( account , " Could not fetch balance of bank account $account : $balanceResult " )
2020-01-26 11:23:02 +00:00
}
2020-07-12 10:14:56 +00:00
balance = balanceResult . entries [ 0 ] . ready . value . bigDecimalValue . toBigDecimal ( )
2020-01-26 11:23:02 +00:00
}
// 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 ) {
2020-09-19 01:15:56 +00:00
log . error ( " Could not get fetch account transactions of bank account $account : $result " , result . getJobStatus ( ) . exceptions )
return GetTransactionsResponse ( account , " Could not fetch account transactions of bank account $account : $result " )
2020-01-26 11:23:02 +00:00
}
2020-09-19 01:23:21 +00:00
return GetTransactionsResponse ( RetrievedAccountData ( account , true , balance . toBigDecimal ( ) ,
2020-09-22 04:06:11 +00:00
accountTransactionMapper . mapTransactions ( account , result ) , listOf ( ) , parameter . fromDate , parameter . toDate ) )
2020-01-26 11:23:02 +00:00
}
catch ( e : Exception ) {
2020-09-22 04:06:11 +00:00
log . error ( " Could not get account transactions for bank ${credentials.bankCode} " , e )
2020-09-19 01:15:56 +00:00
return GetTransactionsResponse ( account , e . getInnerExceptionMessage ( ) )
2020-01-26 11:23:02 +00:00
}
finally {
closeConnection ( connection )
}
}
closeConnection ( connection )
2020-09-19 01:15:56 +00:00
return GetTransactionsResponse ( account , connection . error ?. getInnerExceptionMessage ( ) ?: " Could not connect " )
2020-01-26 11:23:02 +00:00
}
2020-09-22 04:06:11 +00:00
protected open fun executeJobsForGetAccountTransactions ( handle : HBCIHandler , parameter : GetTransactionsParameter ) : Triple < HBCIJob ? , HBCIJob , HBCIExecStatus > {
2020-09-19 02:05:34 +00:00
val konto = mapper . mapToKonto ( parameter . account )
2020-01-26 11:23:02 +00:00
// 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 )
}
2020-09-02 14:54:33 +00:00
override fun transferMoneyAsync ( data : TransferMoneyData , callback : ( BankingClientResponse ) -> Unit ) {
2020-07-10 12:03:08 +00:00
asyncRunner . runAsync {
2020-09-02 14:54:33 +00:00
callback ( transferMoney ( data ) )
2020-01-26 11:23:02 +00:00
}
}
2020-09-02 14:54:33 +00:00
open fun transferMoney ( data : TransferMoneyData ) : BankingClientResponse {
2020-01-26 11:23:02 +00:00
val connection = connect ( )
connection . handle ?. let { handle ->
try {
2020-09-02 14:54:33 +00:00
createTransferCashJob ( handle , data )
2020-01-26 11:23:02 +00:00
val status = handle . execute ( )
return BankingClientResponse ( status . isOK , status . toString ( ) )
} catch ( e : Exception ) {
2020-09-02 14:54:33 +00:00
log . error ( " Could not transfer cash for account ${data.account} " , e )
2020-08-16 21:55:24 +00:00
return BankingClientResponse ( false , e . getInnerExceptionMessage ( ) )
2020-01-26 11:23:02 +00:00
}
finally {
closeConnection ( connection )
}
}
2020-09-19 01:15:56 +00:00
return BankingClientResponse ( false , connection . error ?. getInnerExceptionMessage ( ) ?: " Could not connect " )
2020-01-26 11:23:02 +00:00
}
2020-09-02 14:54:33 +00:00
protected open fun createTransferCashJob ( handle : HBCIHandler , data : TransferMoneyData ) {
2020-09-24 00:53:09 +00:00
// TODO: implement real-time transfer
2020-01-26 11:23:02 +00:00
val transferCashJob = handle . newJob ( " UebSEPA " )
2020-09-02 14:54:33 +00:00
val source = mapper . mapToKonto ( data . account )
2020-01-26 11:23:02 +00:00
val destination = mapper . mapToKonto ( data )
val amount = Value ( data . amount , " EUR " )
transferCashJob . setParam ( " src " , source )
transferCashJob . setParam ( " dst " , destination )
transferCashJob . setParam ( " btg " , amount )
2020-09-24 00:53:09 +00:00
transferCashJob . setParam ( " usage " , data . reference )
2020-01-26 11:23:02 +00:00
transferCashJob . addToQueue ( )
}
2020-09-22 04:06:11 +00:00
override fun dataChanged ( bank : TypedBankData ) {
2020-09-24 02:22:40 +00:00
if ( bank . bankCode != credentials . bankCode || bank . userName != credentials . customerId || bank . password != credentials . password ) {
2020-09-08 14:25:28 +00:00
getPassportFile ( credentials ) . delete ( )
}
2020-09-22 04:06:11 +00:00
credentials . bankCode = bank . bankCode
2020-09-24 02:22:40 +00:00
credentials . customerId = bank . userName
2020-09-22 04:06:11 +00:00
credentials . password = bank . password
2020-09-08 14:25:28 +00:00
}
2020-09-22 04:06:11 +00:00
override fun deletedBank ( bank : TypedBankData , wasLastAccountWithThisCredentials : Boolean ) {
2020-09-13 23:00:09 +00:00
getPassportFile ( credentials ) . delete ( )
}
2020-09-08 14:25:28 +00:00
2020-01-26 11:23:02 +00:00
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 ( )
2020-09-22 04:06:11 +00:00
HBCIUtils . init ( props , HbciCallback ( credentials , bank , mapper , callback ) )
2020-01-26 11:23:02 +00:00
// 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.
2020-09-08 14:25:28 +00:00
val passportFile = getPassportFile ( credentials )
2020-01-26 11:23:02 +00:00
// 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 )
}
2020-09-08 14:25:28 +00:00
protected open fun getPassportFile ( credentials : AccountCredentials ) : File {
val hbciClientFolder = File ( dataFolder , " hbci4j-client " )
hbciClientFolder . mkdirs ( )
return File ( hbciClientFolder , " passport_ ${credentials.bankCode} _ ${credentials.customerId} .dat " )
}
2020-01-26 11:23:02 +00:00
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 ) }
}
}