Replaced callbacks with coroutines

This commit is contained in:
dankito 2022-02-19 13:17:02 +01:00
parent bdd28f2587
commit 54c430af2b
14 changed files with 375 additions and 426 deletions

View File

@ -3,6 +3,7 @@ package net.codinux.banking.fints4k.android
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.datetime.LocalDate
import net.dankito.banking.fints.FinTsClientDeprecated
import net.dankito.banking.fints.callback.SimpleFinTsClientCallback
@ -27,10 +28,11 @@ class Presenter {
fun retrieveAccountData(bankCode: String, customerId: String, pin: String, finTs3ServerAddress: String, retrievedResult: (AddAccountResponse) -> Unit) {
fintsClient.addAccountAsync(AddAccountParameter(bankCode, customerId, pin, finTs3ServerAddress)) { response ->
GlobalScope.launch(Dispatchers.IO) {
val response = fintsClient.addAccountAsync(AddAccountParameter(bankCode, customerId, pin, finTs3ServerAddress))
log.info("Retrieved response from ${response.bank.bankName} for ${response.bank.customerName}")
GlobalScope.launch(Dispatchers.Main) {
withContext(Dispatchers.Main) {
retrievedResult(response)
}
}

View File

@ -1,3 +1,5 @@
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.dankito.banking.fints.FinTsClientDeprecated
import net.dankito.banking.fints.model.AccountTransaction
import net.dankito.banking.fints.model.AddAccountParameter
@ -23,7 +25,8 @@ class AccountTransactionsView(props: AccountTransactionsViewProps) : RComponent<
// due to CORS your bank's servers can not be requested directly from browser -> set a CORS proxy url in main.kt
// TODO: set your credentials here
props.client.addAccountAsync(AddAccountParameter("", "", "", "")) { response ->
GlobalScope.launch {
val response = props.client.addAccountAsync(AddAccountParameter("", "", "", ""))
if (response.successful) {
val balance = response.retrievedData.sumOf { it.balance?.amount?.string?.replace(',', '.')?.toDoubleOrNull() ?: 0.0 } // i know, double is not an appropriate data type for amounts

View File

@ -44,7 +44,7 @@ open class FinTsClientDeprecated(
open fun getAnonymousBankInfoAsync(bank: BankData, callback: (FinTsClientResponse) -> Unit) {
GlobalScope.launch {
getAnonymousBankInfo(bank, callback)
callback(getAnonymousBankInfo(bank))
}
}
@ -54,90 +54,74 @@ open class FinTsClientDeprecated(
*
* On success [bank] parameter is updated afterwards.
*/
open fun getAnonymousBankInfo(bank: BankData, callback: (FinTsClientResponse) -> Unit) {
open suspend fun getAnonymousBankInfo(bank: BankData): FinTsClientResponse {
val context = JobContext(JobContextType.AnonymousBankInfo, this.callback, product, bank)
jobExecutor.getAnonymousBankInfo(context) { response ->
callback(FinTsClientResponse(context, response))
}
val response = jobExecutor.getAnonymousBankInfo(context)
return FinTsClientResponse(context, response)
}
open fun addAccountAsync(parameter: AddAccountParameter, callback: (AddAccountResponse) -> Unit) {
open suspend fun addAccountAsync(parameter: AddAccountParameter): AddAccountResponse {
val bank = parameter.bank
val context = JobContext(JobContextType.AddAccount, this.callback, product, bank)
/* First dialog: Get user's basic data like BPD, customer system ID and her TAN methods */
jobExecutor.retrieveBasicDataLikeUsersTanMethods(context, parameter.preferredTanMethods, parameter.preferredTanMedium) { newUserInfoResponse ->
val newUserInfoResponse = jobExecutor.retrieveBasicDataLikeUsersTanMethods(context, parameter.preferredTanMethods, parameter.preferredTanMedium)
if (newUserInfoResponse.successful == false) { // bank parameter (FinTS server address, ...) already seem to be wrong
callback(AddAccountResponse(context, newUserInfoResponse))
return@retrieveBasicDataLikeUsersTanMethods
}
/* Second dialog, executed in retrieveBasicDataLikeUsersTanMethods() if required: some banks require that in order to initialize a dialog with
strong customer authorization TAN media is required */
addAccountGetAccountsAndTransactions(context, parameter, callback)
if (newUserInfoResponse.successful == false) { // bank parameter (FinTS server address, ...) already seem to be wrong
return AddAccountResponse(context, newUserInfoResponse)
}
/* Second dialog, executed in retrieveBasicDataLikeUsersTanMethods() if required: some banks require that in order to initialize a dialog with
strong customer authorization TAN media is required */
return addAccountGetAccountsAndTransactions(context, parameter)
}
protected open fun addAccountGetAccountsAndTransactions(context: JobContext, parameter: AddAccountParameter,
callback: (AddAccountResponse) -> Unit) {
protected open suspend fun addAccountGetAccountsAndTransactions(context: JobContext, parameter: AddAccountParameter): AddAccountResponse {
/* Third dialog: Now we can initialize our first dialog with strong customer authorization. Use it to get UPD and customer's accounts */
jobExecutor.getAccounts(context) { getAccountsResponse ->
val getAccountsResponse = jobExecutor.getAccounts(context)
if (getAccountsResponse.successful == false) {
callback(AddAccountResponse(context, getAccountsResponse))
return@getAccounts
}
if (getAccountsResponse.successful == false) {
return AddAccountResponse(context, getAccountsResponse)
}
/* Fourth dialog (if requested): Try to retrieve account balances and transactions of last 90 days without TAN */
/* Fourth dialog (if requested): Try to retrieve account balances and transactions of last 90 days without TAN */
if (parameter.fetchBalanceAndTransactions) {
addAccountGetAccountBalancesAndTransactions(context, getAccountsResponse, callback)
}
else {
addAccountDone(context, getAccountsResponse, listOf(), callback)
}
if (parameter.fetchBalanceAndTransactions) {
return addAccountGetAccountBalancesAndTransactions(context, getAccountsResponse)
}
else {
return addAccountDone(context, getAccountsResponse, listOf())
}
}
protected open fun addAccountGetAccountBalancesAndTransactions(context: JobContext, getAccountsResponse: BankResponse,
callback: (AddAccountResponse) -> Unit) {
protected open suspend fun addAccountGetAccountBalancesAndTransactions(context: JobContext, getAccountsResponse: BankResponse): AddAccountResponse {
val bank = context.bank
val retrievedTransactionsResponses = mutableListOf<GetAccountTransactionsResponse>()
val accountsSupportingRetrievingTransactions = bank.accounts.filter { it.supportsRetrievingBalance || it.supportsRetrievingAccountTransactions }
val countAccountsSupportingRetrievingTransactions = accountsSupportingRetrievingTransactions.size
var countRetrievedAccounts = 0
if (countAccountsSupportingRetrievingTransactions == 0) {
addAccountDone(context, getAccountsResponse, retrievedTransactionsResponses, callback)
return // not necessary just to make it clearer that code below doesn't get called
if (accountsSupportingRetrievingTransactions.isEmpty()) {
return addAccountDone(context, getAccountsResponse, retrievedTransactionsResponses)
}
accountsSupportingRetrievingTransactions.forEach { account ->
tryGetAccountTransactionsOfLast90DaysWithoutTan(bank, account) { response ->
retrievedTransactionsResponses.add(response)
countRetrievedAccounts++
if (countRetrievedAccounts == countAccountsSupportingRetrievingTransactions) {
addAccountDone(context, getAccountsResponse, retrievedTransactionsResponses, callback)
}
}
retrievedTransactionsResponses.add(tryGetAccountTransactionsOfLast90DaysWithoutTan(bank, account))
}
return addAccountDone(context, getAccountsResponse, retrievedTransactionsResponses)
}
protected open fun addAccountDone(context: JobContext, getAccountsResponse: BankResponse,
retrievedTransactionsResponses: List<GetAccountTransactionsResponse>,
callback: (AddAccountResponse) -> Unit) {
retrievedTransactionsResponses: List<GetAccountTransactionsResponse>): AddAccountResponse {
callback(AddAccountResponse(context, getAccountsResponse, retrievedTransactionsResponses))
return AddAccountResponse(context, getAccountsResponse, retrievedTransactionsResponses)
}
@ -147,9 +131,9 @@ open class FinTsClientDeprecated(
*
* Check if bank supports this.
*/
open fun tryGetAccountTransactionsOfLast90DaysWithoutTan(bank: BankData, account: AccountData, callback: (GetAccountTransactionsResponse) -> Unit) {
open suspend fun tryGetAccountTransactionsOfLast90DaysWithoutTan(bank: BankData, account: AccountData): GetAccountTransactionsResponse {
getAccountTransactionsAsync(createGetAccountTransactionsOfLast90DaysParameter(bank, account), callback)
return getAccountTransactionsAsync(createGetAccountTransactionsOfLast90DaysParameter(bank, account))
}
protected open fun createGetAccountTransactionsOfLast90DaysParameter(bank: BankData, account: AccountData): GetAccountTransactionsParameter {
@ -159,36 +143,35 @@ open class FinTsClientDeprecated(
return GetAccountTransactionsParameter(bank, account, account.supportsRetrievingBalance, ninetyDaysAgo, abortIfTanIsRequired = true)
}
open fun getAccountTransactionsAsync(parameter: GetAccountTransactionsParameter, callback: (GetAccountTransactionsResponse) -> Unit) {
open suspend fun getAccountTransactionsAsync(parameter: GetAccountTransactionsParameter): GetAccountTransactionsResponse {
val context = JobContext(JobContextType.GetTransactions, this.callback, product, parameter.bank, parameter.account)
jobExecutor.getTransactionsAsync(context, parameter, callback)
return jobExecutor.getTransactionsAsync(context, parameter)
}
open fun getTanMediaList(bank: BankData, tanMediaKind: TanMedienArtVersion = TanMedienArtVersion.Alle,
tanMediumClass: TanMediumKlasse = TanMediumKlasse.AlleMedien, callback: (GetTanMediaListResponse) -> Unit) {
open suspend fun getTanMediaList(bank: BankData, tanMediaKind: TanMedienArtVersion = TanMedienArtVersion.Alle,
tanMediumClass: TanMediumKlasse = TanMediumKlasse.AlleMedien): GetTanMediaListResponse {
val context = JobContext(JobContextType.GetTanMedia, this.callback, product, bank)
jobExecutor.getTanMediaList(context, tanMediaKind, tanMediumClass, callback)
return jobExecutor.getTanMediaList(context, tanMediaKind, tanMediumClass)
}
open fun changeTanMedium(newActiveTanMedium: TanGeneratorTanMedium, bank: BankData, callback: (FinTsClientResponse) -> Unit) {
open suspend fun changeTanMedium(newActiveTanMedium: TanGeneratorTanMedium, bank: BankData): FinTsClientResponse {
val context = JobContext(JobContextType.ChangeTanMedium, this.callback, product, bank)
jobExecutor.changeTanMedium(context, newActiveTanMedium) { response ->
callback(FinTsClientResponse(context, response))
}
val response = jobExecutor.changeTanMedium(context, newActiveTanMedium)
return FinTsClientResponse(context, response)
}
open fun doBankTransferAsync(bankTransferData: BankTransferData, bank: BankData, account: AccountData, callback: (FinTsClientResponse) -> Unit) {
open suspend fun doBankTransferAsync(bankTransferData: BankTransferData, bank: BankData, account: AccountData): FinTsClientResponse {
val context = JobContext(JobContextType.TransferMoney, this.callback, product, bank, account)
jobExecutor.doBankTransferAsync(context, bankTransferData, callback)
return jobExecutor.doBankTransferAsync(context, bankTransferData)
}
}

View File

@ -39,22 +39,22 @@ open class FinTsClientForCustomer(
}
open fun addAccountAsync(callback: (AddAccountResponse) -> Unit) {
addAccountAsync(bank.toAddAccountParameter(), callback)
open suspend fun addAccountAsync(): AddAccountResponse {
return addAccountAsync(bank.toAddAccountParameter())
}
open fun addAccountAsync(parameter: AddAccountParameter, callback: (AddAccountResponse) -> Unit) {
client.addAccountAsync(parameter, callback)
open suspend fun addAccountAsync(parameter: AddAccountParameter): AddAccountResponse {
return client.addAccountAsync(parameter)
}
open fun getAccountTransactionsAsync(parameter: GetAccountTransactionsParameter, callback: (GetAccountTransactionsResponse) -> Unit) {
client.getAccountTransactionsAsync(parameter, callback)
open suspend fun getAccountTransactionsAsync(parameter: GetAccountTransactionsParameter): GetAccountTransactionsResponse {
return client.getAccountTransactionsAsync(parameter)
}
open fun doBankTransferAsync(bankTransferData: BankTransferData, account: AccountData, callback: (FinTsClientResponse) -> Unit) {
client.doBankTransferAsync(bankTransferData, bank, account, callback)
open suspend fun doBankTransferAsync(bankTransferData: BankTransferData, account: AccountData): FinTsClientResponse {
return client.doBankTransferAsync(bankTransferData, bank, account)
}
}

View File

@ -41,21 +41,21 @@ open class FinTsJobExecutor(
}
open fun getAnonymousBankInfo(context: JobContext, callback: (BankResponse) -> Unit) {
open suspend fun getAnonymousBankInfo(context: JobContext): BankResponse {
context.startNewDialog()
val message = messageBuilder.createAnonymousDialogInitMessage(context)
getAndHandleResponseForMessage(context, message) { response ->
if (response.successful) {
closeAnonymousDialog(context, response)
}
val response = getAndHandleResponseForMessage(context, message)
callback(response)
if (response.successful) {
closeAnonymousDialog(context, response)
}
return response
}
protected open fun closeAnonymousDialog(context: JobContext, response: BankResponse) {
protected open suspend fun closeAnonymousDialog(context: JobContext, response: BankResponse) {
// bank already closed dialog -> there's no need to send dialog end message
if (shouldNotCloseDialog(context)) {
@ -75,8 +75,8 @@ open class FinTsJobExecutor(
*
* Be aware this method resets BPD, UPD and selected TAN method!
*/
open fun retrieveBasicDataLikeUsersTanMethods(context: JobContext, preferredTanMethods: List<TanMethodType>? = null, preferredTanMedium: String? = null,
closeDialog: Boolean = false, callback: (BankResponse) -> Unit) {
open suspend fun retrieveBasicDataLikeUsersTanMethods(context: JobContext, preferredTanMethods: List<TanMethodType>? = null, preferredTanMedium: String? = null,
closeDialog: Boolean = false): BankResponse {
val bank = context.bank
// just to ensure settings are in its initial state and that bank sends us bank parameter (BPD),
@ -96,38 +96,38 @@ open class FinTsJobExecutor(
val message = messageBuilder.createInitDialogMessage(context)
getAndHandleResponseForMessage(context, message) { response ->
closeDialog(context)
val response = getAndHandleResponseForMessage(context, message)
handleGetUsersTanMethodsResponse(context, response) { getTanMethodsResponse ->
if (bank.tanMethodsAvailableForUser.isEmpty()) { // could not retrieve supported tan methods for user
callback(getTanMethodsResponse)
} else {
getUsersTanMethod(context, preferredTanMethods) {
if (bank.isTanMethodSelected == false) {
callback(getTanMethodsResponse)
} else if (bank.tanMedia.isEmpty() && isJobSupported(bank, CustomerSegmentId.TanMediaList)) { // tan media not retrieved yet
getTanMediaList(context, TanMedienArtVersion.Alle, TanMediumKlasse.AlleMedien, preferredTanMedium) {
callback(getTanMethodsResponse) // TODO: judge if bank requires selecting TAN media and if though evaluate getTanMediaListResponse
}
} else {
callback(getTanMethodsResponse)
}
}
}
closeDialog(context)
val getTanMethodsResponse = handleGetUsersTanMethodsResponse(context, response)
if (bank.tanMethodsAvailableForUser.isEmpty()) { // could not retrieve supported tan methods for user
return getTanMethodsResponse
} else {
getUsersTanMethod(context, preferredTanMethods)
if (bank.isTanMethodSelected == false) {
return getTanMethodsResponse
} else if (bank.tanMedia.isEmpty() && isJobSupported(bank, CustomerSegmentId.TanMediaList)) { // tan media not retrieved yet
getTanMediaList(context, TanMedienArtVersion.Alle, TanMediumKlasse.AlleMedien, preferredTanMedium)
return getTanMethodsResponse // TODO: judge if bank requires selecting TAN media and if though evaluate getTanMediaListResponse
} else {
return getTanMethodsResponse
}
}
}
protected open fun handleGetUsersTanMethodsResponse(context: JobContext, response: BankResponse, callback: (BankResponse) -> Unit) {
protected open suspend fun handleGetUsersTanMethodsResponse(context: JobContext, response: BankResponse): BankResponse {
val getUsersTanMethodsResponse = GetUserTanMethodsResponse(response)
// even though it is required by specification some banks don't support retrieving user's TAN method by setting TAN method to '999'
if (bankDoesNotSupportRetrievingUsersTanMethods(getUsersTanMethodsResponse)) {
getBankDataForNewUserViaAnonymousDialog(context, callback) // TODO: should not be necessary anymore
return getBankDataForNewUserViaAnonymousDialog(context) // TODO: should not be necessary anymore
}
else {
callback(getUsersTanMethodsResponse)
return getUsersTanMethodsResponse
}
}
@ -138,73 +138,70 @@ open class FinTsJobExecutor(
}
// TODO: this is only a quick fix. Find a better and general solution
protected open fun getBankDataForNewUserViaAnonymousDialog(context: JobContext, callback: (BankResponse) -> Unit) {
getAnonymousBankInfo(context) { anonymousBankInfoResponse ->
val bank = context.bank
protected open suspend fun getBankDataForNewUserViaAnonymousDialog(context: JobContext): BankResponse {
val anonymousBankInfoResponse = getAnonymousBankInfo(context)
if (anonymousBankInfoResponse.successful == false) {
callback(anonymousBankInfoResponse)
} else if (bank.tanMethodsSupportedByBank.isEmpty()) { // should only be a theoretical error
callback(BankResponse(true, internalError = "Die TAN Verfahren der Bank konnten nicht ermittelt werden")) // TODO: translate
val bank = context.bank
if (anonymousBankInfoResponse.successful == false) {
return anonymousBankInfoResponse
} else if (bank.tanMethodsSupportedByBank.isEmpty()) { // should only be a theoretical error
return BankResponse(true, internalError = "Die TAN Verfahren der Bank konnten nicht ermittelt werden") // TODO: translate
} else {
bank.tanMethodsAvailableForUser = bank.tanMethodsSupportedByBank
val didSelectTanMethod = getUsersTanMethod(context)
if (didSelectTanMethod) {
val initDialogResponse = initDialogWithStrongCustomerAuthenticationAfterSuccessfulPreconditionChecks(context)
closeDialog(context)
return initDialogResponse
} else {
bank.tanMethodsAvailableForUser = bank.tanMethodsSupportedByBank
getUsersTanMethod(context) { didSelectTanMethod ->
if (didSelectTanMethod) {
initDialogWithStrongCustomerAuthenticationAfterSuccessfulPreconditionChecks(context) { initDialogResponse ->
closeDialog(context)
callback(initDialogResponse)
}
} else {
callback(createNoTanMethodSelectedResponse(bank))
}
}
return createNoTanMethodSelectedResponse(bank)
}
}
}
open fun getAccounts(context: JobContext, callback: (BankResponse) -> Unit) {
initDialogWithStrongCustomerAuthenticationAfterSuccessfulPreconditionChecks(context) { response ->
closeDialog(context)
open suspend fun getAccounts(context: JobContext): BankResponse {
val response = initDialogWithStrongCustomerAuthenticationAfterSuccessfulPreconditionChecks(context)
callback(response)
}
closeDialog(context)
return response
}
open fun getTransactionsAsync(context: JobContext, parameter: GetAccountTransactionsParameter, callback: (GetAccountTransactionsResponse) -> Unit) {
open suspend fun getTransactionsAsync(context: JobContext, parameter: GetAccountTransactionsParameter): GetAccountTransactionsResponse {
val dialogContext = context.startNewDialog() // TODO: initDialogWithStrongCustomerAuthentication() also starts a new dialog in initDialogWithStrongCustomerAuthenticationAfterSuccessfulPreconditionChecks()
initDialogWithStrongCustomerAuthentication(context) { initDialogResponse ->
val initDialogResponse = initDialogWithStrongCustomerAuthentication(context)
if (initDialogResponse.successful == false) {
callback(GetAccountTransactionsResponse(context, initDialogResponse, RetrievedAccountData.unsuccessful(parameter.account)))
}
else {
// we now retrieved the fresh account information from FinTS server, use that one
parameter.account = getUpdatedAccount(context, parameter.account)
if (initDialogResponse.successful == false) {
return GetAccountTransactionsResponse(context, initDialogResponse, RetrievedAccountData.unsuccessful(parameter.account))
}
else {
// we now retrieved the fresh account information from FinTS server, use that one
parameter.account = getUpdatedAccount(context, parameter.account)
mayGetBalance(context, parameter) { balanceResponse ->
if (dialogContext.didBankCloseDialog) {
callback(GetAccountTransactionsResponse(context, balanceResponse ?: initDialogResponse, RetrievedAccountData.unsuccessful(parameter.account)))
}
else {
getTransactionsAfterInitAndGetBalance(context, parameter, balanceResponse, callback)
}
}
val balanceResponse = mayGetBalance(context, parameter)
if (dialogContext.didBankCloseDialog) {
return GetAccountTransactionsResponse(context, balanceResponse ?: initDialogResponse, RetrievedAccountData.unsuccessful(parameter.account))
} else {
return getTransactionsAfterInitAndGetBalance(context, parameter, balanceResponse)
}
}
}
private fun getUpdatedAccount(context: JobContext, account: AccountData): AccountData {
protected open fun getUpdatedAccount(context: JobContext, account: AccountData): AccountData {
return context.bank.accounts.firstOrNull { it.accountIdentifier == account.accountIdentifier } ?: account
}
protected open fun getTransactionsAfterInitAndGetBalance(context: JobContext, parameter: GetAccountTransactionsParameter,
balanceResponse: BankResponse?, callback: (GetAccountTransactionsResponse) -> Unit) {
protected open suspend fun getTransactionsAfterInitAndGetBalance(context: JobContext, parameter: GetAccountTransactionsParameter,
balanceResponse: BankResponse?): GetAccountTransactionsResponse {
var balance: Money? = balanceResponse?.getFirstSegmentById<BalanceSegment>(InstituteSegmentId.Balance)?.let {
Money(it.balance, it.currency)
}
@ -234,29 +231,28 @@ open class FinTsJobExecutor(
}
}
getAndHandleResponseForMessage(context, message) { response ->
closeDialog(context)
val response = getAndHandleResponseForMessage(context, message)
val successful = response.tanRequiredButWeWereToldToAbortIfSo
|| (response.successful && (parameter.alsoRetrieveBalance == false || balance != null))
val fromDate = parameter.fromDate
?: parameter.account.countDaysForWhichTransactionsAreKept?.let { LocalDate.todayAtSystemDefaultTimeZone().minusDays(it) }
?: bookedTransactions.minByOrNull { it.valueDate.millisSinceEpochAtEuropeBerlin }?.valueDate
val retrievedData = RetrievedAccountData(parameter.account, successful, balance, bookedTransactions, unbookedTransactions, fromDate, parameter.toDate ?: LocalDate.todayAtEuropeBerlin(), response.internalError)
closeDialog(context)
callback(GetAccountTransactionsResponse(context, response, retrievedData,
if (parameter.maxCountEntries != null) parameter.isSettingMaxCountEntriesAllowedByBank else null))
}
val successful = response.tanRequiredButWeWereToldToAbortIfSo
|| (response.successful && (parameter.alsoRetrieveBalance == false || balance != null))
val fromDate = parameter.fromDate
?: parameter.account.countDaysForWhichTransactionsAreKept?.let { LocalDate.todayAtSystemDefaultTimeZone().minusDays(it) }
?: bookedTransactions.minByOrNull { it.valueDate.millisSinceEpochAtEuropeBerlin }?.valueDate
val retrievedData = RetrievedAccountData(parameter.account, successful, balance, bookedTransactions, unbookedTransactions, fromDate, parameter.toDate ?: LocalDate.todayAtEuropeBerlin(), response.internalError)
return GetAccountTransactionsResponse(context, response, retrievedData,
if (parameter.maxCountEntries != null) parameter.isSettingMaxCountEntriesAllowedByBank else null)
}
protected open fun mayGetBalance(context: JobContext, parameter: GetAccountTransactionsParameter, callback: (BankResponse?) -> Unit) {
protected open suspend fun mayGetBalance(context: JobContext, parameter: GetAccountTransactionsParameter): BankResponse? {
if (parameter.alsoRetrieveBalance && parameter.account.supportsRetrievingBalance) {
val message = messageBuilder.createGetBalanceMessage(context, parameter.account)
getAndHandleResponseForMessage(context, message, callback)
}
else {
callback(null)
return getAndHandleResponseForMessage(context, message)
} else {
return null
}
}
@ -272,38 +268,37 @@ open class FinTsJobExecutor(
*
* If you change customer system id during a dialog your messages get rejected by bank institute.
*/
protected open fun synchronizeCustomerSystemId(context: JobContext, callback: (FinTsClientResponse) -> Unit) {
protected open suspend fun synchronizeCustomerSystemId(context: JobContext): FinTsClientResponse {
context.startNewDialog()
val message = messageBuilder.createSynchronizeCustomerSystemIdMessage(context)
getAndHandleResponseForMessage(context, message) { response ->
if (response.successful) {
closeDialog(context)
}
val response = getAndHandleResponseForMessage(context, message)
callback(FinTsClientResponse(context, response))
if (response.successful) {
closeDialog(context)
}
return FinTsClientResponse(context, response)
}
open fun getTanMediaList(context: JobContext, tanMediaKind: TanMedienArtVersion = TanMedienArtVersion.Alle, tanMediumClass: TanMediumKlasse = TanMediumKlasse.AlleMedien,
callback: (GetTanMediaListResponse) -> Unit) {
getTanMediaList(context, tanMediaKind, tanMediumClass, null, callback)
open suspend fun getTanMediaList(context: JobContext, tanMediaKind: TanMedienArtVersion = TanMedienArtVersion.Alle, tanMediumClass: TanMediumKlasse = TanMediumKlasse.AlleMedien): GetTanMediaListResponse {
return getTanMediaList(context, tanMediaKind, tanMediumClass, null)
}
protected open fun getTanMediaList(context: JobContext, tanMediaKind: TanMedienArtVersion = TanMedienArtVersion.Alle, tanMediumClass: TanMediumKlasse = TanMediumKlasse.AlleMedien,
preferredTanMedium: String? = null, callback: (GetTanMediaListResponse) -> Unit) {
protected open suspend fun getTanMediaList(context: JobContext, tanMediaKind: TanMedienArtVersion = TanMedienArtVersion.Alle, tanMediumClass: TanMediumKlasse = TanMediumKlasse.AlleMedien,
preferredTanMedium: String? = null): GetTanMediaListResponse {
sendMessageInNewDialogAndHandleResponse(context, CustomerSegmentId.TanMediaList, false, {
val response = sendMessageInNewDialogAndHandleResponse(context, CustomerSegmentId.TanMediaList, false) {
messageBuilder.createGetTanMediaListMessage(context, tanMediaKind, tanMediumClass)
}) { response ->
handleGetTanMediaListResponse(context, response, preferredTanMedium, callback)
}
return handleGetTanMediaListResponse(context, response, preferredTanMedium)
}
protected open fun handleGetTanMediaListResponse(context: JobContext, response: BankResponse, preferredTanMedium: String? = null, callback: (GetTanMediaListResponse) -> Unit) {
protected open fun handleGetTanMediaListResponse(context: JobContext, response: BankResponse, preferredTanMedium: String? = null): GetTanMediaListResponse {
val bank = context.bank
// TAN media list (= TAN generator list) is only returned for users with chipTAN TAN methods
@ -318,83 +313,80 @@ open class FinTsJobExecutor(
?: bank.tanMedia.firstOrNull { it.mediumName != null }
}
callback(GetTanMediaListResponse(context, response, tanMediaList))
return GetTanMediaListResponse(context, response, tanMediaList)
}
open fun changeTanMedium(context: JobContext, newActiveTanMedium: TanGeneratorTanMedium, callback: (BankResponse) -> Unit) {
open suspend fun changeTanMedium(context: JobContext, newActiveTanMedium: TanGeneratorTanMedium): BankResponse {
val bank = context.bank
if (bank.changeTanMediumParameters?.enteringAtcAndTanRequired == true) {
context.callback.enterTanGeneratorAtc(bank, newActiveTanMedium) { enteredAtc ->
if (enteredAtc.hasAtcBeenEntered == false) {
val message = "Bank requires to enter ATC and TAN in order to change TAN medium." // TODO: translate
callback(BankResponse(false, internalError = message))
}
else {
sendChangeTanMediumMessage(context, newActiveTanMedium, enteredAtc, callback)
}
val enteredAtc = context.callback.enterTanGeneratorAtc(bank, newActiveTanMedium)
if (enteredAtc.hasAtcBeenEntered == false) {
val message = "Bank requires to enter ATC and TAN in order to change TAN medium." // TODO: translate
return BankResponse(false, internalError = message)
} else {
return sendChangeTanMediumMessage(context, newActiveTanMedium, enteredAtc)
}
}
else {
sendChangeTanMediumMessage(context, newActiveTanMedium, null, callback)
return sendChangeTanMediumMessage(context, newActiveTanMedium, null)
}
}
protected open fun sendChangeTanMediumMessage(context: JobContext, newActiveTanMedium: TanGeneratorTanMedium, enteredAtc: EnterTanGeneratorAtcResult?,
callback: (BankResponse) -> Unit) {
protected open suspend fun sendChangeTanMediumMessage(context: JobContext, newActiveTanMedium: TanGeneratorTanMedium, enteredAtc: EnterTanGeneratorAtcResult?): BankResponse {
sendMessageInNewDialogAndHandleResponse(context, null, true, {
return sendMessageInNewDialogAndHandleResponse(context, null, true) {
messageBuilder.createChangeTanMediumMessage(context, newActiveTanMedium, enteredAtc?.tan, enteredAtc?.atc)
}, callback)
}
}
open fun doBankTransferAsync(context: JobContext, bankTransferData: BankTransferData, callback: (FinTsClientResponse) -> Unit) {
open suspend fun doBankTransferAsync(context: JobContext, bankTransferData: BankTransferData): FinTsClientResponse {
sendMessageInNewDialogAndHandleResponse(context, null, true, {
val response = sendMessageInNewDialogAndHandleResponse(context, null, true) {
val updatedAccount = getUpdatedAccount(context, context.account!!)
messageBuilder.createBankTransferMessage(context, bankTransferData, updatedAccount)
}) { response ->
callback(FinTsClientResponse(context, response))
}
return FinTsClientResponse(context, response)
}
protected open fun getAndHandleResponseForMessage(context: JobContext, message: MessageBuilderResult, callback: (BankResponse) -> Unit) {
requestExecutor.getAndHandleResponseForMessage(message, context,
{ tanResponse, bankResponse, tanRequiredCallback ->
protected open suspend fun getAndHandleResponseForMessage(context: JobContext, message: MessageBuilderResult): BankResponse {
val response = requestExecutor.getAndHandleResponseForMessage(message, context, { tanResponse, bankResponse ->
// if we receive a message that tells us a TAN is required below callback doesn't get called for that message -> update data here
// for Hypovereinsbank it's absolutely necessary to update bank data (more specific: PinInfo / HIPINS) after first strong authentication dialog init response
// as HIPINS differ in anonymous and in authenticated dialog. Anonymous dialog tells us for HKSAL and HKKAZ no TAN is needed
updateBankAndCustomerDataIfResponseSuccessful(context, bankResponse)
handleEnteringTanRequired(context, tanResponse, bankResponse, tanRequiredCallback)
}) { response ->
// TODO: really update data only on complete successfully response? as it may contain useful information anyway // TODO: extract method for this code part
updateBankAndCustomerDataIfResponseSuccessful(context, response)
callback(response)
}
handleEnteringTanRequired(context, tanResponse, bankResponse)
})
// TODO: really update data only on complete successfully response? as it may contain useful information anyway // TODO: extract method for this code part
updateBankAndCustomerDataIfResponseSuccessful(context, response)
return response
}
protected open fun fireAndForgetMessage(context: JobContext, message: MessageBuilderResult) {
protected open suspend fun fireAndForgetMessage(context: JobContext, message: MessageBuilderResult) {
requestExecutor.fireAndForgetMessage(context, message)
}
protected open fun handleEnteringTanRequired(context: JobContext, tanResponse: TanResponse, response: BankResponse, callback: (BankResponse) -> Unit) {
protected open suspend fun handleEnteringTanRequired(context: JobContext, tanResponse: TanResponse, response: BankResponse): BankResponse {
val bank = context.bank // TODO: copy required data to TanChallenge
val tanChallenge = createTanChallenge(tanResponse, bank)
val userDidCancelEnteringTan = ObjectReference(false)
context.callback.enterTan(bank, tanChallenge) { enteredTanResult ->
userDidCancelEnteringTan.value = true
val enteredTanResult = context.callback.enterTan(bank, tanChallenge)
userDidCancelEnteringTan.value = true
handleEnterTanResult(context, enteredTanResult, tanResponse, response, callback)
}
return handleEnterTanResult(context, enteredTanResult, tanResponse, response)
mayRetrieveAutomaticallyIfUserEnteredDecoupledTan(context, tanChallenge, tanResponse, userDidCancelEnteringTan)
// TODO:
// mayRetrieveAutomaticallyIfUserEnteredDecoupledTan(context, tanChallenge, tanResponse, userDidCancelEnteringTan)
}
protected open fun createTanChallenge(tanResponse: TanResponse, bank: BankData): TanChallenge {
@ -431,36 +423,36 @@ open class FinTsJobExecutor(
log.info("automaticallyRetrieveIfUserEnteredDecoupledTan() called for $tanChallenge")
}
protected open fun handleEnterTanResult(context: JobContext, enteredTanResult: EnterTanResult, tanResponse: TanResponse,
response: BankResponse, callback: (BankResponse) -> Unit) {
protected open suspend fun handleEnterTanResult(context: JobContext, enteredTanResult: EnterTanResult, tanResponse: TanResponse,
response: BankResponse): BankResponse {
if (enteredTanResult.changeTanMethodTo != null) {
handleUserAsksToChangeTanMethodAndResendLastMessage(context, enteredTanResult.changeTanMethodTo, callback)
return handleUserAsksToChangeTanMethodAndResendLastMessage(context, enteredTanResult.changeTanMethodTo)
}
else if (enteredTanResult.changeTanMediumTo is TanGeneratorTanMedium) {
handleUserAsksToChangeTanMediumAndResendLastMessage(context, enteredTanResult.changeTanMediumTo,
enteredTanResult.changeTanMediumResultCallback, callback)
return handleUserAsksToChangeTanMediumAndResendLastMessage(context, enteredTanResult.changeTanMediumTo,
enteredTanResult.changeTanMediumResultCallback)
}
else if (enteredTanResult.enteredTan == null) {
// i tried to send a HKTAN with cancelJob = true but then i saw there are no tan methods that support cancellation (at least not at my bank)
// but it's not required anyway, tan times out after some time. Simply don't respond anything and close dialog
response.tanRequiredButUserDidNotEnterOne = true
callback(response)
return response
}
else {
sendTanToBank(context, enteredTanResult.enteredTan, tanResponse, callback)
return sendTanToBank(context, enteredTanResult.enteredTan, tanResponse)
}
}
protected open fun sendTanToBank(context: JobContext, enteredTan: String, tanResponse: TanResponse, callback: (BankResponse) -> Unit) {
protected open suspend fun sendTanToBank(context: JobContext, enteredTan: String, tanResponse: TanResponse): BankResponse {
val message = messageBuilder.createSendEnteredTanMessage(context, enteredTan, tanResponse)
getAndHandleResponseForMessage(context, message, callback)
return getAndHandleResponseForMessage(context, message)
}
protected open fun handleUserAsksToChangeTanMethodAndResendLastMessage(context: JobContext, changeTanMethodTo: TanMethod, callback: (BankResponse) -> Unit) {
protected open suspend fun handleUserAsksToChangeTanMethodAndResendLastMessage(context: JobContext, changeTanMethodTo: TanMethod): BankResponse {
context.bank.selectedTanMethod = changeTanMethodTo
@ -469,131 +461,125 @@ open class FinTsJobExecutor(
lastCreatedMessage?.let { closeDialog(context) }
resendMessageInNewDialog(context, lastCreatedMessage, callback)
return resendMessageInNewDialog(context, lastCreatedMessage)
}
protected open fun handleUserAsksToChangeTanMediumAndResendLastMessage(context: JobContext, changeTanMediumTo: TanGeneratorTanMedium,
changeTanMediumResultCallback: ((FinTsClientResponse) -> Unit)?,
callback: (BankResponse) -> Unit) {
protected open suspend fun handleUserAsksToChangeTanMediumAndResendLastMessage(context: JobContext, changeTanMediumTo: TanGeneratorTanMedium,
changeTanMediumResultCallback: ((FinTsClientResponse) -> Unit)?): BankResponse {
val lastCreatedMessage = context.dialog.currentMessage
lastCreatedMessage?.let { closeDialog(context) }
changeTanMedium(context, changeTanMediumTo) { changeTanMediumResponse ->
changeTanMediumResultCallback?.invoke(FinTsClientResponse(context, changeTanMediumResponse))
val changeTanMediumResponse = changeTanMedium(context, changeTanMediumTo)
if (changeTanMediumResponse.successful == false || lastCreatedMessage == null) {
callback(changeTanMediumResponse)
}
else {
resendMessageInNewDialog(context, lastCreatedMessage, callback)
}
changeTanMediumResultCallback?.invoke(FinTsClientResponse(context, changeTanMediumResponse))
if (changeTanMediumResponse.successful == false || lastCreatedMessage == null) {
return changeTanMediumResponse
}
else {
return resendMessageInNewDialog(context, lastCreatedMessage)
}
}
protected open fun resendMessageInNewDialog(context: JobContext, lastCreatedMessage: MessageBuilderResult?, callback: (BankResponse) -> Unit) {
protected open suspend fun resendMessageInNewDialog(context: JobContext, lastCreatedMessage: MessageBuilderResult?): BankResponse {
if (lastCreatedMessage != null) { // do not use previousDialogContext.currentMessage as this may is previous dialog's dialog close message
context.startNewDialog(chunkedResponseHandler = context.dialog.chunkedResponseHandler)
initDialogWithStrongCustomerAuthentication(context) { initDialogResponse ->
if (initDialogResponse.successful == false) {
callback(initDialogResponse)
}
else {
val newMessage = messageBuilder.rebuildMessage(context, lastCreatedMessage)
val initDialogResponse = initDialogWithStrongCustomerAuthentication(context)
getAndHandleResponseForMessage(context, newMessage) { response ->
closeDialog(context)
if (initDialogResponse.successful == false) {
return initDialogResponse
} else {
val newMessage = messageBuilder.rebuildMessage(context, lastCreatedMessage)
callback(response)
}
}
val response = getAndHandleResponseForMessage(context, newMessage)
closeDialog(context)
return response
}
}
else {
val errorMessage = "There's no last action (like retrieve account transactions, transfer money, ...) to re-send with new TAN method. Probably an internal programming error." // TODO: translate
callback(BankResponse(false, internalError = errorMessage)) // should never come to this
return BankResponse(false, internalError = errorMessage) // should never come to this
}
}
protected open fun sendMessageInNewDialogAndHandleResponse(context: JobContext, segmentForNonStrongCustomerAuthenticationTwoStepTanProcess: CustomerSegmentId? = null,
closeDialog: Boolean = true, createMessage: () -> MessageBuilderResult, callback: (BankResponse) -> Unit) {
protected open suspend fun sendMessageInNewDialogAndHandleResponse(context: JobContext, segmentForNonStrongCustomerAuthenticationTwoStepTanProcess: CustomerSegmentId? = null,
closeDialog: Boolean = true, createMessage: () -> MessageBuilderResult): BankResponse {
context.startNewDialog(closeDialog)
if (segmentForNonStrongCustomerAuthenticationTwoStepTanProcess == null) {
initDialogWithStrongCustomerAuthentication(context) { initDialogResponse ->
sendMessageAndHandleResponseAfterDialogInitialization(context, initDialogResponse, createMessage, callback)
}
}
else {
initDialogMessageWithoutStrongCustomerAuthenticationAfterSuccessfulChecks(context, segmentForNonStrongCustomerAuthenticationTwoStepTanProcess) { initDialogResponse ->
sendMessageAndHandleResponseAfterDialogInitialization(context, initDialogResponse, createMessage, callback)
}
val initDialogResponse = if (segmentForNonStrongCustomerAuthenticationTwoStepTanProcess == null) {
initDialogWithStrongCustomerAuthentication(context)
} else {
initDialogMessageWithoutStrongCustomerAuthenticationAfterSuccessfulChecks(context, segmentForNonStrongCustomerAuthenticationTwoStepTanProcess)
}
return sendMessageAndHandleResponseAfterDialogInitialization(context, initDialogResponse, createMessage)
}
private fun sendMessageAndHandleResponseAfterDialogInitialization(context: JobContext, initDialogResponse: BankResponse,
createMessage: () -> MessageBuilderResult, callback: (BankResponse) -> Unit) {
protected open suspend fun sendMessageAndHandleResponseAfterDialogInitialization(context: JobContext, initDialogResponse: BankResponse,
createMessage: () -> MessageBuilderResult): BankResponse {
if (initDialogResponse.successful == false) {
callback(initDialogResponse)
return initDialogResponse
}
else {
val message = createMessage()
getAndHandleResponseForMessage(context, message) { response ->
closeDialog(context)
val response = getAndHandleResponseForMessage(context, message)
callback(response)
}
closeDialog(context)
return response
}
}
protected open fun initDialogWithStrongCustomerAuthentication(context: JobContext, callback: (BankResponse) -> Unit) {
protected open suspend fun initDialogWithStrongCustomerAuthentication(context: JobContext): BankResponse {
// we first need to retrieve supported tan methods and jobs before we can do anything
ensureBasicBankDataRetrieved(context) { retrieveBasicBankDataResponse ->
if (retrieveBasicBankDataResponse.successful == false) {
callback(retrieveBasicBankDataResponse)
val retrieveBasicBankDataResponse = ensureBasicBankDataRetrieved(context)
if (retrieveBasicBankDataResponse.successful == false) {
return retrieveBasicBankDataResponse
}
else {
// as in the next step we have to supply user's tan method, ensure user selected his or her
val tanMethodSelectedResponse = ensureTanMethodIsSelected(context)
if (tanMethodSelectedResponse.successful == false) {
return tanMethodSelectedResponse
}
else {
// as in the next step we have to supply user's tan method, ensure user selected his or her
ensureTanMethodIsSelected(context) { tanMethodSelectedResponse ->
if (tanMethodSelectedResponse.successful == false) {
callback(tanMethodSelectedResponse)
}
else {
initDialogWithStrongCustomerAuthenticationAfterSuccessfulPreconditionChecks(context, callback)
}
}
return initDialogWithStrongCustomerAuthenticationAfterSuccessfulPreconditionChecks(context)
}
}
}
protected open fun initDialogWithStrongCustomerAuthenticationAfterSuccessfulPreconditionChecks(context: JobContext, callback: (BankResponse) -> Unit) {
protected open suspend fun initDialogWithStrongCustomerAuthenticationAfterSuccessfulPreconditionChecks(context: JobContext): BankResponse {
context.startNewDialog(false) // don't know if it's ok for all invocations of this method to set closeDialog to false (was actually only set in getAccounts())
val message = messageBuilder.createInitDialogMessage(context)
getAndHandleResponseForMessage(context, message, callback)
return getAndHandleResponseForMessage(context, message)
}
protected open fun initDialogMessageWithoutStrongCustomerAuthenticationAfterSuccessfulChecks(context: JobContext, segmentIdForTwoStepTanProcess: CustomerSegmentId?,
callback: (BankResponse) -> Unit) {
protected open suspend fun initDialogMessageWithoutStrongCustomerAuthenticationAfterSuccessfulChecks(context: JobContext, segmentIdForTwoStepTanProcess: CustomerSegmentId?): BankResponse {
val message = messageBuilder.createInitDialogMessageWithoutStrongCustomerAuthentication(context, segmentIdForTwoStepTanProcess)
getAndHandleResponseForMessage(context, message, callback)
return getAndHandleResponseForMessage(context, message)
}
protected open fun closeDialog(context: JobContext) {
protected open suspend fun closeDialog(context: JobContext) {
// bank already closed dialog -> there's no need to send dialog end message
if (shouldNotCloseDialog(context)) {
@ -605,82 +591,80 @@ open class FinTsJobExecutor(
fireAndForgetMessage(context, dialogEndRequestBody)
}
private fun shouldNotCloseDialog(context: JobContext): Boolean {
protected open fun shouldNotCloseDialog(context: JobContext): Boolean {
return context.dialog.closeDialog == false || context.dialog.didBankCloseDialog
}
protected open fun ensureBasicBankDataRetrieved(context: JobContext, callback: (BankResponse) -> Unit) {
protected open suspend fun ensureBasicBankDataRetrieved(context: JobContext): BankResponse {
val bank = context.bank
if (bank.tanMethodsSupportedByBank.isEmpty() || bank.supportedJobs.isEmpty()) {
retrieveBasicDataLikeUsersTanMethods(context) { getBankInfoResponse ->
if (getBankInfoResponse.successful == false) {
callback(getBankInfoResponse)
} else if (bank.tanMethodsSupportedByBank.isEmpty() || bank.supportedJobs.isEmpty()) {
callback(BankResponse(false, internalError =
"Could not retrieve basic bank data like supported tan methods or supported jobs")) // TODO: translate // TODO: add as messageToShowToUser
} else {
callback(BankResponse(true))
}
val getBankInfoResponse = retrieveBasicDataLikeUsersTanMethods(context)
if (getBankInfoResponse.successful == false) {
return getBankInfoResponse
} else if (bank.tanMethodsSupportedByBank.isEmpty() || bank.supportedJobs.isEmpty()) {
return BankResponse(false, internalError =
"Could not retrieve basic bank data like supported tan methods or supported jobs") // TODO: translate // TODO: add as messageToShowToUser
} else {
return BankResponse(true)
}
}
else {
callback(BankResponse(true))
return BankResponse(true)
}
}
protected open fun ensureTanMethodIsSelected(context: JobContext, callback: (BankResponse) -> Unit) {
protected open suspend fun ensureTanMethodIsSelected(context: JobContext): BankResponse {
val bank = context.bank
if (bank.isTanMethodSelected == false) {
if (bank.tanMethodsAvailableForUser.isEmpty()) {
retrieveBasicDataLikeUsersTanMethods(context) { retrieveBasicDataResponse ->
callback(retrieveBasicDataResponse)
}
return retrieveBasicDataLikeUsersTanMethods(context)
}
else {
getUsersTanMethod(context) {
callback(createNoTanMethodSelectedResponse(bank))
}
getUsersTanMethod(context)
return createNoTanMethodSelectedResponse(bank)
}
}
else {
callback(createNoTanMethodSelectedResponse(bank))
return createNoTanMethodSelectedResponse(bank)
}
}
private fun createNoTanMethodSelectedResponse(bank: BankData): BankResponse {
protected open fun createNoTanMethodSelectedResponse(bank: BankData): BankResponse {
val noTanMethodSelected = !!!bank.isTanMethodSelected
val errorMessage = if (noTanMethodSelected) "User did not select a TAN method" else null // TODO: translate
return BankResponse(true, noTanMethodSelected = noTanMethodSelected, internalError = errorMessage)
}
open fun getUsersTanMethod(context: JobContext, preferredTanMethods: List<TanMethodType>? = null, done: (Boolean) -> Unit) {
open suspend fun getUsersTanMethod(context: JobContext, preferredTanMethods: List<TanMethodType>? = null): Boolean {
val bank = context.bank
if (bank.tanMethodsAvailableForUser.size == 1) { // user has only one TAN method -> set it and we're done
bank.selectedTanMethod = bank.tanMethodsAvailableForUser.first()
done(true)
return true
}
else {
tanMethodSelector.findPreferredTanMethod(bank.tanMethodsAvailableForUser, preferredTanMethods)?.let {
bank.selectedTanMethod = it
done(true)
return
return true
}
// we know user's supported tan methods, now ask user which one to select
val suggestedTanMethod = tanMethodSelector.getSuggestedTanMethod(bank.tanMethodsAvailableForUser)
context.callback.askUserForTanMethod(bank.tanMethodsAvailableForUser, suggestedTanMethod) { selectedTanMethod ->
if (selectedTanMethod != null) {
bank.selectedTanMethod = selectedTanMethod
done(true)
}
else {
done(false)
}
val selectedTanMethod = context.callback.askUserForTanMethod(bank.tanMethodsAvailableForUser, suggestedTanMethod)
if (selectedTanMethod != null) {
bank.selectedTanMethod = selectedTanMethod
return true
}
else {
return false
}
}
}

View File

@ -21,79 +21,78 @@ open class RequestExecutor(
) {
companion object {
private val log = LoggerFactory.getLogger(FinTsJobExecutor::class)
private val log = LoggerFactory.getLogger(RequestExecutor::class)
}
open fun getAndHandleResponseForMessage(message: MessageBuilderResult, context: JobContext,
tanRequiredCallback: (TanResponse, BankResponse, callback: (BankResponse) -> Unit) -> Unit, callback: (BankResponse) -> Unit) {
open suspend fun getAndHandleResponseForMessage(message: MessageBuilderResult, context: JobContext, tanRequiredCallback: suspend (TanResponse, BankResponse) -> BankResponse): BankResponse {
if (message.createdMessage == null) {
log.error("Could not create FinTS message to be sent to bank. isJobAllowed ${message.isJobAllowed}, isJobVersionSupported = ${message.isJobVersionSupported}," +
"allowedVersions = ${message.allowedVersions}, supportedVersions = ${message.supportedVersions}.")
callback(BankResponse(false, messageThatCouldNotBeCreated = message, internalError = "Could not create FinTS message to be sent to bank")) // TODO: translate
return BankResponse(false, messageThatCouldNotBeCreated = message, internalError = "Could not create FinTS message to be sent to bank") // TODO: translate
}
else {
getAndHandleResponseForMessage(context, message.createdMessage) { response ->
handleMayRequiresTan(context, response, tanRequiredCallback) { handledResponse ->
// if there's a Aufsetzpunkt (continuationId) set, then response is not complete yet, there's more information to fetch by sending this Aufsetzpunkt
handledResponse.aufsetzpunkt?.let { continuationId ->
if (handledResponse.followUpResponse == null) { // for re-sent messages followUpResponse is already set and dialog already closed -> would be overwritten with an error response that dialog is closed
if (message.isSendEnteredTanMessage() == false) { // for sending TAN no follow up message can be created -> filter out, otherwise chunkedResponseHandler would get called twice for same response
context.dialog.chunkedResponseHandler?.invoke(handledResponse)
}
val response = getAndHandleResponseForMessage(context, message.createdMessage)
getFollowUpMessageForContinuationId(context, handledResponse, continuationId, message, tanRequiredCallback) { followUpResponse ->
handledResponse.followUpResponse = followUpResponse
handledResponse.hasFollowUpMessageButCouldNotReceiveIt = handledResponse.followUpResponse == null
val handledResponse = handleMayRequiresTan(context, response, tanRequiredCallback)
callback(handledResponse)
}
}
else {
callback(handledResponse)
}
// if there's a Aufsetzpunkt (continuationId) set, then response is not complete yet, there's more information to fetch by sending this Aufsetzpunkt
handledResponse.aufsetzpunkt?.let { continuationId ->
if (handledResponse.followUpResponse == null) { // for re-sent messages followUpResponse is already set and dialog already closed -> would be overwritten with an error response that dialog is closed
if (message.isSendEnteredTanMessage() == false) { // for sending TAN no follow up message can be created -> filter out, otherwise chunkedResponseHandler would get called twice for same response
context.dialog.chunkedResponseHandler?.invoke(handledResponse)
}
?: run {
// e.g. response = enter TAN response, but handledResponse is then response after entering TAN, e.g. account transactions
// -> chunkedResponseHandler would get called for same handledResponse multiple times
if (response == handledResponse) {
context.dialog.chunkedResponseHandler?.invoke(handledResponse)
}
callback(handledResponse)
}
val followUpResponse = getFollowUpMessageForContinuationId(context, handledResponse, continuationId, message, tanRequiredCallback)
handledResponse.followUpResponse = followUpResponse
handledResponse.hasFollowUpMessageButCouldNotReceiveIt = handledResponse.followUpResponse == null
return handledResponse
}
else {
return handledResponse
}
}
?: run {
// e.g. response = enter TAN response, but handledResponse is then response after entering TAN, e.g. account transactions
// -> chunkedResponseHandler would get called for same handledResponse multiple times
if (response == handledResponse) {
context.dialog.chunkedResponseHandler?.invoke(handledResponse)
}
return handledResponse
}
}
}
protected open fun getAndHandleResponseForMessage(context: JobContext, requestBody: String, callback: (BankResponse) -> Unit) {
protected open suspend fun getAndHandleResponseForMessage(context: JobContext, requestBody: String): BankResponse {
addMessageLog(context, MessageLogEntryType.Sent, requestBody)
getResponseForMessage(requestBody, context.bank.finTs3ServerAddress) { webResponse ->
val response = handleResponse(context, webResponse)
val webResponse = getResponseForMessage(requestBody, context.bank.finTs3ServerAddress)
val dialog = context.dialog
dialog.response = response
val response = handleResponse(context, webResponse)
response.messageHeader?.let { header -> dialog.dialogId = header.dialogId }
dialog.didBankCloseDialog = response.didBankCloseDialog
val dialog = context.dialog
dialog.response = response
callback(response)
}
response.messageHeader?.let { header -> dialog.dialogId = header.dialogId }
dialog.didBankCloseDialog = response.didBankCloseDialog
return response
}
protected open fun getResponseForMessage(requestBody: String, finTs3ServerAddress: String, callback: (WebClientResponse) -> Unit) {
protected open suspend fun getResponseForMessage(requestBody: String, finTs3ServerAddress: String): WebClientResponse {
val encodedRequestBody = base64Service.encode(requestBody)
webClient.post(finTs3ServerAddress, encodedRequestBody, "application/octet-stream", IWebClient.DefaultUserAgent, callback)
return webClient.post(finTs3ServerAddress, encodedRequestBody, "application/octet-stream", IWebClient.DefaultUserAgent)
}
open fun fireAndForgetMessage(context: JobContext, message: MessageBuilderResult) {
open suspend fun fireAndForgetMessage(context: JobContext, message: MessageBuilderResult) {
message.createdMessage?.let { requestBody ->
addMessageLog(context, MessageLogEntryType.Sent, requestBody)
getResponseForMessage(requestBody, context.bank.finTs3ServerAddress) { }
getResponseForMessage(requestBody, context.bank.finTs3ServerAddress)
// if really needed add received response to message log here
}
@ -129,34 +128,30 @@ open class RequestExecutor(
}
protected open fun getFollowUpMessageForContinuationId(context: JobContext, response: BankResponse, continuationId: String, message: MessageBuilderResult,
tanRequiredCallback: (TanResponse, BankResponse, callback: (BankResponse) -> Unit) -> Unit,
callback: (BankResponse?) -> Unit) {
protected open suspend fun getFollowUpMessageForContinuationId(context: JobContext, response: BankResponse, continuationId: String, message: MessageBuilderResult,
tanRequiredCallback: suspend (TanResponse, BankResponse) -> BankResponse): BankResponse? {
messageBuilder.rebuildMessageWithContinuationId(context, message, continuationId)?.let { followUpMessage ->
getAndHandleResponseForMessage(followUpMessage, context, tanRequiredCallback, callback)
return getAndHandleResponseForMessage(followUpMessage, context, tanRequiredCallback)
}
?: run { callback(null) }
return null
}
protected open fun handleMayRequiresTan(context: JobContext, response: BankResponse,
tanRequiredCallback: (TanResponse, BankResponse, callback: (BankResponse) -> Unit) -> Unit,
callback: (BankResponse) -> Unit) { // TODO: use response from DialogContext
protected open suspend fun handleMayRequiresTan(context: JobContext, response: BankResponse,
tanRequiredCallback: suspend (TanResponse, BankResponse) -> BankResponse): BankResponse { // TODO: use response from DialogContext
if (response.isStrongAuthenticationRequired) {
if (context.dialog.abortIfTanIsRequired) {
response.tanRequiredButWeWereToldToAbortIfSo = true
callback(response)
return
return response
}
else if (response.tanResponse != null) {
response.tanResponse?.let { tanResponse ->
tanRequiredCallback(tanResponse, response, callback)
return tanRequiredCallback(tanResponse, response)
}
return
}
}
@ -167,7 +162,7 @@ open class RequestExecutor(
// TODO: also check '9931 Sperrung des Kontos nach %1 Fehlversuchen' -> if %1 == 3 synchronize TAN generator
// as it's quite unrealistic that user entered TAN wrong three times, in most cases TAN generator is not synchronized
callback(response)
return response
}

View File

@ -16,15 +16,15 @@ interface FinTsClientCallback {
* If you do not support an enter tan dialog or if your enter tan dialog supports selecting a TAN method, it's
* best returning [suggestedTanMethod] and to not show an extra select TAN method dialog.
*/
fun askUserForTanMethod(supportedTanMethods: List<TanMethod>, suggestedTanMethod: TanMethod?, callback: (TanMethod?) -> Unit)
suspend fun askUserForTanMethod(supportedTanMethods: List<TanMethod>, suggestedTanMethod: TanMethod?): TanMethod?
fun enterTan(bank: BankData, tanChallenge: TanChallenge, callback: (EnterTanResult) -> Unit)
suspend fun enterTan(bank: BankData, tanChallenge: TanChallenge): EnterTanResult
/**
* This method gets called for chipTan TAN generators when the bank asks the customer to synchronize her/his TAN generator.
*
* If you do not support entering TAN generator ATC, return [EnterTanGeneratorAtcResult.userDidNotEnterAtc]
*/
fun enterTanGeneratorAtc(bank: BankData, tanMedium: TanGeneratorTanMedium, callback: (EnterTanGeneratorAtcResult) -> Unit)
suspend fun enterTanGeneratorAtc(bank: BankData, tanMedium: TanGeneratorTanMedium): EnterTanGeneratorAtcResult
}

View File

@ -6,18 +6,16 @@ import net.dankito.banking.fints.model.*
open class NoOpFinTsClientCallback : FinTsClientCallback {
override fun askUserForTanMethod(supportedTanMethods: List<TanMethod>,
suggestedTanMethod: TanMethod?, callback: (TanMethod?) -> Unit) {
callback(suggestedTanMethod)
override suspend fun askUserForTanMethod(supportedTanMethods: List<TanMethod>, suggestedTanMethod: TanMethod?): TanMethod? {
return suggestedTanMethod
}
override fun enterTan(bank: BankData, tanChallenge: TanChallenge, callback: (EnterTanResult) -> Unit) {
callback(EnterTanResult.userDidNotEnterTan())
override suspend fun enterTan(bank: BankData, tanChallenge: TanChallenge): EnterTanResult {
return EnterTanResult.userDidNotEnterTan()
}
override fun enterTanGeneratorAtc(bank: BankData, tanMedium: TanGeneratorTanMedium, callback: (EnterTanGeneratorAtcResult) -> Unit) {
callback(EnterTanGeneratorAtcResult.userDidNotEnterAtc())
override suspend fun enterTanGeneratorAtc(bank: BankData, tanMedium: TanGeneratorTanMedium): EnterTanGeneratorAtcResult {
return EnterTanGeneratorAtcResult.userDidNotEnterAtc()
}
}

View File

@ -5,9 +5,9 @@ import net.dankito.banking.fints.model.*
open class SimpleFinTsClientCallback(
protected val enterTan: ((bank: BankData, tanChallenge: TanChallenge) -> EnterTanResult)? = null,
protected val enterTanGeneratorAtc: ((bank: BankData, tanMedium: TanGeneratorTanMedium) -> EnterTanGeneratorAtcResult)? = null,
protected val askUserForTanMethod: ((supportedTanMethods: List<TanMethod>, suggestedTanMethod: TanMethod?) -> TanMethod?)? = null
protected open val enterTan: ((bank: BankData, tanChallenge: TanChallenge) -> EnterTanResult)? = null,
protected open val enterTanGeneratorAtc: ((bank: BankData, tanMedium: TanGeneratorTanMedium) -> EnterTanGeneratorAtcResult)? = null,
protected open val askUserForTanMethod: ((supportedTanMethods: List<TanMethod>, suggestedTanMethod: TanMethod?) -> TanMethod?)? = null
) : FinTsClientCallback {
constructor() : this(null) // Swift does not support default parameter values -> create constructor overloads
@ -15,18 +15,17 @@ open class SimpleFinTsClientCallback(
constructor(enterTan: ((bank: BankData, tanChallenge: TanChallenge) -> EnterTanResult)?) : this(enterTan, null)
override fun askUserForTanMethod(supportedTanMethods: List<TanMethod>,
suggestedTanMethod: TanMethod?, callback: (TanMethod?) -> Unit) {
override suspend fun askUserForTanMethod(supportedTanMethods: List<TanMethod>, suggestedTanMethod: TanMethod?): TanMethod? {
callback(askUserForTanMethod?.invoke(supportedTanMethods, suggestedTanMethod) ?: suggestedTanMethod)
return askUserForTanMethod?.invoke(supportedTanMethods, suggestedTanMethod) ?: suggestedTanMethod
}
override fun enterTan(bank: BankData, tanChallenge: TanChallenge, callback: (EnterTanResult) -> Unit) {
callback(enterTan?.invoke(bank, tanChallenge) ?: EnterTanResult.userDidNotEnterTan())
override suspend fun enterTan(bank: BankData, tanChallenge: TanChallenge): EnterTanResult {
return enterTan?.invoke(bank, tanChallenge) ?: EnterTanResult.userDidNotEnterTan()
}
override fun enterTanGeneratorAtc(bank: BankData, tanMedium: TanGeneratorTanMedium, callback: (EnterTanGeneratorAtcResult) -> Unit) {
callback(enterTanGeneratorAtc?.invoke(bank, tanMedium) ?: EnterTanGeneratorAtcResult.userDidNotEnterAtc())
override suspend fun enterTanGeneratorAtc(bank: BankData, tanMedium: TanGeneratorTanMedium): EnterTanGeneratorAtcResult {
return enterTanGeneratorAtc?.invoke(bank, tanMedium) ?: EnterTanGeneratorAtcResult.userDidNotEnterAtc()
}
}

View File

@ -20,6 +20,10 @@ interface IWebClient {
}
fun post(url: String, body: String, contentType: String = "application/octet-stream", userAgent: String = DefaultUserAgent, callback: (WebClientResponse) -> Unit)
suspend fun post(url: String, body: String): WebClientResponse { // some platforms don't support default parameters
return post(url, body)
}
suspend fun post(url: String, body: String, contentType: String = "application/octet-stream", userAgent: String = DefaultUserAgent): WebClientResponse
}

View File

@ -35,13 +35,11 @@ open class KtorWebClient : IWebClient {
}
override fun post(url: String, body: String, contentType: String, userAgent: String, callback: (WebClientResponse) -> Unit) {
GlobalScope.async {
postInCoroutine(url, body, contentType, userAgent, callback)
}
override suspend fun post(url: String, body: String, contentType: String, userAgent: String): WebClientResponse {
return postInCoroutine(url, body, contentType, userAgent)
}
protected open suspend fun postInCoroutine(url: String, body: String, contentType: String, userAgent: String, callback: (WebClientResponse) -> Unit) {
protected open suspend fun postInCoroutine(url: String, body: String, contentType: String, userAgent: String): WebClientResponse {
try {
val clientResponse = client.post(url) {
contentType(ContentType.Application.OctetStream)
@ -50,11 +48,11 @@ open class KtorWebClient : IWebClient {
val responseBody = clientResponse.bodyAsText()
callback(WebClientResponse(clientResponse.status.value == 200, clientResponse.status.value, body = responseBody))
return WebClientResponse(clientResponse.status.value == 200, clientResponse.status.value, body = responseBody)
} catch (e: Exception) {
log.error(e) { "Could not send request to url '$url'" }
callback(WebClientResponse(false, error = e))
return WebClientResponse(false, error = e)
}
}

View File

@ -10,8 +10,8 @@ class ProxyingWebClient(proxyUrl: String, private val delegate: IWebClient) : IW
private val proxyUrl = if (proxyUrl.endsWith("/")) proxyUrl else proxyUrl + "/"
override fun post(url: String, body: String, contentType: String, userAgent: String, callback: (WebClientResponse) -> Unit) {
delegate.post(proxyUrl + url, body, contentType, userAgent, callback)
override suspend fun post(url: String, body: String, contentType: String, userAgent: String): WebClientResponse {
return delegate.post(proxyUrl + url, body, contentType, userAgent)
}
}

View File

@ -1,13 +1,10 @@
import kotlinx.coroutines.runBlocking
import kotlinx.datetime.LocalDate
import net.dankito.banking.fints.FinTsClientDeprecated
import net.dankito.banking.fints.FinTsJobExecutor
import net.dankito.banking.fints.RequestExecutor
import net.dankito.banking.fints.callback.SimpleFinTsClientCallback
import net.dankito.banking.fints.model.AddAccountParameter
import net.dankito.banking.fints.model.RetrievedAccountData
import net.dankito.banking.fints.response.client.AddAccountResponse
import net.dankito.banking.fints.webclient.BlockingKtorWebClient
import net.dankito.utils.multiplatform.extensions.*
import platform.posix.exit
@ -25,13 +22,13 @@ class Application {
fun retrieveAccountData(bankCode: String, customerId: String, pin: String, finTs3ServerAddress: String) {
runBlocking {
val client = FinTsClientDeprecated(SimpleFinTsClientCallback(), FinTsJobExecutor(RequestExecutor(webClient = BlockingKtorWebClient())))
val client = FinTsClientDeprecated(SimpleFinTsClientCallback())
client.addAccountAsync(AddAccountParameter(bankCode, customerId, pin, finTs3ServerAddress)) { response ->
println("Retrieved response from ${response.bank.bankName} for ${response.bank.customerName}")
val response = client.addAccountAsync(AddAccountParameter(bankCode, customerId, pin, finTs3ServerAddress))
displayRetrievedAccountData(response)
}
println("Retrieved response from ${response.bank.bankName} for ${response.bank.customerName}")
displayRetrievedAccountData(response)
}
}

View File

@ -1,14 +0,0 @@
package net.dankito.banking.fints.webclient
import kotlinx.coroutines.runBlocking
open class BlockingKtorWebClient : KtorWebClient() {
override fun post(url: String, body: String, contentType: String, userAgent: String, callback: (WebClientResponse) -> Unit) {
runBlocking {
postInCoroutine(url, body, contentType, userAgent, callback)
}
}
}