Compare commits

...

34 Commits

Author SHA1 Message Date
dankito c5b7967ce1 Added tanMethodsNotSupportedByApplication to filter out TanMethods not supported by application (by default: ChipTanUsb) 2024-09-10 03:26:39 +02:00
dankito 99c864bcf1 Belongs to set default bank values, IntelliJ didn't show me these files in commit dialog 2024-09-10 01:24:13 +02:00
dankito 97604d59c9 Implemented changing TAN method (but better find instance in FinTsData instead of creating a new instance) 2024-09-10 01:21:33 +02:00
dankito aa7b7afaf0 Passing known bank data on to FinTsClient as e.g. bank names returned from bank server are often quite bad, e.g. DB24 for Deutsche Bank 2024-09-09 23:01:57 +02:00
dankito 73b760ae68 Updated tanExpirationTime's new data type 2024-09-09 03:40:49 +02:00
dankito 1f8fecca7c Passing fints4k's new addTanExpiredCallback on to BankingClient 2024-09-09 03:40:25 +02:00
dankito 384ab2fd9e Renamed finTsServerAddress to serverAddress and made it nullable 2024-09-09 00:37:54 +02:00
dankito 432e5016a9 Added JsonIgnore 2024-09-09 00:36:34 +02:00
dankito 705740a0d0 Disabled test as it's not a real test 2024-09-08 22:58:52 +02:00
dankito d8499b4ce2 Updated to new fints4k model 2024-09-08 22:58:28 +02:00
dankito 8f5024b169 Added tanExpirationTime and challengeCreationTimestamp 2024-09-08 22:50:07 +02:00
dankito 4298fc9e40 Added isNumericTan 2024-09-08 22:46:47 +02:00
dankito 4acb58b571 Added finTsServerAddress 2024-09-08 22:44:08 +02:00
dankito 95b37f0cb8 Added section comment 2024-09-08 22:27:01 +02:00
dankito 2c2db147b4 Renamed otherPartyBankCode to otherPartyBankId and made reference nullable 2024-09-08 22:09:59 +02:00
dankito e6aec071a2 Using the default values from GetAccountDataOptions. Fixes that otherwise fints4k uses other preferred TAN methods than BankingClient 2024-09-08 21:56:51 +02:00
dankito 376cb08a9f Renamed transactionsRetentionDays to serverTransactionsRetentionDays 2024-09-08 21:55:31 +02:00
dankito 288af22ac6 Added AccountTransaction documentation from fints4k 2024-09-05 23:37:25 +02:00
dankito 713bfa4b50 Matched AccountTransaction property names that ones from fints4k 2024-09-05 23:26:40 +02:00
dankito adf8cfa750 Added convenience methods for features 2024-09-05 23:11:52 +02:00
dankito 783675d82a Extracted constant DefaultCurrency 2024-09-05 23:07:59 +02:00
dankito a680b6534c Implemented setting account's displayIndex 2024-09-05 23:03:11 +02:00
dankito cb45c181ae Renamed UserAccount to User 2024-09-05 23:00:17 +02:00
dankito c35026bfcc Implemented transferMoneyAsync() 2024-09-05 22:54:32 +02:00
dankito 675066c216 Added convenience constructor 2024-09-05 22:43:37 +02:00
dankito d48b708a97 Renamed InstantPayment to InstantTransfer 2024-09-05 22:43:14 +02:00
dankito 4153afa814 Fixed typo 2024-09-05 22:41:59 +02:00
dankito ccf38e2c07 Added clarification about the userId and made it nullable 2024-09-05 22:41:20 +02:00
dankito a6caa40267 Fixed using identifier instead of non-unique medium name and suffixed selected properties with -Identifier 2024-09-05 22:31:49 +02:00
dankito 55767f88e4 Changed property order a bit 2024-09-05 22:28:03 +02:00
dankito 4c860e7b20 Changed order of BankAccount properties 2024-09-05 22:25:30 +02:00
dankito 9512db3402 Renamed unbooked- to prebookedTransactions 2024-09-05 22:14:01 +02:00
dankito 7353b0347e Renamed countDaysForWhichTransactionsAreKeptOnBankServer to transactionsRetentionDays 2024-09-05 22:06:06 +02:00
dankito 1e5c83c369 Renamed haveAllTransactionsBeenRetrieved to haveAllRetainedTransactionsBeenRetrieved and made it evaluating countDaysForWhichTransactionsAreKeptOnBankServer and retrievedTransactionsFrom instead of being set to a fixed value 2024-09-05 22:05:07 +02:00
30 changed files with 548 additions and 137 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 {
@ -36,7 +38,16 @@ interface BankingClient {
* Convenience wrapper around [getAccountDataAsync].
* Updates account's transactions beginning from [BankAccount.lastTransactionsRetrievalTime].
* This may requires TAN if [BankAccount.lastTransactionsRetrievalTime] is older than 90 days.
*
* Optionally specify which [accounts] should be updated. If not specified all accounts will be updated.
*/
suspend fun updateAccountTransactionsAsync(user: UserAccount, accounts: List<BankAccount>? = null): Response<List<GetTransactionsResponse>>
suspend fun updateAccountTransactionsAsync(user: User, 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.UserAccount
import net.codinux.banking.client.model.Amount
import net.codinux.banking.client.model.BankAccount
import net.codinux.banking.client.model.User
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
@ -12,7 +16,7 @@ abstract class BankingClientForUserBase(
protected val client: BankingClient
) : BankingClientForUser {
private lateinit var user: UserAccount
private lateinit var user: User
override suspend fun getAccountDataAsync(options: GetAccountDataOptions) =
client.getAccountDataAsync(GetAccountDataRequest(credentials, options)).also {
@ -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.User
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)
fun BankingClient.updateAccountTransactions(user: User, accounts: List<BankAccount>? = null) = runBlocking {
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

@ -5,9 +5,9 @@ import net.codinux.banking.client.model.config.NoArgConstructor
@NoArgConstructor
open class AccountTransaction(
val amount: Amount = Amount.Zero,
val amount: Amount = Amount.Zero, // TODO: a string is really bad in UI, find a better solution
val currency: String,
val unparsedReference: String, // Alternative: purpose (or Remittance information)
val reference: String?, // Alternative: Remittance information, Transaction description, (payment) purpose, payment reference
/**
* Transaction date (Buchungstag) - der Tag, an dem ein Zahlungsvorgang in das System einer Bank eingegangen ist.
@ -22,52 +22,115 @@ open class AccountTransaction(
*/
val valueDate: LocalDate,
// deutsche Begriffe: "Transaktionspartei" oder "Beteiligte Partei"
// Englische: Transaction party (ist die beste Wahl für eine neutrale und übergreifende Beschreibung),
// Counterparty (ist nützlich in formellen oder finanziellen Kontexten), Participant (ist breiter gefasst, aber weniger präzise)
/**
* Name of the Payer (debtor) or Payee (creditor)
*/
val otherPartyName: String? = null, // Alternatives: Parties involved, Transaction parties.single names: Beneficiary, Payee respectively Payer, Debtor
val otherPartyBankCode: String? = null,
/**
* Bank Identifier, in most cases BIC, of the Payer or Payee
*/
val otherPartyBankId: String? = null,
/**
* Account Identifier, in most cases IBAN, of the Payer or Payee
*/
val otherPartyAccountId: String? = null,
val bookingText: String? = null,
val information: String? = null,
val statementNumber: Int? = null,
val sequenceNumber: Int? = null,
/**
* Buchungstext, z. B. DAUERAUFTRAG, BARGELDAUSZAHLUNG, ONLINE-UEBERWEISUNG, FOLGELASTSCHRIFT, ...
*/
val postingText: String? = null,
val openingBalance: Amount? = null,
val closingBalance: Amount? = null,
val endToEndReference: String? = null,
/**
* Auszugsnummer
*/
val statementNumber: Int? = null,
/**
* Blattnummer
*/
val sheetNumber: Int? = null,
/**
* Kundenreferenz.
*/
val customerReference: String? = null,
/**
* Bankreferenz
*/
val bankReference: String? = null,
/**
* Währungsart und Umsatzbetrag in Ursprungswährung
*/
val furtherInformation: String? = null,
/* Information mostly of direct debit (Lastschrift) */
val endToEndReference: String? = null,
val mandateReference: String? = null,
val creditorIdentifier: String? = null,
val originatorsIdentificationCode: String? = null,
/**
* Summe aus Auslagenersatz und Bearbeitungsprovision bei einer nationalen Rücklastschrift
* sowie optionalem Zinsausgleich.
*/
val compensationAmount: String? = null,
/**
* Betrag der ursprünglichen Lastschrift
*/
val originalAmount: String? = null,
val sepaReference: String? = null,
/**
* Abweichender Überweisender oder Zahlungsempfänger
*/
val deviantOriginator: String? = null,
/**
* Abweichender Zahlungsempfänger oder Zahlungspflichtiger
*/
val deviantRecipient: String? = null,
val referenceWithNoSpecialType: String? = null,
val primaNotaNumber: String? = null,
val textKeySupplement: String? = null,
val currencyType: String? = null,
val bookingKey: String? = null,
val referenceForTheAccountOwner: String? = null,
val referenceOfTheAccountServicingInstitution: String? = null,
val supplementaryDetails: String? = null,
/**
* Primanoten-Nr.
*/
val journalNumber: String? = null,
/**
* Bei R-Transaktionen siehe Tabelle der
* SEPA-Rückgabecodes, bei SEPALastschriften siehe optionale Belegung
* bei GVC 104 und GVC 105 (GVC = Geschäftsvorfallcode)
*/
val textKeyAddition: String? = null,
val transactionReferenceNumber: String? = null,
val relatedReferenceNumber: String? = null,
/**
* Referenznummer, die vom Sender als eindeutige Kennung für die Nachricht vergeben wurde
* (z.B. als Referenz auf stornierte Nachrichten).
*/
val orderReferenceNumber: String? = null,
/**
* Bezugsreferenz
*/
val referenceNumber: String? = null,
/**
* Storno, ob die Buchung storniert wurde(?).
* Aus:
* RC = Storno Haben
* RD = Storno Soll
*/
val isReversal: Boolean = false,
var userSetDisplayName: String? = null,
var category: String? = null,
var notes: String? = null,
) {
val reference: String
get() = sepaReference ?: unparsedReference
open val identifier by lazy {
"$amount $currency $bookingDate $valueDate $unparsedReference $sepaReference $otherPartyName $otherPartyBankCode $otherPartyAccountId"
"$amount $currency $bookingDate $valueDate $reference $otherPartyName $otherPartyBankId $otherPartyAccountId"
}
override fun toString() = "${valueDate.dayOfMonth}.${valueDate.monthNumber}.${valueDate.year} ${amount.toString().padStart(4, ' ')} ${if (currency == "EUR") "€" else currency} ${otherPartyName ?: ""} - $reference"

View File

@ -1,35 +1,33 @@
package net.codinux.banking.client.model
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import kotlinx.datetime.*
import net.codinux.banking.client.model.config.JsonIgnore
import net.codinux.banking.client.model.config.NoArgConstructor
@Suppress("RUNTIME_ANNOTATION_NOT_SUPPORTED")
@NoArgConstructor
open class BankAccount(
val identifier: String,
val subAccountNumber: String? = null,
val iban: String? = null,
val productName: String? = null,
var accountHolderName: String,
val type: BankAccountType = BankAccountType.Other,
val iban: String? = null,
val subAccountNumber: String? = null,
val productName: String? = null,
val currency: String = "EUR",
val currency: String = DefaultValues.DefaultCurrency,
var accountLimit: String? = null,
// var balance: BigDecimal = BigDecimal.ZERO,
var balance: Amount = Amount.Zero, // TODO: add a BigDecimal library
val isAccountTypeSupportedByApplication: Boolean = false,
val features: Set<BankAccountFeatures> = emptySet(),
// var balance: BigDecimal = BigDecimal.ZERO,
var balance: Amount = Amount.Zero, // TODO: add a BigDecimal library
val serverTransactionsRetentionDays: Int? = null,
open var lastTransactionsRetrievalTime: Instant? = null,
var retrievedTransactionsFrom: LocalDate? = null,
var haveAllTransactionsBeenRetrieved: Boolean = false,
val countDaysForWhichTransactionsAreKept: Int? = null,
open val bookedTransactions: MutableList<AccountTransaction> = mutableListOf(),
open val unbookedTransactions: MutableList<UnbookedAccountTransaction> = mutableListOf(),
open val prebookedTransactions: MutableList<PrebookedAccountTransaction> = mutableListOf(),
var userSetDisplayName: String? = null,
var displayIndex: Int = 0,
@ -41,8 +39,40 @@ open class BankAccount(
open val displayName: String
get() = userSetDisplayName ?: productName ?: identifier
fun supportsAnyFeature(vararg features: BankAccountFeatures): Boolean =
@get:JsonIgnore
open val supportsTransactionRetrieval: Boolean
get() = supportsAnyFeature(BankAccountFeatures.RetrieveBalance)
@get:JsonIgnore
open val supportsBalanceRetrieval: Boolean
get() = supportsAnyFeature(BankAccountFeatures.RetrieveTransactions)
@get:JsonIgnore
open val supportsMoneyTransfer: Boolean
get() = supportsAnyFeature(BankAccountFeatures.TransferMoney)
@get:JsonIgnore
open val supportsInstantTransfer: Boolean
get() = supportsAnyFeature(BankAccountFeatures.InstantTransfer)
open fun supportsAnyFeature(vararg features: BankAccountFeatures): Boolean =
features.any { this.features.contains(it) }
/**
* Determines if all transactions that are retained on bank server have been fetched.
*
* Does this by comparing [serverTransactionsRetentionDays] to [retrievedTransactionsFrom].
*/
open val haveAllRetainedTransactionsBeenRetrieved: Boolean by lazy {
val fromDay = retrievedTransactionsFrom
if (fromDay == null) {
false
} else {
// if countDaysForWhichTransactionsAreKeptOnBankServer is not set, we cannot know for how long bank server keeps transactions. We then assume for 90 days
val storageDays = serverTransactionsRetentionDays ?: 90
fromDay < Clock.System.now().toLocalDateTime(TimeZone.of("Europe/Berlin")).date.minus(storageDays, DateTimeUnit.DAY)
}
}
override fun toString() = "$type $identifier $productName (IBAN: $iban)"
}

View File

@ -4,5 +4,5 @@ enum class BankAccountFeatures {
RetrieveTransactions,
RetrieveBalance,
TransferMoney,
InstantPayment
InstantTransfer
}

View File

@ -8,5 +8,7 @@ open class BankAccountIdentifier(
val subAccountNumber: String?,
val iban: String?
) {
constructor(account: BankAccount) : this(account.identifier, account.subAccountNumber, account.iban)
override fun toString() = "$identifier, $iban"
}

View File

@ -0,0 +1,13 @@
package net.codinux.banking.client.model
import net.codinux.banking.client.model.config.NoArgConstructor
@NoArgConstructor
open class BankInfo(
val name: String,
val bic: String,
val serverAddress: String,
val bankingGroup: BankingGroup? = null
) {
override fun toString() = "$name $bic $bankingGroup"
}

View File

@ -0,0 +1,20 @@
package net.codinux.banking.client.model
import net.codinux.banking.client.model.config.NoArgConstructor
/**
* Contains only the basic info of a bank, just enough that a client application can display it to the user
* and the user knows exactly which bank is meant / referred to.
*
* As e.g. when adding a new account, client application has no data about the bank locally, so it can use this
* information to display it to the user.
*/
@NoArgConstructor
open class BankViewInfo(
val bankCode: String,
var loginName: String,
val bankName: String,
val bankingGroup: BankingGroup? = null
) {
override fun toString() = "$bankCode $bankName $loginName"
}

View File

@ -0,0 +1,5 @@
package net.codinux.banking.client.model
object DefaultValues {
const val DefaultCurrency = "EUR"
}

View File

@ -3,5 +3,5 @@ package net.codinux.banking.client.model
import net.codinux.banking.client.model.config.NoArgConstructor
@NoArgConstructor
open class UnbookedAccountTransaction {
open class PrebookedAccountTransaction {
}

View File

@ -7,7 +7,7 @@ import net.codinux.banking.client.model.tan.TanMethod
@Suppress("RUNTIME_ANNOTATION_NOT_SUPPORTED")
@NoArgConstructor
open class UserAccount(
open class User(
val bankCode: String,
var loginName: String,
/**
@ -19,7 +19,17 @@ open class UserAccount(
val bic: String,
val customerName: String,
val userId: String = loginName,
/**
* The customer is the person or organisation that owns the account at the bank.
*
* The user is the person that access a customer's account via e.g. FinTS. The user may but not necessarily is
* identical with the customer. E.g. an employee from the bookkeeping departement or a tax consultant may only
* access an account for someone else.
*
* So in most cases the userId is identical with the customerId = loginName in our speech, but there are rare cases
* where the userId differs from customerId.
*/
val userId: String? = null,
open val accounts: List<BankAccount> = emptyList(),
@ -29,7 +39,7 @@ open class UserAccount(
* As [tanMethods] also contains selected TanMethod, we didn't want to duplicate this object. Use
* [selectedTanMethod] to get selected TanMethod or iterate over [tanMethods] and filter selected one by this id.
*/
val selectedTanMethodId: String? = null,
val selectedTanMethodIdentifier: String? = null,
open val tanMethods: List<TanMethod> = listOf(),
/**
@ -38,18 +48,20 @@ open class UserAccount(
* As [tanMedia] also contains selected TanMedium, we didn't want to duplicate this object. Use [selectedTanMedium]
* to get selected TanMedium or iterate over [tanMedia] and filter selected one by this medium name.
*/
val selectedTanMediumName: String? = null,
val selectedTanMediumIdentifier: String? = null,
open val tanMedia: List<TanMedium> = listOf(),
var bankingGroup: BankingGroup? = null,
var iconUrl: String? = null,
open var serverAddress: String? = null
) {
var wrongCredentialsEntered: Boolean = false
var userSetDisplayName: String? = null
var displayIndex: Int = 0
var iconUrl: String? = null
var wrongCredentialsEntered: Boolean = false
@get:JsonIgnore
open val displayName: String
@ -58,11 +70,11 @@ open class UserAccount(
@get:JsonIgnore
val selectedTanMethod: TanMethod
get() = tanMethods.first { it.identifier == selectedTanMethodId }
get() = tanMethods.first { it.identifier == selectedTanMethodIdentifier }
@get:JsonIgnore
val selectedTanMedium: TanMedium?
get() = tanMedia.firstOrNull { it.mediumName == selectedTanMediumName }
get() = tanMedia.firstOrNull { it.identifier == selectedTanMediumIdentifier }
override fun toString() = "$bankName $loginName, ${accounts.size} accounts"

View File

@ -1,17 +0,0 @@
package net.codinux.banking.client.model
import net.codinux.banking.client.model.config.NoArgConstructor
/**
* Contains only the basic info of a [UserAccount], just enough that a client application can display it to the user
* and the user knows exactly which [UserAccount] is meant / referred.
*/
@NoArgConstructor
open class UserAccountViewInfo(
val bankCode: String,
var loginName: String,
val bankName: String,
val bankingGroup: BankingGroup? = null
) {
override fun toString() = "$bankCode $bankName $loginName"
}

View File

@ -36,6 +36,8 @@ open class GetAccountDataOptions(
*/
val preferredTanMethods: List<TanMethodType>? = TanMethodType.NonVisualOrImageBased,
val tanMethodsNotSupportedByApplication: List<TanMethodType> = TanMethodType.TanMethodsNotSupportedByMostApplications,
val abortIfTanIsRequired: Boolean = false,
// there's also the option preferredTanMedium, but can hardly find a use case for it as we

View File

@ -1,11 +1,12 @@
package net.codinux.banking.client.model.request
import net.codinux.banking.client.model.AccountCredentials
import net.codinux.banking.client.model.BankInfo
import net.codinux.banking.client.model.config.NoArgConstructor
import net.codinux.banking.client.model.options.GetAccountDataOptions
@NoArgConstructor
open class GetAccountDataRequest(bankCode: String, loginName: String, password: String, val options: GetAccountDataOptions? = null)
open class GetAccountDataRequest(bankCode: String, loginName: String, password: String, val options: GetAccountDataOptions? = null, val bankInfo: BankInfo? = null)
: AccountCredentials(bankCode, loginName, password) {
constructor(credentials: AccountCredentials, options: GetAccountDataOptions? = null)

View File

@ -0,0 +1,79 @@
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.DefaultValues
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 = DefaultValues.DefaultCurrency,
/**
* 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,
val tanMethodsNotSupportedByApplication: List<TanMethodType> = TanMethodType.TanMethodsNotSupportedByMostApplications
) {
override fun toString() = "$amount to $recipientName - $paymentReference"
}

View File

@ -0,0 +1,49 @@
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.DefaultValues
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 = DefaultValues.DefaultCurrency,
paymentReference: String? = null,
instantTransfer: Boolean = false,
preferredTanMethods: List<TanMethodType>? = TanMethodType.NonVisualOrImageBased,
tanMethodsNotSupportedByApplication: List<TanMethodType> = TanMethodType.TanMethodsNotSupportedByMostApplications,
) : TransferMoneyRequest(senderAccount, recipientName, recipientAccountIdentifier, recipientBankIdentifier, amount, currency, paymentReference, instantTransfer, preferredTanMethods, tanMethodsNotSupportedByApplication) {
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, request.tanMethodsNotSupportedByApplication)
override fun toString() = "$bankCode $loginName ${super.toString()}"
}

View File

@ -1,14 +1,14 @@
package net.codinux.banking.client.model.response
import net.codinux.banking.client.model.AccountTransaction
import net.codinux.banking.client.model.UserAccount
import net.codinux.banking.client.model.User
import net.codinux.banking.client.model.config.JsonIgnore
import net.codinux.banking.client.model.config.NoArgConstructor
@Suppress("RUNTIME_ANNOTATION_NOT_SUPPORTED")
@NoArgConstructor
open class GetAccountDataResponse(
val user: UserAccount
val user: User
) {
@get:JsonIgnore

View File

@ -5,7 +5,7 @@ import kotlinx.datetime.LocalDate
import net.codinux.banking.client.model.AccountTransaction
import net.codinux.banking.client.model.Amount
import net.codinux.banking.client.model.BankAccount
import net.codinux.banking.client.model.UnbookedAccountTransaction
import net.codinux.banking.client.model.PrebookedAccountTransaction
import net.codinux.banking.client.model.config.NoArgConstructor
@NoArgConstructor
@ -13,7 +13,7 @@ open class GetTransactionsResponse(
val account: BankAccount,
val balance: Amount? = null,
val bookedTransactions: List<AccountTransaction>,
val unbookedTransactions: List<UnbookedAccountTransaction>,
val prebookedTransactions: List<PrebookedAccountTransaction>,
val transactionsRetrievalTime: Instant,
val retrievedTransactionsFrom: LocalDate? = null,
val retrievedTransactionsTo: LocalDate? = null

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

@ -5,7 +5,7 @@ import net.codinux.banking.client.model.config.NoArgConstructor
@NoArgConstructor
open class EnterTanResult(
val enteredTan: String?,
// val changeTanMethodTo: TanMethod? = null,
val changeTanMethodTo: TanMethod? = null,
// val changeTanMediumTo: TanMedium? = null,
// val changeTanMediumResultCallback: ((BankingClientResponse) -> Unit)? = null
)

View File

@ -1,8 +1,10 @@
package net.codinux.banking.client.model.tan
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import net.codinux.banking.client.model.BankAccountViewInfo
import net.codinux.banking.client.model.UserAccount
import net.codinux.banking.client.model.UserAccountViewInfo
import net.codinux.banking.client.model.User
import net.codinux.banking.client.model.BankViewInfo
import net.codinux.banking.client.model.config.JsonIgnore
import net.codinux.banking.client.model.config.NoArgConstructor
@ -24,7 +26,7 @@ open class TanChallenge(
* When adding an account, frontend has no UserAccount object in BankingClientCallback to know which TanMethods are
* available for User.
* Also on other calls to bank server, bank server may returned an updated list of available TanMethods, so that
* [UserAccount] may contains an outdated list of available TanMethods.
* [User] may contains an outdated list of available TanMethods.
*
* Therefore i added list with up to date TanMethods here to ensure EnterTanDialog can display user's up to date TanMethods.
*/
@ -41,8 +43,15 @@ open class TanChallenge(
open val tanImage: TanImage? = null,
open val flickerCode: FlickerCode? = null,
open val user: UserAccountViewInfo,
open val account: BankAccountViewInfo? = null
open val user: BankViewInfo,
open val account: BankAccountViewInfo? = null,
/**
* Datum und Uhrzeit, bis zu welchem Zeitpunkt eine TAN auf Basis der gesendeten Challenge gültig ist. Nach Ablauf der Gültigkeitsdauer wird die entsprechende TAN entwertet.
*
* In server's time zone, that is Europe/Berlin.
*/
val tanExpirationTime: Instant? = null,
val challengeCreationTimestamp: Instant = Clock.System.now()
) {
@get:JsonIgnore
@ -55,7 +64,17 @@ open class TanChallenge(
/**
* Principally a no-op method, not implemented for all client, only implementing client for not: FinTs4jBankingClient.
* Not implemented for all client, only implementing client for now: FinTs4jBankingClient.
*
* If a TAN expires either when [TanChallenge.tanExpirationTime] or a default timeout (15 min) is exceeded,
* you can add a callback to get notified when TAN expired e.g. to close a EnterTanDialog.
*/
open fun addTanExpiredCallback(callback: () -> Unit) {
}
/**
* Principally a no-op method, not implemented for all client, only implementing client for now: FinTs4jBankingClient.
*
* If a TAN is requested for a decoupled TAN method like [TanMethodType.DecoupledTan] or [TanMethodType.DecoupledPushTan],
* you can add a callback to get notified when user approved TAN in her app e.g. to close a EnterTanDialog.

View File

@ -53,7 +53,7 @@ open class TanMedium(
val displayName: String by lazy {
identifier + " " + when (status) {
TanMediumStatus.Used -> "Aktive"
TanMediumStatus.Used -> "Aktiv"
TanMediumStatus.Available -> "Verfügbar"
TanMediumStatus.ActiveFollowUpCard -> " Folgekarte, aktiv bei erster Nutzung"
TanMediumStatus.AvailableFollowUpCard -> " Folgekarte, die erst aktiviert werden muss"

View File

@ -1,7 +1,9 @@
package net.codinux.banking.client.model.tan
import net.codinux.banking.client.model.config.JsonIgnore
import net.codinux.banking.client.model.config.NoArgConstructor
@Suppress("RUNTIME_ANNOTATION_NOT_SUPPORTED")
@NoArgConstructor
open class TanMethod(
open val displayName: String,
@ -10,5 +12,10 @@ open class TanMethod(
open val maxTanInputLength: Int? = null,
open val allowedTanFormat: AllowedTanFormat = AllowedTanFormat.Alphanumeric
) {
@get:JsonIgnore
open val isNumericTan: Boolean
get() = allowedTanFormat == AllowedTanFormat.Numeric
override fun toString() = "$displayName ($type, ${identifier})"
}

View File

@ -43,6 +43,8 @@ enum class TanMethodType {
addAll(listOf(TanMethodType.ChipTanManuell)) // this is quite inconvenient for user, so i added it as last
}
val TanMethodsNotSupportedByMostApplications = listOf(TanMethodType.ChipTanUsb)
}
}

View File

@ -22,6 +22,8 @@ open class BridgeFintTsToBankingClientCallback(
bankingClientCallback.enterTan(mapper.mapTanChallenge(tanChallenge)) { enterTanResult ->
if (enterTanResult.enteredTan != null) {
tanChallenge.userEnteredTan(enterTanResult.enteredTan!!)
} else if (enterTanResult.changeTanMethodTo != null) {
tanChallenge.userAsksToChangeTanMethod(mapper.mapTanMethod(enterTanResult.changeTanMethodTo!!))
} else {
tanChallenge.userDidNotEnterTan()
}

View File

@ -4,9 +4,10 @@ import net.codinux.banking.client.BankingClient
import net.codinux.banking.client.BankingClientCallback
import net.codinux.banking.client.model.BankAccount
import net.codinux.banking.client.model.BankAccountFeatures
import net.codinux.banking.client.model.UserAccount
import net.codinux.banking.client.model.User
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,18 +21,18 @@ 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> {
val response = client.getAccountDataAsync(mapper.mapToGetAccountDataParameter(request, request.options ?: GetAccountDataOptions()))
return mapper.map(response)
return mapper.map(response, request.bankInfo)
}
override suspend fun updateAccountTransactionsAsync(user: UserAccount, accounts: List<BankAccount>?): Response<List<GetTransactionsResponse>> {
override suspend fun updateAccountTransactionsAsync(user: User, accounts: List<BankAccount>?): Response<List<GetTransactionsResponse>> {
val accountsToRequest = (accounts ?: user.accounts).filter { it.supportsAnyFeature(BankAccountFeatures.RetrieveBalance, BankAccountFeatures.RetrieveBalance) }
if (accountsToRequest.isNotEmpty()) {
@ -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,8 @@ 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.GetAccountDataRequest
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
@ -20,12 +22,14 @@ import net.dankito.banking.client.model.parameter.GetAccountDataParameter
import net.dankito.banking.client.model.parameter.RetrieveTransactions
import net.dankito.banking.client.model.response.ErrorCode
import net.codinux.banking.fints.mapper.FinTsModelMapper
import net.codinux.banking.fints.messages.datenelemente.implementierte.signatur.Sicherheitsfunktion
import net.codinux.banking.fints.model.*
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanMedium
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.MobilePhoneTanMedium
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
@ -36,41 +40,59 @@ open class FinTs4kMapper {
protected val bankingGroupMapper = BankingGroupMapper()
open fun mapToGetAccountDataParameter(credentials: AccountCredentials, options: GetAccountDataOptions) = GetAccountDataParameter(
credentials.bankCode, credentials.loginName, credentials.password,
options.accounts.map { BankAccountIdentifierImpl(it.identifier, it.subAccountNumber, it.iban) },
open fun mapToGetAccountDataParameter(request: GetAccountDataRequest, options: GetAccountDataOptions) = GetAccountDataParameter(
request.bankCode, request.loginName, request.password,
options.accounts.map { mapBankAccountIdentifier(it) },
options.retrieveBalance,
RetrieveTransactions.valueOf(options.retrieveTransactions.name), options.retrieveTransactionsFrom, options.retrieveTransactionsTo,
preferredTanMethods = options.preferredTanMethods?.map { mapTanMethodType(it) },
abortIfTanIsRequired = options.abortIfTanIsRequired
tanMethodsNotSupportedByApplication = options.tanMethodsNotSupportedByApplication.map { mapTanMethodType(it) },
abortIfTanIsRequired = options.abortIfTanIsRequired,
defaultBankValues = request.bankInfo?.let { mapToBankData(request, it) }
)
open fun mapToUpdateAccountTransactionsParameter(user: UserAccount, account: BankAccount, finTsModel: BankData?): GetAccountDataParameter {
protected open fun mapToBankData(credentials: AccountCredentials, bank: BankInfo): BankData = BankData(
credentials.bankCode, credentials.loginName, credentials.password,
bank.serverAddress, bank.bic, bank.name
)
open fun mapToUpdateAccountTransactionsParameter(user: User, account: BankAccount, finTsModel: BankData?): GetAccountDataParameter {
val defaults = GetAccountDataOptions()
val accountIdentifier = BankAccountIdentifierImpl(account.identifier, account.subAccountNumber, account.iban)
val from = account.lastTransactionsRetrievalTime?.toLocalDateTime(TimeZone.EuropeBerlin)?.date // TODO: in case lastTransactionsUpdateTime is not set, this would retrieve all transactions (and require a TAN im most cases)
val retrieveTransactions = if (from != null) RetrieveTransactions.AccordingToRetrieveFromAndTo else RetrieveTransactions.OfLast90Days
val retrieveTransactions = if (from != null) RetrieveTransactions.AccordingToRetrieveFromAndTo else RetrieveTransactions.valueOf(defaults.retrieveTransactions.name)
// val preferredTanMethods = listOf(mapTanMethodType(user.selectedTanMethod.type)) // TODO: currently we aren't saving TanMethods in database, re-enable as soon as TanMethods get saved
val preferredTanMethods = emptyList<net.codinux.banking.fints.model.TanMethodType>()
val preferredTanMethods = defaults.preferredTanMethods?.map { mapTanMethodType(it) }
return GetAccountDataParameter(user.bankCode, user.loginName, user.password!!, listOf(accountIdentifier), true,
retrieveTransactions, from,
preferredTanMethods = preferredTanMethods,
preferredTanMedium = user.selectedTanMediumName,
preferredTanMedium = user.selectedTanMediumIdentifier,
finTsModel = finTsModel
)
}
open fun mapBankAccountIdentifier(account: BankAccountIdentifier): BankAccountIdentifierImpl =
BankAccountIdentifierImpl(account.identifier, account.subAccountNumber, account.iban)
open fun mapTanMethod(method: TanMethod) = net.codinux.banking.fints.model.TanMethod( // TODO: get instance from FinTsData, don't create manually
method.displayName, Sicherheitsfunktion.entries.first { it.code == method.identifier }, mapTanMethodType(method.type), null, method.maxTanInputLength, mapAllowedTanFormat(method.allowedTanFormat)
)
protected open fun mapTanMethodType(type: TanMethodType): net.codinux.banking.fints.model.TanMethodType =
net.codinux.banking.fints.model.TanMethodType.valueOf(type.name)
protected open fun mapAllowedTanFormat(allowedTanFormat: AllowedTanFormat?): net.codinux.banking.fints.messages.datenelemente.implementierte.tan.AllowedTanFormat =
allowedTanFormat?.let { net.codinux.banking.fints.messages.datenelemente.implementierte.tan.AllowedTanFormat.valueOf(it.name) } ?: net.codinux.banking.fints.messages.datenelemente.implementierte.tan.AllowedTanFormat.Alphanumeric
open fun map(response: net.dankito.banking.client.model.response.GetAccountDataResponse): Response<GetAccountDataResponse> {
return if (response.successful && response.customerAccount != null) {
Response.success(GetAccountDataResponse(mapUser(response.customerAccount!!)))
open fun map(response: net.dankito.banking.client.model.response.GetAccountDataResponse, bank: BankInfo? = null): Response<GetAccountDataResponse> =
if (response.successful && response.customerAccount != null) {
Response.success(GetAccountDataResponse(mapUser(response.customerAccount!!, bank)))
} 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
@ -102,7 +124,7 @@ open class FinTs4kMapper {
}
open fun mapToUserAccountViewInfo(bank: BankData): UserAccountViewInfo = UserAccountViewInfo(
open fun mapToUserAccountViewInfo(bank: BankData): BankViewInfo = BankViewInfo(
bank.bankCode, bank.customerId, bank.bankName, getBankingGroup(bank.bankName, bank.bic)
)
@ -113,15 +135,17 @@ open class FinTs4kMapper {
)
protected open fun mapUser(user: net.dankito.banking.client.model.CustomerAccount) = UserAccount(
protected open fun mapUser(user: net.dankito.banking.client.model.CustomerAccount, bank: BankInfo? = null) = User(
user.bankCode, user.loginName, user.password,
user.bankName, user.bic, user.customerName, user.userId,
user.accounts.map { mapAccount(it) },
bank?.name ?: user.bankName, user.bic, user.customerName, user.userId,
user.accounts.map { mapAccount(it) }.sortedBy { it.type }
.onEachIndexed { index, bankAccount -> bankAccount.displayIndex = index },
user.selectedTanMethod?.securityFunction?.code, user.tanMethods.map { mapTanMethod(it) },
user.selectedTanMedium?.mediumName, user.tanMedia.map { mapTanMedium(it) },
getBankingGroup(user.bankName, user.bic)
bank?.bankingGroup ?: getBankingGroup(user.bankName, user.bic),
user.finTsServerAddress
)
protected open fun getBankingGroup(bankName: String, bic: String): BankingGroup? =
@ -129,12 +153,12 @@ open class FinTs4kMapper {
protected open fun mapAccount(account: net.dankito.banking.client.model.BankAccount) = BankAccount(
account.identifier, account.accountHolderName, mapAccountType(account.type), account.iban, account.subAccountNumber,
account.productName, account.currency, account.accountLimit, mapAmount(account.balance),
account.identifier, account.subAccountNumber, account.iban, account.productName, account.accountHolderName,
mapAccountType(account.type), account.currency, account.accountLimit,
account.isAccountTypeSupportedByApplication, mapFeatures(account),
mapAmount(account.balance),
account.serverTransactionsRetentionDays,
account.lastTransactionsRetrievalTime, account.retrievedTransactionsFrom,
// TODO: map haveAllTransactionsBeenRetrieved
countDaysForWhichTransactionsAreKept = account.countDaysForWhichTransactionsAreKept,
bookedTransactions = mapBookedTransactions(account).toMutableList()
)
@ -152,7 +176,7 @@ open class FinTs4kMapper {
add(BankAccountFeatures.TransferMoney)
}
if (account.supportsInstantPayment) {
add(BankAccountFeatures.InstantPayment)
add(BankAccountFeatures.InstantTransfer)
}
}
@ -161,25 +185,28 @@ open class FinTs4kMapper {
account.bookedTransactions.map { mapTransaction(it) }
protected open fun mapTransaction(transaction: net.dankito.banking.client.model.AccountTransaction): AccountTransaction = AccountTransaction(
mapAmount(transaction.amount), transaction.amount.currency.code, transaction.unparsedReference,
mapAmount(transaction.amount), transaction.amount.currency.code, transaction.reference,
transaction.bookingDate, transaction.valueDate,
transaction.otherPartyName, transaction.otherPartyBankCode, transaction.otherPartyAccountId,
transaction.bookingText, null,
transaction.statementNumber, transaction.sequenceNumber,
transaction.otherPartyName, transaction.otherPartyBankId, transaction.otherPartyAccountId,
transaction.postingText,
mapNullableAmount(transaction.openingBalance), mapNullableAmount(transaction.closingBalance),
transaction.endToEndReference, transaction.customerReference, transaction.mandateReference,
transaction.statementNumber, transaction.sheetNumber,
transaction.customerReference, transaction.bankReference,
transaction.furtherInformation,
transaction.endToEndReference, transaction.mandateReference,
transaction.creditorIdentifier, transaction.originatorsIdentificationCode,
transaction.compensationAmount, transaction.originalAmount,
transaction.sepaReference,
transaction.deviantOriginator, transaction.deviantRecipient,
transaction.referenceWithNoSpecialType, transaction.primaNotaNumber, transaction.textKeySupplement,
transaction.referenceWithNoSpecialType,
transaction.currencyType, transaction.bookingKey,
transaction.referenceForTheAccountOwner, transaction.referenceOfTheAccountServicingInstitution,
transaction.supplementaryDetails,
transaction.journalNumber, transaction.textKeyAddition,
transaction.transactionReferenceNumber, transaction.relatedReferenceNumber
transaction.orderReferenceNumber, transaction.referenceNumber,
transaction.isReversal
)
protected open fun mapNullableAmount(amount: Money?) = amount?.let { mapAmount(it) }
@ -204,7 +231,11 @@ open class FinTs4kMapper {
val tanImage = if (challenge is ImageTanChallenge) mapTanImage(challenge.image) else null
val flickerCode = if (challenge is FlickerCodeTanChallenge) mapFlickerCode(challenge.flickerCode) else null
return object : TanChallenge(type, action, challenge.messageToShowToUser, selectedTanMethodId, tanMethods, selectedTanMediumName, tanMedia, tanImage, flickerCode, user, account) {
return object : TanChallenge(type, action, challenge.messageToShowToUser, selectedTanMethodId, tanMethods, selectedTanMediumName, tanMedia, tanImage, flickerCode, user, account, challenge.tanExpirationTime, challenge.challengeCreationTimestamp) {
override fun addTanExpiredCallback(callback: () -> Unit) {
challenge.addTanExpiredCallback(callback)
}
override fun addUserApprovedDecoupledTanCallback(callback: () -> Unit) {
challenge.addUserApprovedDecoupledTanCallback(callback)
}
@ -220,7 +251,7 @@ open class FinTs4kMapper {
protected open fun mapActionRequiringTan(action: net.codinux.banking.fints.model.ActionRequiringTan): ActionRequiringTan =
ActionRequiringTan.valueOf(action.name)
protected open fun mapTanMethod(method: net.codinux.banking.fints.model.TanMethod): TanMethod = TanMethod(
open fun mapTanMethod(method: net.codinux.banking.fints.model.TanMethod): TanMethod = TanMethod(
method.displayName, mapTanMethodType(method.type), method.securityFunction.code, method.maxTanInputLength, mapAllowedTanFormat(method.allowedTanFormat)
)
@ -270,7 +301,27 @@ 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) },
request.tanMethodsNotSupportedByApplication.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())

View File

@ -3,11 +3,9 @@ package net.codinux.banking.client.fints4k
import kotlinx.coroutines.test.runTest
import net.codinux.banking.client.SimpleBankingClientCallback
import net.codinux.banking.client.model.response.ResponseType
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import kotlin.test.*
@Ignore
class FinTs4kBankingClientTest {
companion object {