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.* import net.dankito.banking.ui.model.mapper.IModelCreator 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.banking.util.* import net.dankito.utils.ThreadPool import net.dankito.utils.multiplatform.* import net.dankito.utils.multiplatform.Date 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( protected val bank: TypedBankData, modelCreator: IModelCreator, protected val dataFolder: File, protected val asyncRunner: IAsyncRunner = ThreadPoolAsyncRunner(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(bank) protected val mapper = hbci4jModelMapper(modelCreator) protected val accountTransactionMapper = AccountTransactionMapper(modelCreator) override val messageLogWithoutSensitiveData: List = listOf() // TODO: implement override fun addAccountAsync(callback: (AddAccountResponse) -> Unit) { asyncRunner.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(bank, "Keine Konten ermittelbar") // TODO: translate } this.bank.accounts = mapper.mapAccounts(bank, accounts, passport) return tryToRetrieveAccountTransactionsForAddedAccounts(bank) } } return AddAccountResponse(bank, connection.error?.getInnerExceptionMessage() ?: "Could not connect") } protected open fun tryToRetrieveAccountTransactionsForAddedAccounts(bank: TypedBankData): AddAccountResponse { var userCancelledAction = false val retrievedData = bank.accounts.map { account -> if (account.supportsRetrievingAccountTransactions) { val response = getTransactionsOfLast90Days(account) if (response.userCancelledAction) { userCancelledAction = true } response.retrievedData.first() } else { RetrievedAccountData.unsuccessful(account) } } return AddAccountResponse(bank, retrievedData, null, false, userCancelledAction) } /** * According to PSD2 for the account transactions 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 account transactions of the last 90 days and see if a second factor is required * or not. */ open fun getTransactionsOfLast90DaysAsync(account: TypedBankAccount, callback: (GetTransactionsResponse) -> Unit) { asyncRunner.runAsync { callback(getTransactionsOfLast90Days(account)) } } /** * According to PSD2 for the account transactions 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 account transactions of the last 90 days and see if a second factor is required * or not. */ open fun getTransactionsOfLast90Days(account: TypedBankAccount): GetTransactionsResponse { val ninetyDaysAgo = Date(Date.today.time - NinetyDaysInMilliseconds) return getTransactions(GetTransactionsParameter(account, account.supportsRetrievingBalance, ninetyDaysAgo)) // TODO: implement abortIfTanIsRequired } override fun getTransactionsAsync(parameter: GetTransactionsParameter, callback: (GetTransactionsResponse) -> Unit) { asyncRunner.runAsync { callback(getTransactions(parameter)) } } protected open fun getTransactions(parameter: GetTransactionsParameter): GetTransactionsResponse { val connection = connect() val account = parameter.account connection.handle?.let { handle -> try { val (nullableBalanceJob, accountTransactionsJob, status) = executeJobsForGetAccountTransactions(handle, parameter) // Pruefen, ob die Kommunikation mit der Bank grundsaetzlich geklappt hat if (!status.isOK) { log.error("Could not connect to bank ${credentials.bankCode} $status: ${status.errorString}") return GetTransactionsResponse(account,"Could not connect to bank ${credentials.bankCode}: $status") } // 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 $account: $balanceResult", balanceResult.getJobStatus().exceptions) return GetTransactionsResponse(account,"Could not fetch balance of bank account $account: $balanceResult") } balance = balanceResult.entries[0].ready.value.bigDecimalValue.toBigDecimal() } // 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 $account: $result", result.getJobStatus().exceptions) return GetTransactionsResponse(account,"Could not fetch account transactions of bank account $account: $result") } return GetTransactionsResponse(RetrievedAccountData(account, true, balance.toBigDecimal(), accountTransactionMapper.mapTransactions(account, result), listOf(), parameter.fromDate, parameter.toDate)) } catch(e: Exception) { log.error("Could not get account transactions for bank ${credentials.bankCode}", e) return GetTransactionsResponse(account, e.getInnerExceptionMessage()) } finally { closeConnection(connection) } } closeConnection(connection) return GetTransactionsResponse(account, connection.error?.getInnerExceptionMessage() ?: "Could not connect") } protected open fun executeJobsForGetAccountTransactions(handle: HBCIHandler, parameter: GetTransactionsParameter): Triple { val konto = mapper.mapToKonto(parameter.account) // 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, callback: (BankingClientResponse) -> Unit) { asyncRunner.runAsync { callback(transferMoney(data)) } } open fun transferMoney(data: TransferMoneyData): BankingClientResponse { val connection = connect() connection.handle?.let { handle -> try { createTransferCashJob(handle, data) val status = handle.execute() return BankingClientResponse(status.isOK, status.toString()) } catch(e: Exception) { log.error("Could not transfer cash for account ${data.account}" , e) return BankingClientResponse(false, e.getInnerExceptionMessage()) } finally { closeConnection(connection) } } return BankingClientResponse(false, connection.error?.getInnerExceptionMessage() ?: "Could not connect") } protected open fun createTransferCashJob(handle: HBCIHandler, data: TransferMoneyData) { // TODO: implement real-time transfer val transferCashJob = handle.newJob("UebSEPA") val source = mapper.mapToKonto(data.account) 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.reference) transferCashJob.addToQueue() } override fun dataChanged(bank: TypedBankData) { if (bank.bankCode != credentials.bankCode || bank.userName != credentials.customerId || bank.password != credentials.password) { getPassportFile(credentials).delete() } credentials.bankCode = bank.bankCode credentials.customerId = bank.userName credentials.password = bank.password } override fun deletedBank(bank: TypedBankData, wasLastAccountWithThisCredentials: Boolean) { getPassportFile(credentials).delete() } 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, bank, 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. val passportFile = getPassportFile(credentials) // 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 getPassportFile(credentials: AccountCredentials): File { val hbciClientFolder = File(dataFolder, "hbci4j-client") hbciClientFolder.mkdirs() return File(hbciClientFolder, "passport_${credentials.bankCode}_${credentials.customerId}.dat") } 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) } } }