Implemented a test account to fulfill Apple's requirements
This commit is contained in:
parent
5d0f74c5e7
commit
7da0c989b2
|
@ -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
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue