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 package net.dankito.banking.persistence
import net.dankito.banking.ui.model.Account import net.dankito.banking.ui.model.Account
import net.dankito.banking.ui.model.AccountTransaction
import net.dankito.banking.ui.model.BankAccount
interface IBankingPersistence { interface IBankingPersistence {
@ -11,4 +13,7 @@ interface IBankingPersistence {
fun readPersistedAccounts(): List<Account> 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.TanMediumStatus
import net.dankito.banking.ui.model.tan.TanProcedure import net.dankito.banking.ui.model.tan.TanProcedure
import java.math.BigDecimal 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( open class Account(
val bank: Bank, val bank: Bank,
val customerId: String, val customerId: String,
@ -22,6 +23,10 @@ open class Account(
internal constructor() : this(Bank(), "", "", "") // for object deserializers internal constructor() : this(Bank(), "", "", "") // for object deserializers
var id: String = UUID.randomUUID().toString()
protected set
var supportedTanProcedures: List<TanProcedure> = listOf() var supportedTanProcedures: List<TanProcedure> = listOf()
var selectedTanProcedure: TanProcedure? = null var selectedTanProcedure: TanProcedure? = null

View File

@ -7,7 +7,7 @@ import java.text.DateFormat
import java.util.* 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( open class AccountTransaction(
val amount: BigDecimal, val amount: BigDecimal,
val bookingDate: Date, val bookingDate: Date,
@ -25,6 +25,10 @@ open class AccountTransaction(
internal constructor() : this(BigDecimal.ZERO, Date(),"", null, null, null, null, BigDecimal.ZERO, "", BankAccount()) internal constructor() : this(BigDecimal.ZERO, Date(),"", null, null, null, null, BigDecimal.ZERO, "", BankAccount())
var id: String = UUID.randomUUID().toString()
protected set
val showOtherPartyName: Boolean val showOtherPartyName: Boolean
get() = otherPartyName.isNullOrBlank() == false /* && type != "ENTGELTABSCHLUSS" && type != "AUSZAHLUNG" */ // TODO 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.JsonIdentityInfo
import com.fasterxml.jackson.annotation.ObjectIdGenerators import com.fasterxml.jackson.annotation.ObjectIdGenerators
import java.math.BigDecimal 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( open class BankAccount @JvmOverloads constructor(
val account: Account, val account: Account,
val identifier: String, val identifier: String,
@ -21,10 +22,13 @@ open class BankAccount @JvmOverloads constructor(
bookedAccountTransactions: List<AccountTransaction> = listOf() bookedAccountTransactions: List<AccountTransaction> = listOf()
) { ) {
internal constructor() : this(Account(), "", "", null, null) // for object deserializers internal constructor() : this(Account(), "", "", null, null) // for object deserializers
var id: String = UUID.randomUUID().toString()
protected set
val displayName: String val displayName: String
get() { get() {
var displayName = identifier var displayName = identifier

View File

@ -242,13 +242,22 @@ open class BankingPresenter(
entry.key.balance = entry.value 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) { protected open fun persistAccount(account: Account) {
persister.saveOrUpdateAccount(account, accounts) 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) { open fun transferMoneyAsync(bankAccount: BankAccount, data: TransferMoneyData, callback: (BankingClientResponse) -> Unit) {
getClientForAccount(bankAccount.account)?.let { client -> getClientForAccount(bankAccount.account)?.let { client ->

View File

@ -57,6 +57,7 @@ dependencies {
implementation project(':fints4javaBankingClient') implementation project(':fints4javaBankingClient')
implementation project(':BankingPersistenceJson') implementation project(':BankingPersistenceJson')
implementation project(':LuceneBankingPersistence')
implementation "com.github.clans:fab:$clansFloatingActionButtonVersion" 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.RouterAndroid
import net.dankito.banking.fints4java.android.util.Base64ServiceAndroid import net.dankito.banking.fints4java.android.util.Base64ServiceAndroid
import net.dankito.banking.fints4javaBankingClientCreator import net.dankito.banking.fints4javaBankingClientCreator
import net.dankito.banking.persistence.BankingPersistenceJson
import net.dankito.banking.persistence.IBankingPersistence 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.IBankingClientCreator
import net.dankito.banking.ui.IRouter import net.dankito.banking.ui.IRouter
import net.dankito.banking.ui.presenter.BankingPresenter import net.dankito.banking.ui.presenter.BankingPresenter
@ -92,8 +94,14 @@ class BankingModule(internal val mainActivity: AppCompatActivity) {
@Provides @Provides
@Singleton @Singleton
fun provideBankingPersistence(@Named(DatabaseFolderKey) databaseFolder: File, serializer: ISerializer) : IBankingPersistence { fun provideBankingPersistence(@Named(IndexFolderKey) indexFolder: File, @Named(DatabaseFolderKey) databaseFolder: File, serializer: ISerializer) : IBankingPersistence {
return BankingPersistenceJson(File(databaseFolder, "accounts.json"), serializer) return LuceneBankingPersistence(databaseFolder, indexFolder, serializer)
}
@Provides
@Singleton
fun provideRemitteeSearcher(@Named(IndexFolderKey) indexFolder: File) : IRemitteeSearcher {
return LuceneRemitteeSearcher(indexFolder)
} }
@Provides @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.appcompat.app.AppCompatActivity
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.otaliastudios.autocomplete.Autocomplete 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.*
import kotlinx.android.synthetic.main.dialog_add_account.view.* import kotlinx.android.synthetic.main.dialog_add_account.view.*
import net.dankito.banking.fints4java.android.R import net.dankito.banking.fints4java.android.R
import net.dankito.banking.fints4java.android.di.BankingComponent import net.dankito.banking.fints4java.android.di.BankingComponent
import net.dankito.banking.fints4java.android.ui.adapter.presenter.BankInfoPresenter 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.model.responses.AddAccountResponse
import net.dankito.banking.ui.presenter.BankingPresenter import net.dankito.banking.ui.presenter.BankingPresenter
import net.dankito.fints.model.BankInfo import net.dankito.fints.model.BankInfo
@ -76,15 +76,9 @@ open class AddAccountDialog : DialogFragment() {
} }
private fun initBankListAutocompletion(rootView: View) { private fun initBankListAutocompletion(rootView: View) {
val autocompleteCallback = object : AutocompleteCallback<BankInfo> { val autocompleteCallback = StandardAutocompleteCallback<BankInfo> { _, item ->
bankSelected(item)
override fun onPopupItemClicked(editable: Editable, item: BankInfo): Boolean { true
bankSelected(item)
return true
}
override fun onPopupVisibilityChanged(shown: Boolean) {}
} }
Autocomplete.on<BankInfo>(rootView.edtxtBankCode) Autocomplete.on<BankInfo>(rootView.edtxtBankCode)

View File

@ -1,21 +1,29 @@
package net.dankito.banking.fints4java.android.ui.dialogs package net.dankito.banking.fints4java.android.ui.dialogs
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle import android.os.Bundle
import android.text.TextWatcher import android.text.TextWatcher
import android.text.method.DigitsKeyListener import android.text.method.DigitsKeyListener
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.EditText
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.DialogFragment 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.*
import kotlinx.android.synthetic.main.dialog_transfer_money.view.* import kotlinx.android.synthetic.main.dialog_transfer_money.view.*
import net.dankito.banking.fints4java.android.R import net.dankito.banking.fints4java.android.R
import net.dankito.banking.fints4java.android.di.BankingComponent 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.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.ui.listener.ListItemSelectedListener
import net.dankito.banking.fints4java.android.util.StandardAutocompleteCallback
import net.dankito.banking.fints4java.android.util.StandardTextWatcher 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.BankAccount
import net.dankito.banking.ui.model.parameters.TransferMoneyData import net.dankito.banking.ui.model.parameters.TransferMoneyData
import net.dankito.banking.ui.model.responses.BankingClientResponse import net.dankito.banking.ui.model.responses.BankingClientResponse
@ -50,6 +58,9 @@ open class TransferMoneyDialog : DialogFragment() {
@Inject @Inject
protected lateinit var presenter: BankingPresenter protected lateinit var presenter: BankingPresenter
@Inject
protected lateinit var remitteeSearcher: IRemitteeSearcher
init { init {
BankingComponent.component.inject(this) BankingComponent.component.inject(this)
@ -96,7 +107,8 @@ open class TransferMoneyDialog : DialogFragment() {
preselectedBankAccount?.let { rootView.spnBankAccounts.setSelection(adapter.getItems().indexOf(it)) } 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 { rootView.edtxtRemitteeName.addTextChangedListener(checkRequiredDataWatcher {
checkIfEnteredRemitteeNameIsValid() checkIfEnteredRemitteeNameIsValid()
}) })
@ -124,6 +136,21 @@ open class TransferMoneyDialog : DialogFragment() {
rootView.btnTransferMoney.setOnClickListener { transferMoney() } 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() { override fun onStart() {
super.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() { protected open fun transferMoney() {
getEnteredAmount()?.let { amount -> // should only come at this stage when a valid amount has been entered getEnteredAmount()?.let { amount -> // should only come at this stage when a valid amount has been entered
val data = TransferMoneyData( 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_padding">4dp</dimen>
<dimen name="list_item_bank_account_text_size">13sp</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_height">40dp</dimen>
<dimen name="view_tan_image_controls_buttons_width">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: 'java-library'
apply plugin: 'kotlin' apply plugin: 'kotlin'
apply plugin: 'kotlin-kapt'
sourceCompatibility = "1.7" sourceCompatibility = "1.7"
targetCompatibility = "1.7" targetCompatibility = "1.7"

View File

@ -1,6 +1,8 @@
package net.dankito.banking.persistence package net.dankito.banking.persistence
import net.dankito.banking.ui.model.Account 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.ISerializer
import net.dankito.utils.serialization.JacksonJsonSerializer import net.dankito.utils.serialization.JacksonJsonSerializer
import java.io.File import java.io.File
@ -29,4 +31,9 @@ open class BankingPersistenceJson(
return serializer.deserializeListOr(jsonFile, Account::class.java, listOf()) 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 ':hbci4jBankingClient'
include ':BankingPersistenceJson' include ':BankingPersistenceJson'
include ':LuceneBankingPersistence'
include ':fints4javaAndroidApp' include ':fints4javaAndroidApp'
@ -17,4 +18,5 @@ include ':BankingJavaFxControls'
include ':BankingJavaFxApp' include ':BankingJavaFxApp'
project(':BankingPersistenceJson').projectDir = "$rootDir/persistence/json/BankingPersistenceJson/" as File project(':BankingPersistenceJson').projectDir = "$rootDir/persistence/json/BankingPersistenceJson/" as File
project(':LuceneBankingPersistence').projectDir = "$rootDir/persistence/LuceneBankingPersistence/" as File