Implemented transferMoney()

This commit is contained in:
dankito 2022-02-23 01:43:41 +01:00
parent 6512f45955
commit 8671bf058d
17 changed files with 264 additions and 25 deletions

View File

@ -2,10 +2,7 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.dankito.banking.client.model.AccountTransaction
import net.dankito.banking.fints.model.TanChallenge
import react.RBuilder
import react.RComponent
import react.Props
import react.State
import react.*
import react.dom.*
import styled.styledDiv
@ -13,7 +10,8 @@ external interface AccountTransactionsViewProps : Props {
var presenter: Presenter
}
data class AccountTransactionsViewState(val balance: String, val transactions: Collection<AccountTransaction>, val enterTanChallenge: TanChallenge? = null) : State
data class AccountTransactionsViewState(val balance: String, val transactions: Collection<AccountTransaction>, var showTransferMoneyView: Boolean = false,
val enterTanChallenge: TanChallenge? = null) : State
@JsExport
class AccountTransactionsView(props: AccountTransactionsViewProps) : RComponent<AccountTransactionsViewProps, AccountTransactionsViewState>(props) {
@ -22,7 +20,7 @@ class AccountTransactionsView(props: AccountTransactionsViewProps) : RComponent<
init {
state = AccountTransactionsViewState("", listOf())
props.presenter.enterTanCallback = { setState(AccountTransactionsViewState(state.balance, state.transactions, it)) }
props.presenter.enterTanCallback = { setState(AccountTransactionsViewState(state.balance, state.transactions, state.showTransferMoneyView, it)) }
// due to CORS your bank's servers can not be requested directly from browser -> set a CORS proxy url in main.kt
// TODO: set your credentials here
@ -47,6 +45,21 @@ class AccountTransactionsView(props: AccountTransactionsViewProps) : RComponent<
}
}
button {
span { +"Transfer Money" }
attrs {
onMouseUp = { setState { showTransferMoneyView = !showTransferMoneyView } }
}
}
if (state.showTransferMoneyView) {
child(TransferMoneyView::class) {
attrs {
presenter = props.presenter
}
}
}
p {
+"Saldo: ${state.balance}"
}

View File

@ -3,12 +3,12 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.dankito.banking.client.model.parameter.GetAccountDataParameter
import net.dankito.banking.client.model.parameter.TransferMoneyParameter
import net.dankito.banking.client.model.response.GetAccountDataResponse
import net.dankito.banking.client.model.response.TransferMoneyResponse
import net.dankito.banking.fints.FinTsClient
import net.dankito.banking.fints.callback.SimpleFinTsClientCallback
import net.dankito.banking.fints.model.TanChallenge
import net.dankito.banking.fints.model.TanMethod
import net.dankito.banking.fints.model.TanMethodType
import net.dankito.banking.fints.model.*
import net.dankito.banking.fints.webclient.KtorWebClient
import net.dankito.banking.fints.webclient.ProxyingWebClient
import net.dankito.utils.multiplatform.log.LoggerFactory
@ -44,4 +44,23 @@ open class Presenter {
}
}
open fun transferMoney(recipientName: String, recipientAccountIdentifier: String, recipientBankIdentifier: String?, reference: String?,
amount: String, instantPayment: Boolean = false, response: (TransferMoneyResponse) -> Unit) {
GlobalScope.launch(Dispatchers.Unconfined) {
// TODO: set your credentials here
val transferMoneyResponse = fintsClient.transferMoneyAsync(TransferMoneyParameter("", "", "", null, recipientName,
recipientAccountIdentifier, recipientBankIdentifier, Money(Amount(amount), Currency.DefaultCurrencyCode), reference, instantPayment))
if (transferMoneyResponse.successful) {
log.info("Successfully transferred $amount to $recipientName")
} else {
log.error("Could not transfer $amount to $recipientName: ${transferMoneyResponse.error} ${transferMoneyResponse.errorMessage}")
}
withContext(Dispatchers.Main) {
response(transferMoneyResponse)
}
}
}
}

View File

@ -0,0 +1,26 @@
import react.*
import react.dom.html.ReactHTML.div
import react.dom.html.ReactHTML.input
import react.dom.html.ReactHTML.span
external interface TextInputFieldProps : Props {
var label: String
var valueChanged: (String) -> Unit
}
val TextInputField = FC<TextInputFieldProps> { props ->
div {
span { +"${props.label}: " }
input {
type = react.dom.html.InputType.text
onChange = { event ->
val enteredValue = event.target.value
props.valueChanged(enteredValue)}
}
}
}

View File

@ -0,0 +1,56 @@
import react.*
import react.dom.*
external interface TransferMoneyViewProps : Props {
var presenter: Presenter
}
class TransferMoneyViewState(var recipientName: String = "", var recipientAccountIdentifier: String = "", var recipientBankIdentifier: String? = null,
var reference: String = "", var amount: String = "", var instantPayment: Boolean = false) : State
@JsExport
class TransferMoneyView(props: TransferMoneyViewProps) : RComponent<TransferMoneyViewProps, TransferMoneyViewState>(props) {
override fun RBuilder.render() {
div {
TextInputField {
attrs {
label = "Recipient name"
valueChanged = { newValue -> setState { recipientName = newValue } }
}
}
TextInputField {
attrs {
label = "IBAN"
valueChanged = { newValue -> setState { recipientAccountIdentifier = newValue } }
}
}
TextInputField {
attrs {
label = "Amount"
valueChanged = { newValue -> setState { amount = newValue } }
}
}
TextInputField {
attrs {
label = "Reference"
valueChanged = { newValue -> setState { reference = newValue } }
}
}
button {
span { +"Transfer" }
attrs {
onMouseUp = {
props.presenter.transferMoney(state.recipientName, state.recipientAccountIdentifier, state.recipientBankIdentifier, state.reference, state.amount, state.instantPayment) { } }
}
}
}
}
}

View File

@ -1,9 +1,5 @@
import kotlinx.browser.document
import kotlinx.browser.window
import net.dankito.banking.fints.FinTsClientDeprecated
import net.dankito.banking.fints.callback.SimpleFinTsClientCallback
import net.dankito.banking.fints.webclient.KtorWebClient
import net.dankito.banking.fints.webclient.ProxyingWebClient
import react.dom.render
fun main() {

View File

@ -0,0 +1,40 @@
package net.dankito.banking.client.model.parameter
import net.dankito.banking.client.model.BankAccountIdentifier
import net.dankito.banking.fints.model.BankData
import net.dankito.banking.fints.model.Money
import net.dankito.banking.fints.model.TanMethodType
open class TransferMoneyParameter(
bankCode: String,
loginName: String,
password: String,
/**
* The account from which the money should be withdrawn.
* If not specified fints4k retrieves all bank accounts and checks if there is exactly one that supports money transfer.
* If no or more than one bank account supports money transfer, the error codes NoAccountSupportsMoneyTransfer or MoreThanOneAccountSupportsMoneyTransfer are returned.
*/
open val remittanceAccount: BankAccountIdentifier? = null,
open val recipientName: String,
/**
* The identifier of recipient's account. In most cases the IBAN.
*/
open val recipientAccountIdentifier: String,
/**
* The identifier of recipient's bank. In most cases the BIC.
* Can be omitted for German banks as the BIC can be derived from IBAN.
*/
open val recipientBankIdentifier: String? = null,
open val amount: Money,
open val reference: String? = null,
open val instantPayment: Boolean = false,
preferredTanMethods: List<TanMethodType>? = null,
preferredTanMedium: String? = null,
abortIfTanIsRequired: Boolean = false,
finTsModel: BankData? = null
) : FinTsClientParameter(bankCode, loginName, password, preferredTanMethods, preferredTanMedium, abortIfTanIsRequired, finTsModel)

View File

@ -21,6 +21,10 @@ enum class ErrorCode {
NoneOfTheAccountsSupportsRetrievingData,
DidNotRetrieveAllAccountData
DidNotRetrieveAllAccountData,
NoAccountSupportsMoneyTransfer,
MoreThanOneAccountSupportsMoneyTransfer
}

View File

@ -15,4 +15,7 @@ open class FinTsClientResponse(
open val successful: Boolean
get() = error == null
open val errorCodeAndMessage: String
get() = "$error${errorMessage?.let { " $it" }}}"
}

View File

@ -0,0 +1,12 @@
package net.dankito.banking.client.model.response
import net.dankito.banking.fints.model.BankData
import net.dankito.banking.fints.model.MessageLogEntry
open class TransferMoneyResponse(
error: ErrorCode?,
errorMessage: String?,
messageLogWithoutSensitiveData: List<MessageLogEntry>,
finTsModel: BankData? = null
) : FinTsClientResponse(error, errorMessage, messageLogWithoutSensitiveData, finTsModel)

View File

@ -1,12 +1,15 @@
package net.dankito.banking.fints
import kotlinx.datetime.LocalDate
import net.dankito.banking.client.model.parameter.FinTsClientParameter
import net.dankito.banking.fints.callback.FinTsClientCallback
import net.dankito.banking.fints.model.*
import net.dankito.banking.client.model.parameter.GetAccountDataParameter
import net.dankito.banking.client.model.parameter.RetrieveTransactions
import net.dankito.banking.client.model.parameter.TransferMoneyParameter
import net.dankito.banking.client.model.response.ErrorCode
import net.dankito.banking.client.model.response.GetAccountDataResponse
import net.dankito.banking.client.model.response.TransferMoneyResponse
import net.dankito.banking.fints.mapper.FinTsModelMapper
import net.dankito.banking.fints.response.client.FinTsClientResponse
import net.dankito.banking.fints.response.client.GetAccountInfoResponse
@ -109,7 +112,52 @@ open class FinTsClient @JvmOverloads constructor(
return LocalDate.todayAtEuropeBerlin().minusDays(90)
}
protected open suspend fun getAccountInfo(param: GetAccountDataParameter, bank: BankData): GetAccountInfoResponse {
open suspend fun transferMoneyAsync(param: TransferMoneyParameter): TransferMoneyResponse {
val finTsServerAddress = finTsServerAddressFinder.findFinTsServerAddress(param.bankCode)
if (finTsServerAddress.isNullOrBlank()) {
return TransferMoneyResponse(ErrorCode.BankDoesNotSupportFinTs3, "Either bank does not FinTS 3.0 or we don't know its FinTS server address", listOf(), null)
}
val bank = BankData(param.bankCode, param.loginName, param.password, finTsServerAddress, "")
val remittanceAccount = param.remittanceAccount
if (remittanceAccount == null) { // then first retrieve customer's bank accounts
val getAccountInfoResponse = getAccountInfo(param, bank)
if (getAccountInfoResponse.successful == false) {
return TransferMoneyResponse(mapper.mapErrorCode(getAccountInfoResponse), mapper.mapErrorMessages(getAccountInfoResponse),
getAccountInfoResponse.messageLogWithoutSensitiveData, bank)
} else {
return transferMoneyAsync(param, getAccountInfoResponse.bank, getAccountInfoResponse.bank.accounts, getAccountInfoResponse)
}
} else {
return transferMoneyAsync(param, bank, listOf(mapper.mapToAccountData(remittanceAccount, param)), null)
}
}
protected open suspend fun transferMoneyAsync(param: TransferMoneyParameter, bank: BankData, accounts: List<AccountData>, previousJobResponse: FinTsClientResponse?): TransferMoneyResponse {
val accountsSupportingTransfer = accounts.filter { it.supportsTransferringMoney }
if (accountsSupportingTransfer.isEmpty()) {
return TransferMoneyResponse(ErrorCode.NoAccountSupportsMoneyTransfer, "None of the accounts $accounts supports money transfer", previousJobResponse?.messageLogWithoutSensitiveData ?: listOf(), bank)
} else if (accountsSupportingTransfer.size > 1) {
return TransferMoneyResponse(ErrorCode.MoreThanOneAccountSupportsMoneyTransfer, "More than one of the accounts $accountsSupportingTransfer supports money transfer, so we cannot clearly determine which one to use for this transfer", previousJobResponse?.messageLogWithoutSensitiveData ?: listOf(), bank)
}
val recipientBankIdentifier = param.recipientBankIdentifier ?: "" // TODO: determine BIC from recipientBankCode if it's a German bank
val context = JobContext(JobContextType.TransferMoney, this.callback, product, bank, accountsSupportingTransfer.first())
val response = jobExecutor.transferMoneyAsync(context, BankTransferData(param.recipientName, param.recipientAccountIdentifier, recipientBankIdentifier, param.amount, param.reference, param.instantPayment))
return TransferMoneyResponse(mapper.mapErrorCode(response), mapper.mapErrorMessages(response), mapper.mergeMessageLog(previousJobResponse, response), bank)
}
protected open suspend fun getAccountInfo(param: FinTsClientParameter, bank: BankData): GetAccountInfoResponse {
param.finTsModel?.let {
// TODO: implement
// return GetAccountInfoResponse(it)
}
val context = JobContext(JobContextType.AddAccount, this.callback, product, bank) // TODO: add / change JobContextType
/* First dialog: Get user's basic data like BPD, customer system ID and her TAN methods */

View File

@ -10,9 +10,7 @@ import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.*
import net.dankito.banking.fints.model.*
import net.dankito.banking.fints.response.BankResponse
import net.dankito.banking.fints.response.client.*
import net.dankito.banking.fints.response.segments.*
import net.dankito.banking.fints.webclient.IWebClient
import kotlin.jvm.JvmOverloads
/**
@ -166,7 +164,7 @@ open class FinTsClientDeprecated(
open suspend fun doBankTransferAsync(bankTransferData: BankTransferData, bank: BankData, account: AccountData): FinTsClientResponse {
val context = JobContext(JobContextType.TransferMoney, this.callback, product, bank, account)
return jobExecutor.doBankTransferAsync(context, bankTransferData)
return jobExecutor.transferMoneyAsync(context, bankTransferData)
}
}

View File

@ -342,7 +342,7 @@ open class FinTsJobExecutor(
}
open suspend fun doBankTransferAsync(context: JobContext, bankTransferData: BankTransferData): FinTsClientResponse {
open suspend fun transferMoneyAsync(context: JobContext, bankTransferData: BankTransferData): FinTsClientResponse {
val response = sendMessageInNewDialogAndHandleResponse(context, null, true) {
val updatedAccount = getUpdatedAccount(context, context.account!!)

View File

@ -2,6 +2,7 @@ package net.dankito.banking.fints.mapper
import net.dankito.banking.client.model.*
import net.dankito.banking.client.model.AccountTransaction
import net.dankito.banking.client.model.parameter.FinTsClientParameter
import net.dankito.banking.client.model.parameter.GetAccountDataParameter
import net.dankito.banking.client.model.response.ErrorCode
import net.dankito.banking.fints.messages.datenelemente.abgeleiteteformate.Laenderkennzeichen
@ -13,13 +14,15 @@ import net.dankito.banking.fints.response.segments.AccountType
open class FinTsModelMapper {
open fun mapToAccountData(credentials: BankAccountIdentifier, param: GetAccountDataParameter): AccountData {
open fun mapToAccountData(credentials: BankAccountIdentifier, param: FinTsClientParameter): AccountData {
val accountData = AccountData(credentials.identifier, credentials.subAccountNumber, Laenderkennzeichen.Germany, param.bankCode,
credentials.iban, param.loginName, null, null, "", null, null, listOf(), listOf())
// TODO: where to know from if account supports retrieving balance and transactions?
accountData.setSupportsFeature(AccountFeature.RetrieveBalance, true)
accountData.setSupportsFeature(AccountFeature.RetrieveAccountTransactions, true)
accountData.setSupportsFeature(AccountFeature.TransferMoney, true)
accountData.setSupportsFeature(AccountFeature.RealTimeTransfer, true)
return accountData
}

View File

@ -32,7 +32,7 @@ open class SepaBankTransferBase(
"RecipientIban" to data.recipientAccountId.replace(" ", ""),
"RecipientBic" to data.recipientBankCode.replace(" ", ""),
"Amount" to data.amount.amount.string.replace(',', '.'), // TODO: check if ',' or '.' should be used as decimal separator
"Reference" to if (data.reference.isEmpty()) " " else messageCreator.convertDiacriticsAndReservedXmlCharacters(data.reference),
"Reference" to if (data.reference.isNullOrBlank()) " " else messageCreator.convertDiacriticsAndReservedXmlCharacters(data.reference),
"RequestedExecutionDate" to RequestedExecutionDateValueForNotScheduledTransfers
),
messageCreator

View File

@ -6,6 +6,6 @@ open class BankTransferData(
val recipientAccountId: String,
val recipientBankCode: String,
val amount: Money,
val reference: String,
val reference: String?,
val realTimeTransfer: Boolean = false
)

View File

@ -2,26 +2,29 @@ import kotlinx.datetime.LocalDate
import net.dankito.banking.client.model.AccountTransaction
import net.dankito.banking.client.model.CustomerAccount
import net.dankito.banking.client.model.parameter.GetAccountDataParameter
import net.dankito.banking.client.model.parameter.TransferMoneyParameter
import net.dankito.banking.fints.FinTsClient
import net.dankito.banking.fints.callback.SimpleFinTsClientCallback
import net.dankito.banking.fints.getAccountData
import net.dankito.banking.fints.model.TanChallenge
import net.dankito.banking.fints.transferMoney
import net.dankito.utils.multiplatform.extensions.*
class NativeApp {
private val client = FinTsClient(SimpleFinTsClientCallback { tanChallenge -> enterTan(tanChallenge) })
fun retrieveAccountData(bankCode: String, loginName: String, password: String) {
retrieveAccountData(GetAccountDataParameter(bankCode, loginName, password))
}
fun retrieveAccountData(param: GetAccountDataParameter) {
val client = FinTsClient(SimpleFinTsClientCallback { tanChallenge -> enterTan(tanChallenge) })
val response = client.getAccountData(param)
if (response.error != null) {
println("An error occurred: ${response.error}${response.errorMessage?.let { " $it" }}")
println("An error occurred: ${response.errorCodeAndMessage}")
}
response.customerAccount?.let { account ->
@ -32,6 +35,17 @@ class NativeApp {
}
fun transferMoney(param: TransferMoneyParameter) {
val response = client.transferMoney(param)
if (response.error != null) {
println("Could not transfer ${param.amount} to ${param.recipientName}: ${response.errorCodeAndMessage}")
} else {
println("Successfully transferred ${param.amount} to ${param.recipientName}")
}
}
private fun enterTan(tanChallenge: TanChallenge) {
println("A TAN is required:")
println(tanChallenge.messageToShowToUser)

View File

@ -2,7 +2,9 @@ package net.dankito.banking.fints
import kotlinx.coroutines.runBlocking
import net.dankito.banking.client.model.parameter.GetAccountDataParameter
import net.dankito.banking.client.model.parameter.TransferMoneyParameter
import net.dankito.banking.client.model.response.GetAccountDataResponse
import net.dankito.banking.client.model.response.TransferMoneyResponse
fun FinTsClient.getAccountData(bankCode: String, loginName: String, password: String): GetAccountDataResponse {
@ -11,4 +13,9 @@ fun FinTsClient.getAccountData(bankCode: String, loginName: String, password: St
fun FinTsClient.getAccountData(param: GetAccountDataParameter): GetAccountDataResponse {
return runBlocking { getAccountDataAsync(param) }
}
fun FinTsClient.transferMoney(param: TransferMoneyParameter): TransferMoneyResponse {
return runBlocking { transferMoneyAsync(param) }
}