Implemented transferMoneyAsync()

This commit is contained in:
dankito 2024-09-05 22:54:32 +02:00
parent 675066c216
commit c35026bfcc
9 changed files with 233 additions and 16 deletions

View File

@ -3,9 +3,11 @@ package net.codinux.banking.client
import net.codinux.banking.client.model.*
import net.codinux.banking.client.model.options.RetrieveTransactions
import net.codinux.banking.client.model.request.GetAccountDataRequest
import net.codinux.banking.client.model.request.TransferMoneyRequestForUser
import net.codinux.banking.client.model.response.GetAccountDataResponse
import net.codinux.banking.client.model.response.GetTransactionsResponse
import net.codinux.banking.client.model.response.Response
import net.codinux.banking.client.model.response.TransferMoneyResponse
interface BankingClient {
@ -39,4 +41,11 @@ interface BankingClient {
*/
suspend fun updateAccountTransactionsAsync(user: UserAccount, accounts: List<BankAccount>? = null): Response<List<GetTransactionsResponse>>
suspend fun transferMoneyAsync(bankCode: String, loginName: String, password: String, recipientName: String,
recipientAccountIdentifier: String, amount: Amount, paymentReference: String? = null) =
transferMoneyAsync(TransferMoneyRequestForUser(bankCode, loginName, password, null, recipientName, recipientAccountIdentifier, null, amount, paymentReference = paymentReference))
suspend fun transferMoneyAsync(request: TransferMoneyRequestForUser): Response<TransferMoneyResponse>
}

View File

@ -1,11 +1,14 @@
package net.codinux.banking.client
import net.codinux.banking.client.model.Amount
import net.codinux.banking.client.model.BankAccount
import net.codinux.banking.client.model.options.GetAccountDataOptions
import net.codinux.banking.client.model.options.RetrieveTransactions
import net.codinux.banking.client.model.request.TransferMoneyRequest
import net.codinux.banking.client.model.response.GetAccountDataResponse
import net.codinux.banking.client.model.response.GetTransactionsResponse
import net.codinux.banking.client.model.response.Response
import net.codinux.banking.client.model.response.TransferMoneyResponse
interface BankingClientForUser {
@ -37,6 +40,11 @@ interface BankingClientForUser {
* Updates account's transactions beginning from [BankAccount.lastTransactionsRetrievalTime].
* This may requires TAN if [BankAccount.lastTransactionsRetrievalTime] is older than 90 days.
*/
suspend fun updateAccountTransactionsAsync(): Response<List<GetTransactionsResponse>>
suspend fun updateAccountTransactionsAsync(accounts: List<BankAccount>? = null): Response<List<GetTransactionsResponse>>
suspend fun transferMoneyAsync(recipientName: String, recipientAccountIdentifier: String, amount: Amount, paymentReference: String? = null): Response<TransferMoneyResponse>
suspend fun transferMoneyAsync(request: TransferMoneyRequest): Response<TransferMoneyResponse>
}

View File

@ -1,9 +1,13 @@
package net.codinux.banking.client
import net.codinux.banking.client.model.AccountCredentials
import net.codinux.banking.client.model.Amount
import net.codinux.banking.client.model.BankAccount
import net.codinux.banking.client.model.UserAccount
import net.codinux.banking.client.model.options.GetAccountDataOptions
import net.codinux.banking.client.model.request.GetAccountDataRequest
import net.codinux.banking.client.model.request.TransferMoneyRequest
import net.codinux.banking.client.model.request.TransferMoneyRequestForUser
import net.codinux.banking.client.model.response.GetTransactionsResponse
import net.codinux.banking.client.model.response.Response
@ -21,7 +25,14 @@ abstract class BankingClientForUserBase(
}
}
override suspend fun updateAccountTransactionsAsync(): Response<List<GetTransactionsResponse>> =
client.updateAccountTransactionsAsync(user)
override suspend fun updateAccountTransactionsAsync(accounts: List<BankAccount>?): Response<List<GetTransactionsResponse>> =
client.updateAccountTransactionsAsync(user, accounts)
override suspend fun transferMoneyAsync(recipientName: String, recipientAccountIdentifier: String, amount: Amount, paymentReference: String?) =
transferMoneyAsync(TransferMoneyRequest(null, recipientName, recipientAccountIdentifier, null, amount, paymentReference = paymentReference))
override suspend fun transferMoneyAsync(request: TransferMoneyRequest) =
client.transferMoneyAsync(TransferMoneyRequestForUser(user.bankCode, user.loginName, user.password!!, request))
}

View File

@ -1,32 +1,58 @@
package net.codinux.banking.client
import kotlinx.coroutines.runBlocking
import net.codinux.banking.client.model.Amount
import net.codinux.banking.client.model.BankAccount
import net.codinux.banking.client.model.UserAccount
import net.codinux.banking.client.model.options.GetAccountDataOptions
import net.codinux.banking.client.model.request.GetAccountDataRequest
import net.codinux.banking.client.model.request.TransferMoneyRequest
import net.codinux.banking.client.model.request.TransferMoneyRequestForUser
/* BankingClient */
fun BankingClient.getAccountData(bankCode: String, loginName: String, password: String) = runBlocking {
this@getAccountData.getAccountDataAsync(bankCode, loginName, password)
getAccountDataAsync(bankCode, loginName, password)
}
fun BankingClient.getAccountData(request: GetAccountDataRequest) = runBlocking {
this@getAccountData.getAccountDataAsync(request)
getAccountDataAsync(request)
}
fun BankingClient.updateAccountTransactions(user: UserAccount, accounts: List<BankAccount>? = null) = runBlocking {
this@updateAccountTransactions.updateAccountTransactionsAsync(user, accounts)
updateAccountTransactionsAsync(user, accounts)
}
fun BankingClient.transferMoney(bankCode: String, loginName: String, password: String, recipientName: String,
recipientAccountIdentifier: String, amount: Amount, paymentReference: String? = null) = runBlocking {
transferMoneyAsync(bankCode, loginName, password, recipientName, recipientAccountIdentifier, amount, paymentReference)
}
fun BankingClient.transferMoney(request: TransferMoneyRequestForUser) = runBlocking {
transferMoneyAsync(request)
}
/* BankingClientForUser */
fun BankingClientForUser.getAccountData() = runBlocking {
this@getAccountData.getAccountDataAsync()
getAccountDataAsync()
}
fun BankingClientForUser.getAccountData(options: GetAccountDataOptions) = runBlocking {
this@getAccountData.getAccountDataAsync(options)
getAccountDataAsync(options)
}
fun BankingClientForUser.updateAccountTransactions() = runBlocking {
this@updateAccountTransactions.updateAccountTransactionsAsync()
updateAccountTransactionsAsync()
}
fun BankingClientForUser.transferMoney(recipientName: String, recipientAccountIdentifier: String, amount: Amount, paymentReference: String? = null) = runBlocking {
transferMoneyAsync(recipientName, recipientAccountIdentifier, amount, paymentReference)
}
fun BankingClientForUser.transferMoney(request: TransferMoneyRequest) = runBlocking {
transferMoneyAsync(request)
}

View File

@ -0,0 +1,76 @@
package net.codinux.banking.client.model.request
import net.codinux.banking.client.model.Amount
import net.codinux.banking.client.model.BankAccountIdentifier
import net.codinux.banking.client.model.config.NoArgConstructor
import net.codinux.banking.client.model.tan.TanMethodType
@NoArgConstructor
open class TransferMoneyRequest(
/* Sender settings */
/**
* The account from which the money should be withdrawn.
* If not specified client 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 senderAccount: BankAccountIdentifier? = null,
/* Recipient settings */
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,
/* Transfer data */
open val amount: Amount,
open val currency: String = "EUR",
/**
* The purpose of payment. An optional value that tells the reason for the transfer.
*
* May not be longer than 140 characters. Some characters are forbidden (TODO: add reference of forbidden characters).
*/
open val paymentReference: String? = null, // Alternativ: Purpose of payment
/**
* If transfer should be executed as 'real-time transfer', that is the money is in less than 10 seconds
* transferred to the account of the recipient.
*
* May costs extra fees.
*
* Not supported by all sender and recipient banks.
*/
open val instantTransfer: Boolean = false, // Alternativ: Instant payment ("Instant payment" ist ebenfalls weit verbreitet und wird oft im Kontext von digitalen Zahlungen verwendet, bei denen die Zahlung in Echtzeit erfolgt. Es kann jedoch breiter gefasst sein und umfasst nicht nur Banktransfers, sondern auch andere Arten von Sofortzahlungen (z.B. mobile Zahlungen).)
/**
* Specifies which [TanMethodType] should be preferred when having to choose between multiple available for user
* without requesting the user to choose one.
*
* By default we don't ask the user which TanMethod she prefers but choose one that could match best. If she really
* likes to use a different one, she can select another one in EnterTanDialog.
*
* By default we prefer non visual TanMethods (like AppTan and SMS) over image based TanMethods (like QR-code and
* photoTan) and exclude ChipTanUsb, which is not supported by application, and Flickercode, which is hard to
* implement and therefore most applications have not implemented.
*
* Console apps can only handle non visual TanMethods.
* But also graphical applications prefer non visual TanMethods as then they only have to display a text field to input
* TAN, and then image based TanMethods as then they additionally only have to display an image.
*/
val preferredTanMethods: List<TanMethodType>? = TanMethodType.NonVisualOrImageBased
) {
override fun toString() = "$amount to $recipientName - $paymentReference"
}

View File

@ -0,0 +1,47 @@
package net.codinux.banking.client.model.request
import net.codinux.banking.client.model.Amount
import net.codinux.banking.client.model.BankAccountIdentifier
import net.codinux.banking.client.model.config.NoArgConstructor
import net.codinux.banking.client.model.tan.TanMethodType
/**
* For documentation see [TransferMoneyRequest].
*/
@NoArgConstructor
open class TransferMoneyRequestForUser(
/* Sender settings */
val bankCode: String,
val loginName: String,
val password: String,
senderAccount: BankAccountIdentifier? = null,
/* Recipient settings */
recipientName: String,
recipientAccountIdentifier: String,
recipientBankIdentifier: String? = null,
/* Transfer data */
amount: Amount,
currency: String = "EUR",
paymentReference: String? = null,
instantTransfer: Boolean = false,
preferredTanMethods: List<TanMethodType>? = TanMethodType.NonVisualOrImageBased
) : TransferMoneyRequest(senderAccount, recipientName, recipientAccountIdentifier, recipientBankIdentifier, amount, currency, paymentReference, instantTransfer, preferredTanMethods) {
constructor(bankCode: String, loginName: String, password: String, request: TransferMoneyRequest)
: this(bankCode, loginName, password, request.senderAccount, request.recipientName, request.recipientAccountIdentifier, request.recipientBankIdentifier,
request.amount, request.currency, request.paymentReference, request.instantTransfer, request.preferredTanMethods)
override fun toString() = "$bankCode $loginName ${super.toString()}"
}

View File

@ -0,0 +1,9 @@
package net.codinux.banking.client.model.response
import net.codinux.banking.client.model.config.NoArgConstructor
/**
* Transfer money process does not return any data, only if successful or not (and in latter case an error message).
*/
@NoArgConstructor
open class TransferMoneyResponse

View File

@ -7,6 +7,7 @@ import net.codinux.banking.client.model.BankAccountFeatures
import net.codinux.banking.client.model.UserAccount
import net.codinux.banking.client.model.options.GetAccountDataOptions
import net.codinux.banking.client.model.request.GetAccountDataRequest
import net.codinux.banking.client.model.request.TransferMoneyRequestForUser
import net.codinux.banking.client.model.response.*
import net.codinux.banking.fints.FinTsClient
import net.codinux.banking.fints.config.FinTsClientConfiguration
@ -20,9 +21,9 @@ open class FinTs4kBankingClient(
constructor(callback: BankingClientCallback) : this(FinTsClientConfiguration(), callback)
protected val mapper = FinTs4kMapper()
protected open val mapper = FinTs4kMapper()
protected val client = FinTsClient(config, BridgeFintTsToBankingClientCallback(callback, mapper))
protected open val client = FinTsClient(config, BridgeFintTsToBankingClientCallback(callback, mapper))
override suspend fun getAccountDataAsync(request: GetAccountDataRequest): Response<GetAccountDataResponse> {
@ -55,4 +56,11 @@ open class FinTs4kBankingClient(
return Response.error(ErrorType.NoneOfTheAccountsSupportsRetrievingData, "Keiner der Konten unterstützt das Abholen der Umsätze oder des Kontostands") // TODO: translate
}
override suspend fun transferMoneyAsync(request: TransferMoneyRequestForUser): Response<TransferMoneyResponse> {
val response = client.transferMoneyAsync(mapper.mapToTransferMoneyParameter(request))
return mapper.mapTransferMoneyResponse(response)
}
}

View File

@ -8,6 +8,7 @@ import net.codinux.banking.client.model.AccountTransaction
import net.codinux.banking.client.model.Amount
import net.codinux.banking.client.model.tan.*
import net.codinux.banking.client.model.options.GetAccountDataOptions
import net.codinux.banking.client.model.request.TransferMoneyRequestForUser
import net.codinux.banking.client.model.response.*
import net.codinux.banking.client.model.tan.ActionRequiringTan
import net.codinux.banking.client.model.tan.TanChallenge
@ -26,6 +27,7 @@ import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.Mobil
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanGeneratorTanMedium
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanMediumStatus
import net.dankito.banking.banklistcreator.prettifier.BankingGroupMapper
import net.dankito.banking.client.model.parameter.TransferMoneyParameter
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
@ -38,7 +40,7 @@ open class FinTs4kMapper {
open fun mapToGetAccountDataParameter(credentials: AccountCredentials, options: GetAccountDataOptions) = GetAccountDataParameter(
credentials.bankCode, credentials.loginName, credentials.password,
options.accounts.map { BankAccountIdentifierImpl(it.identifier, it.subAccountNumber, it.iban) },
options.accounts.map { mapBankAccountIdentifier(it) },
options.retrieveBalance,
RetrieveTransactions.valueOf(options.retrieveTransactions.name), options.retrieveTransactionsFrom, options.retrieveTransactionsTo,
preferredTanMethods = options.preferredTanMethods?.map { mapTanMethodType(it) },
@ -60,17 +62,19 @@ open class FinTs4kMapper {
)
}
open fun mapBankAccountIdentifier(account: BankAccountIdentifier): BankAccountIdentifierImpl =
BankAccountIdentifierImpl(account.identifier, account.subAccountNumber, account.iban)
protected open fun mapTanMethodType(type: TanMethodType): net.codinux.banking.fints.model.TanMethodType =
net.codinux.banking.fints.model.TanMethodType.valueOf(type.name)
open fun map(response: net.dankito.banking.client.model.response.GetAccountDataResponse): Response<GetAccountDataResponse> {
return if (response.successful && response.customerAccount != null) {
open fun map(response: net.dankito.banking.client.model.response.GetAccountDataResponse): Response<GetAccountDataResponse> =
if (response.successful && response.customerAccount != null) {
Response.success(GetAccountDataResponse(mapUser(response.customerAccount!!)))
} else {
mapError(response)
}
}
open fun map(responses: List<Triple<BankAccount, GetAccountDataParameter, net.dankito.banking.client.model.response.GetAccountDataResponse>>): Response<List<GetTransactionsResponse>> {
val type = if (responses.all { it.third.successful }) ResponseType.Success else ResponseType.Error
@ -270,7 +274,26 @@ open class FinTs4kMapper {
FlickerCode(flickerCode.challengeHHD_UC, flickerCode.parsedDataSet, mapException(flickerCode.decodingError))
protected open fun <T> mapError(response: net.dankito.banking.client.model.response.GetAccountDataResponse): Response<T> {
/* Transfer Money */
open fun mapToTransferMoneyParameter(request: TransferMoneyRequestForUser): TransferMoneyParameter = TransferMoneyParameter(
request.bankCode, request.loginName, request.password, request.senderAccount?.let { mapBankAccountIdentifier(it) },
request.recipientName, request.recipientAccountIdentifier, request.recipientBankIdentifier,
mapToMoney(request.amount, request.currency), request.paymentReference, request.instantTransfer,
request.preferredTanMethods?.map { mapTanMethodType(it) }
)
open fun mapTransferMoneyResponse(response: net.dankito.banking.client.model.response.TransferMoneyResponse): Response<TransferMoneyResponse> =
if (response.successful) {
Response.success(TransferMoneyResponse())
} else {
mapError(response)
}
open fun mapToMoney(amount: Amount, currency: String): Money = Money(amount.amount, currency)
protected open fun <T> mapError(response: net.dankito.banking.client.model.response.FinTsClientResponse): Response<T> {
return if (response.error != null) {
Response.error(ErrorType.valueOf(response.error!!.name), if (response.error == ErrorCode.BankReturnedError) null else response.errorMessage,
if (response.error == ErrorCode.BankReturnedError && response.errorMessage !== null) listOf(response.errorMessage!!) else emptyList())