Implemented a test account to fulfill Apple's requirements

This commit is contained in:
dankito 2020-10-28 15:42:33 +01:00
parent 5d0f74c5e7
commit 7da0c989b2
3 changed files with 233 additions and 4 deletions

View File

@ -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<MessageLogEntry> = 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<TypedBankAccount> {
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<RetrievedAccountData> {
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<IAccountTransaction> {
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<IAccountTransaction> {
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
}

View File

@ -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)
}
}

View File

@ -33,6 +33,7 @@ import net.dankito.banking.util.extraction.ITextExtractorRegistry
import net.dankito.banking.util.extraction.NoOpInvoiceDataExtractor import net.dankito.banking.util.extraction.NoOpInvoiceDataExtractor
import net.dankito.banking.util.extraction.NoOpTextExtractorRegistry import net.dankito.banking.util.extraction.NoOpTextExtractorRegistry
import net.codinux.banking.tools.epcqrcode.* import net.codinux.banking.tools.epcqrcode.*
import net.dankito.banking.service.testaccess.TestAccessBankingClientCreator
import net.dankito.utils.multiplatform.* import net.dankito.utils.multiplatform.*
import net.dankito.utils.multiplatform.log.LoggerFactory import net.dankito.utils.multiplatform.log.LoggerFactory
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@ -73,6 +74,9 @@ open class BankingPresenter(
protected val MessageLogEntryDateFormatter = DateFormatter("yyyy.MM.dd HH:mm:ss.SSS") 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) private val log = LoggerFactory.getLogger(BankingPresenter::class)
} }
@ -145,7 +149,7 @@ open class BankingPresenter(
val deserializedBanks = persister.readPersistedBanks() val deserializedBanks = persister.readPersistedBanks()
deserializedBanks.forEach { bank -> deserializedBanks.forEach { bank ->
val newClient = bankingClientCreator.createClient(bank, dataFolder, asyncRunner, callback) val newClient = getBankingClientCreatorForBank(bank).createClient(bank, dataFolder, asyncRunner, callback)
addClientForBank(bank, newClient) addClientForBank(bank, newClient)
@ -174,7 +178,7 @@ open class BankingPresenter(
val bank = modelCreator.createBank(bankInfo.bankCode, userName, password, bankInfo.pinTanAddress ?: "", bankInfo.name, bankInfo.bic, "") val bank = modelCreator.createBank(bankInfo.bankCode, userName, password, bankInfo.pinTanAddress ?: "", bankInfo.name, bankInfo.bic, "")
bank.savePassword = savePassword 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() val startDate = Date()
@ -182,7 +186,7 @@ open class BankingPresenter(
if (response.successful) { if (response.successful) {
try { try {
handleSuccessfullyAddedBank(response.bank, newClient, response, startDate) 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" } 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) { protected open fun handleSuccessfullyAddedBank(bank: TypedBankData, newClient: IBankingClient, response: AddAccountResponse, startDate: Date) {
bank.displayIndex = allBanks.size bank.displayIndex = allBanks.size
@ -202,7 +214,7 @@ open class BankingPresenter(
findIconForBankAsync(bank) 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 -> response.retrievedData.forEach { retrievedData ->
retrievedAccountTransactions(GetTransactionsResponse(retrievedData), startDate, false) retrievedAccountTransactions(GetTransactionsResponse(retrievedData), startDate, false)
@ -210,6 +222,10 @@ open class BankingPresenter(
} }
protected open fun findIconForBankAsync(bank: TypedBankData) { protected open fun findIconForBankAsync(bank: TypedBankData) {
if (isTestAccount(bank)) { // show default icon for test account
return
}
bankIconFinder.findIconForBankAsync(bank.bankName) { bankIconUrl -> bankIconFinder.findIconForBankAsync(bank.bankName) { bankIconUrl ->
bankIconUrl?.let { bankIconUrl?.let {
try { try {
@ -700,6 +716,11 @@ open class BankingPresenter(
} }
open fun findBanksByNameBankCodeOrCity(query: String?): List<BankInfo> { open fun findBanksByNameBankCodeOrCity(query: String?): List<BankInfo> {
// to provide test access as request by Apple
if (query == TestAccountBankCode) {
return listOf(TestAccountBankInfo)
}
return bankFinder.findBankByNameBankCodeOrCity(query) return bankFinder.findBankByNameBankCodeOrCity(query)
.sortedBy { it.name.toLowerCase() } .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<TypedBankData>) -> Unit): Boolean { open fun addBanksChangedListener(listener: (List<TypedBankData>) -> Unit): Boolean {
return banksChangedListeners.add(listener) return banksChangedListeners.add(listener)
} }