Compare commits
17 Commits
62276e2a02
...
bd052587c5
Author | SHA1 | Date |
---|---|---|
dankito | bd052587c5 | |
dankito | 36391c8f20 | |
dankito | 7f60e02340 | |
dankito | 85b66aa040 | |
dankito | a9ebad0793 | |
dankito | 5bfe492ada | |
dankito | f17c9bb781 | |
dankito | 469ee275c9 | |
dankito | ba662fda15 | |
dankito | 80656bdcdd | |
dankito | 000a169a00 | |
dankito | 9a2bc6b430 | |
dankito | c3c0b2830e | |
dankito | aab562ccf4 | |
dankito | b210c1e7fa | |
dankito | b0b0fa6140 | |
dankito | f1dad3bc26 |
|
@ -1,8 +1,10 @@
|
|||
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.response.GetAccountDataResponse
|
||||
import net.codinux.banking.client.model.response.GetTransactionsResponse
|
||||
import net.codinux.banking.client.model.response.Response
|
||||
|
||||
interface BankingClient {
|
||||
|
@ -30,4 +32,11 @@ interface BankingClient {
|
|||
*/
|
||||
suspend fun getAccountDataAsync(request: GetAccountDataRequest): Response<GetAccountDataResponse>
|
||||
|
||||
/**
|
||||
* Convenience wrapper around [getAccountDataAsync].
|
||||
* Updates account's transactions beginning from [BankAccount.lastTransactionsRetrievalTime].
|
||||
* This may requires TAN if [BankAccount.lastTransactionsRetrievalTime] is older than 90 days.
|
||||
*/
|
||||
suspend fun updateAccountTransactionsAsync(user: UserAccount, accounts: List<BankAccount>? = null): Response<List<GetTransactionsResponse>>
|
||||
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
package net.codinux.banking.client
|
||||
|
||||
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.response.GetAccountDataResponse
|
||||
import net.codinux.banking.client.model.response.GetTransactionsResponse
|
||||
import net.codinux.banking.client.model.response.Response
|
||||
|
||||
interface BankingClientForUser {
|
||||
|
@ -30,4 +32,11 @@ interface BankingClientForUser {
|
|||
*/
|
||||
suspend fun getAccountDataAsync(options: GetAccountDataOptions): Response<GetAccountDataResponse>
|
||||
|
||||
/**
|
||||
* Convenience wrapper around [getAccountDataAsync].
|
||||
* 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>>
|
||||
|
||||
}
|
|
@ -1,15 +1,27 @@
|
|||
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.options.GetAccountDataOptions
|
||||
import net.codinux.banking.client.model.request.GetAccountDataRequest
|
||||
import net.codinux.banking.client.model.response.GetTransactionsResponse
|
||||
import net.codinux.banking.client.model.response.Response
|
||||
|
||||
abstract class BankingClientForUserBase(
|
||||
protected val credentials: AccountCredentials,
|
||||
protected val client: BankingClient
|
||||
) : BankingClientForUser {
|
||||
|
||||
private lateinit var user: UserAccount
|
||||
|
||||
override suspend fun getAccountDataAsync(options: GetAccountDataOptions) =
|
||||
client.getAccountDataAsync(GetAccountDataRequest(credentials, options))
|
||||
client.getAccountDataAsync(GetAccountDataRequest(credentials, options)).also {
|
||||
it.data?.user?.let { retrievedUser ->
|
||||
this.user = retrievedUser
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun updateAccountTransactionsAsync(): Response<List<GetTransactionsResponse>> =
|
||||
client.updateAccountTransactionsAsync(user)
|
||||
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package net.codinux.banking.client.service
|
||||
|
||||
import net.codinux.banking.client.model.AccountTransaction
|
||||
|
||||
open class BankingModelService {
|
||||
|
||||
/**
|
||||
* It's not possible to retrieve only new transactions from bank server (almost no bank implements HKKAN job). So
|
||||
* for updating account transactions we start at the date of latest account transactions retrieval time (e.g.
|
||||
* transactions have at last been fetched at 01. September 12:00, then there may have been some other transactions
|
||||
* been booked on September 1st after 12:00 o'clock).
|
||||
*
|
||||
* Therefore retrieved account transactions may contain transactions that we already have locally. This method filters
|
||||
* from [retrievedTransactions] those already in [existingTransactions] and returns only that ones, that are not in
|
||||
* [existingTransactions].
|
||||
*/
|
||||
open fun findNewTransactions(retrievedTransactions: List<AccountTransaction>, existingTransactions: List<AccountTransaction>): List<AccountTransaction> {
|
||||
val existingTransactionsByIdentifier = existingTransactions.associateBy { it.identifier }
|
||||
val existingTransactionsIdentifiers = existingTransactionsByIdentifier.keys
|
||||
|
||||
return retrievedTransactions.filter { transaction -> existingTransactionsIdentifiers.contains(transaction.identifier) == false }
|
||||
}
|
||||
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
package net.codinux.banking.client
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.codinux.banking.client.model.BankAccount
|
||||
import net.codinux.banking.client.model.UserAccount
|
||||
import net.codinux.banking.client.model.options.GetAccountDataOptions
|
||||
import net.codinux.banking.client.model.request.GetAccountDataRequest
|
||||
|
||||
|
@ -12,6 +14,11 @@ fun BankingClient.getAccountData(request: GetAccountDataRequest) = runBlocking {
|
|||
this@getAccountData.getAccountDataAsync(request)
|
||||
}
|
||||
|
||||
fun BankingClient.updateAccountTransactions(user: UserAccount, accounts: List<BankAccount>? = null) = runBlocking {
|
||||
this@updateAccountTransactions.updateAccountTransactionsAsync(user, accounts)
|
||||
}
|
||||
|
||||
|
||||
fun BankingClientForUser.getAccountData() = runBlocking {
|
||||
this@getAccountData.getAccountDataAsync()
|
||||
}
|
||||
|
@ -19,3 +26,7 @@ fun BankingClientForUser.getAccountData() = runBlocking {
|
|||
fun BankingClientForUser.getAccountData(options: GetAccountDataOptions) = runBlocking {
|
||||
this@getAccountData.getAccountDataAsync(options)
|
||||
}
|
||||
|
||||
fun BankingClientForUser.updateAccountTransactions() = runBlocking {
|
||||
this@updateAccountTransactions.updateAccountTransactionsAsync()
|
||||
}
|
|
@ -7,7 +7,7 @@ import net.codinux.banking.client.model.config.NoArgConstructor
|
|||
open class AccountTransaction(
|
||||
val amount: Amount = Amount.Zero,
|
||||
val currency: String,
|
||||
val reference: String, // Alternative: purpose (or Remittance information)
|
||||
val unparsedReference: String, // Alternative: purpose (or Remittance information)
|
||||
|
||||
/**
|
||||
* Transaction date (Buchungstag) - der Tag, an dem ein Zahlungsvorgang in das System einer Bank eingegangen ist.
|
||||
|
@ -62,5 +62,13 @@ open class AccountTransaction(
|
|||
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"
|
||||
}
|
||||
|
||||
override fun toString() = "${valueDate.dayOfMonth}.${valueDate.monthNumber}.${valueDate.year} ${amount.toString().padStart(4, ' ')} ${if (currency == "EUR") "€" else currency} ${otherPartyName ?: ""} - $reference"
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
package net.codinux.banking.client.model
|
||||
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.datetime.LocalDate
|
||||
import net.codinux.banking.client.model.config.JsonIgnore
|
||||
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||
|
||||
@NoArgConstructor
|
||||
|
@ -14,13 +16,14 @@ open class BankAccount(
|
|||
val currency: String = "EUR",
|
||||
var accountLimit: String? = null,
|
||||
|
||||
val isAccountTypeSupportedByApplication: Boolean = true,
|
||||
val features: Set<BankAccountFeatures> = emptySet(),
|
||||
|
||||
// var balance: BigDecimal = BigDecimal.ZERO,
|
||||
var balance: Amount = Amount.Zero, // TODO: add a BigDecimal library
|
||||
|
||||
val isAccountTypeSupportedByApplication: Boolean = false,
|
||||
val features: Set<BankAccountFeatures> = emptySet(),
|
||||
|
||||
open var lastTransactionsRetrievalTime: Instant? = null,
|
||||
var retrievedTransactionsFrom: LocalDate? = null,
|
||||
var retrievedTransactionsTo: LocalDate? = null,
|
||||
|
||||
var haveAllTransactionsBeenRetrieved: Boolean = false,
|
||||
val countDaysForWhichTransactionsAreKept: Int? = null,
|
||||
|
@ -34,5 +37,12 @@ open class BankAccount(
|
|||
var hideAccount: Boolean = false,
|
||||
var includeInAutomaticAccountsUpdate: Boolean = true
|
||||
) {
|
||||
@get:JsonIgnore
|
||||
open val displayName: String
|
||||
get() = userSetDisplayName ?: productName ?: identifier
|
||||
|
||||
fun supportsAnyFeature(vararg features: BankAccountFeatures): Boolean =
|
||||
features.any { this.features.contains(it) }
|
||||
|
||||
override fun toString() = "$type $identifier $productName (IBAN: $iban)"
|
||||
}
|
|
@ -51,6 +51,11 @@ open class UserAccount(
|
|||
var displayIndex: Int = 0
|
||||
|
||||
|
||||
@get:JsonIgnore
|
||||
open val displayName: String
|
||||
get() = userSetDisplayName ?: bankName
|
||||
|
||||
|
||||
@get:JsonIgnore
|
||||
val selectedTanMethod: TanMethod
|
||||
get() = tanMethods.first { it.identifier == selectedTanMethodId }
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
package net.codinux.banking.client.model.response
|
||||
|
||||
import kotlinx.datetime.Instant
|
||||
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.config.NoArgConstructor
|
||||
|
||||
@NoArgConstructor
|
||||
open class GetTransactionsResponse(
|
||||
val account: BankAccount,
|
||||
val balance: Amount? = null,
|
||||
val bookedTransactions: List<AccountTransaction>,
|
||||
val unbookedTransactions: List<UnbookedAccountTransaction>,
|
||||
val transactionsRetrievalTime: Instant,
|
||||
val retrievedTransactionsFrom: LocalDate? = null,
|
||||
val retrievedTransactionsTo: LocalDate? = null
|
||||
) {
|
||||
override fun toString() = "${account.productName} $balance, ${bookedTransactions.size} booked transactions from $retrievedTransactionsFrom"
|
||||
}
|
|
@ -9,9 +9,9 @@ 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,
|
||||
open val type: TanChallengeType,
|
||||
open val forAction: ActionRequiringTan,
|
||||
open val messageToShowToUser: String,
|
||||
|
||||
/**
|
||||
* Identifier of selected TanMethod.
|
||||
|
@ -19,7 +19,7 @@ open class TanChallenge(
|
|||
* 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,
|
||||
open val selectedTanMethodId: String,
|
||||
/**
|
||||
* When adding an account, frontend has no UserAccount object in BankingClientCallback to know which TanMethods are
|
||||
* available for User.
|
||||
|
@ -28,7 +28,7 @@ open class TanChallenge(
|
|||
*
|
||||
* 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>,
|
||||
open val availableTanMethods: List<TanMethod>,
|
||||
|
||||
/**
|
||||
* Identifier of selected TanMedium.
|
||||
|
@ -36,24 +36,35 @@ open class TanChallenge(
|
|||
* 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(),
|
||||
open val selectedTanMediumName: String? = null,
|
||||
open val availableTanMedia: List<TanMedium> = emptyList(),
|
||||
|
||||
val tanImage: TanImage? = null,
|
||||
val flickerCode: FlickerCode? = null,
|
||||
val user: UserAccountViewInfo,
|
||||
val account: BankAccountViewInfo? = null
|
||||
open val tanImage: TanImage? = null,
|
||||
open val flickerCode: FlickerCode? = null,
|
||||
open val user: UserAccountViewInfo,
|
||||
open val account: BankAccountViewInfo? = null
|
||||
) {
|
||||
|
||||
@get:JsonIgnore
|
||||
val selectedTanMethod: TanMethod
|
||||
open val selectedTanMethod: TanMethod
|
||||
get() = availableTanMethods.first { it.identifier == selectedTanMethodId }
|
||||
|
||||
@get:JsonIgnore
|
||||
val selectedTanMedium: TanMedium?
|
||||
open val selectedTanMedium: TanMedium?
|
||||
get() = availableTanMedia.firstOrNull { it.mediumName == selectedTanMediumName }
|
||||
|
||||
|
||||
/**
|
||||
* Principally a no-op method, not implemented for all client, only implementing client for not: 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.
|
||||
*/
|
||||
open fun addUserApprovedDecoupledTanCallback(callback: () -> Unit) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
override fun toString(): String {
|
||||
return "$selectedTanMethod $forAction: $messageToShowToUser" + when (type) {
|
||||
TanChallengeType.EnterTan -> ""
|
||||
|
|
|
@ -4,6 +4,10 @@ import kotlinx.datetime.LocalDate
|
|||
import net.codinux.banking.client.model.config.NoArgConstructor
|
||||
|
||||
@NoArgConstructor
|
||||
/**
|
||||
* 'TanGenerator' is in most cases a debit card, but can also be something like "BestSign" app (Postbank).
|
||||
* In latter case [cardNumber] can also be, contrary to specification, be an empty string.
|
||||
*/
|
||||
open class TanGeneratorTanMedium(
|
||||
val cardNumber: String,
|
||||
val cardSequenceNumber: String? = null,
|
||||
|
|
|
@ -16,5 +16,49 @@ open class TanMedium(
|
|||
*/
|
||||
val mobilePhone: MobilePhoneTanMedium? = null
|
||||
) {
|
||||
|
||||
/**
|
||||
* Using only [mediumName] as identifier does not work as e.g. Sparkasse names all their TanGenerator TAN media
|
||||
* "SparkassenCard (Debitkarte)" - so it's not possible to differentiate between them solely by medium name.
|
||||
*/
|
||||
val identifier: String by lazy {
|
||||
// TODO: translate
|
||||
var id = mediumName ?: when (type) {
|
||||
TanMediumType.MobilePhone -> "Mobiltelefon"
|
||||
TanMediumType.TanGenerator -> "Tan Generator"
|
||||
TanMediumType.Generic -> "Unbenanntes TAN Medium"
|
||||
}
|
||||
|
||||
if (mobilePhone != null) {
|
||||
id += " " + (mobilePhone.concealedPhoneNumber ?: mobilePhone.phoneNumber)
|
||||
}
|
||||
if (tanGenerator != null) {
|
||||
if (tanGenerator.cardNumber.isNotBlank()) {
|
||||
id += " " + tanGenerator.cardNumber
|
||||
}
|
||||
if (tanGenerator.cardSequenceNumber.isNullOrBlank() == false) {
|
||||
id += " " + tanGenerator.cardSequenceNumber
|
||||
}
|
||||
|
||||
if (tanGenerator.validFrom != null && tanGenerator.validTo != null) {
|
||||
id += ", gültig von " + tanGenerator.validFrom.let { "${it.dayOfMonth}.${it.monthNumber}${it.year}" } +
|
||||
" - " + tanGenerator.validTo.let { "${it.dayOfMonth}.${it.monthNumber}${it.year}" }
|
||||
} else if (tanGenerator.validTo != null) {
|
||||
id += ", gültig bis " + tanGenerator.validTo.let { "${it.dayOfMonth}.${it.monthNumber}${it.year}" }
|
||||
}
|
||||
}
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
val displayName: String by lazy {
|
||||
identifier + " " + when (status) {
|
||||
TanMediumStatus.Used -> "Aktive"
|
||||
TanMediumStatus.Available -> "Verfügbar"
|
||||
TanMediumStatus.ActiveFollowUpCard -> " Folgekarte, aktiv bei erster Nutzung"
|
||||
TanMediumStatus.AvailableFollowUpCard -> " Folgekarte, die erst aktiviert werden muss"
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString() = "$mediumName $status"
|
||||
}
|
|
@ -4,11 +4,11 @@ import net.codinux.banking.client.model.config.NoArgConstructor
|
|||
|
||||
@NoArgConstructor
|
||||
open class TanMethod(
|
||||
val displayName: String,
|
||||
val type: TanMethodType,
|
||||
val identifier: String,
|
||||
val maxTanInputLength: Int? = null,
|
||||
val allowedTanFormat: AllowedTanFormat = AllowedTanFormat.Alphanumeric
|
||||
open val displayName: String,
|
||||
open val type: TanMethodType,
|
||||
open val identifier: String,
|
||||
open val maxTanInputLength: Int? = null,
|
||||
open val allowedTanFormat: AllowedTanFormat = AllowedTanFormat.Alphanumeric
|
||||
) {
|
||||
override fun toString() = "$displayName ($type, ${identifier})"
|
||||
}
|
|
@ -17,22 +17,30 @@ enum class TanMethodType {
|
|||
|
||||
AppTan,
|
||||
|
||||
DecoupledTan,
|
||||
|
||||
DecoupledPushTan,
|
||||
|
||||
photoTan,
|
||||
|
||||
QrCode
|
||||
;
|
||||
|
||||
|
||||
val isDecoupledMethod: Boolean
|
||||
get() = this == DecoupledTan || this == DecoupledPushTan
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
val NonVisual = listOf(TanMethodType.AppTan, TanMethodType.SmsTan, TanMethodType.ChipTanManuell, TanMethodType.EnterTan)
|
||||
val NonVisual = listOf(TanMethodType.DecoupledTan, TanMethodType.DecoupledPushTan, 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(listOf(TanMethodType.DecoupledTan, TanMethodType.DecoupledPushTan, TanMethodType.AppTan, TanMethodType.SmsTan, TanMethodType.EnterTan))
|
||||
addAll(ImageBased)
|
||||
addAll(listOf(TanMethodType.ChipTanManuell)) // this is quite inconvenient for user, so i added it at last
|
||||
addAll(listOf(TanMethodType.ChipTanManuell)) // this is quite inconvenient for user, so i added it as last
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -77,7 +77,7 @@ kotlin {
|
|||
dependencies {
|
||||
api(project(":BankingClient"))
|
||||
|
||||
api("net.codinux.banking:fints4k:1.0.0-Alpha-12")
|
||||
api("net.codinux.banking:fints4k:1.0.0-Alpha-13-SNAPSHOT")
|
||||
|
||||
api("org.jetbrains.kotlinx:kotlinx-datetime:$kotlinxDateTimeVersion")
|
||||
}
|
||||
|
|
|
@ -2,12 +2,15 @@ package net.codinux.banking.client.fints4k
|
|||
|
||||
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.options.GetAccountDataOptions
|
||||
import net.codinux.banking.client.model.request.GetAccountDataRequest
|
||||
import net.codinux.banking.client.model.response.GetAccountDataResponse
|
||||
import net.codinux.banking.client.model.response.Response
|
||||
import net.codinux.banking.client.model.response.*
|
||||
import net.codinux.banking.fints.FinTsClient
|
||||
import net.codinux.banking.fints.config.FinTsClientConfiguration
|
||||
import net.codinux.banking.fints.model.BankData
|
||||
|
||||
open class FinTs4kBankingClient(
|
||||
config: FinTsClientConfiguration = FinTsClientConfiguration(),
|
||||
|
@ -28,4 +31,28 @@ open class FinTs4kBankingClient(
|
|||
return mapper.map(response)
|
||||
}
|
||||
|
||||
override suspend fun updateAccountTransactionsAsync(user: UserAccount, accounts: List<BankAccount>?): Response<List<GetTransactionsResponse>> {
|
||||
val accountsToRequest = (accounts ?: user.accounts).filter { it.supportsAnyFeature(BankAccountFeatures.RetrieveBalance, BankAccountFeatures.RetrieveBalance) }
|
||||
|
||||
if (accountsToRequest.isNotEmpty()) {
|
||||
var finTsModel: BankData? = null
|
||||
|
||||
val responses = accountsToRequest.map { account ->
|
||||
val parameter = mapper.mapToUpdateAccountTransactionsParameter(user, account, finTsModel)
|
||||
|
||||
val response = client.getAccountDataAsync(parameter)
|
||||
|
||||
if (response.finTsModel != null) {
|
||||
finTsModel = response.finTsModel // so that basic account data doesn't have to be retrieved another time if user has multiple accounts
|
||||
}
|
||||
|
||||
Triple(account, parameter, response)
|
||||
}
|
||||
|
||||
return mapper.map(responses)
|
||||
}
|
||||
|
||||
return Response.error(ErrorType.NoneOfTheAccountsSupportsRetrievingData, "Keiner der Konten unterstützt das Abholen der Umsätze oder des Kontostands") // TODO: translate
|
||||
}
|
||||
|
||||
}
|
|
@ -5,7 +5,7 @@ import net.codinux.banking.client.BankingClientForUserBase
|
|||
import net.codinux.banking.client.model.AccountCredentials
|
||||
import net.codinux.banking.fints.config.FinTsClientConfiguration
|
||||
|
||||
open class FinTs4KBankingClientForUser(credentials: AccountCredentials, config: FinTsClientConfiguration = FinTsClientConfiguration(), callback: BankingClientCallback)
|
||||
open class FinTs4kBankingClientForUser(credentials: AccountCredentials, config: FinTsClientConfiguration = FinTsClientConfiguration(), callback: BankingClientCallback)
|
||||
: BankingClientForUserBase(credentials, FinTs4kBankingClient(config, callback)) {
|
||||
|
||||
constructor(bankCode: String, loginName: String, password: String, callback: BankingClientCallback)
|
|
@ -1,5 +1,8 @@
|
|||
package net.codinux.banking.client.fints4k
|
||||
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import net.codinux.banking.client.model.*
|
||||
import net.codinux.banking.client.model.AccountTransaction
|
||||
import net.codinux.banking.client.model.Amount
|
||||
|
@ -11,6 +14,7 @@ import net.codinux.banking.client.model.tan.TanChallenge
|
|||
import net.codinux.banking.client.model.tan.TanImage
|
||||
import net.codinux.banking.client.model.tan.TanMethod
|
||||
import net.codinux.banking.client.model.tan.TanMethodType
|
||||
import net.codinux.banking.fints.extensions.EuropeBerlin
|
||||
import net.dankito.banking.client.model.BankAccountIdentifierImpl
|
||||
import net.dankito.banking.client.model.parameter.GetAccountDataParameter
|
||||
import net.dankito.banking.client.model.parameter.RetrieveTransactions
|
||||
|
@ -41,6 +45,21 @@ open class FinTs4kMapper {
|
|||
abortIfTanIsRequired = options.abortIfTanIsRequired
|
||||
)
|
||||
|
||||
open fun mapToUpdateAccountTransactionsParameter(user: UserAccount, account: BankAccount, finTsModel: BankData?): GetAccountDataParameter {
|
||||
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 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>()
|
||||
|
||||
return GetAccountDataParameter(user.bankCode, user.loginName, user.password!!, listOf(accountIdentifier), true,
|
||||
retrieveTransactions, from,
|
||||
preferredTanMethods = preferredTanMethods,
|
||||
preferredTanMedium = user.selectedTanMediumName,
|
||||
finTsModel = finTsModel
|
||||
)
|
||||
}
|
||||
|
||||
protected open fun mapTanMethodType(type: TanMethodType): net.codinux.banking.fints.model.TanMethodType =
|
||||
net.codinux.banking.fints.model.TanMethodType.valueOf(type.name)
|
||||
|
||||
|
@ -53,6 +72,35 @@ open class FinTs4kMapper {
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// TODO: update UserAccount and BankAccount objects according to retrieved data
|
||||
val mappedResponses = responses.map { (account, param, getAccountDataResponse) ->
|
||||
val user = getAccountDataResponse.customerAccount
|
||||
val finTsBankAccount = user?.accounts?.firstOrNull { it.identifier == account.identifier && it.subAccountNumber == account.subAccountNumber }
|
||||
|
||||
if (getAccountDataResponse.successful && user != null && finTsBankAccount != null) {
|
||||
if (finTsBankAccount.lastTransactionsRetrievalTime != null) {
|
||||
account.lastTransactionsRetrievalTime = finTsBankAccount.lastTransactionsRetrievalTime
|
||||
}
|
||||
if (account.retrievedTransactionsFrom == null || (finTsBankAccount.retrievedTransactionsFrom != null
|
||||
&& account.retrievedTransactionsFrom!! < finTsBankAccount.retrievedTransactionsFrom!!)) {
|
||||
account.retrievedTransactionsFrom = finTsBankAccount.retrievedTransactionsFrom
|
||||
}
|
||||
|
||||
Response.success(GetTransactionsResponse(account, mapAmount(finTsBankAccount.balance), mapBookedTransactions(finTsBankAccount), emptyList(),
|
||||
finTsBankAccount.lastTransactionsRetrievalTime ?: Clock.System.now(), param.retrieveTransactionsFrom, param.retrieveTransactionsTo))
|
||||
} else {
|
||||
mapError(getAccountDataResponse)
|
||||
}
|
||||
}
|
||||
|
||||
val data = mappedResponses.filter { it.type == ResponseType.Success }.mapNotNull { it.data }
|
||||
|
||||
return (object : Response<List<GetTransactionsResponse>>(type, data, mappedResponses.firstNotNullOfOrNull { it.error }) { })
|
||||
}
|
||||
|
||||
|
||||
open fun mapToUserAccountViewInfo(bank: BankData): UserAccountViewInfo = UserAccountViewInfo(
|
||||
bank.bankCode, bank.customerId, bank.bankName, getBankingGroup(bank.bankName, bank.bic)
|
||||
|
@ -82,12 +130,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, account.isAccountTypeSupportedByApplication,
|
||||
mapFeatures(account),
|
||||
mapAmount(account.balance), account.retrievedTransactionsFrom, account.retrievedTransactionsTo,
|
||||
account.productName, account.currency, account.accountLimit, mapAmount(account.balance),
|
||||
account.isAccountTypeSupportedByApplication, mapFeatures(account),
|
||||
account.lastTransactionsRetrievalTime, account.retrievedTransactionsFrom,
|
||||
// TODO: map haveAllTransactionsBeenRetrieved
|
||||
countDaysForWhichTransactionsAreKept = account.countDaysForWhichTransactionsAreKept,
|
||||
bookedTransactions = account.bookedTransactions.map { mapTransaction(it) }.toMutableList()
|
||||
bookedTransactions = mapBookedTransactions(account).toMutableList()
|
||||
)
|
||||
|
||||
protected open fun mapAccountType(type: net.dankito.banking.client.model.BankAccountType): BankAccountType =
|
||||
|
@ -109,6 +157,9 @@ open class FinTs4kMapper {
|
|||
}
|
||||
|
||||
|
||||
protected open fun mapBookedTransactions(account: net.dankito.banking.client.model.BankAccount): List<AccountTransaction> =
|
||||
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,
|
||||
transaction.bookingDate, transaction.valueDate,
|
||||
|
@ -144,7 +195,8 @@ open class FinTs4kMapper {
|
|||
val selectedTanMethodId = challenge.tanMethod.securityFunction.code
|
||||
|
||||
val tanMedia = challenge.bank.tanMedia.map { mapTanMedium(it) }
|
||||
val selectedTanMediumName = challenge.bank.selectedTanMedium?.mediumName
|
||||
// TanMedium has not natural id in FinTS model so we have to create our own one
|
||||
val selectedTanMediumName = challenge.bank.selectedTanMedium?.let { selected -> tanMedia.firstOrNull { it == selected } }?.identifier
|
||||
|
||||
val user = mapToUserAccountViewInfo(challenge.bank)
|
||||
val account = challenge.account?.let { mapToBankAccountViewInfo(it) }
|
||||
|
@ -152,7 +204,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 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) {
|
||||
override fun addUserApprovedDecoupledTanCallback(callback: () -> Unit) {
|
||||
challenge.addUserApprovedDecoupledTanCallback(callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun mapTanChallengeType(challenge: net.codinux.banking.fints.model.TanChallenge): TanChallengeType = when {
|
||||
|
|
|
@ -19,7 +19,7 @@ class FinTs4kBankingClientTest {
|
|||
}
|
||||
|
||||
|
||||
private val underTest = FinTs4KBankingClientForUser(bankCode, loginName, password, SimpleBankingClientCallback { tanChallenge, callback ->
|
||||
private val underTest = FinTs4kBankingClientForUser(bankCode, loginName, password, SimpleBankingClientCallback { tanChallenge, callback ->
|
||||
|
||||
})
|
||||
|
||||
|
|
58
README.md
58
README.md
|
@ -51,11 +51,11 @@ class ShowUsage {
|
|||
|
||||
|
||||
fun getAccountData() {
|
||||
val client = FinTs4kBankingClientForUser(bankCode, loginName, password, SimpleBankingClientCallback())
|
||||
val client = FinTs4kBankingClient(SimpleBankingClientCallback())
|
||||
|
||||
val response = client.getAccountData()
|
||||
val response = client.getAccountData(bankCode, loginName, password) // that's all
|
||||
|
||||
printReceivedData(response)
|
||||
printReceivedData(response) // now print retrieved data (save it to database, display it in UI, ...)
|
||||
}
|
||||
|
||||
|
||||
|
@ -82,15 +82,6 @@ class ShowUsage {
|
|||
|
||||
This fetches the booked account transactions of the last 90 days. In most cases no TAN is required for this.
|
||||
|
||||
In case there is, add TAN handling in Client Callback:
|
||||
|
||||
```kotlin
|
||||
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
|
||||
})
|
||||
```
|
||||
|
||||
You can also specify options e.g. which transactions should be retrieved:
|
||||
|
||||
```kotlin
|
||||
|
@ -104,6 +95,15 @@ val options = GetAccountDataOptions(
|
|||
val response = client.getAccountData(options)
|
||||
```
|
||||
|
||||
Retrieving transactions older than 90 days requires a TAN, so add TAN handling in Client Callback:
|
||||
|
||||
```kotlin
|
||||
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
|
||||
})
|
||||
```
|
||||
|
||||
Add some error handling by checking `response.error`:
|
||||
|
||||
```kotlin
|
||||
|
@ -111,3 +111,37 @@ response.error?.let{ error ->
|
|||
println("Could not fetch account data: ${error.internalError ?: error.errorMessagesFromBank.joinToString()}")
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Update Account Transactions
|
||||
|
||||
The data model saves when it retrieved account transactions the last time (in `BankAccount.lastTransactionsRetrievalTime`).
|
||||
So you only need to call `FinTs4kBankingClient.updateAccountTransactions()` to retrieve all transactions starting from
|
||||
`BankAccount.lastTransactionsRetrievalTime`.
|
||||
|
||||
But as we can only specify from which day on account transactions should be retrieved, response may contain some transactions
|
||||
from the day of `lastTransactionsRetrievalTime` that we already have locally. To filter out these you can use
|
||||
`BankingModelService().findNewTransactions(retrieveTransactions, existingTransactions)`:
|
||||
|
||||
```kotlin
|
||||
fun updateAccountTransactions() {
|
||||
val client = FinTs4kBankingClientForUser(bankCode, loginName, password, SimpleBankingClientCallback())
|
||||
|
||||
// simulate account transactions we retrieved last time
|
||||
val today = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date
|
||||
val lastCallToBankServer = client.getAccountData(GetAccountDataOptions(RetrieveTransactions.AccordingToRetrieveFromAndTo, retrieveTransactionsFrom = today.minusDays(90), retrieveTransactionsTo = today.minusDays(30)))
|
||||
|
||||
if (lastCallToBankServer.data != null) { // now update account transactions
|
||||
val existingTransactions = lastCallToBankServer.data!!.bookedTransactions
|
||||
|
||||
val updateTransactionsResponse = client.updateAccountTransactions()
|
||||
|
||||
if (updateTransactionsResponse.data != null) {
|
||||
val retrievedTransactions = updateTransactionsResponse.data!!.flatMap { it.bookedTransactions }
|
||||
val newTransactions = BankingModelService().findNewTransactions(retrievedTransactions, existingTransactions)
|
||||
// `retrievedTransactions` may contain transactions we already have locally, `newTransactions` only
|
||||
// transactions that are not in `existingTransactions`
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
|
@ -12,5 +12,5 @@ repositories {
|
|||
|
||||
|
||||
dependencies {
|
||||
implementation("net.codinux.banking.client:fints4k-banking-client:0.5.0")
|
||||
implementation(project(":fints4k-banking-client"))
|
||||
}
|
|
@ -1,7 +1,11 @@
|
|||
package net.codinux.banking.client.fints4k.example
|
||||
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import net.codinux.banking.client.SimpleBankingClientCallback
|
||||
import net.codinux.banking.client.fints4k.FinTs4kBankingClient
|
||||
import net.codinux.banking.client.fints4k.FinTs4kBankingClientForUser
|
||||
import net.codinux.banking.client.getAccountData
|
||||
import net.codinux.banking.client.model.options.GetAccountDataOptions
|
||||
|
@ -9,6 +13,9 @@ import net.codinux.banking.client.model.options.RetrieveTransactions
|
|||
import net.codinux.banking.client.model.response.GetAccountDataResponse
|
||||
import net.codinux.banking.client.model.response.Response
|
||||
import net.codinux.banking.client.model.tan.EnterTanResult
|
||||
import net.codinux.banking.client.service.BankingModelService
|
||||
import net.codinux.banking.client.updateAccountTransactions
|
||||
import net.codinux.banking.fints.extensions.minusDays
|
||||
|
||||
fun main() {
|
||||
val showUsage = ShowUsage()
|
||||
|
@ -27,11 +34,11 @@ class ShowUsage {
|
|||
|
||||
|
||||
fun getAccountDataSimpleExample() {
|
||||
val client = FinTs4kBankingClientForUser(bankCode, loginName, password, SimpleBankingClientCallback())
|
||||
val client = FinTs4kBankingClient(SimpleBankingClientCallback())
|
||||
|
||||
val response = client.getAccountData()
|
||||
val response = client.getAccountData(bankCode, loginName, password) // that's all
|
||||
|
||||
printReceivedData(response)
|
||||
printReceivedData(response) // now print retrieved data (save it to database, display it in UI, ...)
|
||||
}
|
||||
|
||||
fun getAccountDataFullExample() {
|
||||
|
@ -57,6 +64,28 @@ class ShowUsage {
|
|||
}
|
||||
|
||||
|
||||
fun updateAccountTransactions() {
|
||||
val client = FinTs4kBankingClientForUser(bankCode, loginName, password, SimpleBankingClientCallback())
|
||||
|
||||
// simulate account transactions we retrieved last time
|
||||
val today = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date
|
||||
val lastCallToBankServer = client.getAccountData(GetAccountDataOptions(RetrieveTransactions.AccordingToRetrieveFromAndTo, retrieveTransactionsFrom = today.minusDays(90), retrieveTransactionsTo = today.minusDays(30)))
|
||||
|
||||
if (lastCallToBankServer.data != null) { // now update account transactions
|
||||
val existingTransactions = lastCallToBankServer.data!!.bookedTransactions
|
||||
|
||||
val updateTransactionsResponse = client.updateAccountTransactions()
|
||||
|
||||
if (updateTransactionsResponse.data != null) {
|
||||
val retrievedTransactions = updateTransactionsResponse.data!!.flatMap { it.bookedTransactions }
|
||||
val newTransactions = BankingModelService().findNewTransactions(retrievedTransactions, existingTransactions)
|
||||
// `retrievedTransactions` may contain transactions we already have locally, `newTransactions` only
|
||||
// transactions that are not in `existingTransactions`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun printReceivedData(response: Response<GetAccountDataResponse>) {
|
||||
response.data?.let { data ->
|
||||
val user = data.user
|
||||
|
|
Loading…
Reference in New Issue