Implemented displaying remittees from all account transactions so that user can choose between them and get bank transfer done faster

This commit is contained in:
dankito 2020-04-25 02:45:37 +02:00
parent c8d0e7861c
commit 16d6656343
25 changed files with 660 additions and 22 deletions

View File

@ -1,6 +1,8 @@
package net.dankito.banking.persistence
import net.dankito.banking.ui.model.Account
import net.dankito.banking.ui.model.AccountTransaction
import net.dankito.banking.ui.model.BankAccount
interface IBankingPersistence {
@ -11,4 +13,7 @@ interface IBankingPersistence {
fun readPersistedAccounts(): List<Account>
fun saveOrUpdateAccountTransactions(bankAccount: BankAccount, transactions: List<AccountTransaction>)
}

View File

@ -0,0 +1,8 @@
package net.dankito.banking.search
interface IRemitteeSearcher {
fun findRemittees(query: String): List<Remittee>
}

View File

@ -0,0 +1,18 @@
package net.dankito.banking.search
data class Remittee(
val name: String,
val iban: String,
val bic: String
) {
internal constructor() : this("", "", "") // for object deserializers
override fun toString(): String {
return name
}
}

View File

@ -6,9 +6,10 @@ import net.dankito.banking.ui.model.tan.TanMedium
import net.dankito.banking.ui.model.tan.TanMediumStatus
import net.dankito.banking.ui.model.tan.TanProcedure
import java.math.BigDecimal
import java.util.*
@JsonIdentityInfo(generator= ObjectIdGenerators.UUIDGenerator::class) // to avoid stack overflow due to circular references // TODO: remove again, add custom domain object
@JsonIdentityInfo(property = "id", generator = ObjectIdGenerators.PropertyGenerator::class) // to avoid stack overflow due to circular references
open class Account(
val bank: Bank,
val customerId: String,
@ -22,6 +23,10 @@ open class Account(
internal constructor() : this(Bank(), "", "", "") // for object deserializers
var id: String = UUID.randomUUID().toString()
protected set
var supportedTanProcedures: List<TanProcedure> = listOf()
var selectedTanProcedure: TanProcedure? = null

View File

@ -7,7 +7,7 @@ import java.text.DateFormat
import java.util.*
@JsonIdentityInfo(generator= ObjectIdGenerators.UUIDGenerator::class) // to avoid stack overflow due to circular references // TODO: remove again, add custom domain object
@JsonIdentityInfo(property = "id", generator = ObjectIdGenerators.PropertyGenerator::class) // to avoid stack overflow due to circular references
open class AccountTransaction(
val amount: BigDecimal,
val bookingDate: Date,
@ -25,6 +25,10 @@ open class AccountTransaction(
internal constructor() : this(BigDecimal.ZERO, Date(),"", null, null, null, null, BigDecimal.ZERO, "", BankAccount())
var id: String = UUID.randomUUID().toString()
protected set
val showOtherPartyName: Boolean
get() = otherPartyName.isNullOrBlank() == false /* && type != "ENTGELTABSCHLUSS" && type != "AUSZAHLUNG" */ // TODO

View File

@ -3,9 +3,10 @@ package net.dankito.banking.ui.model
import com.fasterxml.jackson.annotation.JsonIdentityInfo
import com.fasterxml.jackson.annotation.ObjectIdGenerators
import java.math.BigDecimal
import java.util.*
@JsonIdentityInfo(generator= ObjectIdGenerators.UUIDGenerator::class) // to avoid stack overflow due to circular references // TODO: remove again, add custom domain object
@JsonIdentityInfo(property = "id", generator = ObjectIdGenerators.PropertyGenerator::class) // to avoid stack overflow due to circular references
open class BankAccount @JvmOverloads constructor(
val account: Account,
val identifier: String,
@ -21,10 +22,13 @@ open class BankAccount @JvmOverloads constructor(
bookedAccountTransactions: List<AccountTransaction> = listOf()
) {
internal constructor() : this(Account(), "", "", null, null) // for object deserializers
var id: String = UUID.randomUUID().toString()
protected set
val displayName: String
get() {
var displayName = identifier

View File

@ -242,13 +242,22 @@ open class BankingPresenter(
entry.key.balance = entry.value
}
persistAccount(bankAccount.account)
persistAccount(bankAccount.account) // only needed because of balance
persistAccountTransactions(response.bookedTransactions, response.unbookedTransactions)
}
protected open fun persistAccount(account: Account) {
persister.saveOrUpdateAccount(account, accounts)
}
protected open fun persistAccountTransactions(bookedTransactions: Map<BankAccount, List<AccountTransaction>>, unbookedTransactions: Map<BankAccount, List<Any>>) {
bookedTransactions.forEach {
persister.saveOrUpdateAccountTransactions(it.key, it.value)
}
// TODO: someday also persist unbooked transactions
}
open fun transferMoneyAsync(bankAccount: BankAccount, data: TransferMoneyData, callback: (BankingClientResponse) -> Unit) {
getClientForAccount(bankAccount.account)?.let { client ->

View File

@ -57,6 +57,7 @@ dependencies {
implementation project(':fints4javaBankingClient')
implementation project(':BankingPersistenceJson')
implementation project(':LuceneBankingPersistence')
implementation "com.github.clans:fab:$clansFloatingActionButtonVersion"

View File

@ -7,8 +7,10 @@ import dagger.Provides
import net.dankito.banking.fints4java.android.RouterAndroid
import net.dankito.banking.fints4java.android.util.Base64ServiceAndroid
import net.dankito.banking.fints4javaBankingClientCreator
import net.dankito.banking.persistence.BankingPersistenceJson
import net.dankito.banking.persistence.IBankingPersistence
import net.dankito.banking.persistence.LuceneBankingPersistence
import net.dankito.banking.search.IRemitteeSearcher
import net.dankito.banking.search.LuceneRemitteeSearcher
import net.dankito.banking.ui.IBankingClientCreator
import net.dankito.banking.ui.IRouter
import net.dankito.banking.ui.presenter.BankingPresenter
@ -92,8 +94,14 @@ class BankingModule(internal val mainActivity: AppCompatActivity) {
@Provides
@Singleton
fun provideBankingPersistence(@Named(DatabaseFolderKey) databaseFolder: File, serializer: ISerializer) : IBankingPersistence {
return BankingPersistenceJson(File(databaseFolder, "accounts.json"), serializer)
fun provideBankingPersistence(@Named(IndexFolderKey) indexFolder: File, @Named(DatabaseFolderKey) databaseFolder: File, serializer: ISerializer) : IBankingPersistence {
return LuceneBankingPersistence(databaseFolder, indexFolder, serializer)
}
@Provides
@Singleton
fun provideRemitteeSearcher(@Named(IndexFolderKey) indexFolder: File) : IRemitteeSearcher {
return LuceneRemitteeSearcher(indexFolder)
}
@Provides

View File

@ -0,0 +1,28 @@
package net.dankito.banking.fints4java.android.ui.adapter
import android.view.View
import net.dankito.banking.fints4java.android.R
import net.dankito.banking.fints4java.android.ui.adapter.viewholder.RemitteeViewHolder
import net.dankito.banking.search.Remittee
import net.dankito.utils.android.ui.adapter.ListRecyclerAdapter
open class RemitteeListAdapter(protected val itemClicked: ((Remittee) -> Unit)? = null) : ListRecyclerAdapter<Remittee, RemitteeViewHolder>() {
override fun getListItemLayoutId() = R.layout.list_item_remittee
override fun createViewHolder(itemView: View): RemitteeViewHolder {
return RemitteeViewHolder(itemView)
}
override fun bindItemToView(viewHolder: RemitteeViewHolder, item: Remittee) {
viewHolder.txtvwRemitteeName.text = item.name
viewHolder.txtvwRemitteeBankCode.text = item.iban
viewHolder.itemView.setOnClickListener {
itemClicked?.invoke(item)
}
}
}

View File

@ -0,0 +1,36 @@
package net.dankito.banking.fints4java.android.ui.adapter.presenter
import android.content.Context
import androidx.recyclerview.widget.RecyclerView
import com.otaliastudios.autocomplete.RecyclerViewPresenter
import kotlinx.coroutines.*
import net.dankito.banking.fints4java.android.ui.adapter.RemitteeListAdapter
import net.dankito.banking.search.IRemitteeSearcher
import net.dankito.banking.search.Remittee
import net.dankito.utils.Stopwatch
open class RemitteePresenter(protected val remitteeSearcher: IRemitteeSearcher, context: Context) : RecyclerViewPresenter<Remittee>(context) {
protected val adapter = RemitteeListAdapter { dispatchClick(it) }
protected var lastSearchRemitteeJob: Job? = null
override fun instantiateAdapter(): RecyclerView.Adapter<*> {
return adapter
}
override fun onQuery(query: CharSequence?) {
lastSearchRemitteeJob?.cancel()
lastSearchRemitteeJob = GlobalScope.launch(Dispatchers.IO) {
val potentialRemittees = Stopwatch.logDuration("findRemittees()") { remitteeSearcher.findRemittees(query?.toString() ?: "") }
withContext(Dispatchers.Main) {
adapter.items = potentialRemittees
}
}
}
}

View File

@ -0,0 +1,15 @@
package net.dankito.banking.fints4java.android.ui.adapter.viewholder
import android.view.View
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.list_item_remittee.view.*
open class RemitteeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val txtvwRemitteeName: TextView = itemView.txtvwRemitteeName
val txtvwRemitteeBankCode: TextView = itemView.txtvwRemitteeBankCode
}

View File

@ -15,12 +15,12 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.DialogFragment
import com.otaliastudios.autocomplete.Autocomplete
import com.otaliastudios.autocomplete.AutocompleteCallback
import kotlinx.android.synthetic.main.dialog_add_account.*
import kotlinx.android.synthetic.main.dialog_add_account.view.*
import net.dankito.banking.fints4java.android.R
import net.dankito.banking.fints4java.android.di.BankingComponent
import net.dankito.banking.fints4java.android.ui.adapter.presenter.BankInfoPresenter
import net.dankito.banking.fints4java.android.util.StandardAutocompleteCallback
import net.dankito.banking.ui.model.responses.AddAccountResponse
import net.dankito.banking.ui.presenter.BankingPresenter
import net.dankito.fints.model.BankInfo
@ -76,15 +76,9 @@ open class AddAccountDialog : DialogFragment() {
}
private fun initBankListAutocompletion(rootView: View) {
val autocompleteCallback = object : AutocompleteCallback<BankInfo> {
override fun onPopupItemClicked(editable: Editable, item: BankInfo): Boolean {
bankSelected(item)
return true
}
override fun onPopupVisibilityChanged(shown: Boolean) {}
val autocompleteCallback = StandardAutocompleteCallback<BankInfo> { _, item ->
bankSelected(item)
true
}
Autocomplete.on<BankInfo>(rootView.edtxtBankCode)

View File

@ -1,21 +1,29 @@
package net.dankito.banking.fints4java.android.ui.dialogs
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.text.TextWatcher
import android.text.method.DigitsKeyListener
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.DialogFragment
import com.otaliastudios.autocomplete.Autocomplete
import kotlinx.android.synthetic.main.dialog_transfer_money.*
import kotlinx.android.synthetic.main.dialog_transfer_money.view.*
import net.dankito.banking.fints4java.android.R
import net.dankito.banking.fints4java.android.di.BankingComponent
import net.dankito.banking.fints4java.android.ui.adapter.BankAccountsAdapter
import net.dankito.banking.fints4java.android.ui.adapter.presenter.RemitteePresenter
import net.dankito.banking.fints4java.android.ui.listener.ListItemSelectedListener
import net.dankito.banking.fints4java.android.util.StandardAutocompleteCallback
import net.dankito.banking.fints4java.android.util.StandardTextWatcher
import net.dankito.banking.search.IRemitteeSearcher
import net.dankito.banking.search.Remittee
import net.dankito.banking.ui.model.BankAccount
import net.dankito.banking.ui.model.parameters.TransferMoneyData
import net.dankito.banking.ui.model.responses.BankingClientResponse
@ -50,6 +58,9 @@ open class TransferMoneyDialog : DialogFragment() {
@Inject
protected lateinit var presenter: BankingPresenter
@Inject
protected lateinit var remitteeSearcher: IRemitteeSearcher
init {
BankingComponent.component.inject(this)
@ -96,7 +107,8 @@ open class TransferMoneyDialog : DialogFragment() {
preselectedBankAccount?.let { rootView.spnBankAccounts.setSelection(adapter.getItems().indexOf(it)) }
}
// TODO: add autocompletion by searching for name in account entries
initRemitteeAutocompletion(rootView.edtxtRemitteeName)
rootView.edtxtRemitteeName.addTextChangedListener(checkRequiredDataWatcher {
checkIfEnteredRemitteeNameIsValid()
})
@ -124,6 +136,21 @@ open class TransferMoneyDialog : DialogFragment() {
rootView.btnTransferMoney.setOnClickListener { transferMoney() }
}
private fun initRemitteeAutocompletion(edtxtRemitteeName: EditText) {
val autocompleteCallback = StandardAutocompleteCallback<Remittee> { _, item ->
remitteeSelected(item)
true
}
Autocomplete.on<Remittee>(edtxtRemitteeName)
.with(6f)
.with(ColorDrawable(Color.WHITE))
.with(autocompleteCallback)
.with(RemitteePresenter(remitteeSearcher, edtxtRemitteeName.context))
.build()
}
override fun onStart() {
super.onStart()
@ -159,6 +186,13 @@ open class TransferMoneyDialog : DialogFragment() {
}
}
protected open fun remitteeSelected(item: Remittee) {
edtxtRemitteeName.setText(item.name)
edtxtRemitteeBic.setText(item.bic)
edtxtRemitteeIban.setText(item.iban)
}
protected open fun transferMoney() {
getEnteredAmount()?.let { amount -> // should only come at this stage when a valid amount has been entered
val data = TransferMoneyData(

View File

@ -0,0 +1,20 @@
package net.dankito.banking.fints4java.android.util
import android.text.Editable
import com.otaliastudios.autocomplete.AutocompleteCallback
open class StandardAutocompleteCallback<T>(
protected val onPopupVisibilityChanged: ((shown: Boolean) -> Unit)? = null,
protected val onPopupItemClicked: ((editable: Editable?, item: T) -> Boolean)? = null
) : AutocompleteCallback<T> {
override fun onPopupItemClicked(editable: Editable?, item: T): Boolean {
return onPopupItemClicked?.invoke(editable, item) ?: false
}
override fun onPopupVisibilityChanged(shown: Boolean) {
onPopupVisibilityChanged?.invoke(shown)
}
}

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="@dimen/list_item_remittee_height"
android:layout_gravity="center_vertical"
>
<TextView
android:id="@+id/txtvwRemitteeName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextAppearance.AppCompat.Medium"
android:gravity="center_vertical"
android:textSize="@dimen/list_item_bank_info_bank_name_text_size"
/>
<TextView
android:id="@+id/txtvwRemitteeBankCode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/list_item_remittee_bank_code_margin_top"
android:layout_marginBottom="@dimen/list_item_remittee_bank_code_margin_bottom"
style="@style/TextAppearance.AppCompat.Small"
android:gravity="center_vertical"
/>
</LinearLayout>

View File

@ -50,6 +50,10 @@
<dimen name="list_item_bank_account_padding">4dp</dimen>
<dimen name="list_item_bank_account_text_size">13sp</dimen>
<dimen name="list_item_remittee_height">60dp</dimen>
<dimen name="list_item_remittee_bank_code_margin_top">6dp</dimen>
<dimen name="list_item_remittee_bank_code_margin_bottom">6dp</dimen>
<dimen name="view_tan_image_controls_buttons_height">40dp</dimen>
<dimen name="view_tan_image_controls_buttons_width">40dp</dimen>

View File

@ -0,0 +1,28 @@
apply plugin: 'java-library'
apply plugin: 'kotlin'
sourceCompatibility = "1.7"
targetCompatibility = "1.7"
compileKotlin {
kotlinOptions.jvmTarget = "1.6"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "1.6"
}
dependencies {
implementation project(":BankingUiCommon")
implementation project(":BankingPersistenceJson")
implementation "net.dankito.search:lucene-4-utils:$luceneUtilsVersion"
testImplementation "junit:junit:$junitVersion"
testImplementation "org.assertj:assertj-core:$assertJVersion"
testImplementation "org.mockito:mockito-core:$mockitoVersion"
}

View File

@ -0,0 +1,39 @@
package net.dankito.banking
import java.io.File
class LuceneConfig {
companion object {
const val BankAccountIdFieldName = "bank_account_id"
const val IdFieldName = "id"
const val OtherPartyNameFieldName = "other_party_name"
const val OtherPartyBankCodeFieldName = "other_party_bank_code"
const val OtherPartyAccountIdFieldName = "other_party_account_id"
const val BookingDateFieldName = "booking_date"
const val BookingDateSortFieldName = "booking_date_sort"
const val UsageFieldName = "usage"
const val BookingTextFieldName = "booking_text"
const val AmountFieldName = "amount"
const val CurrencyFieldName = "currency"
const val BalanceFieldName = "balance"
fun getAccountTransactionsIndexFolder(indexFolder: File): File {
return File(indexFolder, "account_transactions")
}
}
}

View File

@ -0,0 +1,65 @@
package net.dankito.banking.persistence
import net.dankito.banking.LuceneConfig
import net.dankito.banking.LuceneConfig.Companion.AmountFieldName
import net.dankito.banking.LuceneConfig.Companion.BalanceFieldName
import net.dankito.banking.LuceneConfig.Companion.BankAccountIdFieldName
import net.dankito.banking.LuceneConfig.Companion.BookingDateFieldName
import net.dankito.banking.LuceneConfig.Companion.BookingDateSortFieldName
import net.dankito.banking.LuceneConfig.Companion.BookingTextFieldName
import net.dankito.banking.LuceneConfig.Companion.CurrencyFieldName
import net.dankito.banking.LuceneConfig.Companion.IdFieldName
import net.dankito.banking.LuceneConfig.Companion.OtherPartyAccountIdFieldName
import net.dankito.banking.LuceneConfig.Companion.OtherPartyBankCodeFieldName
import net.dankito.banking.LuceneConfig.Companion.OtherPartyNameFieldName
import net.dankito.banking.LuceneConfig.Companion.UsageFieldName
import net.dankito.banking.ui.model.AccountTransaction
import net.dankito.banking.ui.model.BankAccount
import net.dankito.utils.lucene.index.DocumentsWriter
import net.dankito.utils.lucene.index.FieldBuilder
import net.dankito.utils.serialization.ISerializer
import net.dankito.utils.serialization.JacksonJsonSerializer
import java.io.Closeable
import java.io.File
open class LuceneBankingPersistence(
databaseFolder: File,
indexFolder: File,
serializer: ISerializer = JacksonJsonSerializer()
) : BankingPersistenceJson(File(databaseFolder, "accounts.json"), serializer), IBankingPersistence, Closeable {
protected val fields = FieldBuilder()
protected val writer = DocumentsWriter(LuceneConfig.getAccountTransactionsIndexFolder(indexFolder))
override fun close() {
writer.close()
}
override fun saveOrUpdateAccountTransactions(bankAccount: BankAccount, transactions: List<AccountTransaction>) {
transactions.forEach { transaction ->
writer.updateDocumentForNonNullFields(IdFieldName, transaction.id,
fields.keywordField(BankAccountIdFieldName, bankAccount.id),
fields.nullableFullTextSearchField(OtherPartyNameFieldName, transaction.otherPartyName, true),
fields.fullTextSearchField(UsageFieldName, transaction.usage, true),
fields.nullableFullTextSearchField(BookingTextFieldName, transaction.bookingText, true),
fields.nullableStoredField(OtherPartyBankCodeFieldName, transaction.otherPartyBankCode),
fields.nullableStoredField(OtherPartyAccountIdFieldName, transaction.otherPartyAccountId),
fields.storedField(BookingDateFieldName, transaction.bookingDate),
fields.storedField(AmountFieldName, transaction.amount),
fields.storedField(CurrencyFieldName, transaction.currency),
fields.nullableStoredField(BalanceFieldName, transaction.balance),
fields.sortField(BookingDateSortFieldName, transaction.bookingDate)
)
}
writer.flushChangesToDisk()
}
}

View File

@ -0,0 +1,42 @@
package net.dankito.banking.search
import net.dankito.banking.LuceneConfig
import net.dankito.banking.LuceneConfig.Companion.OtherPartyAccountIdFieldName
import net.dankito.banking.LuceneConfig.Companion.OtherPartyBankCodeFieldName
import net.dankito.banking.LuceneConfig.Companion.OtherPartyNameFieldName
import net.dankito.utils.lucene.mapper.PropertyDescription
import net.dankito.utils.lucene.mapper.PropertyType
import net.dankito.utils.lucene.search.QueryBuilder
import net.dankito.utils.lucene.search.Searcher
import java.io.File
open class LuceneRemitteeSearcher(indexFolder: File) : IRemitteeSearcher {
companion object {
private val properties = listOf(
PropertyDescription(PropertyType.NullableString, OtherPartyNameFieldName, Remittee::name),
PropertyDescription(PropertyType.NullableString, OtherPartyBankCodeFieldName, Remittee::bic),
PropertyDescription(PropertyType.NullableString, OtherPartyAccountIdFieldName, Remittee::iban)
)
}
protected val queries = QueryBuilder()
protected val searcher = Searcher(LuceneConfig.getAccountTransactionsIndexFolder(indexFolder))
override fun findRemittees(query: String): List<Remittee> {
val luceneQuery = queries.createQueriesForSingleTerms(query.toLowerCase()) { singleTerm ->
listOf(
queries.fulltextQuery(OtherPartyNameFieldName, singleTerm)
)
}
return searcher.searchAndMap(luceneQuery, Remittee::class.java, properties).toSet().toList()
}
}

View File

@ -0,0 +1,235 @@
package net.dankito.banking.search
import net.dankito.banking.persistence.LuceneBankingPersistence
import net.dankito.banking.ui.model.Account
import net.dankito.banking.ui.model.AccountTransaction
import net.dankito.banking.ui.model.BankAccount
import net.dankito.utils.io.FileUtils
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito.mock
import java.io.File
import java.math.BigDecimal
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ThreadLocalRandom
class LuceneRemitteeSearcherTest {
companion object {
private val dataFolder = File("testData")
private val databaseFolder = File(dataFolder, "db")
private val indexFolder = File(dataFolder, "index")
private val BookingDate = "27.03.2020"
private val OtherPartyName = "Mahatma Gandhi"
private val OtherPartyBankCode = "12345678"
private val OtherPartyAccountId = "0987654321"
private val Amount = BigDecimal.valueOf(123.45)
private val bankAccountMock = BankAccount(mock(Account::class.java), "", "", null, null)
private val dateFormat = SimpleDateFormat("dd.MM.yyyy")
}
private val fileUtils = FileUtils()
private val bankingPersistence = LuceneBankingPersistence(databaseFolder, indexFolder)
private val underTest = LuceneRemitteeSearcher(indexFolder)
@Before
fun setUp() {
clearDataFolder()
}
@After
fun tearDown() {
bankingPersistence.close()
clearDataFolder()
}
private fun clearDataFolder() {
fileUtils.deleteFolderRecursively(dataFolder)
}
@Test
fun findRemittees_ByFullName() {
// given
val query = OtherPartyName
val before = underTest.findRemittees(query)
assertThat(before).isEmpty()
bankingPersistence.saveOrUpdateAccountTransactions(bankAccountMock, listOf(
createTransaction(bankAccountMock, BookingDate, Amount, OtherPartyName, OtherPartyBankCode, OtherPartyAccountId),
createTransaction(),
createTransaction()
))
// when
val result = underTest.findRemittees(query)
// then
assertThat(result).hasSize(1)
assertThat(result.first().name).isEqualTo(OtherPartyName)
assertThat(result.first().bic).isEqualTo(OtherPartyBankCode)
assertThat(result.first().iban).isEqualTo(OtherPartyAccountId)
}
@Test
fun findRemittees_ByPartialName() {
// given
val query = "gand"
val before = underTest.findRemittees(query)
assertThat(before).isEmpty()
bankingPersistence.saveOrUpdateAccountTransactions(bankAccountMock, listOf(
createTransaction(bankAccountMock, BookingDate, Amount, OtherPartyName, OtherPartyBankCode, OtherPartyAccountId),
createTransaction(),
createTransaction()
))
// when
val result = underTest.findRemittees(query)
// then
assertThat(result).hasSize(1)
assertThat(result.first().name).isEqualTo(OtherPartyName)
assertThat(result.first().bic).isEqualTo(OtherPartyBankCode)
assertThat(result.first().iban).isEqualTo(OtherPartyAccountId)
}
@Test
fun findRemittees_SimilarNames() {
// given
val query = "gand"
val secondOtherPartyName = "Gandalf"
val before = underTest.findRemittees(query)
assertThat(before).isEmpty()
bankingPersistence.saveOrUpdateAccountTransactions(bankAccountMock, listOf(
createTransaction(bankAccountMock, BookingDate, Amount, OtherPartyName, OtherPartyBankCode, OtherPartyAccountId),
createTransaction(otherPartyName = secondOtherPartyName),
createTransaction()
))
// when
val result = underTest.findRemittees(query)
// then
assertThat(result).hasSize(2)
assertThat(result.map { it.name }).containsExactlyInAnyOrder(OtherPartyName, secondOtherPartyName)
}
@Test
fun findRemittees_DuplicateEntries() {
// given
val query = OtherPartyName
val before = underTest.findRemittees(query)
assertThat(before).isEmpty()
bankingPersistence.saveOrUpdateAccountTransactions(bankAccountMock, listOf(
createTransaction(bankAccountMock, BookingDate, Amount, OtherPartyName, OtherPartyBankCode, OtherPartyAccountId),
createTransaction(bankAccountMock, "01.02.2020", Amount, OtherPartyName, OtherPartyBankCode, OtherPartyAccountId),
createTransaction(bankAccountMock, "03.04.2020", Amount, OtherPartyName, OtherPartyBankCode, OtherPartyAccountId),
createTransaction(),
createTransaction()
))
// when
val result = underTest.findRemittees(query)
// then
assertThat(result).hasSize(1)
assertThat(result.first().name).isEqualTo(OtherPartyName)
assertThat(result.first().bic).isEqualTo(OtherPartyBankCode)
assertThat(result.first().iban).isEqualTo(OtherPartyAccountId)
}
@Test
fun findRemittees_OtherName() {
// given
val query = "Mandela"
val before = underTest.findRemittees(query)
assertThat(before).isEmpty()
bankingPersistence.saveOrUpdateAccountTransactions(bankAccountMock, listOf(
createTransaction(bankAccountMock, BookingDate, Amount, OtherPartyName, OtherPartyBankCode, OtherPartyAccountId),
createTransaction(),
createTransaction()
))
// when
val result = underTest.findRemittees(query)
// then
assertThat(result).isEmpty()
}
private fun createTransaction(bankAccount: BankAccount = bankAccountMock, bookingDate: String, amount: BigDecimal = randomBigDecimal(),
otherPartyName: String = randomString(), otherPartyBankCode: String = randomString(),
otherPartyAccountId: String = randomString(), usage: String = randomString()): AccountTransaction {
return createTransaction(bankAccount, dateFormat.parse(bookingDate), amount, otherPartyName,
otherPartyBankCode, otherPartyAccountId, usage)
}
private fun createTransaction(bankAccount: BankAccount = bankAccountMock, bookingDate: Date = randomDate(), amount: BigDecimal = randomBigDecimal(),
otherPartyName: String = randomString(), otherPartyBankCode: String = randomString(),
otherPartyAccountId: String = randomString(), usage: String = randomString()): AccountTransaction {
return AccountTransaction(amount, bookingDate, usage, otherPartyName, otherPartyBankCode, otherPartyAccountId, null, null, "EUR", bankAccount)
}
private fun randomString(): String {
return UUID.randomUUID().toString()
}
private fun randomDate(): Date {
val pseudoRandomLong = ThreadLocalRandom.current().nextLong(0, Date().time)
return Date(pseudoRandomLong)
}
private fun randomBigDecimal(): BigDecimal {
val pseudoRandomDouble = ThreadLocalRandom.current().nextDouble(-5-000.0, 12_000.0)
return BigDecimal.valueOf(pseudoRandomDouble)
}
}

View File

@ -1,8 +1,6 @@
apply plugin: 'java-library'
apply plugin: 'kotlin'
apply plugin: 'kotlin-kapt'
sourceCompatibility = "1.7"
targetCompatibility = "1.7"

View File

@ -1,6 +1,8 @@
package net.dankito.banking.persistence
import net.dankito.banking.ui.model.Account
import net.dankito.banking.ui.model.AccountTransaction
import net.dankito.banking.ui.model.BankAccount
import net.dankito.utils.serialization.ISerializer
import net.dankito.utils.serialization.JacksonJsonSerializer
import java.io.File
@ -29,4 +31,9 @@ open class BankingPersistenceJson(
return serializer.deserializeListOr(jsonFile, Account::class.java, listOf())
}
override fun saveOrUpdateAccountTransactions(bankAccount: BankAccount, transactions: List<AccountTransaction>) {
// done when called saveOrUpdateAccount()
}
}

View File

@ -10,6 +10,7 @@ include ':fints4javaBankingClient'
include ':hbci4jBankingClient'
include ':BankingPersistenceJson'
include ':LuceneBankingPersistence'
include ':fints4javaAndroidApp'
@ -18,3 +19,4 @@ include ':BankingJavaFxApp'
project(':BankingPersistenceJson').projectDir = "$rootDir/persistence/json/BankingPersistenceJson/" as File
project(':LuceneBankingPersistence').projectDir = "$rootDir/persistence/LuceneBankingPersistence/" as File