diff --git a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/service/testaccess/TestAccessBankingClient.kt b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/service/testaccess/TestAccessBankingClient.kt new file mode 100644 index 00000000..81abc492 --- /dev/null +++ b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/service/testaccess/TestAccessBankingClient.kt @@ -0,0 +1,171 @@ +package net.dankito.banking.service.testaccess + +import kotlin.random.Random +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.IAsyncRunner +import net.dankito.utils.multiplatform.BigDecimal +import net.dankito.utils.multiplatform.Date + + +/** + * Apple requires a test access. So this class implements a banking client that just returns fake data + */ +open class TestAccessBankingClient( + protected val bank: TypedBankData, + protected val modelCreator: IModelCreator, + protected val asyncRunner: IAsyncRunner, + callback: BankingClientCallback +) : IBankingClient { + + companion object { + const val MillisecondsOfADay = 24 * 60 * 60 * 1000L + } + + + override val messageLogWithoutSensitiveData: List = listOf() + + + override fun addAccountAsync(callback: (AddAccountResponse) -> Unit) { + asyncRunner.runAsync { // for Android it's essential to get off UI thread + bank.customerName = "Horst" + bank.supportedTanMethods = listOf() + bank.tanMedia = listOf() + + bank.accounts = createAccounts(bank) + + callback(AddAccountResponse(bank, createRetrievedAccountData(bank))) + } + } + + override fun getTransactionsAsync(parameter: GetTransactionsParameter, callback: (GetTransactionsResponse) -> Unit) { + asyncRunner.runAsync { + callback(GetTransactionsResponse(createRetrievedAccountData(parameter.account))) + } + } + + + override fun transferMoneyAsync(data: TransferMoneyData, callback: (BankingClientResponse) -> Unit) { + asyncRunner.runAsync { + callback(BankingClientResponse(true, null)) + } + } + + + override fun dataChanged(bank: TypedBankData) { + // nothing to do + } + + override fun deletedBank(bank: TypedBankData, wasLastAccountWithThisCredentials: Boolean) { + // nothing to do + } + + + protected open fun createAccounts(bank: TypedBankData): List { + val checkingAccount = createAccount(bank, "Girokonto Deluxe", "DELiebe", BankAccountType.CheckingAccount) + val fixedTermDepositAccount = createAccount(bank, "Tagesgeld Minus", "DEKuscheln", BankAccountType.FixedTermDepositAccount) + val creditCardAccount = createAccount(bank, "Credit card golden super plus", "12345678", BankAccountType.CreditCardAccount) + + setAccountFeatures(checkingAccount) + setAccountFeatures(fixedTermDepositAccount, false, false) + creditCardAccount.supportsRetrievingAccountTransactions = true + + return listOf( + checkingAccount, + fixedTermDepositAccount, + creditCardAccount + ) + } + + protected open fun createAccount(bank: TypedBankData, productName: String, identifier: String, type: BankAccountType) : TypedBankAccount { + val account = modelCreator.createAccount(bank, productName, identifier) + + account.isAccountTypeSupportedByApplication = true + account.accountHolderName = bank.customerName + account.type = type + + account.countDaysForWhichTransactionsAreKept = 90 + + return account + } + + protected open fun setAccountFeatures(account: TypedBankAccount, supportsRealTimeTransfer: Boolean = true, supportsTransferringMoney: Boolean = true) { + account.supportsRetrievingBalance = true + account.supportsRetrievingAccountTransactions = true + account.supportsTransferringMoney = supportsTransferringMoney + account.supportsRealTimeTransfer = supportsRealTimeTransfer + } + + + protected open fun createRetrievedAccountData(bank: TypedBankData): List { + return bank.accounts.map { createRetrievedAccountData(it) } + } + + protected open fun createRetrievedAccountData(account: TypedBankAccount): RetrievedAccountData { + val balance = createAmount() + val transactionsStartDate = account.retrievedTransactionsUpTo ?: Date(Date.today.millisSinceEpoch - 90 * MillisecondsOfADay) + val transactionsEndDate = Date() + + return RetrievedAccountData(account, true, balance, createBookedTransactions(account, transactionsStartDate, transactionsEndDate), + listOf(), transactionsStartDate, transactionsEndDate) + } + + protected open fun createBookedTransactions(account: TypedBankAccount, transactionsStartDate: Date, transactionsEndDate: Date): List { + val countDays = ((transactionsEndDate.millisSinceEpoch - transactionsStartDate.millisSinceEpoch) / MillisecondsOfADay).toInt() + + return IntRange(1, countDays).flatMap { dayIndex -> + val valueDate = Date(transactionsEndDate.millisSinceEpoch - (countDays - dayIndex) * MillisecondsOfADay) + + createAccountTransactionForDay(account, valueDate) + } + } + + protected open fun createAccountTransactionForDay(account: TypedBankAccount, valueDate: Date): List { + val random = defaultRandom + val countTransactionsForDay = random.nextInt(0, 4) + + return IntRange(0, countTransactionsForDay - 1).map { + createAccountTransaction(account, valueDate) + } + } + + protected open fun createAccountTransaction(account: TypedBankAccount, valueDate: Date): IAccountTransaction { + val random = defaultRandom + + val amount = createAmount(random) + val otherPartyName = getOtherPartyName(random) + val otherPartyBankCode = null + val otherPartyAccountId = "DEMirHerzlichEgal" + val bookingText = "Überweisung" + + return modelCreator.createTransaction(account, amount, "EUR", "Reference", valueDate, otherPartyName, otherPartyBankCode, otherPartyAccountId, + bookingText, valueDate) + } + + protected open fun getOtherPartyName(random: Random): String? { + val otherPartyNames = listOf("Mahatma Gandhi", "Mutter Theresa", "Nelson Mandela", "Schnappi das Krokodil", "Winnie Puh", "Albert Einstein", "Heinrich VIII.", "Andreas Scheuer") + val otherPartyNameIndex = random.nextInt(0, otherPartyNames.size) + + return otherPartyNames[otherPartyNameIndex] + } + + protected open fun createAmount(): BigDecimal { + return createAmount(defaultRandom) + } + + protected open fun createAmount(random: Random): BigDecimal { + val amountAsDouble = random.nextDouble(-10_000.01, 10_000.01) + + return BigDecimal(amountAsDouble) + } + + protected open val defaultRandom: Random = Random.Default + +} \ No newline at end of file diff --git a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/service/testaccess/TestAccessBankingClientCreator.kt b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/service/testaccess/TestAccessBankingClientCreator.kt new file mode 100644 index 00000000..cc8aad2a --- /dev/null +++ b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/service/testaccess/TestAccessBankingClientCreator.kt @@ -0,0 +1,28 @@ +package net.dankito.banking.service.testaccess + +import net.dankito.banking.ui.BankingClientCallback +import net.dankito.banking.ui.IBankingClient +import net.dankito.banking.ui.IBankingClientCreator +import net.dankito.banking.fints.webclient.IWebClient +import net.dankito.banking.fints.webclient.KtorWebClient +import net.dankito.banking.ui.model.TypedBankData +import net.dankito.banking.ui.model.mapper.IModelCreator +import net.dankito.banking.util.IAsyncRunner +import net.dankito.utils.multiplatform.File + + +open class TestAccessBankingClientCreator( + protected val modelCreator: IModelCreator +) : IBankingClientCreator { + + override fun createClient( + bank: TypedBankData, + dataFolder: File, + asyncRunner: IAsyncRunner, + callback: BankingClientCallback + ): IBankingClient { + + return TestAccessBankingClient(bank, modelCreator, asyncRunner, callback) + } + +} \ No newline at end of file diff --git a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/presenter/BankingPresenter.kt b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/presenter/BankingPresenter.kt index 68231bbd..8fcb9912 100644 --- a/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/presenter/BankingPresenter.kt +++ b/ui/BankingUiCommon/src/commonMain/kotlin/net/dankito/banking/ui/presenter/BankingPresenter.kt @@ -33,6 +33,7 @@ import net.dankito.banking.util.extraction.ITextExtractorRegistry import net.dankito.banking.util.extraction.NoOpInvoiceDataExtractor import net.dankito.banking.util.extraction.NoOpTextExtractorRegistry import net.codinux.banking.tools.epcqrcode.* +import net.dankito.banking.service.testaccess.TestAccessBankingClientCreator import net.dankito.utils.multiplatform.* import net.dankito.utils.multiplatform.log.LoggerFactory import kotlin.collections.ArrayList @@ -73,6 +74,9 @@ open class BankingPresenter( protected val MessageLogEntryDateFormatter = DateFormatter("yyyy.MM.dd HH:mm:ss.SSS") + protected const val TestAccountBankCode = "00000000" + protected val TestAccountBankInfo = BankInfo("Testbank", TestAccountBankCode, "RIEKDEMM", "80809", "München", "https://rie.ka/route/to/love", "FinTS V3.0") + private val log = LoggerFactory.getLogger(BankingPresenter::class) } @@ -145,7 +149,7 @@ open class BankingPresenter( val deserializedBanks = persister.readPersistedBanks() deserializedBanks.forEach { bank -> - val newClient = bankingClientCreator.createClient(bank, dataFolder, asyncRunner, callback) + val newClient = getBankingClientCreatorForBank(bank).createClient(bank, dataFolder, asyncRunner, callback) addClientForBank(bank, newClient) @@ -174,7 +178,7 @@ open class BankingPresenter( val bank = modelCreator.createBank(bankInfo.bankCode, userName, password, bankInfo.pinTanAddress ?: "", bankInfo.name, bankInfo.bic, "") bank.savePassword = savePassword - val newClient = bankingClientCreator.createClient(bank, dataFolder, asyncRunner, this.callback) + val newClient = getBankingClientCreatorForBank(bank).createClient(bank, dataFolder, asyncRunner, this.callback) val startDate = Date() @@ -182,7 +186,7 @@ open class BankingPresenter( if (response.successful) { try { handleSuccessfullyAddedBank(response.bank, newClient, response, startDate) - } catch (e: Exception) { + } catch (e: Exception) { // TODO: show error to user. Otherwise she has no idea what's going on log.error(e) { "Could not save successfully added bank" } } } @@ -191,6 +195,14 @@ open class BankingPresenter( } } + protected open fun getBankingClientCreatorForBank(bank: TypedBankData): IBankingClientCreator { + if (isTestAccount(bank)) { + return TestAccessBankingClientCreator(modelCreator) + } + + return bankingClientCreator + } + protected open fun handleSuccessfullyAddedBank(bank: TypedBankData, newClient: IBankingClient, response: AddAccountResponse, startDate: Date) { bank.displayIndex = allBanks.size @@ -202,7 +214,7 @@ open class BankingPresenter( findIconForBankAsync(bank) - persistBankOffUiThread(bank) + persistBankOffUiThread(bank) // TODO: if persisting bank throws an exception then () never gets called -> it's data is lost. Due database error maybe forever but also for this session / app run response.retrievedData.forEach { retrievedData -> retrievedAccountTransactions(GetTransactionsResponse(retrievedData), startDate, false) @@ -210,6 +222,10 @@ open class BankingPresenter( } protected open fun findIconForBankAsync(bank: TypedBankData) { + if (isTestAccount(bank)) { // show default icon for test account + return + } + bankIconFinder.findIconForBankAsync(bank.bankName) { bankIconUrl -> bankIconUrl?.let { try { @@ -700,6 +716,11 @@ open class BankingPresenter( } open fun findBanksByNameBankCodeOrCity(query: String?): List { + // to provide test access as request by Apple + if (query == TestAccountBankCode) { + return listOf(TestAccountBankInfo) + } + return bankFinder.findBankByNameBankCodeOrCity(query) .sortedBy { it.name.toLowerCase() } } @@ -1046,6 +1067,15 @@ open class BankingPresenter( } + protected open fun isTestAccount(bank: TypedBankData): Boolean { + return isTestAccount(bank.bankCode) + } + + protected open fun isTestAccount(bankCode: String): Boolean { + return bankCode == TestAccountBankCode + } + + open fun addBanksChangedListener(listener: (List) -> Unit): Boolean { return banksChangedListeners.add(listener) }