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:
parent
c8d0e7861c
commit
16d6656343
|
@ -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>)
|
||||
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package net.dankito.banking.search
|
||||
|
||||
|
||||
interface IRemitteeSearcher {
|
||||
|
||||
fun findRemittees(query: String): List<Remittee>
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -57,6 +57,7 @@ dependencies {
|
|||
implementation project(':fints4javaBankingClient')
|
||||
|
||||
implementation project(':BankingPersistenceJson')
|
||||
implementation project(':LuceneBankingPersistence')
|
||||
|
||||
implementation "com.github.clans:fab:$clansFloatingActionButtonVersion"
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,8 +1,6 @@
|
|||
apply plugin: 'java-library'
|
||||
apply plugin: 'kotlin'
|
||||
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
|
||||
sourceCompatibility = "1.7"
|
||||
targetCompatibility = "1.7"
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue