Implemented instant payment money transfer

This commit is contained in:
dankito 2020-05-13 02:38:33 +02:00
parent 8c6c65c7b0
commit bea45cb661
20 changed files with 96 additions and 21 deletions

View File

@ -867,6 +867,7 @@ open class FinTsClient @JvmOverloads constructor(
account.setSupportsFeature(AccountFeature.RetrieveAccountTransactions, messageBuilder.supportsGetTransactions(account))
account.setSupportsFeature(AccountFeature.RetrieveBalance, messageBuilder.supportsGetBalance(account))
account.setSupportsFeature(AccountFeature.TransferMoney, messageBuilder.supportsBankTransfer(account))
account.setSupportsFeature(AccountFeature.InstantPayment, messageBuilder.supportsSepaInstantPaymentBankTransfer(account))
}
protected open fun mapToTanProcedures(tanInfo: TanInfo): List<TanProcedure> {

View File

@ -12,7 +12,7 @@ import net.dankito.fints.messages.segmente.SegmentNumberGenerator
import net.dankito.fints.messages.segmente.Synchronisierung
import net.dankito.fints.messages.segmente.id.CustomerSegmentId
import net.dankito.fints.messages.segmente.implementierte.*
import net.dankito.fints.messages.segmente.implementierte.sepa.SepaEinzelueberweisung
import net.dankito.fints.messages.segmente.implementierte.sepa.SepaBankTransferBase
import net.dankito.fints.messages.segmente.implementierte.tan.TanGeneratorListeAnzeigen
import net.dankito.fints.messages.segmente.implementierte.tan.TanGeneratorTanMediumAnOderUmmelden
import net.dankito.fints.messages.segmente.implementierte.umsaetze.*
@ -199,16 +199,18 @@ open class MessageBuilder(protected val generator: ISegmentNumberGenerator = Seg
}
open fun createBankTransferMessage(bankTransferData: BankTransferData, account: AccountData, dialogContext: DialogContext): MessageBuilderResult {
open fun createBankTransferMessage(data: BankTransferData, account: AccountData, dialogContext: DialogContext): MessageBuilderResult {
val messageBuilderResultAndNullableUrn = supportsBankTransferAndSepaVersion(account)
val segmentId = if (data.instantPayment) CustomerSegmentId.SepaInstantPaymentBankTransfer else CustomerSegmentId.SepaBankTransfer
val messageBuilderResultAndNullableUrn = supportsBankTransferAndSepaVersion(account, segmentId)
val result = messageBuilderResultAndNullableUrn.first
val urn = messageBuilderResultAndNullableUrn.second
if (result.isJobVersionSupported && urn != null) {
val segments = listOf(
SepaEinzelueberweisung(generator.resetSegmentNumber(2), urn, dialogContext.customer, account, dialogContext.bank.bic, bankTransferData),
ZweiSchrittTanEinreichung(generator.getNextSegmentNumber(), TanProcess.TanProcess4, CustomerSegmentId.SepaBankTransfer)
SepaBankTransferBase(segmentId, generator.resetSegmentNumber(2), urn, dialogContext.customer, account, dialogContext.bank.bic, data),
ZweiSchrittTanEinreichung(generator.getNextSegmentNumber(), TanProcess.TanProcess4, segmentId)
)
return createMessageBuilderResult(dialogContext, segments)
@ -218,11 +220,15 @@ open class MessageBuilder(protected val generator: ISegmentNumberGenerator = Seg
}
open fun supportsBankTransfer(account: AccountData): Boolean {
return supportsBankTransferAndSepaVersion(account).first.isJobVersionSupported
return supportsBankTransferAndSepaVersion(account, CustomerSegmentId.SepaBankTransfer).first.isJobVersionSupported
}
protected open fun supportsBankTransferAndSepaVersion(account: AccountData): Pair<MessageBuilderResult, String?> {
val result = getSupportedVersionsOfJob(CustomerSegmentId.SepaBankTransfer, account, listOf(1))
open fun supportsSepaInstantPaymentBankTransfer(account: AccountData): Boolean {
return supportsBankTransferAndSepaVersion(account, CustomerSegmentId.SepaInstantPaymentBankTransfer).first.isJobVersionSupported
}
protected open fun supportsBankTransferAndSepaVersion(account: AccountData, segmentId: CustomerSegmentId): Pair<MessageBuilderResult, String?> {
val result = getSupportedVersionsOfJob(segmentId, account, listOf(1))
if (result.isJobVersionSupported) {

View File

@ -23,6 +23,8 @@ enum class CustomerSegmentId(override val id: String) : ISegmentId {
SepaBankTransfer("HKCCS"),
SepaInstantPaymentBankTransfer("HKIPZ"),
SepaAccountInfoParameters("HKSPA") // not implemented, retrieved automatically with UPD
}

View File

@ -6,7 +6,8 @@ import net.dankito.fints.model.BankTransferData
import net.dankito.fints.model.CustomerData
open class SepaEinzelueberweisung(
open class SepaBankTransferBase(
segmentId: CustomerSegmentId,
segmentNumber: Int,
sepaDescriptorUrn: String,
debitor: CustomerData,
@ -17,7 +18,7 @@ open class SepaEinzelueberweisung(
)
: SepaSegment(
segmentNumber,
CustomerSegmentId.SepaBankTransfer,
segmentId,
1,
sepaDescriptorUrn,
if (sepaDescriptorUrn.contains("pain.001.003.03", true)) "pain.001.003.03.xml" else "pain.001.001.03.xml",

View File

@ -7,6 +7,8 @@ enum class AccountFeature {
RetrieveBalance,
TransferMoney
TransferMoney,
InstantPayment
}

View File

@ -8,5 +8,6 @@ open class BankTransferData(
val creditorIban: String,
val creditorBic: String,
val amount: BigDecimal,
val usage: String
val usage: String,
val instantPayment: Boolean = false
)

View File

@ -1,5 +1,6 @@
package net.dankito.fints.messages.segmente.implementierte.sepa
import net.dankito.fints.messages.segmente.id.CustomerSegmentId
import net.dankito.fints.model.AccountData
import net.dankito.fints.model.BankTransferData
import net.dankito.fints.model.CustomerData
@ -7,7 +8,7 @@ import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
class SepaEinzelueberweisungTest {
class SepaBankTransferBaseTest {
companion object {
@ -28,7 +29,7 @@ class SepaEinzelueberweisungTest {
fun format_Pain001_001_03() {
// given
val underTest = SepaEinzelueberweisung(segmentNumber,
val underTest = SepaBankTransferBase(CustomerSegmentId.SepaBankTransfer, segmentNumber,
"urn:iso:std:iso:20022:tech:xsd:pain.001.001.03",
CustomerData("", "", "", debitorName),
AccountData("", null, 0, "", debitorIban, "", null, null, "", null, null, listOf()),
@ -50,7 +51,7 @@ class SepaEinzelueberweisungTest {
fun format_Pain001_003_03() {
// given
val underTest = SepaEinzelueberweisung(segmentNumber,
val underTest = SepaBankTransferBase(CustomerSegmentId.SepaBankTransfer, segmentNumber,
"urn:iso:std:iso:20022:tech:xsd:pain.001.003.03",
CustomerData("", "", "", debitorName),
AccountData("", null, 0, "", debitorIban, "", null, null, "", null, null, listOf()),

View File

@ -104,6 +104,7 @@ open class TransferMoneyDialog : DialogFragment() {
rootView.spnBankAccounts.adapter = adapter
rootView.spnBankAccounts.onItemSelectedListener = ListItemSelectedListener(adapter) { selectedBankAccount ->
this.bankAccount = selectedBankAccount
setInstantPaymentControlsVisibility(rootView)
}
preselectedBankAccount?.let { rootView.spnBankAccounts.setSelection(adapter.getItems().indexOf(it)) }
}
@ -138,11 +139,23 @@ open class TransferMoneyDialog : DialogFragment() {
val decimalSeparator = DecimalFormatSymbols.getInstance().getDecimalSeparator()
rootView.edtxtAmount.keyListener = DigitsKeyListener.getInstance("0123456789$decimalSeparator")
setInstantPaymentControlsVisibility(rootView)
rootView.btnCancel.setOnClickListener { dismiss() }
rootView.btnTransferMoney.setOnClickListener { transferMoney() }
}
private fun setInstantPaymentControlsVisibility(rootView: View) {
rootView.chkbxInstantPayment.visibility =
if (bankAccount.supportsInstantPaymentMoneyTransfer) {
View.VISIBLE
}
else {
View.GONE
}
}
private fun transferMoneyIfEnterPressed(editText: EditText) {
editText.addEnterPressedListener {
if (isRequiredDataEntered()) {
@ -221,7 +234,8 @@ open class TransferMoneyDialog : DialogFragment() {
edtxtRemitteeIban.text.toString().replace(" ", ""),
edtxtRemitteeBic.text.toString().replace(" ", ""),
amount,
edtxtUsage.text.toString()
edtxtUsage.text.toString(),
chkbxInstantPayment.isChecked
)
presenter.transferMoneyAsync(bankAccount, data) {

View File

@ -155,6 +155,18 @@
</com.google.android.material.textfield.TextInputLayout>
<CheckBox
android:id="@+id/chkbxInstantPayment"
android:layout_width="match_parent"
android:layout_height="30dp"
android:text="@string/dialog_transfer_money_instant_payment"
style="@style/TextAppearance.AppCompat.Medium"
android:layout_marginTop="@dimen/dialog_transfer_money_instant_payment_margin_top"
android:layout_marginBottom="@dimen/dialog_transfer_money_instant_payment_margin_bottom"
android:visibility="gone"
/>
<RelativeLayout
android:id="@+id/lytButtonBar"
android:layout_width="match_parent"

View File

@ -54,6 +54,7 @@
<string name="dialog_transfer_money_remittee_bic">BIC (wird automatisch eingetragen):</string>
<string name="dialog_transfer_money_amount">Betrag:</string>
<string name="dialog_transfer_money_usage">Verwendungszweck:</string>
<string name="dialog_transfer_money_instant_payment">Echtzeitüberweisung</string>
<string name="dialog_transfer_money_transfer">Überweisen</string>
<string name="dialog_transfer_money_message_transfer_successful">%1$s %2$s wurden erfolgreich an %3$s überwiesen.</string>
<string name="dialog_transfer_money_message_transfer_failed">Konnte nicht %1$s %2$s an %3$s überweisen.\n\nFehlermeldung Ihrer Bank:\n\n%4$s</string>

View File

@ -45,6 +45,8 @@
<dimen name="dialog_transfer_money_label_margin_right">4dp</dimen>
<dimen name="dialog_transfer_money_bank_accounts_margin_bottom">12dp</dimen>
<dimen name="dialog_transfer_money_autocomplete_fields_height">50dp</dimen>
<dimen name="dialog_transfer_money_instant_payment_margin_top">6dp</dimen>
<dimen name="dialog_transfer_money_instant_payment_margin_bottom">6dp</dimen>
<dimen name="dialog_transfer_money_buttons_width">120dp</dimen>
<dimen name="list_item_bank_account_padding">4dp</dimen>

View File

@ -54,6 +54,7 @@
<string name="dialog_transfer_money_remittee_bic">BIC (will be entered automatically):</string>
<string name="dialog_transfer_money_amount">Amount:</string>
<string name="dialog_transfer_money_usage">Usage:</string>
<string name="dialog_transfer_money_instant_payment">Instant payment</string>
<string name="dialog_transfer_money_transfer">Transfer</string>
<string name="dialog_transfer_money_message_transfer_successful">Successfully transferred %1$s %2$s to %3$s.</string>
<string name="dialog_transfer_money_message_transfer_failed">Could not transfer %1$s %2$s to %3$s.\n\nError message from your bank:\n\n%4$s</string>

View File

@ -61,6 +61,7 @@ transfer.money.dialog.remittee.bank.label=Bank:
transfer.money.dialog.remittee.bic.label=BIC:
transfer.money.dialog.amount.label=Amount:
transfer.money.dialog.usage.label=Usage:
transfer.money.dialog.instant.payment.label=Instant payment
transfer.money.dialog.transfer.money.label=Transfer
transfer.money.dialog.bank.name.will.be.entered.automatically=Will be entered automatically
transfer.money.dialog.bank.not.found.for.iban=No bank found for this IBAN

View File

@ -61,6 +61,7 @@ transfer.money.dialog.remittee.bank.label=Bank:
transfer.money.dialog.remittee.bic.label=BIC:
transfer.money.dialog.amount.label=Betrag:
transfer.money.dialog.usage.label=Verwendungszweck:
transfer.money.dialog.instant.payment.label=Echtzeitüberweisung
transfer.money.dialog.transfer.money.label=Überweisen
transfer.money.dialog.bank.name.will.be.entered.automatically=Wird automatisch eingetragen
transfer.money.dialog.bank.not.found.for.iban=Für diese IBAN wurde keine Bank gefunden

View File

@ -36,7 +36,7 @@ open class TransferMoneyDialog @JvmOverloads constructor(
}
protected val selectedBankAccount = SimpleObjectProperty<BankAccount>(preselectedBankAccount ?: presenter.bankAccounts.firstOrNull())
protected val selectedBankAccount = SimpleObjectProperty<BankAccount>(preselectedBankAccount ?: presenter.bankAccounts.firstOrNull { it.supportsTransferringMoney })
protected val remitteeName = SimpleStringProperty(preselectedValues?.creditorName ?: "")
@ -52,6 +52,10 @@ open class TransferMoneyDialog @JvmOverloads constructor(
protected val usage = SimpleStringProperty(preselectedValues?.usage ?: "")
protected val instantPayment = SimpleBooleanProperty(false)
protected val supportsInstantPayment = SimpleBooleanProperty(selectedBankAccount.value?.supportsInstantPaymentMoneyTransfer ?: false)
protected val requiredDataEntered = SimpleBooleanProperty(false)
@ -61,6 +65,8 @@ open class TransferMoneyDialog @JvmOverloads constructor(
init {
selectedBankAccount.addListener { _, _, newValue -> selectedBankAccountChanged(newValue) }
remitteeName.addListener { _, _, _ -> checkIfRequiredDataEnteredOnUiThread() }
remitteeIban.addListener { _, _, newValue -> tryToGetBicFromIban(newValue) }
remitteeBic.addListener { _, _, _ -> checkIfRequiredDataEnteredOnUiThread() }
@ -155,6 +161,16 @@ open class TransferMoneyDialog @JvmOverloads constructor(
fixedHeight = TextFieldHeight
}
}
field {
alignment = Pos.CENTER_LEFT
checkbox(messages["transfer.money.dialog.instant.payment.label"], instantPayment) {
fixedHeight = TextFieldHeight
enableWhen(supportsInstantPayment)
}
}
}
}
@ -194,6 +210,14 @@ open class TransferMoneyDialog @JvmOverloads constructor(
}
private fun selectedBankAccountChanged(newValue: BankAccount?) {
supportsInstantPayment.value = newValue?.supportsInstantPaymentMoneyTransfer ?: false
if (supportsInstantPayment.value == false) {
instantPayment.value = false
}
}
protected open fun tryToGetBicFromIban(enteredIban: String) {
presenter.findUniqueBankForIbanAsync(enteredIban) { foundBank ->
runLater {
@ -249,7 +273,8 @@ open class TransferMoneyDialog @JvmOverloads constructor(
remitteeIban.value.replace(" ", ""),
remitteeBic.value.replace(" ", ""),
amount.value.toBigDecimal(),
usage.value
usage.value,
instantPayment.value
)
presenter.transferMoneyAsync(bankAccount, data) {

View File

@ -19,6 +19,7 @@ open class BankAccount @JvmOverloads constructor(
var supportsRetrievingAccountTransactions: Boolean = false,
var supportsRetrievingBalance: Boolean = false,
var supportsTransferringMoney: Boolean = false,
var supportsInstantPaymentMoneyTransfer: Boolean = false,
bookedAccountTransactions: List<AccountTransaction> = listOf()
) {

View File

@ -9,7 +9,8 @@ open class TransferMoneyData(
val creditorIban: String,
val creditorBic: String,
val amount: BigDecimal,
val usage: String
val usage: String,
val instantPayment: Boolean = false
) {
companion object {

View File

@ -119,7 +119,7 @@ open class fints4javaBankingClient(
callback(BankingClientResponse(false, "Cannot find account for ${bankAccount.identifier}")) // TODO: translate
}
else {
val mappedData = BankTransferData(data.creditorName, data.creditorIban, data.creditorBic, data.amount, data.usage)
val mappedData = BankTransferData(data.creditorName, data.creditorIban, data.creditorBic, data.amount, data.usage, data.instantPayment)
client.doBankTransferAsync(mappedData, account) { response ->
saveData()

View File

@ -105,7 +105,8 @@ open class fints4javaModelMapper {
return BankAccount(account, accountData.accountIdentifier, accountData.accountHolderName, accountData.iban,
accountData.subAccountAttribute, BigDecimal.ZERO, accountData.currency ?: "EUR",
mapBankAccountType(accountData.accountType), accountData.supportsFeature(AccountFeature.RetrieveAccountTransactions),
accountData.supportsFeature(AccountFeature.RetrieveBalance), accountData.supportsFeature(AccountFeature.TransferMoney))
accountData.supportsFeature(AccountFeature.RetrieveBalance), accountData.supportsFeature(AccountFeature.TransferMoney),
accountData.supportsFeature(AccountFeature.InstantPayment))
}
open fun mapBankAccountType(type: AccountType?): BankAccountType {

View File

@ -266,6 +266,7 @@ open class hbci4jBankingClient(
}
protected open fun createTransferCashJob(handle: HBCIHandler, data: TransferMoneyData, bankAccount: BankAccount) {
// TODO: implement instant payment
val transferCashJob = handle.newJob("UebSEPA")
val source = mapper.mapToKonto(bank, bankAccount)