Implemented validating and auto correcting user input in TransferMoneyDialog
This commit is contained in:
parent
321814a0ca
commit
137d35ac02
|
@ -16,6 +16,8 @@ expect class BigDecimal {
|
||||||
constructor(double: Double)
|
constructor(double: Double)
|
||||||
|
|
||||||
|
|
||||||
|
val isPositive: Boolean
|
||||||
|
|
||||||
fun format(countDecimalPlaces: Int): String
|
fun format(countDecimalPlaces: Int): String
|
||||||
|
|
||||||
}
|
}
|
|
@ -12,7 +12,7 @@ actual fun Collection<BigDecimal>.sum(): BigDecimal {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
actual class BigDecimal(val decimal: NSDecimalNumber) { // it's almost impossible to derive from NSDecimalNumber so i keep it as property
|
actual class BigDecimal(val decimal: NSDecimalNumber) : Comparable<BigDecimal> { // it's almost impossible to derive from NSDecimalNumber so i keep it as property
|
||||||
|
|
||||||
actual companion object {
|
actual companion object {
|
||||||
actual val Zero = BigDecimal(0.0)
|
actual val Zero = BigDecimal(0.0)
|
||||||
|
@ -23,6 +23,9 @@ actual class BigDecimal(val decimal: NSDecimalNumber) { // it's almost impossibl
|
||||||
actual constructor(decimal: String) : this(decimal.toDouble())
|
actual constructor(decimal: String) : this(decimal.toDouble())
|
||||||
|
|
||||||
|
|
||||||
|
actual val isPositive: Boolean
|
||||||
|
get() = this >= Zero
|
||||||
|
|
||||||
actual fun format(countDecimalPlaces: Int): String {
|
actual fun format(countDecimalPlaces: Int): String {
|
||||||
val formatter = NSNumberFormatter()
|
val formatter = NSNumberFormatter()
|
||||||
|
|
||||||
|
@ -33,9 +36,18 @@ actual class BigDecimal(val decimal: NSDecimalNumber) { // it's almost impossibl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun compareTo(other: BigDecimal): Int {
|
||||||
|
return when (decimal.compare(other.decimal)) {
|
||||||
|
NSOrderedSame -> 0
|
||||||
|
NSOrderedAscending -> -1
|
||||||
|
NSOrderedDescending -> 1
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (other is BigDecimal) {
|
if (other is BigDecimal) {
|
||||||
return this.decimal.compare(other.decimal) == NSOrderedSame
|
return this.compareTo(other) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.equals(other)
|
return super.equals(other)
|
||||||
|
|
|
@ -26,6 +26,9 @@ actual class BigDecimal actual constructor(decimal: String) : java.math.BigDecim
|
||||||
actual constructor(double: Double) : this(java.math.BigDecimal.valueOf(double).toPlainString()) // for object deserializers
|
actual constructor(double: Double) : this(java.math.BigDecimal.valueOf(double).toPlainString()) // for object deserializers
|
||||||
|
|
||||||
|
|
||||||
|
actual val isPositive: Boolean
|
||||||
|
get() = this >= ZERO
|
||||||
|
|
||||||
actual fun format(countDecimalPlaces: Int): String {
|
actual fun format(countDecimalPlaces: Int): String {
|
||||||
return String.format("%.0${countDecimalPlaces}f", this)
|
return String.format("%.0${countDecimalPlaces}f", this)
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ open class SepaMessageCreator : ISepaMessageCreator {
|
||||||
|
|
||||||
override fun containsOnlyAllowedCharacters(stringToTest: String): Boolean {
|
override fun containsOnlyAllowedCharacters(stringToTest: String): Boolean {
|
||||||
return AllowedSepaCharactersPattern.matches(stringToTest)
|
return AllowedSepaCharactersPattern.matches(stringToTest)
|
||||||
|
&& convertDiacriticsAndReservedXmlCharacters(stringToTest) == stringToTest
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun convertDiacriticsAndReservedXmlCharacters(input: String): String {
|
override fun convertDiacriticsAndReservedXmlCharacters(input: String): String {
|
||||||
|
|
|
@ -12,6 +12,7 @@ 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.google.android.material.textfield.TextInputLayout
|
||||||
import com.otaliastudios.autocomplete.Autocomplete
|
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.*
|
||||||
|
@ -31,6 +32,7 @@ import net.dankito.banking.ui.model.responses.BankingClientResponse
|
||||||
import net.dankito.banking.ui.presenter.BankingPresenter
|
import net.dankito.banking.ui.presenter.BankingPresenter
|
||||||
import net.dankito.banking.util.InputValidator
|
import net.dankito.banking.util.InputValidator
|
||||||
import net.dankito.banking.bankfinder.BankInfo
|
import net.dankito.banking.bankfinder.BankInfo
|
||||||
|
import net.dankito.banking.util.ValidationResult
|
||||||
import net.dankito.utils.multiplatform.toBigDecimal
|
import net.dankito.utils.multiplatform.toBigDecimal
|
||||||
import net.dankito.utils.android.extensions.asActivity
|
import net.dankito.utils.android.extensions.asActivity
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
@ -57,7 +59,17 @@ open class TransferMoneyDialog : DialogFragment() {
|
||||||
protected val inputValidator = InputValidator() // TODO: move to presenter
|
protected val inputValidator = InputValidator() // TODO: move to presenter
|
||||||
|
|
||||||
|
|
||||||
protected var foundBankForEnteredIban = false
|
protected var validRemitteeNameEntered = false
|
||||||
|
|
||||||
|
protected var validRemitteeIbanEntered = false
|
||||||
|
|
||||||
|
protected var validRemitteeBicEntered = false
|
||||||
|
|
||||||
|
protected var validUsageEntered = true
|
||||||
|
|
||||||
|
protected var validAmountEntered = false
|
||||||
|
|
||||||
|
protected var didJustCorrectInput = mutableMapOf<TextInputLayout, Boolean>()
|
||||||
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
|
@ -113,10 +125,13 @@ open class TransferMoneyDialog : DialogFragment() {
|
||||||
initRemitteeAutocompletion(rootView.edtxtRemitteeName)
|
initRemitteeAutocompletion(rootView.edtxtRemitteeName)
|
||||||
|
|
||||||
rootView.edtxtRemitteeName.addTextChangedListener(checkRequiredDataWatcher {
|
rootView.edtxtRemitteeName.addTextChangedListener(checkRequiredDataWatcher {
|
||||||
checkIfEnteredRemitteeNameIsValid()
|
checkIfEnteredRemitteeNameIsValidWhileUserIsTyping()
|
||||||
})
|
})
|
||||||
|
|
||||||
rootView.edtxtRemitteeIban.addTextChangedListener(StandardTextWatcher { tryToGetBicFromIban(it) })
|
rootView.edtxtRemitteeIban.addTextChangedListener(StandardTextWatcher {
|
||||||
|
checkIfEnteredRemitteeIbanIsValidWhileUserIsTyping()
|
||||||
|
tryToGetBicFromIban(it)
|
||||||
|
})
|
||||||
|
|
||||||
rootView.edtxtRemitteeBic.addTextChangedListener(checkRequiredDataWatcher())
|
rootView.edtxtRemitteeBic.addTextChangedListener(checkRequiredDataWatcher())
|
||||||
rootView.edtxtAmount.addTextChangedListener(checkRequiredDataWatcher())
|
rootView.edtxtAmount.addTextChangedListener(checkRequiredDataWatcher())
|
||||||
|
@ -124,8 +139,8 @@ open class TransferMoneyDialog : DialogFragment() {
|
||||||
checkIfEnteredUsageTextIsValid()
|
checkIfEnteredUsageTextIsValid()
|
||||||
})
|
})
|
||||||
|
|
||||||
rootView.edtxtRemitteeName.setOnFocusChangeListener { _, hasFocus -> if (hasFocus == false) checkIfEnteredRemitteeNameIsValid() }
|
rootView.edtxtRemitteeName.setOnFocusChangeListener { _, hasFocus -> if (hasFocus == false) checkIfEnteredRemitteeNameIsValidAfterFocusLost() }
|
||||||
rootView.edtxtRemitteeIban.setOnFocusChangeListener { _, hasFocus -> if (hasFocus == false) checkIfEnteredRemitteeIbanIsValid() }
|
rootView.edtxtRemitteeIban.setOnFocusChangeListener { _, hasFocus -> if (hasFocus == false) checkIfEnteredRemitteeIbanIsValidAfterFocusLost() }
|
||||||
rootView.edtxtRemitteeBic.setOnFocusChangeListener { _, hasFocus -> if (hasFocus == false) checkIfEnteredRemitteeBicIsValid() }
|
rootView.edtxtRemitteeBic.setOnFocusChangeListener { _, hasFocus -> if (hasFocus == false) checkIfEnteredRemitteeBicIsValid() }
|
||||||
rootView.edtxtAmount.setOnFocusChangeListener { _, hasFocus -> if (hasFocus == false) checkIfEnteredAmountIsValid() }
|
rootView.edtxtAmount.setOnFocusChangeListener { _, hasFocus -> if (hasFocus == false) checkIfEnteredAmountIsValid() }
|
||||||
rootView.edtxtUsage.setOnFocusChangeListener { _, hasFocus -> if (hasFocus == false) checkIfEnteredUsageTextIsValid() }
|
rootView.edtxtUsage.setOnFocusChangeListener { _, hasFocus -> if (hasFocus == false) checkIfEnteredUsageTextIsValid() }
|
||||||
|
@ -290,8 +305,8 @@ open class TransferMoneyDialog : DialogFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun tryToGetBicFromIban(enteredText: CharSequence) {
|
protected open fun tryToGetBicFromIban(enteredIban: CharSequence) {
|
||||||
presenter.findUniqueBankForIbanAsync(enteredText.toString()) { foundBank ->
|
presenter.findUniqueBankForIbanAsync(enteredIban.toString()) { foundBank ->
|
||||||
context?.asActivity()?.runOnUiThread {
|
context?.asActivity()?.runOnUiThread {
|
||||||
showValuesForFoundBankOnUiThread(foundBank)
|
showValuesForFoundBankOnUiThread(foundBank)
|
||||||
}
|
}
|
||||||
|
@ -299,72 +314,51 @@ open class TransferMoneyDialog : DialogFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showValuesForFoundBankOnUiThread(foundBank: BankInfo?) {
|
private fun showValuesForFoundBankOnUiThread(foundBank: BankInfo?) {
|
||||||
foundBankForEnteredIban = foundBank != null
|
validRemitteeBicEntered = foundBank != null
|
||||||
|
|
||||||
edtxtRemitteeBank.setText(if (foundBank != null) (foundBank.name + " " + foundBank.city) else "")
|
edtxtRemitteeBank.setText(if (foundBank != null) (foundBank.name + " " + foundBank.city) else "")
|
||||||
|
|
||||||
edtxtRemitteeBic.setText(foundBank?.bic ?: "") // TODO: check if user entered BIC to not overwrite self entered BIC
|
edtxtRemitteeBic.setText(foundBank?.bic ?: "") // TODO: check if user entered BIC to not overwrite self entered BIC
|
||||||
lytRemitteeBic.error = null
|
lytRemitteeBic.error = null // TODO: show information here if BIC hasn't been found
|
||||||
|
|
||||||
if (foundBankForEnteredIban) {
|
|
||||||
lytRemitteeIban.error = null
|
|
||||||
}
|
|
||||||
|
|
||||||
checkIfRequiredDataEnteredOnUiThread()
|
checkIfRequiredDataEnteredOnUiThread()
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun checkIfRequiredDataEnteredOnUiThread() {
|
protected open fun checkIfRequiredDataEnteredOnUiThread() {
|
||||||
btnTransferMoney.isEnabled = isRemitteeNameValid() && isRemitteeIbanValid()
|
btnTransferMoney.isEnabled = validRemitteeNameEntered && validRemitteeIbanEntered
|
||||||
&& isRemitteeBicValid()
|
&& validRemitteeBicEntered
|
||||||
&& isAmountGreaterZero() && isUsageTextValid()
|
&& validAmountEntered && validUsageEntered
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun checkIfEnteredRemitteeNameIsValid() {
|
protected open fun checkIfEnteredRemitteeNameIsValidWhileUserIsTyping() {
|
||||||
if (isRemitteeNameValid()) {
|
|
||||||
lytRemitteeName.error = null
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
val enteredName = edtxtRemitteeName.text.toString()
|
|
||||||
|
|
||||||
if (enteredName.isEmpty()) {
|
|
||||||
lytRemitteeName.error = context?.getString(R.string.error_no_name_entered)
|
|
||||||
}
|
|
||||||
else if (inputValidator.hasRemitteeNameValidLength(enteredName) == false) {
|
|
||||||
lytRemitteeName.error = context?.getString(R.string.error_entered_name_too_long)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
lytRemitteeName.error = context?.getString(
|
|
||||||
R.string.error_invalid_sepa_characters_entered, inputValidator.getInvalidSepaCharacters(enteredName))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun isRemitteeNameValid(): Boolean {
|
|
||||||
val enteredRemitteeName = edtxtRemitteeName.text.toString()
|
val enteredRemitteeName = edtxtRemitteeName.text.toString()
|
||||||
|
val validationResult = inputValidator.validateRemitteeNameWhileTyping(enteredRemitteeName)
|
||||||
|
|
||||||
return inputValidator.isRemitteeNameValid(enteredRemitteeName)
|
this.validRemitteeNameEntered = validationResult.validationSuccessfulOrCouldCorrectString
|
||||||
|
|
||||||
|
showValidationResult(lytRemitteeName, validationResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun checkIfEnteredRemitteeIbanIsValid() {
|
protected open fun checkIfEnteredRemitteeNameIsValidAfterFocusLost() {
|
||||||
|
val enteredRemitteeName = edtxtRemitteeName.text.toString()
|
||||||
|
val validationResult = inputValidator.validateRemitteeName(enteredRemitteeName)
|
||||||
|
|
||||||
|
this.validRemitteeNameEntered = validationResult.validationSuccessfulOrCouldCorrectString
|
||||||
|
|
||||||
|
if (validationResult.validationSuccessful == false) { // only update hint / error if validation fails, don't hide previous hint / error otherwise
|
||||||
|
showValidationResult(lytRemitteeName, validationResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun checkIfEnteredRemitteeIbanIsValidWhileUserIsTyping() {
|
||||||
val enteredIban = edtxtRemitteeIban.text.toString()
|
val enteredIban = edtxtRemitteeIban.text.toString()
|
||||||
|
val validationResult = inputValidator.validateIbanWhileTyping(enteredIban)
|
||||||
|
|
||||||
if (isRemitteeIbanValid()) {
|
this.validRemitteeIbanEntered = validationResult.validationSuccessfulOrCouldCorrectString
|
||||||
lytRemitteeIban.error = null
|
|
||||||
}
|
|
||||||
else if (enteredIban.isBlank()) {
|
|
||||||
lytRemitteeIban.error = context?.getString(R.string.error_no_iban_entered)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
val invalidIbanCharacters = inputValidator.getInvalidIbanCharacters(enteredIban)
|
|
||||||
if (invalidIbanCharacters.isNotEmpty()) {
|
|
||||||
lytRemitteeIban.error = context?.getString(R.string.error_invalid_iban_characters_entered, invalidIbanCharacters)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
lytRemitteeIban.error = context?.getString(R.string.error_invalid_iban_pattern_entered)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (foundBankForEnteredIban || enteredIban.isBlank()) {
|
showValidationResult(lytRemitteeIban, validationResult)
|
||||||
|
|
||||||
|
if (validRemitteeBicEntered || enteredIban.isBlank()) {
|
||||||
lytRemitteeBic.error = null
|
lytRemitteeBic.error = null
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -372,56 +366,31 @@ open class TransferMoneyDialog : DialogFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun isRemitteeIbanValid(): Boolean {
|
protected open fun checkIfEnteredRemitteeIbanIsValidAfterFocusLost() {
|
||||||
return inputValidator.isValidIban(edtxtRemitteeIban.text.toString())
|
val validationResult = inputValidator.validateIban(edtxtRemitteeIban.text.toString())
|
||||||
|
|
||||||
|
this.validRemitteeIbanEntered = validationResult.validationSuccessfulOrCouldCorrectString
|
||||||
|
|
||||||
|
if (validationResult.validationSuccessful == false) { // only update hint / error if validation fails, don't hide previous hint / error otherwise
|
||||||
|
showValidationResult(lytRemitteeIban, validationResult)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun checkIfEnteredRemitteeBicIsValid() {
|
protected open fun checkIfEnteredRemitteeBicIsValid() {
|
||||||
if (isRemitteeBicValid()) {
|
val enteredBic = edtxtRemitteeBic.text.toString()
|
||||||
lytRemitteeBic.error = null
|
val validationResult = inputValidator.validateBic(enteredBic)
|
||||||
}
|
|
||||||
else {
|
|
||||||
val enteredBic = edtxtRemitteeBic.text.toString()
|
|
||||||
|
|
||||||
if (enteredBic.isBlank()) {
|
this.validRemitteeBicEntered = validationResult.validationSuccessfulOrCouldCorrectString
|
||||||
lytRemitteeBic.error = context?.getString(R.string.error_no_bic_entered)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
val invalidBicCharacters = inputValidator.getInvalidBicCharacters(enteredBic)
|
|
||||||
if (invalidBicCharacters.isNotEmpty()) {
|
|
||||||
lytRemitteeBic.error = context?.getString(R.string.error_invalid_bic_characters_entered, invalidBicCharacters)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
lytRemitteeBic.error = context?.getString(R.string.error_invalid_bic_pattern_entered)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun isRemitteeBicValid(): Boolean {
|
showValidationResult(lytRemitteeBic, validationResult)
|
||||||
return inputValidator.isValidBic(edtxtRemitteeBic.text.toString())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun checkIfEnteredAmountIsValid() {
|
protected open fun checkIfEnteredAmountIsValid() {
|
||||||
if (isAmountGreaterZero()) {
|
val validationResult = inputValidator.validateAmount(edtxtAmount.text.toString())
|
||||||
lytAmount.error = null
|
|
||||||
}
|
|
||||||
else if (edtxtAmount.text.toString().isBlank()) {
|
|
||||||
lytAmount.error = context?.getString(R.string.error_no_amount_entered)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
lytAmount.error = context?.getString(R.string.error_invalid_amount_entered)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun isAmountGreaterZero(): Boolean {
|
this.validAmountEntered = validationResult.validationSuccessfulOrCouldCorrectString
|
||||||
try {
|
|
||||||
getEnteredAmount()?.let { amount ->
|
|
||||||
return amount > BigDecimal.ZERO
|
|
||||||
}
|
|
||||||
} catch (ignored: Exception) { }
|
|
||||||
|
|
||||||
return false
|
showValidationResult(lytAmount, validationResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun getEnteredAmount(): BigDecimal? {
|
protected open fun getEnteredAmount(): BigDecimal? {
|
||||||
|
@ -435,22 +404,40 @@ open class TransferMoneyDialog : DialogFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun checkIfEnteredUsageTextIsValid() {
|
protected open fun checkIfEnteredUsageTextIsValid() {
|
||||||
val enteredUsage = edtxtUsage.text.toString()
|
val validationResult = inputValidator.validateUsage(edtxtUsage.text.toString())
|
||||||
|
|
||||||
if (isUsageTextValid()) {
|
this.validUsageEntered = validationResult.validationSuccessfulOrCouldCorrectString
|
||||||
lytUsage.error = null
|
|
||||||
}
|
showValidationResult(lytUsage, validationResult)
|
||||||
else if (inputValidator.hasUsageValidLength(enteredUsage) == false) {
|
|
||||||
lytUsage.error = context?.getString(R.string.error_entered_usage_too_long)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
lytUsage.error = context?.getString(R.string.error_invalid_sepa_characters_entered,
|
|
||||||
inputValidator.getInvalidSepaCharacters(enteredUsage))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun isUsageTextValid(): Boolean {
|
protected open fun showValidationResult(textInputLayout: TextInputLayout, validationResult: ValidationResult) {
|
||||||
return inputValidator.isUsageValid(edtxtUsage.text.toString())
|
if (didJustCorrectInput.containsKey(textInputLayout)) { // we have just auto corrected TextInputLayout's EditText's text below, don't overwrite its displayed hints and error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validationResult.didCorrectString) {
|
||||||
|
textInputLayout.editText?.let { editText ->
|
||||||
|
val selectionStart = editText.selectionStart
|
||||||
|
val selectionEnd = editText.selectionEnd
|
||||||
|
val lengthDiff = validationResult.correctedInputString.length - validationResult.inputString.length
|
||||||
|
|
||||||
|
didJustCorrectInput.put(textInputLayout, true)
|
||||||
|
|
||||||
|
editText.setText(validationResult.correctedInputString)
|
||||||
|
|
||||||
|
if (validationResult.correctedInputString.isNotEmpty()) {
|
||||||
|
editText.setSelection(selectionStart + lengthDiff, selectionEnd + lengthDiff)
|
||||||
|
}
|
||||||
|
|
||||||
|
didJustCorrectInput.remove(textInputLayout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textInputLayout.error = validationResult.validationError
|
||||||
|
if (validationResult.validationError == null) { // don't overwrite error text
|
||||||
|
textInputLayout.helperText = validationResult.validationHint
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -2,6 +2,7 @@ package net.dankito.banking.util
|
||||||
|
|
||||||
import net.dankito.banking.fints.messages.segmente.implementierte.sepa.ISepaMessageCreator
|
import net.dankito.banking.fints.messages.segmente.implementierte.sepa.ISepaMessageCreator
|
||||||
import net.dankito.banking.fints.messages.segmente.implementierte.sepa.SepaMessageCreator
|
import net.dankito.banking.fints.messages.segmente.implementierte.sepa.SepaMessageCreator
|
||||||
|
import net.dankito.utils.multiplatform.BigDecimal
|
||||||
|
|
||||||
|
|
||||||
open class InputValidator {
|
open class InputValidator {
|
||||||
|
@ -10,6 +11,10 @@ open class InputValidator {
|
||||||
|
|
||||||
const val RemitteNameMaxLength = 70
|
const val RemitteNameMaxLength = 70
|
||||||
|
|
||||||
|
const val IbanMaxLength = 34
|
||||||
|
|
||||||
|
const val BicMaxLength = 11
|
||||||
|
|
||||||
const val UsageMaxLength = 140
|
const val UsageMaxLength = 140
|
||||||
|
|
||||||
|
|
||||||
|
@ -58,6 +63,94 @@ open class InputValidator {
|
||||||
protected val sepaMessageCreator: ISepaMessageCreator = SepaMessageCreator()
|
protected val sepaMessageCreator: ISepaMessageCreator = SepaMessageCreator()
|
||||||
|
|
||||||
|
|
||||||
|
open fun validateRemitteeNameWhileTyping(remitteeNameToTest: String): ValidationResult {
|
||||||
|
return validateRemitteeName(remitteeNameToTest, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun validateRemitteeName(remitteeNameToTest: String): ValidationResult {
|
||||||
|
return validateRemitteeName(remitteeNameToTest, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun validateRemitteeName(remitteeNameToTest: String, userIsStillTyping: Boolean = false): ValidationResult {
|
||||||
|
if (isRemitteeNameValid(remitteeNameToTest)) {
|
||||||
|
return ValidationResult(remitteeNameToTest, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remitteeNameToTest.isEmpty()) {
|
||||||
|
if (userIsStillTyping) { // if user is still typing, don't check if something has been entered yet
|
||||||
|
return ValidationResult(remitteeNameToTest, true)
|
||||||
|
}
|
||||||
|
return ValidationResult(remitteeNameToTest, false, validationError = "Bitte geben Sie den Namen des Empfängers ein") // TODO: translate
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasRemitteeNameValidLength(remitteeNameToTest) == false) {
|
||||||
|
val correctedString = remitteeNameToTest.substring(0, RemitteNameMaxLength)
|
||||||
|
return ValidationResult(remitteeNameToTest, isRemitteeNameValid(correctedString), true, correctedString, "Name darf maximal 70 Zeichen lang sein") // TODO: translate
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val invalidRemitteeNameCharacters = getInvalidSepaCharacters(remitteeNameToTest)
|
||||||
|
|
||||||
|
val correctedString = getCorrectedString(remitteeNameToTest, invalidRemitteeNameCharacters, true)
|
||||||
|
return ValidationResult(remitteeNameToTest, isRemitteeNameValid(correctedString), true, correctedString, null, "Unzulässige(s) Zeichen eingegeben: $invalidRemitteeNameCharacters") // TODO: translate
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun isRemitteeNameValid(stringToTest: String): Boolean {
|
||||||
|
return hasRemitteeNameValidLength(stringToTest)
|
||||||
|
&& containsOnlyValidSepaCharacters(stringToTest)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun hasRemitteeNameValidLength(stringToTest: String): Boolean {
|
||||||
|
return stringToTest.length in 1..RemitteNameMaxLength
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate entered IBAN while user is still typing. Just checks (and corrects) if invalid
|
||||||
|
* characters have been entered or string is too long.
|
||||||
|
*
|
||||||
|
* Doesn't check yet if entered text has the correct pattern, min length etc. as user may is
|
||||||
|
* just about to enter this information.
|
||||||
|
*/
|
||||||
|
open fun validateIbanWhileTyping(ibanToTest: String): ValidationResult {
|
||||||
|
return validateIban(ibanToTest, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun validateIban(ibanToTest: String): ValidationResult {
|
||||||
|
return validateIban(ibanToTest, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun validateIban(ibanToTest: String, userIsStillTyping: Boolean = false): ValidationResult {
|
||||||
|
if (isValidIban(ibanToTest)) {
|
||||||
|
return ValidationResult(ibanToTest, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ibanToTest.isBlank()) {
|
||||||
|
if (userIsStillTyping) { // if user is still typing, don't check if something has been entered yet
|
||||||
|
return ValidationResult(ibanToTest, true)
|
||||||
|
}
|
||||||
|
return ValidationResult(ibanToTest, false, validationError = "Bitte geben Sie die IBAN des Empfängers ein") // TODO: translate
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ibanToTest.length > IbanMaxLength) {
|
||||||
|
val correctedString = ibanToTest.substring(0, IbanMaxLength)
|
||||||
|
return ValidationResult(ibanToTest, isValidIban(correctedString), true, correctedString, null, "Eine IBAN darf maximal 34 Zeichen lang sein") // TODO: translate // TODO: may test country specific IBAN length, e.g. German IBANs have 22 charactersaa
|
||||||
|
}
|
||||||
|
|
||||||
|
val invalidIbanCharacters = getInvalidIbanCharacters(ibanToTest)
|
||||||
|
|
||||||
|
if (invalidIbanCharacters.isNotEmpty()) {
|
||||||
|
val correctedString = getCorrectedString(ibanToTest, invalidIbanCharacters)
|
||||||
|
return ValidationResult(ibanToTest, isValidIban(correctedString), true, correctedString, null, "Unzulässige(s) Zeichen eingegeben: $invalidIbanCharacters") // TODO: translate
|
||||||
|
}
|
||||||
|
else if (userIsStillTyping) { // entered IBAN hasn't required pattern yet but that's ok as user is may just about to provide that information
|
||||||
|
return ValidationResult(ibanToTest, true)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return ValidationResult(ibanToTest, false, validationError = "IBANs haben folgendes Muster: DE12 1234 5678 9012 3456 78") // TODO: translate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
open fun isValidIban(stringToTest: String): Boolean {
|
open fun isValidIban(stringToTest: String): Boolean {
|
||||||
return IbanPattern.matches(stringToTest.replace(" ", ""))
|
return IbanPattern.matches(stringToTest.replace(" ", ""))
|
||||||
}
|
}
|
||||||
|
@ -67,6 +160,31 @@ open class InputValidator {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
open fun validateBic(bicToTest: String): ValidationResult {
|
||||||
|
if (isValidBic(bicToTest)) {
|
||||||
|
return ValidationResult(bicToTest, true)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (bicToTest.isBlank()) {
|
||||||
|
return ValidationResult(bicToTest, false, validationError = "Bitte geben Sie die BIC des Empfängers ein") // TODO: translate
|
||||||
|
}
|
||||||
|
else if (bicToTest.length > BicMaxLength) {
|
||||||
|
val correctedString = bicToTest.substring(0, BicMaxLength)
|
||||||
|
return ValidationResult(bicToTest, isValidBic(correctedString), true, correctedString, null, "Eine IBAN darf maximal 11 Zeichen lang sein") // TODO: translate // TODO: may test country specific IBAN length, e.g. German IBANs have 22 charactersaa
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
val invalidBicCharacters = getInvalidBicCharacters(bicToTest)
|
||||||
|
if (invalidBicCharacters.isNotEmpty()) {
|
||||||
|
val correctedString = getCorrectedString(bicToTest, invalidBicCharacters)
|
||||||
|
return ValidationResult(bicToTest, isValidBic(correctedString), true, correctedString, null, "Unzulässige(s) Zeichen eingegeben: $invalidBicCharacters") // TODO: translate
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return ValidationResult(bicToTest, false, validationError = "Eine BIC besteht aus 8 oder 11 Zeichen und folgt dem Muster: ABCDED12(XYZ)") // TODO: translate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
open fun isValidBic(stringToTest: String): Boolean {
|
open fun isValidBic(stringToTest: String): Boolean {
|
||||||
return BicPattern.matches(stringToTest)
|
return BicPattern.matches(stringToTest)
|
||||||
}
|
}
|
||||||
|
@ -76,23 +194,50 @@ open class InputValidator {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
open fun isRemitteeNameValid(stringToTest: String): Boolean {
|
open fun validateAmount(enteredAmountString: String): ValidationResult {
|
||||||
val convertedString = convertToAllowedSepaCharacters(stringToTest)
|
if (enteredAmountString.isBlank()) {
|
||||||
|
return ValidationResult(enteredAmountString, false, validationError = "Bitte geben Sie den zu überweisenden Betrag ein") // TODO: translate
|
||||||
|
}
|
||||||
|
|
||||||
return hasRemitteeNameValidLength(convertedString)
|
convertAmountString(enteredAmountString)?.let { amount ->
|
||||||
&& containsOnlyValidSepaCharacters(convertedString)
|
if (amount.isPositive && amount != BigDecimal.Zero) {
|
||||||
|
return ValidationResult(enteredAmountString, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ValidationResult(enteredAmountString, false, validationError = "Bitte geben Sie einen Betrag größer 0 ein.") // TODO: translate
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun hasRemitteeNameValidLength(stringToTest: String): Boolean {
|
open fun convertAmountString(enteredAmountString: String): BigDecimal? {
|
||||||
return stringToTest.length in 1..RemitteNameMaxLength
|
try {
|
||||||
|
val amountString = enteredAmountString.replace(',', '.')
|
||||||
|
|
||||||
|
return BigDecimal(amountString)
|
||||||
|
} catch (ignored: Exception) { }
|
||||||
|
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
open fun validateUsage(usageToTest: String): ValidationResult {
|
||||||
|
if (isUsageValid(usageToTest)) {
|
||||||
|
return ValidationResult(usageToTest, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasUsageValidLength(usageToTest) == false) {
|
||||||
|
val correctedString = usageToTest.substring(0, UsageMaxLength)
|
||||||
|
return ValidationResult(usageToTest, isUsageValid(correctedString), true, correctedString, "Verwendungszweck darf nur 140 Zeichen lang sein") // TODO: translate
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val invalidUsageCharacters = getInvalidSepaCharacters(usageToTest)
|
||||||
|
val correctedString = getCorrectedString(usageToTest, invalidUsageCharacters, true)
|
||||||
|
return ValidationResult(usageToTest, isUsageValid(correctedString), true, correctedString, null, "Unzulässige(s) Zeichen eingegeben: $invalidUsageCharacters") // TODO: translate return ValidationResult(remitteeNameToTest, false, validationError = "Unzulässige(s) Zeichen eingegeben: ") // TODO: translate
|
||||||
|
}
|
||||||
|
|
||||||
open fun isUsageValid(stringToTest: String): Boolean {
|
open fun isUsageValid(stringToTest: String): Boolean {
|
||||||
val convertedString = convertToAllowedSepaCharacters(stringToTest)
|
return hasUsageValidLength(stringToTest)
|
||||||
|
&& containsOnlyValidSepaCharacters(stringToTest)
|
||||||
return hasUsageValidLength(convertedString)
|
|
||||||
&& containsOnlyValidSepaCharacters(convertedString)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun hasUsageValidLength(stringToTest: String): Boolean {
|
open fun hasUsageValidLength(stringToTest: String): Boolean {
|
||||||
|
@ -105,7 +250,7 @@ open class InputValidator {
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun getInvalidSepaCharacters(string: String): String {
|
open fun getInvalidSepaCharacters(string: String): String {
|
||||||
return getInvalidCharacters(convertToAllowedSepaCharacters(string), InvalidSepaCharactersPattern)
|
return getInvalidCharacters(string, InvalidSepaCharactersPattern)
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun convertToAllowedSepaCharacters(string: String): String {
|
open fun convertToAllowedSepaCharacters(string: String): String {
|
||||||
|
@ -114,7 +259,19 @@ open class InputValidator {
|
||||||
|
|
||||||
|
|
||||||
open fun getInvalidCharacters(string: String, pattern: Regex): String {
|
open fun getInvalidCharacters(string: String, pattern: Regex): String {
|
||||||
return pattern.findAll(string).joinToString("")
|
return pattern.findAll(string).map { it.value }.joinToString("")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: do not convert XML entities in user's. User will a) not understand what happened and b) afterwards auto correction will not work anymore (i think the issue lies in used Regex: '(&\w{2,4};)').
|
||||||
|
// But take converted XML entities length into account when checking if remittee's name and usage length isn't too long
|
||||||
|
protected open fun getCorrectedString(inputString: String, invalidCharacters: String, convertToAllowedSepaCharacters: Boolean = false): String {
|
||||||
|
var correctedString = if (convertToAllowedSepaCharacters) convertToAllowedSepaCharacters(inputString) else inputString
|
||||||
|
|
||||||
|
invalidCharacters.forEach { invalidChar ->
|
||||||
|
correctedString = correctedString.replace(invalidChar.toString(), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
return correctedString
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package net.dankito.banking.util
|
||||||
|
|
||||||
|
|
||||||
|
open class ValidationResult(
|
||||||
|
open val inputString: String,
|
||||||
|
open val validationSuccessful: Boolean,
|
||||||
|
open val didCorrectString: Boolean = false,
|
||||||
|
open val correctedInputString: String = inputString,
|
||||||
|
open val validationError: String? = null,
|
||||||
|
open val validationHint: String? = null
|
||||||
|
) {
|
||||||
|
|
||||||
|
open val validationSuccessfulOrCouldCorrectString: Boolean = validationSuccessful || didCorrectString
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,687 @@
|
||||||
|
package net.dankito.banking.util
|
||||||
|
|
||||||
|
import ch.tutteli.atrium.api.fluent.en_GB.notToBeNull
|
||||||
|
import ch.tutteli.atrium.api.fluent.en_GB.toBe
|
||||||
|
import ch.tutteli.atrium.api.verbs.expect
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
||||||
|
class InputValidatorTest {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val ValidRemitteeName = "Marieke Musterfrau"
|
||||||
|
|
||||||
|
const val ValidIban = "DE11123456780987654321"
|
||||||
|
|
||||||
|
const val ValidBic = "ABCDDEBBXXX"
|
||||||
|
|
||||||
|
const val ValidUsage = "Usage"
|
||||||
|
|
||||||
|
const val InvalidSepaCharacter = "!"
|
||||||
|
|
||||||
|
const val InvalidUmlaut = "ö"
|
||||||
|
const val ConvertedInvalidUmlaut = "o"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private val underTest = InputValidator()
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getInvalidIbanCharacters() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val invalidIbanCharacters = "ajvz!@#$%^&*()-_=+[]{}'\"\\|/?.,;:<>"
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.getInvalidIbanCharacters("EN${invalidIbanCharacters}1234")
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result).toBe(invalidIbanCharacters)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getInvalidSepaCharacters() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val invalidSepaCharacters = "!€@#$%^*=[]\\|<>"
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.getInvalidSepaCharacters("abcd${invalidSepaCharacters}1234")
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result).toBe(invalidSepaCharacters)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateRemitteeName_EmptyStringEntered() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val enteredName = ""
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateRemitteeName(enteredName)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(false)
|
||||||
|
expect(result.didCorrectString).toBe(false)
|
||||||
|
expect(result.inputString).toBe(enteredName)
|
||||||
|
expect(result.correctedInputString).toBe(enteredName)
|
||||||
|
expect(result.validationHint).toBe(null)
|
||||||
|
expect(result.validationError).notToBeNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateRemitteeName_ValidNameEntered() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val enteredName = ValidRemitteeName
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateRemitteeName(enteredName)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(true)
|
||||||
|
expect(result.didCorrectString).toBe(false)
|
||||||
|
expect(result.inputString).toBe(enteredName)
|
||||||
|
expect(result.correctedInputString).toBe(enteredName)
|
||||||
|
expect(result.validationHint).toBe(null)
|
||||||
|
expect(result.validationError).toBe(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateRemitteeName_UmlautGetsConverted() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val enteredName = ValidRemitteeName + InvalidUmlaut
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateRemitteeName(enteredName)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(true)
|
||||||
|
expect(result.didCorrectString).toBe(true)
|
||||||
|
expect(result.inputString).toBe(enteredName)
|
||||||
|
expect(result.correctedInputString).toBe(ValidRemitteeName + ConvertedInvalidUmlaut)
|
||||||
|
expect(result.validationHint?.contains(InvalidUmlaut)).toBe(true)
|
||||||
|
expect(result.validationError).toBe(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateRemitteeName_InvalidCharacterGetsRemoved() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val enteredName = ValidRemitteeName + InvalidSepaCharacter
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateRemitteeName(enteredName)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(true)
|
||||||
|
expect(result.didCorrectString).toBe(true)
|
||||||
|
expect(result.inputString).toBe(enteredName)
|
||||||
|
expect(result.correctedInputString).toBe(ValidRemitteeName)
|
||||||
|
expect(result.validationHint?.contains(InvalidSepaCharacter)).toBe(true)
|
||||||
|
expect(result.validationError).toBe(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateRemitteeName_TooLong() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val nameWithMaxLength = IntRange(0, InputValidator.RemitteNameMaxLength - 1).map { "a" }.joinToString("")
|
||||||
|
val enteredName = nameWithMaxLength + "a"
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateRemitteeName(enteredName)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(true)
|
||||||
|
expect(result.didCorrectString).toBe(true)
|
||||||
|
expect(result.inputString).toBe(enteredName)
|
||||||
|
expect(result.correctedInputString).toBe(nameWithMaxLength)
|
||||||
|
expect(result.validationHint).toBe(null)
|
||||||
|
expect(result.validationError).notToBeNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateIban_EmptyStringEntered() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val enteredIban = ""
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateIban(enteredIban)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(false)
|
||||||
|
expect(result.didCorrectString).toBe(false)
|
||||||
|
expect(result.inputString).toBe(enteredIban)
|
||||||
|
expect(result.correctedInputString).toBe(enteredIban)
|
||||||
|
expect(result.validationHint).toBe(null)
|
||||||
|
expect(result.validationError).notToBeNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateIban_ValidIbanEntered() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val enteredIban = ValidIban
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateIban(enteredIban)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(true)
|
||||||
|
expect(result.didCorrectString).toBe(false)
|
||||||
|
expect(result.inputString).toBe(enteredIban)
|
||||||
|
expect(result.correctedInputString).toBe(enteredIban)
|
||||||
|
expect(result.validationHint).toBe(null)
|
||||||
|
expect(result.validationError).toBe(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateIban_IbanTooShort() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val enteredIban = "DE11"
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateIban(enteredIban)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(false)
|
||||||
|
expect(result.didCorrectString).toBe(false)
|
||||||
|
expect(result.inputString).toBe(enteredIban)
|
||||||
|
expect(result.correctedInputString).toBe(enteredIban)
|
||||||
|
expect(result.validationHint).toBe(null)
|
||||||
|
expect(result.validationError).notToBeNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateIban_UmlautGetsRemoved() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val enteredIban = ValidIban + InvalidUmlaut
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateIban(enteredIban)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(true)
|
||||||
|
expect(result.didCorrectString).toBe(true)
|
||||||
|
expect(result.inputString).toBe(enteredIban)
|
||||||
|
expect(result.correctedInputString).toBe(ValidIban)
|
||||||
|
expect(result.validationHint?.contains(InvalidUmlaut)).toBe(true)
|
||||||
|
expect(result.validationError).toBe(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateIban_InvalidCharacterGetsRemoved() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val enteredIban = ValidIban + InvalidSepaCharacter
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateIban(enteredIban)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(true)
|
||||||
|
expect(result.didCorrectString).toBe(true)
|
||||||
|
expect(result.inputString).toBe(enteredIban)
|
||||||
|
expect(result.correctedInputString).toBe(ValidIban)
|
||||||
|
expect(result.validationHint?.contains(InvalidSepaCharacter)).toBe(true)
|
||||||
|
expect(result.validationError).toBe(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateIban_TooLong() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val ibanWithMaxLength = IntRange(0, InputValidator.IbanMaxLength - 1).map { "1" }.joinToString("")
|
||||||
|
val enteredIban = ibanWithMaxLength + "1"
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateIban(enteredIban)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(false)
|
||||||
|
expect(result.didCorrectString).toBe(true)
|
||||||
|
expect(result.inputString).toBe(enteredIban)
|
||||||
|
expect(result.correctedInputString).toBe(ibanWithMaxLength)
|
||||||
|
expect(result.validationHint).notToBeNull()
|
||||||
|
expect(result.validationError).toBe(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateIbanWhileTyping_EmptyStringEntered() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val enteredIban = ""
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateIbanWhileTyping(enteredIban)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(true) // while user is typing an empty string is ok
|
||||||
|
expect(result.didCorrectString).toBe(false)
|
||||||
|
expect(result.inputString).toBe(enteredIban)
|
||||||
|
expect(result.correctedInputString).toBe(enteredIban)
|
||||||
|
expect(result.validationHint).toBe(null)
|
||||||
|
expect(result.validationError).toBe(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateIbanWhileTyping_ValidIbanEntered() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val enteredIban = ValidIban
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateIbanWhileTyping(enteredIban)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(true)
|
||||||
|
expect(result.didCorrectString).toBe(false)
|
||||||
|
expect(result.inputString).toBe(enteredIban)
|
||||||
|
expect(result.correctedInputString).toBe(enteredIban)
|
||||||
|
expect(result.validationHint).toBe(null)
|
||||||
|
expect(result.validationError).toBe(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateIbanWhileTyping_IbanTooShort() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val enteredIban = "DE11"
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateIbanWhileTyping(enteredIban)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(true) // while user is typing an incomplete IBAN is ok
|
||||||
|
expect(result.didCorrectString).toBe(false)
|
||||||
|
expect(result.inputString).toBe(enteredIban)
|
||||||
|
expect(result.correctedInputString).toBe(enteredIban)
|
||||||
|
expect(result.validationHint).toBe(null)
|
||||||
|
expect(result.validationError).toBe(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateIbanWhileTyping_UmlautGetsRemoved() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val enteredIban = ValidIban + InvalidUmlaut
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateIbanWhileTyping(enteredIban)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(true)
|
||||||
|
expect(result.didCorrectString).toBe(true)
|
||||||
|
expect(result.inputString).toBe(enteredIban)
|
||||||
|
expect(result.correctedInputString).toBe(ValidIban)
|
||||||
|
expect(result.validationHint?.contains(InvalidUmlaut)).toBe(true)
|
||||||
|
expect(result.validationError).toBe(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateIbanWhileTyping_InvalidCharacterGetsRemoved() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val enteredIban = ValidIban + InvalidSepaCharacter
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateIbanWhileTyping(enteredIban)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(true)
|
||||||
|
expect(result.didCorrectString).toBe(true)
|
||||||
|
expect(result.inputString).toBe(enteredIban)
|
||||||
|
expect(result.correctedInputString).toBe(ValidIban)
|
||||||
|
expect(result.validationHint?.contains(InvalidSepaCharacter)).toBe(true)
|
||||||
|
expect(result.validationError).toBe(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateIbanWhileTyping_TooLong() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val ibanWithMaxLength = IntRange(0, InputValidator.IbanMaxLength - 1).map { "1" }.joinToString("")
|
||||||
|
val enteredIban = ibanWithMaxLength + "1"
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateIbanWhileTyping(enteredIban)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(false)
|
||||||
|
expect(result.didCorrectString).toBe(true)
|
||||||
|
expect(result.inputString).toBe(enteredIban)
|
||||||
|
expect(result.correctedInputString).toBe(ibanWithMaxLength)
|
||||||
|
expect(result.validationHint).notToBeNull()
|
||||||
|
expect(result.validationError).toBe(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateBic_EmptyStringEntered() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val enteredBic = ""
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateBic(enteredBic)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(false)
|
||||||
|
expect(result.didCorrectString).toBe(false)
|
||||||
|
expect(result.inputString).toBe(enteredBic)
|
||||||
|
expect(result.correctedInputString).toBe(enteredBic)
|
||||||
|
expect(result.validationHint).toBe(null)
|
||||||
|
expect(result.validationError).notToBeNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateBic_ValidBicEntered() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val enteredBic = ValidBic
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateBic(enteredBic)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(true)
|
||||||
|
expect(result.didCorrectString).toBe(false)
|
||||||
|
expect(result.inputString).toBe(enteredBic)
|
||||||
|
expect(result.correctedInputString).toBe(enteredBic)
|
||||||
|
expect(result.validationHint).toBe(null)
|
||||||
|
expect(result.validationError).toBe(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateBic_BicTooShort() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val enteredBic = "ABCD"
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateBic(enteredBic)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(false)
|
||||||
|
expect(result.didCorrectString).toBe(false)
|
||||||
|
expect(result.inputString).toBe(enteredBic)
|
||||||
|
expect(result.correctedInputString).toBe(enteredBic)
|
||||||
|
expect(result.validationHint).toBe(null)
|
||||||
|
expect(result.validationError).notToBeNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateBic_UmlautGetsRemoved() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val bicWithoutLastPlace = ValidBic.substring(0, ValidBic.length - 1)
|
||||||
|
val enteredBic = bicWithoutLastPlace + InvalidUmlaut
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateBic(enteredBic)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(false)
|
||||||
|
expect(result.didCorrectString).toBe(true)
|
||||||
|
expect(result.inputString).toBe(enteredBic)
|
||||||
|
expect(result.correctedInputString).toBe(bicWithoutLastPlace)
|
||||||
|
expect(result.validationHint?.contains(InvalidUmlaut)).toBe(true)
|
||||||
|
expect(result.validationError).toBe(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateBic_InvalidCharacterGetsRemoved() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val bicWithoutLastPlace = ValidBic.substring(0, ValidBic.length - 1)
|
||||||
|
val enteredBic = bicWithoutLastPlace + InvalidSepaCharacter
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateBic(enteredBic)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(false)
|
||||||
|
expect(result.didCorrectString).toBe(true)
|
||||||
|
expect(result.inputString).toBe(enteredBic)
|
||||||
|
expect(result.correctedInputString).toBe(bicWithoutLastPlace)
|
||||||
|
expect(result.validationHint?.contains(InvalidSepaCharacter)).toBe(true)
|
||||||
|
expect(result.validationError).toBe(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateBic_TooLong() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val bicWithMaxLength = IntRange(0, InputValidator.BicMaxLength - 1).map { "A" }.joinToString("")
|
||||||
|
val enteredBic = bicWithMaxLength + "A"
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateBic(enteredBic)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(true)
|
||||||
|
expect(result.didCorrectString).toBe(true)
|
||||||
|
expect(result.inputString).toBe(enteredBic)
|
||||||
|
expect(result.correctedInputString).toBe(bicWithMaxLength)
|
||||||
|
expect(result.validationHint).notToBeNull()
|
||||||
|
expect(result.validationError).toBe(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateUsage_EmptyStringEntered() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val enteredUsage = ""
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateUsage(enteredUsage)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(true)
|
||||||
|
expect(result.didCorrectString).toBe(false)
|
||||||
|
expect(result.inputString).toBe(enteredUsage)
|
||||||
|
expect(result.correctedInputString).toBe(enteredUsage)
|
||||||
|
expect(result.validationHint).toBe(null)
|
||||||
|
expect(result.validationError).toBe(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateUsage_ValidUsageEntered() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val enteredUsage = ValidUsage
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateUsage(enteredUsage)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(true)
|
||||||
|
expect(result.didCorrectString).toBe(false)
|
||||||
|
expect(result.inputString).toBe(enteredUsage)
|
||||||
|
expect(result.correctedInputString).toBe(enteredUsage)
|
||||||
|
expect(result.validationHint).toBe(null)
|
||||||
|
expect(result.validationError).toBe(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateUsage_UmlautGetsConverted() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val enteredUsage = ValidUsage + InvalidUmlaut
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateUsage(enteredUsage)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(true)
|
||||||
|
expect(result.didCorrectString).toBe(true)
|
||||||
|
expect(result.inputString).toBe(enteredUsage)
|
||||||
|
expect(result.correctedInputString).toBe(ValidUsage + ConvertedInvalidUmlaut)
|
||||||
|
expect(result.validationHint?.contains(InvalidUmlaut)).toBe(true)
|
||||||
|
expect(result.validationError).toBe(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateUsage_InvalidCharacterGetsRemoved() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val enteredUsage = ValidUsage + InvalidSepaCharacter
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateUsage(enteredUsage)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(true)
|
||||||
|
expect(result.didCorrectString).toBe(true)
|
||||||
|
expect(result.inputString).toBe(enteredUsage)
|
||||||
|
expect(result.correctedInputString).toBe(ValidUsage)
|
||||||
|
expect(result.validationHint?.contains(InvalidSepaCharacter)).toBe(true)
|
||||||
|
expect(result.validationError).toBe(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: does not work yet
|
||||||
|
@Test
|
||||||
|
fun validateUsage_AmpersandGetsRemoved() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val invalidSepaCharacter = "&"
|
||||||
|
val enteredUsage = ValidUsage + invalidSepaCharacter
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateUsage(enteredUsage)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(true)
|
||||||
|
expect(result.didCorrectString).toBe(true)
|
||||||
|
expect(result.inputString).toBe(enteredUsage)
|
||||||
|
expect(result.correctedInputString).toBe(ValidUsage)
|
||||||
|
expect(result.validationHint?.contains(invalidSepaCharacter)).toBe(true)
|
||||||
|
expect(result.validationError).toBe(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: does not work yet
|
||||||
|
@Test
|
||||||
|
fun validateUsage_EnteringACharacterAfterConvertingAXmlEntityDoesNotFail() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val convertedXmlEntity = "&"
|
||||||
|
val validSepaCharacter = "h"
|
||||||
|
val enteredUsage = ValidUsage + convertedXmlEntity + validSepaCharacter
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateUsage(enteredUsage)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(true)
|
||||||
|
expect(result.didCorrectString).toBe(true)
|
||||||
|
expect(result.inputString).toBe(enteredUsage)
|
||||||
|
expect(result.correctedInputString).toBe(ValidUsage + convertedXmlEntity + validSepaCharacter)
|
||||||
|
expect(result.validationHint).toBe(null)
|
||||||
|
expect(result.validationError).toBe(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateUsage_TooLong() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val usageWithMaxLength = IntRange(0, InputValidator.UsageMaxLength - 1).map { "a" }.joinToString("")
|
||||||
|
val enteredUsage = usageWithMaxLength + "a"
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateUsage(enteredUsage)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(true)
|
||||||
|
expect(result.didCorrectString).toBe(true)
|
||||||
|
expect(result.inputString).toBe(enteredUsage)
|
||||||
|
expect(result.correctedInputString).toBe(usageWithMaxLength)
|
||||||
|
expect(result.validationHint).toBe(null)
|
||||||
|
expect(result.validationError).notToBeNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateAmount_EmptyStringEntered() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val enteredAmount = ""
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateAmount(enteredAmount)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(false)
|
||||||
|
expect(result.didCorrectString).toBe(false)
|
||||||
|
expect(result.inputString).toBe(enteredAmount)
|
||||||
|
expect(result.correctedInputString).toBe(enteredAmount)
|
||||||
|
expect(result.validationHint).toBe(null)
|
||||||
|
expect(result.validationError).notToBeNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateAmount_ValidAmountEntered() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val enteredAmount = "84,25"
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateAmount(enteredAmount)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(true)
|
||||||
|
expect(result.didCorrectString).toBe(false)
|
||||||
|
expect(result.inputString).toBe(enteredAmount)
|
||||||
|
expect(result.correctedInputString).toBe(enteredAmount)
|
||||||
|
expect(result.validationHint).toBe(null)
|
||||||
|
expect(result.validationError).toBe(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateAmount_ZeroEntered() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val enteredAmount = "0"
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateAmount(enteredAmount)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(false)
|
||||||
|
expect(result.didCorrectString).toBe(false)
|
||||||
|
expect(result.inputString).toBe(enteredAmount)
|
||||||
|
expect(result.correctedInputString).toBe(enteredAmount)
|
||||||
|
expect(result.validationHint).toBe(null)
|
||||||
|
expect(result.validationError).notToBeNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateAmount_NegativeAmountEntered() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
val enteredAmount = "-84,25"
|
||||||
|
|
||||||
|
// when
|
||||||
|
val result = underTest.validateAmount(enteredAmount)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.validationSuccessful).toBe(false)
|
||||||
|
expect(result.didCorrectString).toBe(false)
|
||||||
|
expect(result.inputString).toBe(enteredAmount)
|
||||||
|
expect(result.correctedInputString).toBe(enteredAmount)
|
||||||
|
expect(result.validationHint).toBe(null)
|
||||||
|
expect(result.validationError).notToBeNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,38 +0,0 @@
|
||||||
package net.dankito.banking.util
|
|
||||||
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
|
|
||||||
class InputValidatorTest {
|
|
||||||
|
|
||||||
private val underTest = InputValidator()
|
|
||||||
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun getInvalidIbanCharacters() {
|
|
||||||
|
|
||||||
// given
|
|
||||||
val invalidIbanCharacters = "ajvz!@#$%^&*()-_=+[]{}'\"\\|/?.,;:<>"
|
|
||||||
|
|
||||||
// when
|
|
||||||
val result = underTest.getInvalidIbanCharacters("EN${invalidIbanCharacters}1234")
|
|
||||||
|
|
||||||
// then
|
|
||||||
assertThat(result).isEqualTo(invalidIbanCharacters as Any)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun getInvalidSepaCharacters() {
|
|
||||||
|
|
||||||
// given
|
|
||||||
val invalidSepaCharacters = "!€@#$%^&*_=[]{}\\|;<>"
|
|
||||||
|
|
||||||
// when
|
|
||||||
val result = underTest.getInvalidSepaCharacters("abcd${invalidSepaCharacters}1234")
|
|
||||||
|
|
||||||
// then
|
|
||||||
assertThat(result).isEqualTo(invalidSepaCharacters as Any)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
import SwiftUI
|
||||||
|
import BankingUiSwift
|
||||||
|
|
||||||
|
|
||||||
|
struct ValidationLabel: View {
|
||||||
|
|
||||||
|
private let validationErrorOrHint: String
|
||||||
|
|
||||||
|
private let isHint: Bool
|
||||||
|
|
||||||
|
|
||||||
|
init(_ validationError: String) {
|
||||||
|
self.init(validationError, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ validationResult: ValidationResult) {
|
||||||
|
self.init(validationResult.validationError ?? validationResult.validationHint ?? "",
|
||||||
|
validationResult.validationError == nil && validationResult.validationHint != nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ validationErrorOrHint: String, _ isHint: Bool) {
|
||||||
|
self.validationErrorOrHint = validationErrorOrHint
|
||||||
|
self.isHint = isHint
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
.frame(height: 6)
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text(validationErrorOrHint)
|
||||||
|
.padding(.leading, 16)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
.frame(height: 18)
|
||||||
|
}
|
||||||
|
.font(.callout)
|
||||||
|
.foregroundColor(isHint ? Color.yellow : Color.red)
|
||||||
|
.systemGroupedBackground()
|
||||||
|
.listRowInsets(EdgeInsets())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct ValidationLabel_Previews: PreviewProvider {
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
ValidationLabel("Invalid characters used")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -16,35 +16,44 @@ struct TransferMoneyDialog: View {
|
||||||
|
|
||||||
@State private var remitteeName: String = ""
|
@State private var remitteeName: String = ""
|
||||||
@State private var isValidRemitteeNameEntered = false
|
@State private var isValidRemitteeNameEntered = false
|
||||||
|
@State private var remitteeNameValidationResult: ValidationResult? = nil
|
||||||
|
|
||||||
@State private var showRemitteeAutocompleteList = false
|
@State private var showRemitteeAutocompleteList = false
|
||||||
@State private var remitteeSearchResults = [Remittee]()
|
@State private var remitteeSearchResults = [Remittee]()
|
||||||
|
|
||||||
@State private var remitteeIban: String = ""
|
@State private var remitteeIban: String = ""
|
||||||
@State private var isValidRemitteeIbanEntered = false
|
@State private var isValidRemitteeIbanEntered = false
|
||||||
|
@State private var remitteeIbanValidationResult: ValidationResult? = nil
|
||||||
|
|
||||||
@State private var remitteeBic: String = ""
|
@State private var remitteeBic: String = ""
|
||||||
@State private var isValidRemitteeBicEntered = false
|
@State private var isValidRemitteeBicEntered = false
|
||||||
|
@State private var remitteeBicValidationResult: ValidationResult? = nil
|
||||||
|
|
||||||
@State private var amount = ""
|
@State private var amount = ""
|
||||||
@State private var isValidAmountEntered = false
|
@State private var isValidAmountEntered = false
|
||||||
|
@State private var amountValidationResult: ValidationResult? = nil
|
||||||
|
|
||||||
@State private var usage: String = ""
|
@State private var usage: String = ""
|
||||||
@State private var isValidUsageEntered = true
|
@State private var isValidUsageEntered = true
|
||||||
|
@State private var usageValidationResult: ValidationResult? = nil
|
||||||
|
|
||||||
@State private var instantPayment = false
|
@State private var instantPayment = false
|
||||||
|
|
||||||
@State private var transferMoneyResponseMessage: Message? = nil
|
@State private var transferMoneyResponseMessage: Message? = nil
|
||||||
|
|
||||||
|
private let inputValidator = InputValidator()
|
||||||
|
|
||||||
|
@State private var didJustCorrectEnteredValue = false
|
||||||
|
|
||||||
|
|
||||||
private var account: BankAccount? {
|
private var account: BankAccount? {
|
||||||
if (self.selectedAccountIndex < self.accountsSupportingTransferringMoney.count) {
|
if (self.selectedAccountIndex < self.accountsSupportingTransferringMoney.count) {
|
||||||
return self.accountsSupportingTransferringMoney[selectedAccountIndex]
|
return self.accountsSupportingTransferringMoney[selectedAccountIndex]
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.accountsSupportingTransferringMoney.first
|
return self.accountsSupportingTransferringMoney.first
|
||||||
}
|
}
|
||||||
|
|
||||||
private var supportsInstantPayment: Bool {
|
private var supportsInstantPayment: Bool {
|
||||||
return self.account?.supportsInstantPaymentMoneyTransfer ?? false
|
return self.account?.supportsInstantPaymentMoneyTransfer ?? false
|
||||||
}
|
}
|
||||||
|
@ -95,9 +104,12 @@ struct TransferMoneyDialog: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
UIKitTextField("Remittee Name", text: $remitteeName, focusOnStart: true, focusNextTextFieldOnReturnKeyPress: true, actionOnReturnKeyPress: handleReturnKeyPress) { newValue in
|
LabelledUIKitTextField(label: "Remittee Name", text: $remitteeName, focusOnStart: true, focusNextTextFieldOnReturnKeyPress: true,
|
||||||
self.isValidRemitteeNameEntered = self.remitteeName.isNotBlank
|
isFocussedChanged: validateRemitteeNameOnFocusLost, actionOnReturnKeyPress: handleReturnKeyPress, textChanged: validateRemitteeName)
|
||||||
}
|
|
||||||
|
remitteeNameValidationResult.map { validationError in
|
||||||
|
ValidationLabel(validationError)
|
||||||
|
}
|
||||||
|
|
||||||
if self.showRemitteeAutocompleteList {
|
if self.showRemitteeAutocompleteList {
|
||||||
Section {
|
Section {
|
||||||
|
@ -109,26 +121,26 @@ struct TransferMoneyDialog: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UIKitTextField("Remittee IBAN", text: $remitteeIban, focusNextTextFieldOnReturnKeyPress: true, actionOnReturnKeyPress: handleReturnKeyPress) { newValue in
|
LabelledUIKitTextField(label: "Remittee IBAN", text: $remitteeIban, focusNextTextFieldOnReturnKeyPress: true, isFocussedChanged: validateRemitteeIbanOnFocusLost,
|
||||||
self.isValidRemitteeIbanEntered = newValue.count > 14 // TODO: implement real check if IBAN is valid
|
actionOnReturnKeyPress: handleReturnKeyPress, textChanged: validateRemitteeIban)
|
||||||
self.tryToGetBicFromIban(newValue)
|
|
||||||
}
|
remitteeIbanValidationResult.map { validationError in
|
||||||
|
ValidationLabel(validationError)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
UIKitTextField("Amount", text: $amount, keyboardType: .decimalPad, focusNextTextFieldOnReturnKeyPress: true, actionOnReturnKeyPress: handleReturnKeyPress) { newValue in
|
LabelledUIKitTextField(label: "Amount", text: $amount, keyboardType: .decimalPad, focusNextTextFieldOnReturnKeyPress: true, actionOnReturnKeyPress: handleReturnKeyPress, textChanged: validateAmount)
|
||||||
// TODO: implement DecimalTextField / NumericTextField
|
|
||||||
let filtered = newValue.filter { "0123456789,".contains($0) }
|
amountValidationResult.map { validationError in
|
||||||
if filtered != newValue {
|
ValidationLabel(validationError)
|
||||||
self.amount = filtered
|
}
|
||||||
}
|
|
||||||
|
|
||||||
self.isValidAmountEntered = self.amount.isNotBlank
|
|
||||||
}
|
|
||||||
|
|
||||||
UIKitTextField("Usage", text: $usage, actionOnReturnKeyPress: handleReturnKeyPress) { newValue in
|
LabelledUIKitTextField(label: "Usage", text: $usage, actionOnReturnKeyPress: handleReturnKeyPress, textChanged: validateUsage)
|
||||||
self.isValidUsageEntered = true
|
|
||||||
}
|
usageValidationResult.map { validationError in
|
||||||
|
ValidationLabel(validationError)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if supportsInstantPayment {
|
if supportsInstantPayment {
|
||||||
|
@ -169,9 +181,36 @@ struct TransferMoneyDialog: View {
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func validateRemitteeName(enteredRemitteeName: String) {
|
||||||
|
validateField($remitteeName, $remitteeNameValidationResult, $isValidRemitteeNameEntered) {
|
||||||
|
inputValidator.validateRemitteeNameWhileTyping(remitteeNameToTest: remitteeName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func validateRemitteeNameOnFocusLost(_ isFocussed: Bool) {
|
||||||
|
if isFocussed == false {
|
||||||
|
validateField($remitteeName, $remitteeNameValidationResult, $isValidRemitteeNameEntered) {
|
||||||
|
inputValidator.validateRemitteeName(remitteeNameToTest: remitteeName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func tryToGetBicFromIban(_ enteredIban: String) {
|
private func validateRemitteeIban(_ enteredIban: String) {
|
||||||
|
validateField($remitteeIban, $remitteeIbanValidationResult, $isValidRemitteeIbanEntered) { inputValidator.validateIbanWhileTyping(ibanToTest: enteredIban) }
|
||||||
|
|
||||||
|
tryToGetBicFromIban(enteredIban)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func validateRemitteeIbanOnFocusLost(_ isFocussed: Bool) {
|
||||||
|
if isFocussed == false {
|
||||||
|
validateField($remitteeIban, $remitteeIbanValidationResult, $isValidRemitteeIbanEntered) { inputValidator.validateIban(ibanToTest: remitteeIban) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func tryToGetBicFromIban(_ enteredIban: String) {
|
||||||
let foundBank = presenter.findUniqueBankForIban(iban: enteredIban)
|
let foundBank = presenter.findUniqueBankForIban(iban: enteredIban)
|
||||||
|
|
||||||
if let foundBank = foundBank {
|
if let foundBank = foundBank {
|
||||||
|
@ -186,7 +225,50 @@ struct TransferMoneyDialog: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func isRequiredDataEntered() -> Bool {
|
private func validateAmount(_ enteredAmount: String) {
|
||||||
|
// TODO: implement DecimalTextField / NumericTextField
|
||||||
|
let filtered = enteredAmount.filter { "0123456789,".contains($0) }
|
||||||
|
if filtered != enteredAmount {
|
||||||
|
self.amount = filtered
|
||||||
|
|
||||||
|
return // don't validate field after non decimal character has been entered
|
||||||
|
}
|
||||||
|
|
||||||
|
if amount.isNotBlank {
|
||||||
|
validateField($amount, $amountValidationResult, $isValidAmountEntered) { inputValidator.validateAmount(enteredAmountString: enteredAmount) }
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
isValidAmountEntered = false
|
||||||
|
amountValidationResult = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func validateUsage(enteredUsage: String) {
|
||||||
|
validateField($usage, $usageValidationResult, $isValidUsageEntered) { inputValidator.validateUsage(usageToTest: enteredUsage) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func validateField(_ newValue: Binding<String>, _ validationResult: Binding<ValidationResult?>, _ isValidValueEntered: Binding<Bool>, _ validateValue: () -> ValidationResult) {
|
||||||
|
if (didJustCorrectEnteredValue == false) {
|
||||||
|
let fieldValidationResult = validateValue()
|
||||||
|
|
||||||
|
isValidValueEntered.wrappedValue = fieldValidationResult.validationSuccessfulOrCouldCorrectString
|
||||||
|
|
||||||
|
validationResult.wrappedValue = fieldValidationResult.didCorrectString || fieldValidationResult.validationSuccessful == false ? fieldValidationResult : nil
|
||||||
|
|
||||||
|
if (fieldValidationResult.didCorrectString) {
|
||||||
|
didJustCorrectEnteredValue = true
|
||||||
|
|
||||||
|
newValue.wrappedValue = fieldValidationResult.correctedInputString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
didJustCorrectEnteredValue = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func isRequiredDataEntered() -> Bool {
|
||||||
return account != nil
|
return account != nil
|
||||||
&& isValidRemitteeNameEntered
|
&& isValidRemitteeNameEntered
|
||||||
&& isValidRemitteeIbanEntered
|
&& isValidRemitteeIbanEntered
|
||||||
|
@ -195,15 +277,17 @@ struct TransferMoneyDialog: View {
|
||||||
&& isValidUsageEntered
|
&& isValidUsageEntered
|
||||||
}
|
}
|
||||||
|
|
||||||
func transferMoney() {
|
private func transferMoney() {
|
||||||
let data = TransferMoneyData(creditorName: remitteeName, creditorIban: remitteeIban, creditorBic: remitteeBic, amount: CommonBigDecimal(decimal: amount.replacingOccurrences(of: ",", with: ".")), usage: usage, instantPayment: instantPayment)
|
if let amount = inputValidator.convertAmountString(enteredAmountString: self.amount) {
|
||||||
|
let data = TransferMoneyData(creditorName: remitteeName, creditorIban: remitteeIban, creditorBic: remitteeBic, amount: amount, usage: usage, instantPayment: instantPayment)
|
||||||
presenter.transferMoneyAsync(bankAccount: account!, data: data) { response in
|
|
||||||
self.handleTransferMoneyResponse(data, response)
|
presenter.transferMoneyAsync(bankAccount: account!, data: data) { response in
|
||||||
|
self.handleTransferMoneyResponse(data, response)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleTransferMoneyResponse(_ data: TransferMoneyData, _ response: BankingClientResponse) {
|
private func handleTransferMoneyResponse(_ data: TransferMoneyData, _ response: BankingClientResponse) {
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
self.transferMoneyResponseMessage = Message(message: Text("Successfully transferred \(data.amount) \("€") to \(data.creditorName)"), primaryButton: .ok {
|
self.transferMoneyResponseMessage = Message(message: Text("Successfully transferred \(data.amount) \("€") to \(data.creditorName)"), primaryButton: .ok {
|
||||||
self.presentation.wrappedValue.dismiss()
|
self.presentation.wrappedValue.dismiss()
|
||||||
|
|
Loading…
Reference in New Issue