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-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-06-03 15:49:29 +00:00
import net.dankito.banking.bankfinder.BankInfo
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-07-12 10:14:56 +00:00
import net.dankito.utils.multiplatform.BigDecimal
import net.dankito.utils.multiplatform.Date
import net.dankito.utils.multiplatform.File
import net.dankito.utils.multiplatform.toBigDecimal
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 (
bankInfo : BankInfo ,
customerId : String ,
pin : String ,
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 )
}
protected val credentials = AccountCredentials ( bankInfo . bankCode , customerId , pin )
2020-06-24 18:54:11 +00:00
protected var customer = Customer ( bankInfo . bankCode , customerId , pin ,
bankInfo . pinTanAddress ?: " " , bankInfo . name , bankInfo . bic , " " )
2020-01-26 11:23:02 +00:00
protected val mapper = hbci4jModelMapper ( )
protected val accountTransactionMapper = AccountTransactionMapper ( )
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-06-24 18:54:11 +00:00
return AddAccountResponse ( false , " Keine Konten ermittelbar " , customer ) // TODO: translate
2020-01-26 11:23:02 +00:00
}
2020-06-24 18:54:11 +00:00
this . customer . accounts = mapper . mapBankAccounts ( customer , accounts , passport )
2020-01-26 11:23:02 +00:00
2020-06-24 18:54:11 +00:00
return tryToRetrieveAccountTransactionsForAddedAccounts ( customer )
2020-01-26 11:23:02 +00:00
}
}
2020-06-24 18:54:11 +00:00
return AddAccountResponse ( false , null , customer , error = connection . error )
2020-01-26 11:23:02 +00:00
}
2020-06-14 21:15:52 +00:00
protected open fun tryToRetrieveAccountTransactionsForAddedAccounts ( customer : Customer ) : AddAccountResponse {
2020-01-26 11:23:02 +00:00
val transactionsOfLast90DaysResponses = mutableListOf < GetTransactionsResponse > ( )
val balances = mutableMapOf < BankAccount , BigDecimal > ( )
val bookedTransactions = mutableMapOf < BankAccount , List < AccountTransaction > > ( )
val unbookedTransactions = mutableMapOf < BankAccount , List < Any > > ( )
2020-06-24 18:54:11 +00:00
customer . accounts . forEach { bankAccount ->
2020-01-26 11:23:02 +00:00
if ( bankAccount . supportsRetrievingAccountTransactions ) {
val response = getTransactionsOfLast90Days ( bankAccount )
transactionsOfLast90DaysResponses . add ( response )
2020-05-25 17:05:56 +00:00
bookedTransactions . put ( bankAccount , response . bookedTransactions )
unbookedTransactions . put ( bankAccount , response . unbookedTransactions )
2020-07-12 10:14:56 +00:00
balances . put ( bankAccount , response . balance ?. toBigDecimal ( ) ?: BigDecimal . Zero ) // TODO: really add BigDecimal.Zero if balance couldn't be retrieved?
2020-01-26 11:23:02 +00:00
}
}
val supportsRetrievingTransactionsOfLast90DaysWithoutTan = transactionsOfLast90DaysResponses . firstOrNull { it . isSuccessful } != null
2020-06-14 21:15:52 +00:00
return AddAccountResponse ( true , null , customer , supportsRetrievingTransactionsOfLast90DaysWithoutTan ,
2020-05-25 17:05:56 +00:00
bookedTransactions , unbookedTransactions , balances )
2020-01-26 11:23:02 +00:00
}
/ * *
* 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 ) {
2020-07-10 12:03:08 +00:00
asyncRunner . runAsync {
2020-01-26 11:23:02 +00:00
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 )
2020-05-24 22:51:30 +00:00
return getTransactions ( bankAccount , GetTransactionsParameter ( bankAccount . supportsRetrievingBalance , ninetyDaysAgo ) ) // TODO: implement abortIfTanIsRequired
2020-01-26 11:23:02 +00:00
}
override fun getTransactionsAsync ( bankAccount : BankAccount , parameter : GetTransactionsParameter , callback : ( GetTransactionsResponse ) -> Unit ) {
2020-07-10 12:03:08 +00:00
asyncRunner . runAsync {
2020-01-26 11:23:02 +00:00
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} " )
2020-05-25 17:05:56 +00:00
return GetTransactionsResponse ( bankAccount , false , null , error = Exception ( " Could not connect to bank ${credentials.bankCode} : ${status.toString()} " ) )
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 ) {
log . error ( " Could not fetch balance of bank account $bankAccount : $balanceResult " , balanceResult . getJobStatus ( ) . exceptions )
2020-05-25 17:05:56 +00:00
return GetTransactionsResponse ( bankAccount , false , null , error = Exception ( " Could not fetch balance of bank account $bankAccount : $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 ) {
log . error ( " Could not get fetch account transactions of bank account $bankAccount : $result " , result . getJobStatus ( ) . exceptions )
2020-05-25 17:05:56 +00:00
return GetTransactionsResponse ( bankAccount , false , null , error = Exception ( " Could not fetch account transactions of bank account $bankAccount : $result " ) )
2020-01-26 11:23:02 +00:00
}
2020-05-25 17:05:56 +00:00
return GetTransactionsResponse ( bankAccount , true , null , accountTransactionMapper . mapAccountTransactions ( bankAccount , result ) ,
2020-07-12 10:14:56 +00:00
listOf ( ) , balance ?. toBigDecimal ( ) )
2020-01-26 11:23:02 +00:00
}
catch ( e : Exception ) {
log . error ( " Could not get accounting details for bank ${credentials.bankCode} " , e )
2020-05-25 17:05:56 +00:00
return GetTransactionsResponse ( bankAccount , false , null , error = e )
2020-01-26 11:23:02 +00:00
}
finally {
closeConnection ( connection )
}
}
closeConnection ( connection )
2020-05-25 17:05:56 +00:00
return GetTransactionsResponse ( bankAccount , false , null , error = connection . error )
2020-01-26 11:23:02 +00:00
}
protected open fun executeJobsForGetAccountingEntries ( handle : HBCIHandler , bankAccount : BankAccount , parameter : GetTransactionsParameter ) : Triple < HBCIJob ? , HBCIJob , HBCIExecStatus > {
2020-06-24 18:54:11 +00:00
val konto = mapper . mapToKonto ( bankAccount )
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 )
}
override fun transferMoneyAsync ( data : TransferMoneyData , bankAccount : BankAccount , callback : ( BankingClientResponse ) -> Unit ) {
2020-07-10 12:03:08 +00:00
asyncRunner . runAsync {
2020-01-26 11:23:02 +00:00
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 ) {
2020-05-13 00:38:33 +00:00
// TODO: implement instant payment
2020-01-26 11:23:02 +00:00
val transferCashJob = handle . newJob ( " UebSEPA " )
2020-06-24 18:54:11 +00:00
val source = mapper . mapToKonto ( bankAccount )
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 )
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 ( )
2020-06-24 18:54:11 +00:00
HBCIUtils . init ( props , HbciCallback ( credentials , customer , 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-06-14 19:00:45 +00:00
val hbciClientFolder = File ( dataFolder , " hbci4j-client " )
hbciClientFolder . mkdirs ( )
val passportFile = File ( hbciClientFolder , " passport_ ${credentials.bankCode} _ ${credentials.customerId} .dat " )
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 )
}
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 ) }
}
}