Compare commits

..

18 Commits

Author SHA1 Message Date
dankito 62276e2a02 Bumped version to 0.5.2-SNAPSHOT 2024-09-01 20:20:25 +02:00
dankito 7f407dd8b9 Bumped version to 0.5.1 2024-09-01 20:18:43 +02:00
dankito cf50d04ba0 Updated fints4k version to 1.0.0-Alpha-12 2024-09-01 20:03:43 +02:00
dankito afade80bcb Renamed Customer to User which is the correct term ((many) users can access a customer account at a bank; the one, that accesses a customer account over FinTS is not necessary the customer that owns the account) 2024-08-28 00:00:57 +02:00
dankito a983df42b1 Mapping BankingGroup (TODO: but is duplicated code from BankListCreator); added BankingGroup to CustomerAccountViewInfo 2024-08-27 23:36:18 +02:00
dankito 72889aeeed Removed smsDebitAccount (there should be rarely a use case for it) 2024-08-27 23:22:04 +02:00
dankito 07926ef0c1 Forgot to commit MessageLogEntry 2024-08-27 23:20:32 +02:00
dankito 748b4cf98d Made collection properties open (so that they can be e.g. overwritten with Entity version in sub classes 2024-08-27 23:18:41 +02:00
dankito 79e45f0c02 Added category to AccountTransaction 2024-08-27 23:13:29 +02:00
dankito 5a2018fd97 Added available and selected TanMedia and TanMethods to CustomerAccount 2024-08-27 02:26:32 +02:00
dankito b65658910c Added same annotations to platform specific JsonIgnore as expected annotation has 2024-08-27 02:17:22 +02:00
dankito 37c575a1af Mapping preferredTanMethods 2024-08-27 02:16:03 +02:00
dankito f54c03b7bf Added all available TanMedia to TanChallenge 2024-08-27 02:15:24 +02:00
dankito 046a0c90eb Added all available TanMethods to TanChallenge 2024-08-27 01:27:03 +02:00
dankito 5ff2714512 Implemented setting preferred TanMethodTypes and set it to NonVisualOrImageBase by default 2024-08-26 23:55:10 +02:00
dankito ea1c184abc Added description and clarified that by default the transaction of last 90 days are retrieved 2024-08-26 22:37:30 +02:00
dankito 68dabf9c68 Added note for which plugin to use 2024-08-26 03:13:50 +02:00
dankito 5e77c50ff7 Fixed project name of fints4k-banking-client 2024-08-26 03:13:09 +02:00
36 changed files with 452 additions and 123 deletions

View File

@ -1,14 +1,33 @@
package net.codinux.banking.client
import net.codinux.banking.client.model.options.RetrieveTransactions
import net.codinux.banking.client.model.request.GetAccountDataRequest
import net.codinux.banking.client.model.response.GetAccountDataResponse
import net.codinux.banking.client.model.response.Response
interface BankingClient {
/**
* Retrieves account data like customer name, her bank accounts, their balance and account transactions.
*
* By default the account transactions of the last 90 days are retrieved (which in most cases doesn't require a
* TAN according to PSD2).
*
* If you like to retrieve the transactions of a different period, use the method overload that takes a
* [GetAccountDataRequest] parameter and set [RetrieveTransactions] and may to and from date in its options object.
*/
suspend fun getAccountDataAsync(bankCode: String, loginName: String, password: String) =
getAccountDataAsync(GetAccountDataRequest(bankCode, loginName, password))
/**
* Retrieves account data like customer name, her bank accounts, their balance and account transactions.
*
* By default the account transactions of the last 90 days are retrieved (which in most cases doesn't require a
* TAN according to PSD2).
*
* If you like to retrieve the transactions of a different period, set [RetrieveTransactions] and may to and from
* date in [GetAccountDataRequest.options].
*/
suspend fun getAccountDataAsync(request: GetAccountDataRequest): Response<GetAccountDataResponse>
}

View File

@ -1,14 +0,0 @@
package net.codinux.banking.client
import net.codinux.banking.client.model.options.GetAccountDataOptions
import net.codinux.banking.client.model.response.GetAccountDataResponse
import net.codinux.banking.client.model.response.Response
interface BankingClientForCustomer {
// for languages not supporting default parameters (Java, Swift, JS, ...)
suspend fun getAccountDataAsync() = getAccountDataAsync(GetAccountDataOptions())
suspend fun getAccountDataAsync(options: GetAccountDataOptions): Response<GetAccountDataResponse>
}

View File

@ -0,0 +1,33 @@
package net.codinux.banking.client
import net.codinux.banking.client.model.options.GetAccountDataOptions
import net.codinux.banking.client.model.options.RetrieveTransactions
import net.codinux.banking.client.model.response.GetAccountDataResponse
import net.codinux.banking.client.model.response.Response
interface BankingClientForUser {
/**
* Retrieves account data like customer name, her bank accounts, their balance and account transactions.
*
* By default the account transactions of the last 90 days are retrieved (which in most cases doesn't require a
* TAN according to PSD2).
*
* If you like to retrieve the transactions of a different period, use the method overload that takes a
* [GetAccountDataOptions] parameter and set [RetrieveTransactions] and may to and from date.
*/
// for languages not supporting default parameters (Java, Swift, JS, ...)
suspend fun getAccountDataAsync() = getAccountDataAsync(GetAccountDataOptions())
/**
* Retrieves account data like customer name, her bank accounts, their balance and account transactions.
*
* By default the account transactions of the last 90 days are retrieved (which in most cases doesn't require a
* TAN according to PSD2).
*
* If you like to retrieve the transactions of a different period, set [GetAccountDataOptions.retrieveTransactions]
* and may [GetAccountDataOptions.retrieveTransactionsFrom] and [GetAccountDataOptions.retrieveTransactionsTo].
*/
suspend fun getAccountDataAsync(options: GetAccountDataOptions): Response<GetAccountDataResponse>
}

View File

@ -4,10 +4,10 @@ import net.codinux.banking.client.model.AccountCredentials
import net.codinux.banking.client.model.options.GetAccountDataOptions
import net.codinux.banking.client.model.request.GetAccountDataRequest
abstract class BankingClientForCustomerBase(
abstract class BankingClientForUserBase(
protected val credentials: AccountCredentials,
protected val client: BankingClient
) : BankingClientForCustomer {
) : BankingClientForUser {
override suspend fun getAccountDataAsync(options: GetAccountDataOptions) =
client.getAccountDataAsync(GetAccountDataRequest(credentials, options))

View File

@ -4,7 +4,7 @@ import net.codinux.banking.client.model.options.GetAccountDataOptions
import net.codinux.banking.client.model.response.GetAccountDataResponse
import net.codinux.banking.client.model.response.Response
interface BlockingBankingClientForCustomer {
interface BlockingBankingClientForUser {
// for languages not supporting default parameters (Java, Swift, JS, ...)
fun getAccountData() = getAccountData(GetAccountDataOptions())

View File

@ -4,10 +4,10 @@ import net.codinux.banking.client.model.AccountCredentials
import net.codinux.banking.client.model.options.GetAccountDataOptions
import net.codinux.banking.client.model.request.GetAccountDataRequest
abstract class BlockingBankingClientForCustomerBase(
abstract class BlockingBankingClientForUserBase(
protected val credentials: AccountCredentials,
protected val client: BlockingBankingClient
) : BlockingBankingClientForCustomer {
) : BlockingBankingClientForUser {
override fun getAccountData(options: GetAccountDataOptions) =
client.getAccountData(GetAccountDataRequest(credentials, options))

View File

@ -12,10 +12,10 @@ fun BankingClient.getAccountData(request: GetAccountDataRequest) = runBlocking {
this@getAccountData.getAccountDataAsync(request)
}
fun BankingClientForCustomer.getAccountData() = runBlocking {
fun BankingClientForUser.getAccountData() = runBlocking {
this@getAccountData.getAccountDataAsync()
}
fun BankingClientForCustomer.getAccountData(options: GetAccountDataOptions) = runBlocking {
fun BankingClientForUser.getAccountData(options: GetAccountDataOptions) = runBlocking {
this@getAccountData.getAccountDataAsync(options)
}

View File

@ -12,10 +12,10 @@ fun BankingClient.getAccountData(request: GetAccountDataRequest) = runBlocking {
this@getAccountData.getAccountDataAsync(request)
}
fun BankingClientForCustomer.getAccountData() = runBlocking {
fun BankingClientForUser.getAccountData() = runBlocking {
this@getAccountData.getAccountDataAsync()
}
fun BankingClientForCustomer.getAccountData(options: GetAccountDataOptions) = runBlocking {
fun BankingClientForUser.getAccountData(options: GetAccountDataOptions) = runBlocking {
this@getAccountData.getAccountDataAsync(options)
}

View File

@ -59,6 +59,7 @@ open class AccountTransaction(
val relatedReferenceNumber: String? = null,
var userSetDisplayName: String? = null,
var category: String? = null,
var notes: String? = null,
) {
override fun toString() = "${valueDate.dayOfMonth}.${valueDate.monthNumber}.${valueDate.year} ${amount.toString().padStart(4, ' ')} ${if (currency == "EUR") "€" else currency} ${otherPartyName ?: ""} - $reference"

View File

@ -25,8 +25,8 @@ open class BankAccount(
var haveAllTransactionsBeenRetrieved: Boolean = false,
val countDaysForWhichTransactionsAreKept: Int? = null,
val bookedTransactions: MutableList<AccountTransaction> = mutableListOf(),
val unbookedTransactions: MutableList<UnbookedAccountTransaction> = mutableListOf(),
open val bookedTransactions: MutableList<AccountTransaction> = mutableListOf(),
open val unbookedTransactions: MutableList<UnbookedAccountTransaction> = mutableListOf(),
var userSetDisplayName: String? = null,
var displayIndex: Int = 0,

View File

@ -1,34 +0,0 @@
package net.codinux.banking.client.model
import net.codinux.banking.client.model.config.NoArgConstructor
@NoArgConstructor
open class CustomerAccount(
val bankCode: String,
var loginName: String,
/**
* User may decides to not save password .
*/
var password: String?,
val bankName: String,
val bic: String,
val customerName: String,
val userId: String = loginName,
val accounts: List<BankAccount> = emptyList(),
var bankingGroup: BankingGroup? = null,
var iconUrl: String? = null,
) {
var wrongCredentialsEntered: Boolean = false
var userSetDisplayName: String? = null
var displayIndex: Int = 0
override fun toString() = "$bankName $loginName, ${accounts.size} accounts"
}

View File

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

View File

@ -0,0 +1,12 @@
package net.codinux.banking.client.model
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
open class MessageLogEntry(
open val type: MessageLogEntryType,
open val message: String,
open val messageTrace: String? = null,
open val error: Throwable? = null,
open val time: Instant = Clock.System.now()
)

View File

@ -0,0 +1,9 @@
package net.codinux.banking.client.model
enum class MessageLogEntryType {
Sent,
Received,
Error
}

View File

@ -0,0 +1,65 @@
package net.codinux.banking.client.model
import net.codinux.banking.client.model.config.JsonIgnore
import net.codinux.banking.client.model.config.NoArgConstructor
import net.codinux.banking.client.model.tan.TanMedium
import net.codinux.banking.client.model.tan.TanMethod
@Suppress("RUNTIME_ANNOTATION_NOT_SUPPORTED")
@NoArgConstructor
open class UserAccount(
val bankCode: String,
var loginName: String,
/**
* User may decides to not save password .
*/
var password: String?,
val bankName: String,
val bic: String,
val customerName: String,
val userId: String = loginName,
open val accounts: List<BankAccount> = emptyList(),
/**
* Identifier of selected TanMethod.
*
* 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,
open val tanMethods: List<TanMethod> = listOf(),
/**
* Identifier of selected TanMedium.
*
* 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,
open val tanMedia: List<TanMedium> = listOf(),
var bankingGroup: BankingGroup? = null,
var iconUrl: String? = null,
) {
var wrongCredentialsEntered: Boolean = false
var userSetDisplayName: String? = null
var displayIndex: Int = 0
@get:JsonIgnore
val selectedTanMethod: TanMethod
get() = tanMethods.first { it.identifier == selectedTanMethodId }
@get:JsonIgnore
val selectedTanMedium: TanMedium?
get() = tanMedia.firstOrNull { it.mediumName == selectedTanMediumName }
override fun toString() = "$bankName $loginName, ${accounts.size} accounts"
}

View File

@ -0,0 +1,17 @@
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

@ -3,16 +3,43 @@ package net.codinux.banking.client.model.options
import kotlinx.datetime.LocalDate
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 GetAccountDataOptions(
val retrieveBalance: Boolean = true,
val retrieveTransactions: RetrieveTransactions = RetrieveTransactions.OfLast90Days,
val retrieveTransactionsFrom: LocalDate? = null,
val retrieveTransactionsTo: LocalDate? = null,
val retrieveBalance: Boolean = true,
/**
* Account(s) may should be excluded from data retrieval, so this option enabled to set for which accounts data
* should be retrieved.
*/
val accounts: List<BankAccountIdentifier> = emptyList(),
/**
* 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 abortIfTanIsRequired: Boolean = false,
// account(s) may should get excluded from data retrieval, so add option to set for which accounts data should be retrieved
val accounts: List<BankAccountIdentifier> = emptyList()
// there's also the option preferredTanMedium, but can hardly find a use case for it as we
// cannot know the TanMedium name upfront. In most cases there's only one TanMedium (per TanMethod) anyway.
) {
override fun toString(): String {
return "retrieveBalance=$retrieveBalance, retrieveTransactions=$retrieveTransactions, abortIfTanIsRequired=$abortIfTanIsRequired"

View File

@ -1,20 +1,20 @@
package net.codinux.banking.client.model.response
import net.codinux.banking.client.model.AccountTransaction
import net.codinux.banking.client.model.CustomerAccount
import net.codinux.banking.client.model.UserAccount
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 customer: CustomerAccount
val user: UserAccount
) {
@get:JsonIgnore
val bookedTransactions: List<AccountTransaction>
get() = customer.accounts.flatMap { it.bookedTransactions }.sortedByDescending { it.valueDate }
get() = user.accounts.flatMap { it.bookedTransactions }.sortedByDescending { it.valueDate }
override fun toString() = customer.toString()
override fun toString() = user.toString()
}

View File

@ -1,10 +1,12 @@
package net.codinux.banking.client.model.tan
import net.codinux.banking.client.model.BankAccountIdentifier
import net.codinux.banking.client.model.config.NoArgConstructor
@NoArgConstructor
open class MobilePhoneTanMedium(
val phoneNumber: String?
val phoneNumber: String?,
val concealedPhoneNumber: String? = null
) {
override fun toString() = phoneNumber ?: "No phone number"
}

View File

@ -1,24 +1,61 @@
package net.codinux.banking.client.model.tan
import net.codinux.banking.client.model.BankAccountViewInfo
import net.codinux.banking.client.model.CustomerAccountViewInfo
import net.codinux.banking.client.model.UserAccount
import net.codinux.banking.client.model.UserAccountViewInfo
import net.codinux.banking.client.model.config.JsonIgnore
import net.codinux.banking.client.model.config.NoArgConstructor
@Suppress("RUNTIME_ANNOTATION_NOT_SUPPORTED")
@NoArgConstructor
open class TanChallenge(
val type: TanChallengeType,
val forAction: ActionRequiringTan,
val messageToShowToUser: String,
val tanMethod: TanMethod,
/**
* Identifier of selected TanMethod.
*
* As [availableTanMethods] also contains selected TanMethod, we didn't want to duplicate this object. Use
* [selectedTanMethod] to get selected TanMethod or iterate over [availableTanMethods] and filter selected one by this id.
*/
val selectedTanMethodId: String,
/**
* 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.
*
* Therefore i added list with up to date TanMethods here to ensure EnterTanDialog can display user's up to date TanMethods.
*/
val availableTanMethods: List<TanMethod>,
/**
* Identifier of selected TanMedium.
*
* As [availableTanMedia] also contains selected TanMedium, we didn't want to duplicate this object. Use
* [selectedTanMedium] to get selected TanMedium or iterate over [availableTanMedia] and filter selected one by this medium name.
*/
val selectedTanMediumName: String? = null,
val availableTanMedia: List<TanMedium> = emptyList(),
val tanImage: TanImage? = null,
val flickerCode: FlickerCode? = null,
val customer: CustomerAccountViewInfo,
val user: UserAccountViewInfo,
val account: BankAccountViewInfo? = null
// TODO: add availableTanMethods, selectedTanMedium, availableTanMedia
) {
@get:JsonIgnore
val selectedTanMethod: TanMethod
get() = availableTanMethods.first { it.identifier == selectedTanMethodId }
@get:JsonIgnore
val selectedTanMedium: TanMedium?
get() = availableTanMedia.firstOrNull { it.mediumName == selectedTanMediumName }
override fun toString(): String {
return "$tanMethod $forAction: $messageToShowToUser" + when (type) {
return "$selectedTanMethod $forAction: $messageToShowToUser" + when (type) {
TanChallengeType.EnterTan -> ""
TanChallengeType.Image -> ", Image: $tanImage"
TanChallengeType.Flickercode -> ", FlickerCode: $flickerCode"

View File

@ -1,10 +1,15 @@
package net.codinux.banking.client.model.tan
import kotlinx.datetime.LocalDate
import net.codinux.banking.client.model.config.NoArgConstructor
@NoArgConstructor
open class TanGeneratorTanMedium(
val cardNumber: String
val cardNumber: String,
val cardSequenceNumber: String? = null,
val cardType: Int? = null,
val validFrom: LocalDate? = null,
val validTo: LocalDate? = null
) {
override fun toString() = cardNumber
}

View File

@ -5,7 +5,7 @@ import net.codinux.banking.client.model.config.NoArgConstructor
@NoArgConstructor
open class TanMedium(
val type: TanMediumType,
val displayName: String,
val mediumName: String?,
val status: TanMediumStatus,
/**
* Only set if [type] is [TanMediumType.TanGenerator].
@ -16,5 +16,5 @@ open class TanMedium(
*/
val mobilePhone: MobilePhoneTanMedium? = null
) {
override fun toString() = "$displayName $status"
override fun toString() = "$mediumName $status"
}

View File

@ -1,7 +1,25 @@
package net.codinux.banking.client.model.tan
enum class TanMediumStatus {
/**
* Die Bank zeigt an, dass es eine TAN-Verifikation gegen dieses Medium vornimmt.
*/
Used,
Available
/**
* Das Medium kann genutzt werden, muss aber zuvor mit TAN-Generator an- bzw. ummelden (HKTAU) aktiv gemeldet werden.
*/
Available,
/**
* Mit der ersten Nutzung der Folgekarte wird die zur Zeit aktive Karte gesperrt.
*/
ActiveFollowUpCard,
/**
* Das Medium kann mit dem Geschäftsvorfall TAN-Medium an- bzw. ummelden (HKTAU) aktiv gemeldet werden.
* Die aktuelle Karte kann dann nicht mehr genutzt werden.
*/
AvailableFollowUpCard
}

View File

@ -20,4 +20,21 @@ enum class TanMethodType {
photoTan,
QrCode
;
companion object {
val NonVisual = listOf(TanMethodType.AppTan, TanMethodType.SmsTan, TanMethodType.ChipTanManuell, TanMethodType.EnterTan)
val ImageBased = listOf(TanMethodType.QrCode, TanMethodType.ChipTanQrCode, TanMethodType.photoTan, TanMethodType.ChipTanPhotoTanMatrixCode)
val NonVisualOrImageBased = buildList {
addAll(listOf(TanMethodType.AppTan, TanMethodType.SmsTan, TanMethodType.EnterTan))
addAll(ImageBased)
addAll(listOf(TanMethodType.ChipTanManuell)) // this is quite inconvenient for user, so i added it at last
}
}
}

View File

@ -1,3 +1,5 @@
package net.codinux.banking.client.model.config
@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.CONSTRUCTOR, AnnotationTarget.FIELD, AnnotationTarget.PROPERTY_GETTER)
@Retention(AnnotationRetention.RUNTIME)
actual annotation class JsonIgnore

View File

@ -1,3 +1,5 @@
package net.codinux.banking.client.model.config
@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.CONSTRUCTOR, AnnotationTarget.FIELD, AnnotationTarget.PROPERTY_GETTER)
@Retention(AnnotationRetention.RUNTIME)
actual annotation class JsonIgnore

View File

@ -1,3 +1,5 @@
package net.codinux.banking.client.model.config
@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.CONSTRUCTOR, AnnotationTarget.FIELD, AnnotationTarget.PROPERTY_GETTER)
@Retention(AnnotationRetention.RUNTIME)
actual annotation class JsonIgnore

View File

@ -77,7 +77,7 @@ kotlin {
dependencies {
api(project(":BankingClient"))
api("net.codinux.banking:fints4k:1.0.0-Alpha-12-SNAPSHOT")
api("net.codinux.banking:fints4k:1.0.0-Alpha-12")
api("org.jetbrains.kotlinx:kotlinx-datetime:$kotlinxDateTimeVersion")
}

View File

@ -1,12 +1,12 @@
package net.codinux.banking.client.fints4k
import net.codinux.banking.client.BankingClientCallback
import net.codinux.banking.client.BankingClientForCustomerBase
import net.codinux.banking.client.BankingClientForUserBase
import net.codinux.banking.client.model.AccountCredentials
import net.codinux.banking.fints.config.FinTsClientConfiguration
open class FinTs4kBankingClientForCustomer(credentials: AccountCredentials, config: FinTsClientConfiguration = FinTsClientConfiguration(), callback: BankingClientCallback)
: BankingClientForCustomerBase(credentials, FinTs4kBankingClient(config, callback)) {
open class FinTs4KBankingClientForUser(credentials: AccountCredentials, config: FinTsClientConfiguration = FinTsClientConfiguration(), callback: BankingClientCallback)
: BankingClientForUserBase(credentials, FinTs4kBankingClient(config, callback)) {
constructor(bankCode: String, loginName: String, password: String, callback: BankingClientCallback)
: this(bankCode, loginName, password, FinTsClientConfiguration(), callback)

View File

@ -17,6 +17,11 @@ 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.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 kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
@ -24,27 +29,33 @@ open class FinTs4kMapper {
protected val fintsModelMapper = FinTsModelMapper()
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) },
options.retrieveBalance,
RetrieveTransactions.valueOf(options.retrieveTransactions.name), options.retrieveTransactionsFrom, options.retrieveTransactionsTo,
preferredTanMethods = options.preferredTanMethods?.map { mapTanMethodType(it) },
abortIfTanIsRequired = options.abortIfTanIsRequired
)
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) {
Response.success(GetAccountDataResponse(mapCustomer(response.customerAccount!!)))
Response.success(GetAccountDataResponse(mapUser(response.customerAccount!!)))
} else {
mapError(response)
}
}
open fun mapToCustomerAccountViewInfo(bank: BankData): CustomerAccountViewInfo = CustomerAccountViewInfo(
bank.bankCode, bank.customerId, bank.bankName
open fun mapToUserAccountViewInfo(bank: BankData): UserAccountViewInfo = UserAccountViewInfo(
bank.bankCode, bank.customerId, bank.bankName, getBankingGroup(bank.bankName, bank.bic)
)
open fun mapToBankAccountViewInfo(account: AccountData): BankAccountViewInfo = BankAccountViewInfo(
@ -54,14 +65,22 @@ open class FinTs4kMapper {
)
protected open fun mapCustomer(customer: net.dankito.banking.client.model.CustomerAccount): CustomerAccount = CustomerAccount(
customer.bankCode, customer.loginName, customer.password,
customer.bankName, customer.bic, customer.customerName, customer.userId,
customer.accounts.map { mapAccount(it) }
protected open fun mapUser(user: net.dankito.banking.client.model.CustomerAccount) = UserAccount(
user.bankCode, user.loginName, user.password,
user.bankName, user.bic, user.customerName, user.userId,
user.accounts.map { mapAccount(it) },
user.selectedTanMethod?.securityFunction?.code, user.tanMethods.map { mapTanMethod(it) },
user.selectedTanMedium?.mediumName, user.tanMedia.map { mapTanMedium(it) },
getBankingGroup(user.bankName, user.bic)
)
protected open fun getBankingGroup(bankName: String, bic: String): BankingGroup? =
bankingGroupMapper.getBankingGroup(bankName, bic)
protected open fun mapAccount(account: net.dankito.banking.client.model.BankAccount): BankAccount = BankAccount(
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, account.isAccountTypeSupportedByApplication,
mapFeatures(account),
@ -120,14 +139,20 @@ open class FinTs4kMapper {
open fun mapTanChallenge(challenge: net.codinux.banking.fints.model.TanChallenge): TanChallenge {
val type = mapTanChallengeType(challenge)
val action = mapActionRequiringTan(challenge.forAction)
val tanMethod = mapTanMethod(challenge.tanMethod)
val customer = mapToCustomerAccountViewInfo(challenge.bank)
val tanMethods = challenge.bank.tanMethodsAvailableForUser.map { mapTanMethod(it) }
val selectedTanMethodId = challenge.tanMethod.securityFunction.code
val tanMedia = challenge.bank.tanMedia.map { mapTanMedium(it) }
val selectedTanMediumName = challenge.bank.selectedTanMedium?.mediumName
val user = mapToUserAccountViewInfo(challenge.bank)
val account = challenge.account?.let { mapToBankAccountViewInfo(it) }
val tanImage = if (challenge is ImageTanChallenge) mapTanImage(challenge.image) else null
val flickerCode = if (challenge is FlickerCodeTanChallenge) mapFlickerCode(challenge.flickerCode) else null
return TanChallenge(type, action, challenge.messageToShowToUser, tanMethod, tanImage, flickerCode, customer, account)
return TanChallenge(type, action, challenge.messageToShowToUser, selectedTanMethodId, tanMethods, selectedTanMediumName, tanMedia, tanImage, flickerCode, user, account)
}
protected open fun mapTanChallengeType(challenge: net.codinux.banking.fints.model.TanChallenge): TanChallengeType = when {
@ -157,6 +182,34 @@ open class FinTs4kMapper {
return Base64.Default.encode(bytes)
}
protected open fun mapTanMedium(tanMedium: TanMedium) = net.codinux.banking.client.model.tan.TanMedium(
mapTanMediumType(tanMedium), tanMedium.mediumName, mapTanMediumStatus(tanMedium.status),
(tanMedium as? TanGeneratorTanMedium)?.let { mapTanGeneratorTanMedium(it) },
(tanMedium as? MobilePhoneTanMedium)?.let { mapMobilePhoneTanMedium(it) }
)
protected open fun mapTanMediumStatus(status: TanMediumStatus): net.codinux.banking.client.model.tan.TanMediumStatus = when (status) {
TanMediumStatus.Aktiv -> net.codinux.banking.client.model.tan.TanMediumStatus.Used
TanMediumStatus.Verfuegbar -> net.codinux.banking.client.model.tan.TanMediumStatus.Available
TanMediumStatus.AktivFolgekarte -> net.codinux.banking.client.model.tan.TanMediumStatus.ActiveFollowUpCard
TanMediumStatus.VerfuegbarFolgekarte -> net.codinux.banking.client.model.tan.TanMediumStatus.AvailableFollowUpCard
}
protected open fun mapMobilePhoneTanMedium(tanMedium: MobilePhoneTanMedium) = net.codinux.banking.client.model.tan.MobilePhoneTanMedium(
tanMedium.phoneNumber, tanMedium.concealedPhoneNumber
)
protected open fun mapTanGeneratorTanMedium(tanMedium: TanGeneratorTanMedium) = net.codinux.banking.client.model.tan.TanGeneratorTanMedium(
tanMedium.cardNumber, tanMedium.cardSequenceNumber, tanMedium.cardType,
tanMedium.validFrom, tanMedium.validTo
)
protected open fun mapTanMediumType(tanMedium: TanMedium): TanMediumType = when {
tanMedium is MobilePhoneTanMedium -> TanMediumType.MobilePhone
tanMedium is TanGeneratorTanMedium -> TanMediumType.TanGenerator
else -> TanMediumType.Generic
}
protected open fun mapFlickerCode(flickerCode: net.codinux.banking.fints.tan.FlickerCode): FlickerCode =
FlickerCode(flickerCode.challengeHHD_UC, flickerCode.parsedDataSet, mapException(flickerCode.decodingError))

View File

@ -0,0 +1,72 @@
package net.dankito.banking.banklistcreator.prettifier
import net.codinux.banking.client.model.BankingGroup
// TODO: class has been duplicated from BankListCreator, find a better place for it
class BankingGroupMapper {
fun getBankingGroup(bankName: String, bic: String): BankingGroup? {
val lowercase = bankName.lowercase()
return when {
bankName.contains("Sparda") -> BankingGroup.Sparda
bankName.contains("PSD") -> BankingGroup.PSD
bankName.contains("GLS") -> BankingGroup.GLS
// see https://de.wikipedia.org/wiki/Liste_der_Genossenschaftsbanken_in_Deutschland
bankName.contains("BBBank") || bankName.contains("Evangelische Bank") || bankName.contains("LIGA Bank")
|| bankName.contains("Pax") || bankName.contains("Bank für Kirche und Diakonie") || bankName.contains("Bank im Bistum Essen")
|| bankName.contains("Bank für Schiffahrt") || bankName.contains("Bank für Kirche")
-> BankingGroup.SonstigeGenossenschaftsbank
lowercase.contains("deutsche kreditbank") -> BankingGroup.DKB
// may check against https://de.wikipedia.org/wiki/Liste_der_Sparkassen_in_Deutschland
lowercase.contains("sparkasse") -> BankingGroup.Sparkasse
lowercase.contains("comdirect") -> BankingGroup.Comdirect
lowercase.contains("commerzbank") -> BankingGroup.Commerzbank
lowercase.contains("targo") -> BankingGroup.Targobank
lowercase.contains("santander") -> BankingGroup.Santander
bankName.contains("KfW") -> BankingGroup.KfW
bankName.contains("N26") -> BankingGroup.N26
else -> getBankingGroupByBic(bic)
}
}
private fun getBankingGroupByBic(bic: String): BankingGroup? {
if (bic.length < 4) {
return null
}
if (bic.startsWith("CMCIDEDD")) {
return BankingGroup.Targobank
}
val bankCodeOfBic = bic.substring(0, 4)
return when (bankCodeOfBic) {
"GENO", "VBMH", "VOHA", "VBRS", "DBPB", "VBGT", "FFVB", "WIBA", "VRBU", "MVBM", "VOBA", "ULMV", "VBRT", "VBRA", "VBPF", "VOLO" -> BankingGroup.VolksUndRaiffeisenbanken
"BFSW", // Bank fuer Sozialwirtschaft
"BEVO", // Berliner Volksbank
"DAAE", // apoBank
"MHYP", // Münchener Hypothekenbank
"DZBM", // DZB Bank
"EDEK" // Edekabank
-> BankingGroup.SonstigeGenossenschaftsbank
"BYLA", "SOLA", "NOLA", "WELA", "HELA", "MALA", "BRLA", "NASS", "TRIS", "OSDD", "ESSL", "GOPS", "SBCR", "BRUS" -> BankingGroup.Sparkasse // filter out DBK, (Bayr.) Landesbank, ...
"OLBO" -> BankingGroup.OldenburgischeLandesbank
"DEUT" -> BankingGroup.DeutscheBank
"PBNK" -> BankingGroup.Postbank
"COBA", "DRES" -> BankingGroup.Commerzbank // COBA could also be comdirect, but we cannot differentiate this at this level, this has to do getBankingGroup()
"HYVE" -> BankingGroup.Unicredit
"INGB" -> BankingGroup.ING
"SCFB" -> BankingGroup.Santander
"NORS" -> BankingGroup.Norisbank
"DEGU" -> BankingGroup.Degussa
"OBKL" -> BankingGroup.Oberbank
"MARK" -> BankingGroup.Bundesbank
"KFWI", "DTAB" -> BankingGroup.KfW
"NTSB" -> BankingGroup.N26
"CSDB" -> BankingGroup.Consors
else -> null
}
}
}

View File

@ -19,7 +19,7 @@ class FinTs4kBankingClientTest {
}
private val underTest = FinTs4kBankingClientForCustomer(bankCode, loginName, password, SimpleBankingClientCallback { customer, tanChallenge ->
private val underTest = FinTs4KBankingClientForUser(bankCode, loginName, password, SimpleBankingClientCallback { tanChallenge, callback ->
})

View File

@ -51,7 +51,7 @@ class ShowUsage {
fun getAccountData() {
val client = FinTs4kBankingClientForCustomer(bankCode, loginName, password, SimpleBankingClientCallback())
val client = FinTs4kBankingClientForUser(bankCode, loginName, password, SimpleBankingClientCallback())
val response = client.getAccountData()
@ -61,12 +61,12 @@ class ShowUsage {
private fun printReceivedData(response: Response<GetAccountDataResponse>) {
response.data?.let { data ->
val customer = data.customer
println("Kunde: ${customer.customerName} ${customer.accounts.size} Konten @ ${customer.bic} ${customer.bankName}")
val user = data.user
println("Kunde: ${user.customerName} ${user.accounts.size} Konten @ ${user.bic} ${user.bankName}")
println()
println("Konten:")
customer.accounts.sortedBy { it.type }.forEach { account ->
user.accounts.sortedBy { it.type }.forEach { account ->
println("${account.identifier} ${account.productName} ${account.balance} ${account.currency}")
}
@ -85,7 +85,7 @@ This fetches the booked account transactions of the last 90 days. In most cases
In case there is, add TAN handling in Client Callback:
```kotlin
val client = FinTs4kBankingClientForCustomer(bankCode, loginName, password, SimpleBankingClientCallback { tanChallenge, callback ->
val client = FinTs4kBankingClientForUser(bankCode, loginName, password, SimpleBankingClientCallback { tanChallenge, callback ->
val tan: String? = null // if a TAN is required, add a UI or ...
callback.invoke(EnterTanResult(tan)) // ... set a break point here, get TAN e.g. from your TAN app, set tan variable in debugger view and resume debugger
})

View File

@ -1,5 +1,5 @@
plugins {
kotlin("jvm")
kotlin("jvm") // or kotlin("multiplatform"), depending on your requirements
}

View File

@ -2,7 +2,7 @@ package net.codinux.banking.client.fints4k.example
import kotlinx.datetime.LocalDate
import net.codinux.banking.client.SimpleBankingClientCallback
import net.codinux.banking.client.fints4k.FinTs4kBankingClientForCustomer
import net.codinux.banking.client.fints4k.FinTs4kBankingClientForUser
import net.codinux.banking.client.getAccountData
import net.codinux.banking.client.model.options.GetAccountDataOptions
import net.codinux.banking.client.model.options.RetrieveTransactions
@ -27,7 +27,7 @@ class ShowUsage {
fun getAccountDataSimpleExample() {
val client = FinTs4kBankingClientForCustomer(bankCode, loginName, password, SimpleBankingClientCallback())
val client = FinTs4kBankingClientForUser(bankCode, loginName, password, SimpleBankingClientCallback())
val response = client.getAccountData()
@ -35,7 +35,7 @@ class ShowUsage {
}
fun getAccountDataFullExample() {
val client = FinTs4kBankingClientForCustomer(bankCode, loginName, password, SimpleBankingClientCallback { tanChallenge, callback ->
val client = FinTs4kBankingClientForUser(bankCode, loginName, password, SimpleBankingClientCallback { tanChallenge, callback ->
val tan: String? = null // if a TAN is required, add a UI or ...
callback.invoke(EnterTanResult(tan)) // ... set a break point here, get TAN e.g. from your TAN app, set tan variable in debugger view and resume debugger
})
@ -59,12 +59,12 @@ class ShowUsage {
private fun printReceivedData(response: Response<GetAccountDataResponse>) {
response.data?.let { data ->
val customer = data.customer
println("Kunde: ${customer.customerName} ${customer.accounts.size} Konten @ ${customer.bic} ${customer.bankName}")
val user = data.user
println("Kunde: ${user.customerName} ${user.accounts.size} Konten @ ${user.bic} ${user.bankName}")
println()
println("Konten:")
customer.accounts.sortedBy { it.type }.forEach { account ->
user.accounts.sortedBy { it.type }.forEach { account ->
println("${account.identifier} ${account.productName} ${account.balance} ${account.currency}")
}

View File

@ -12,7 +12,7 @@ buildscript {
allprojects {
group = "net.codinux.banking.client"
version = "0.5.1-SNAPSHOT"
version = "0.5.2-SNAPSHOT"
repositories {
mavenCentral()
@ -34,7 +34,7 @@ tasks.register("publishAllToMavenLocal") {
":BankingClientModel:publishToMavenLocal",
":BankingClient:publishToMavenLocal",
":FinTs4jBankingClient:publishToMavenLocal"
":fints4k-banking-client:publishToMavenLocal"
)
}
@ -43,6 +43,6 @@ tasks.register("publishAll") {
":BankingClientModel:publish",
":BankingClient:publish",
":FinTs4jBankingClient:publish"
":fints4k-banking-client:publish"
)
}