Compare commits

...

6 Commits

12 changed files with 107 additions and 46 deletions

View File

@ -53,6 +53,7 @@ kotlin {
linuxX64() linuxX64()
mingwX64() mingwX64()
iosX64()
iosArm64() iosArm64()
iosSimulatorArm64() iosSimulatorArm64()
macosX64() macosX64()

View File

@ -1,5 +1,6 @@
package net.codinux.banking.client package net.codinux.banking.client
import net.codinux.banking.client.model.MessageLogEntry
import net.codinux.banking.client.model.tan.EnterTanResult import net.codinux.banking.client.model.tan.EnterTanResult
import net.codinux.banking.client.model.tan.TanChallenge import net.codinux.banking.client.model.tan.TanChallenge
@ -7,4 +8,11 @@ interface BankingClientCallback {
fun enterTan(tanChallenge: TanChallenge, callback: (EnterTanResult) -> Unit) fun enterTan(tanChallenge: TanChallenge, callback: (EnterTanResult) -> Unit)
/**
* An optional method, for sure not available for all client implementations (currently only for FinTs4kBankingClient).
*
* Gets fired when a FinTS message get sent to bank server, a FinTS message is received from bank server or an error occurred.
*/
fun messageLogAdded(messageLogEntry: MessageLogEntry)
} }

View File

@ -1,9 +1,11 @@
package net.codinux.banking.client package net.codinux.banking.client
import net.codinux.banking.client.model.MessageLogEntry
import net.codinux.banking.client.model.tan.EnterTanResult import net.codinux.banking.client.model.tan.EnterTanResult
import net.codinux.banking.client.model.tan.TanChallenge import net.codinux.banking.client.model.tan.TanChallenge
open class SimpleBankingClientCallback( open class SimpleBankingClientCallback(
protected val messageLogAdded: ((MessageLogEntry) -> Unit)? = null,
protected val enterTan: ((tanChallenge: TanChallenge, callback: (EnterTanResult) -> Unit) -> Unit)? = null protected val enterTan: ((tanChallenge: TanChallenge, callback: (EnterTanResult) -> Unit) -> Unit)? = null
) : BankingClientCallback { ) : BankingClientCallback {
@ -15,4 +17,8 @@ open class SimpleBankingClientCallback(
} }
} }
override fun messageLogAdded(messageLogEntry: MessageLogEntry) {
messageLogAdded?.invoke(messageLogEntry)
}
} }

View File

@ -63,6 +63,7 @@ kotlin {
linuxX64() linuxX64()
mingwX64() mingwX64()
iosX64()
iosArm64() iosArm64()
iosSimulatorArm64() iosSimulatorArm64()
macosX64() macosX64()

View File

@ -0,0 +1,12 @@
package net.codinux.banking.client.model
import net.codinux.banking.client.model.config.NoArgConstructor
@NoArgConstructor
open class BankAccountIdentifier(
val identifier: String,
val subAccountNumber: String?,
val iban: String?
) {
override fun toString() = "$identifier, $iban"
}

View File

@ -1,6 +1,7 @@
package net.codinux.banking.client.model.options package net.codinux.banking.client.model.options
import kotlinx.datetime.LocalDate 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.config.NoArgConstructor
@NoArgConstructor @NoArgConstructor
@ -9,7 +10,9 @@ open class GetAccountDataOptions(
val retrieveTransactions: RetrieveTransactions = RetrieveTransactions.OfLast90Days, val retrieveTransactions: RetrieveTransactions = RetrieveTransactions.OfLast90Days,
val retrieveTransactionsFrom: LocalDate? = null, val retrieveTransactionsFrom: LocalDate? = null,
val retrieveTransactionsTo: LocalDate? = null, val retrieveTransactionsTo: LocalDate? = null,
val abortIfTanIsRequired: Boolean = false 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()
) { ) {
override fun toString(): String { override fun toString(): String {
return "retrieveBalance=$retrieveBalance, retrieveTransactions=$retrieveTransactions, abortIfTanIsRequired=$abortIfTanIsRequired" return "retrieveBalance=$retrieveBalance, retrieveTransactions=$retrieveTransactions, abortIfTanIsRequired=$abortIfTanIsRequired"

View File

@ -9,11 +9,6 @@ plugins {
} }
repositories {
mavenLocal()
}
kotlin { kotlin {
jvmToolchain(8) jvmToolchain(8)
@ -54,12 +49,13 @@ kotlin {
} }
} }
// wasmJs() // wasmJs() // ktor is not available for wasmJs yet
linuxX64() linuxX64()
mingwX64() mingwX64()
iosX64()
iosArm64() iosArm64()
iosSimulatorArm64() iosSimulatorArm64()
macosX64() macosX64()
@ -81,7 +77,7 @@ kotlin {
dependencies { dependencies {
api(project(":BankingClient")) api(project(":BankingClient"))
api("net.codinux.banking:fints4k:1.0.0-Alpha-11") api("net.codinux.banking:fints4k:1.0.0-Alpha-12-SNAPSHOT")
api("org.jetbrains.kotlinx:kotlinx-datetime:$kotlinxDateTimeVersion") api("org.jetbrains.kotlinx:kotlinx-datetime:$kotlinxDateTimeVersion")
} }

View File

@ -1,11 +1,13 @@
package net.codinux.banking.client.fints4k package net.codinux.banking.client.fints4k
import net.codinux.banking.client.BankingClientCallback import net.codinux.banking.client.BankingClientCallback
import net.dankito.banking.fints.callback.FinTsClientCallback import net.codinux.banking.client.model.MessageLogEntryType
import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.TanGeneratorTanMedium import net.codinux.banking.fints.callback.FinTsClientCallback
import net.dankito.banking.fints.model.BankData import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanGeneratorTanMedium
import net.dankito.banking.fints.model.EnterTanGeneratorAtcResult import net.codinux.banking.fints.model.BankData
import net.dankito.banking.fints.model.TanMethod import net.codinux.banking.fints.model.EnterTanGeneratorAtcResult
import net.codinux.banking.fints.model.MessageLogEntry
import net.codinux.banking.fints.model.TanMethod
open class BridgeFintTsToBankingClientCallback( open class BridgeFintTsToBankingClientCallback(
protected val bankingClientCallback: BankingClientCallback, protected val bankingClientCallback: BankingClientCallback,
@ -16,7 +18,7 @@ open class BridgeFintTsToBankingClientCallback(
return suggestedTanMethod return suggestedTanMethod
} }
override suspend fun enterTan(tanChallenge: net.dankito.banking.fints.model.TanChallenge) { override suspend fun enterTan(tanChallenge: net.codinux.banking.fints.model.TanChallenge) {
bankingClientCallback.enterTan(mapper.mapTanChallenge(tanChallenge)) { enterTanResult -> bankingClientCallback.enterTan(mapper.mapTanChallenge(tanChallenge)) { enterTanResult ->
if (enterTanResult.enteredTan != null) { if (enterTanResult.enteredTan != null) {
tanChallenge.userEnteredTan(enterTanResult.enteredTan!!) tanChallenge.userEnteredTan(enterTanResult.enteredTan!!)
@ -29,4 +31,14 @@ open class BridgeFintTsToBankingClientCallback(
override suspend fun enterTanGeneratorAtc(bank: BankData, tanMedium: TanGeneratorTanMedium): EnterTanGeneratorAtcResult { override suspend fun enterTanGeneratorAtc(bank: BankData, tanMedium: TanGeneratorTanMedium): EnterTanGeneratorAtcResult {
return EnterTanGeneratorAtcResult.userDidNotEnterAtc() return EnterTanGeneratorAtcResult.userDidNotEnterAtc()
} }
override fun messageLogAdded(messageLogEntry: MessageLogEntry) {
val mapped = net.codinux.banking.client.model.MessageLogEntry(
MessageLogEntryType.valueOf(messageLogEntry.type.name),
messageLogEntry.message, messageLogEntry.messageTrace,
messageLogEntry.error, messageLogEntry.time
)
bankingClientCallback.messageLogAdded(mapped)
}
} }

View File

@ -6,15 +6,20 @@ import net.codinux.banking.client.model.options.GetAccountDataOptions
import net.codinux.banking.client.model.request.GetAccountDataRequest import net.codinux.banking.client.model.request.GetAccountDataRequest
import net.codinux.banking.client.model.response.GetAccountDataResponse import net.codinux.banking.client.model.response.GetAccountDataResponse
import net.codinux.banking.client.model.response.Response import net.codinux.banking.client.model.response.Response
import net.dankito.banking.fints.FinTsClient import net.codinux.banking.fints.FinTsClient
import net.codinux.banking.fints.config.FinTsClientConfiguration
open class FinTs4kBankingClient( open class FinTs4kBankingClient(
config: FinTsClientConfiguration = FinTsClientConfiguration(),
callback: BankingClientCallback callback: BankingClientCallback
) : BankingClient { ) : BankingClient {
private val mapper = FinTs4kMapper() constructor(callback: BankingClientCallback) : this(FinTsClientConfiguration(), callback)
private val client = FinTsClient(BridgeFintTsToBankingClientCallback(callback, mapper))
protected val mapper = FinTs4kMapper()
protected val client = FinTsClient(config, BridgeFintTsToBankingClientCallback(callback, mapper))
override suspend fun getAccountDataAsync(request: GetAccountDataRequest): Response<GetAccountDataResponse> { override suspend fun getAccountDataAsync(request: GetAccountDataRequest): Response<GetAccountDataResponse> {

View File

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

View File

@ -15,17 +15,17 @@ import net.dankito.banking.client.model.BankAccountIdentifierImpl
import net.dankito.banking.client.model.parameter.GetAccountDataParameter import net.dankito.banking.client.model.parameter.GetAccountDataParameter
import net.dankito.banking.client.model.parameter.RetrieveTransactions import net.dankito.banking.client.model.parameter.RetrieveTransactions
import net.dankito.banking.client.model.response.ErrorCode import net.dankito.banking.client.model.response.ErrorCode
import net.dankito.banking.fints.mapper.FinTsModelMapper import net.codinux.banking.fints.mapper.FinTsModelMapper
import net.dankito.banking.fints.model.* import net.codinux.banking.fints.model.*
import kotlin.io.encoding.Base64 import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.io.encoding.ExperimentalEncodingApi
open class FinTs4kMapper { open class FinTs4kMapper {
private val fintsModelMapper = FinTsModelMapper() protected val fintsModelMapper = FinTsModelMapper()
fun mapToGetAccountDataParameter(credentials: AccountCredentials, options: GetAccountDataOptions) = GetAccountDataParameter( open fun mapToGetAccountDataParameter(credentials: AccountCredentials, options: GetAccountDataOptions) = GetAccountDataParameter(
credentials.bankCode, credentials.loginName, credentials.password, credentials.bankCode, credentials.loginName, credentials.password,
options.accounts.map { BankAccountIdentifierImpl(it.identifier, it.subAccountNumber, it.iban) }, options.accounts.map { BankAccountIdentifierImpl(it.identifier, it.subAccountNumber, it.iban) },
options.retrieveBalance, options.retrieveBalance,
@ -34,7 +34,7 @@ open class FinTs4kMapper {
) )
fun map(response: net.dankito.banking.client.model.response.GetAccountDataResponse): Response<GetAccountDataResponse> { open fun map(response: net.dankito.banking.client.model.response.GetAccountDataResponse): Response<GetAccountDataResponse> {
return if (response.successful && response.customerAccount != null) { return if (response.successful && response.customerAccount != null) {
Response.success(GetAccountDataResponse(mapCustomer(response.customerAccount!!))) Response.success(GetAccountDataResponse(mapCustomer(response.customerAccount!!)))
} else { } else {
@ -43,25 +43,25 @@ open class FinTs4kMapper {
} }
fun mapToCustomerAccountViewInfo(bank: BankData): CustomerAccountViewInfo = CustomerAccountViewInfo( open fun mapToCustomerAccountViewInfo(bank: BankData): CustomerAccountViewInfo = CustomerAccountViewInfo(
bank.bankCode, bank.customerId, bank.bankName bank.bankCode, bank.customerId, bank.bankName
) )
fun mapToBankAccountViewInfo(account: AccountData): BankAccountViewInfo = BankAccountViewInfo( open fun mapToBankAccountViewInfo(account: AccountData): BankAccountViewInfo = BankAccountViewInfo(
account.accountIdentifier, account.subAccountAttribute, account.accountIdentifier, account.subAccountAttribute,
mapAccountType(fintsModelMapper.map(account.accountType)), mapAccountType(fintsModelMapper.map(account.accountType)),
account.iban, account.productName account.iban, account.productName
) )
private fun mapCustomer(customer: net.dankito.banking.client.model.CustomerAccount): CustomerAccount = CustomerAccount( protected open fun mapCustomer(customer: net.dankito.banking.client.model.CustomerAccount): CustomerAccount = CustomerAccount(
customer.bankCode, customer.loginName, customer.password, customer.bankCode, customer.loginName, customer.password,
customer.bankName, customer.bic, customer.customerName, customer.userId, customer.bankName, customer.bic, customer.customerName, customer.userId,
customer.accounts.map { mapAccount(it) } customer.accounts.map { mapAccount(it) }
) )
private fun mapAccount(account: net.dankito.banking.client.model.BankAccount): BankAccount = BankAccount( protected open fun mapAccount(account: net.dankito.banking.client.model.BankAccount): BankAccount = BankAccount(
account.identifier, account.accountHolderName, mapAccountType(account.type), account.iban, account.subAccountNumber, account.identifier, account.accountHolderName, mapAccountType(account.type), account.iban, account.subAccountNumber,
account.productName, account.currency, account.accountLimit, account.isAccountTypeSupportedByApplication, account.productName, account.currency, account.accountLimit, account.isAccountTypeSupportedByApplication,
mapFeatures(account), mapFeatures(account),
@ -71,10 +71,10 @@ open class FinTs4kMapper {
bookedTransactions = account.bookedTransactions.map { mapTransaction(it) }.toMutableList() bookedTransactions = account.bookedTransactions.map { mapTransaction(it) }.toMutableList()
) )
private fun mapAccountType(type: net.dankito.banking.client.model.BankAccountType): BankAccountType = protected open fun mapAccountType(type: net.dankito.banking.client.model.BankAccountType): BankAccountType =
BankAccountType.valueOf(type.name) BankAccountType.valueOf(type.name)
private fun mapFeatures(account: net.dankito.banking.client.model.BankAccount): Set<BankAccountFeatures> = buildSet { protected open fun mapFeatures(account: net.dankito.banking.client.model.BankAccount): Set<BankAccountFeatures> = buildSet {
if (account.supportsRetrievingBalance) { if (account.supportsRetrievingBalance) {
add(BankAccountFeatures.RetrieveBalance) add(BankAccountFeatures.RetrieveBalance)
} }
@ -90,22 +90,34 @@ open class FinTs4kMapper {
} }
private fun mapTransaction(transaction: net.dankito.banking.client.model.AccountTransaction): AccountTransaction = AccountTransaction( 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.unparsedReference,
transaction.bookingDate, transaction.valueDate, transaction.bookingDate, transaction.valueDate,
transaction.otherPartyName, transaction.otherPartyBankCode, transaction.otherPartyAccountId, transaction.otherPartyName, transaction.otherPartyBankCode, transaction.otherPartyAccountId,
transaction.bookingText, null, transaction.bookingText, null,
transaction.statementNumber, transaction.sequenceNumber, transaction.statementNumber, transaction.sequenceNumber,
mapNullableAmount(transaction.openingBalance), mapNullableAmount(transaction.closingBalance), mapNullableAmount(transaction.openingBalance), mapNullableAmount(transaction.closingBalance),
// TODO: map other properties
transaction.endToEndReference, transaction.customerReference, transaction.mandateReference,
transaction.creditorIdentifier, transaction.originatorsIdentificationCode,
transaction.compensationAmount, transaction.originalAmount,
transaction.sepaReference,
transaction.deviantOriginator, transaction.deviantRecipient,
transaction.referenceWithNoSpecialType, transaction.primaNotaNumber, transaction.textKeySupplement,
transaction.currencyType, transaction.bookingKey,
transaction.referenceForTheAccountOwner, transaction.referenceOfTheAccountServicingInstitution,
transaction.supplementaryDetails,
transaction.transactionReferenceNumber, transaction.relatedReferenceNumber
) )
private fun mapNullableAmount(amount: Money?) = amount?.let { mapAmount(it) } protected open fun mapNullableAmount(amount: Money?) = amount?.let { mapAmount(it) }
private fun mapAmount(amount: Money) = Amount.fromString(amount.amount.string.replace(',', '.')) protected open fun mapAmount(amount: Money) = Amount.fromString(amount.amount.string.replace(',', '.'))
fun mapTanChallenge(challenge: net.dankito.banking.fints.model.TanChallenge): TanChallenge { open fun mapTanChallenge(challenge: net.codinux.banking.fints.model.TanChallenge): TanChallenge {
val type = mapTanChallengeType(challenge) val type = mapTanChallengeType(challenge)
val action = mapActionRequiringTan(challenge.forAction) val action = mapActionRequiringTan(challenge.forAction)
val tanMethod = mapTanMethod(challenge.tanMethod) val tanMethod = mapTanMethod(challenge.tanMethod)
@ -118,38 +130,38 @@ open class FinTs4kMapper {
return TanChallenge(type, action, challenge.messageToShowToUser, tanMethod, tanImage, flickerCode, customer, account) return TanChallenge(type, action, challenge.messageToShowToUser, tanMethod, tanImage, flickerCode, customer, account)
} }
private fun mapTanChallengeType(challenge: net.dankito.banking.fints.model.TanChallenge): TanChallengeType = when { protected open fun mapTanChallengeType(challenge: net.codinux.banking.fints.model.TanChallenge): TanChallengeType = when {
challenge is ImageTanChallenge -> TanChallengeType.Image challenge is ImageTanChallenge -> TanChallengeType.Image
challenge is FlickerCodeTanChallenge -> TanChallengeType.Flickercode challenge is FlickerCodeTanChallenge -> TanChallengeType.Flickercode
else -> TanChallengeType.EnterTan else -> TanChallengeType.EnterTan
} }
private fun mapActionRequiringTan(action: net.dankito.banking.fints.model.ActionRequiringTan): ActionRequiringTan = protected open fun mapActionRequiringTan(action: net.codinux.banking.fints.model.ActionRequiringTan): ActionRequiringTan =
ActionRequiringTan.valueOf(action.name) ActionRequiringTan.valueOf(action.name)
private fun mapTanMethod(method: net.dankito.banking.fints.model.TanMethod): TanMethod = TanMethod( protected 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) method.displayName, mapTanMethodType(method.type), method.securityFunction.code, method.maxTanInputLength, mapAllowedTanFormat(method.allowedTanFormat)
) )
private fun mapTanMethodType(type: net.dankito.banking.fints.model.TanMethodType): TanMethodType = protected open fun mapTanMethodType(type: net.codinux.banking.fints.model.TanMethodType): TanMethodType =
TanMethodType.valueOf(type.name) TanMethodType.valueOf(type.name)
private fun mapAllowedTanFormat(allowedTanFormat: net.dankito.banking.fints.messages.datenelemente.implementierte.tan.AllowedTanFormat?): AllowedTanFormat = protected open fun mapAllowedTanFormat(allowedTanFormat: net.codinux.banking.fints.messages.datenelemente.implementierte.tan.AllowedTanFormat?): AllowedTanFormat =
allowedTanFormat?.let { AllowedTanFormat.valueOf(it.name) } ?: AllowedTanFormat.Alphanumeric allowedTanFormat?.let { AllowedTanFormat.valueOf(it.name) } ?: AllowedTanFormat.Alphanumeric
private fun mapTanImage(image: net.dankito.banking.fints.tan.TanImage): TanImage = protected open fun mapTanImage(image: net.codinux.banking.fints.tan.TanImage): TanImage =
TanImage(image.mimeType, mapToBase64(image.imageBytes), mapException(image.decodingError)) TanImage(image.mimeType, mapToBase64(image.imageBytes), mapException(image.decodingError))
@OptIn(ExperimentalEncodingApi::class) @OptIn(ExperimentalEncodingApi::class)
private fun mapToBase64(bytes: ByteArray): String { protected open fun mapToBase64(bytes: ByteArray): String {
return Base64.Default.encode(bytes) return Base64.Default.encode(bytes)
} }
private fun mapFlickerCode(flickerCode: net.dankito.banking.fints.tan.FlickerCode): FlickerCode = protected open fun mapFlickerCode(flickerCode: net.codinux.banking.fints.tan.FlickerCode): FlickerCode =
FlickerCode(flickerCode.challengeHHD_UC, flickerCode.parsedDataSet, mapException(flickerCode.decodingError)) FlickerCode(flickerCode.challengeHHD_UC, flickerCode.parsedDataSet, mapException(flickerCode.decodingError))
private fun <T> mapError(response: net.dankito.banking.client.model.response.GetAccountDataResponse): Response<T> { protected open fun <T> mapError(response: net.dankito.banking.client.model.response.GetAccountDataResponse): Response<T> {
return if (response.error != null) { return if (response.error != null) {
Response.error(ErrorType.valueOf(response.error!!.name), if (response.error == ErrorCode.BankReturnedError) null else response.errorMessage, 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()) if (response.error == ErrorCode.BankReturnedError && response.errorMessage !== null) listOf(response.errorMessage!!) else emptyList())
@ -158,7 +170,7 @@ open class FinTs4kMapper {
} }
} }
private fun mapException(exception: Exception?): String? = protected open fun mapException(exception: Exception?): String? =
exception?.stackTraceToString() exception?.stackTraceToString()
} }

View File

@ -16,6 +16,7 @@ allprojects {
repositories { repositories {
mavenCentral() mavenCentral()
mavenLocal()
maven { maven {
setUrl("https://maven.dankito.net/api/packages/codinux/maven") setUrl("https://maven.dankito.net/api/packages/codinux/maven")
} }