Compare commits

..

No commits in common. "c158097d3a9f70d1df0dc2f83035d2f3d30a32b0" and "9600e2f11b194694f0a894a058f6affe4d5cab2e" have entirely different histories.

38 changed files with 343 additions and 630 deletions

View File

@ -41,8 +41,8 @@ open class CsvWriter {
protected open suspend fun writeToFile(stream: AsyncStream, valueSeparator: String, customer: CustomerAccount, account: BankAccount, transaction: AccountTransaction) { protected open suspend fun writeToFile(stream: AsyncStream, valueSeparator: String, customer: CustomerAccount, account: BankAccount, transaction: AccountTransaction) {
val amount = if (valueSeparator == ";") transaction.amount.amount.string.replace('.', ',') else transaction.amount.amount.string.replace(',', '.') val amount = if (valueSeparator == ";") transaction.amount.amount.string.replace('.', ',') else transaction.amount.amount.string.replace(',', '.')
stream.writeString(listOf(customer.bankName, account.identifier, transaction.valueDate, amount, transaction.amount.currency, ensureNotNull(transaction.postingText), wrap(transaction.reference ?: ""), stream.writeString(listOf(customer.bankName, account.identifier, transaction.valueDate, amount, transaction.amount.currency, ensureNotNull(transaction.bookingText), wrap(transaction.reference),
ensureNotNull(transaction.otherPartyName), ensureNotNull(transaction.otherPartyBankId), ensureNotNull(transaction.otherPartyAccountId)).joinToString(valueSeparator)) ensureNotNull(transaction.otherPartyName), ensureNotNull(transaction.otherPartyBankCode), ensureNotNull(transaction.otherPartyAccountId)).joinToString(valueSeparator))
stream.writeString(NewLine) stream.writeString(NewLine)
} }

View File

@ -1,26 +0,0 @@
| | |
|--------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------|
| Geschäftsvorfall | Business Transaction / Job |
| Verwendungszweck | Remittance information, reference, (payment) purpose |
| Überweisung | Remittance (techn.), money transfer, bank transfer, wire transfer (Amerik.), credit transfer |
| Buchungsschlüssel | posting key |
| Buchungstext | posting text |
| | |
| Ende-zu-Ende Referenz | End to End Reference |
| Kundenreferenz | Reference of the submitting customer |
| Mandatsreferenz | mandate reference |
| Creditor Identifier | Creditor Identifier |
| Originators Identification Code | Originators Identification Code |
| Compensation Amount | Compensation Amount |
| Original Amount | Original Amount |
| Abweichender Überweisender (CT-AT08) / Abweichender Zahlungsempfänger (DD-AT38) | payers/debtors reference party (for credit transfer / payees / creditors reference party (for a direct debit) |
| Abweichender Zahlungsempfänger (CT-AT28) / Abweichender Zahlungspflichtiger (DDAT15) | payees/creditors reference party / payers/debtors reference party |
| | |
| Überweisender | Payer, debtor |
| Zahlungsempfänger | Payee, creditor |
| Zahlungseingang | Payment receipt |
| Lastschrift | direct debit |
| | |
| | |
| Primanoten-Nr. | Journal no. |
| | |

View File

@ -63,13 +63,8 @@ open class FinTsClient(
return GetAccountDataResponse(ErrorCode.NoneOfTheAccountsSupportsRetrievingData, errorMessage, mapper.map(bank), previousJobMessageLog ?: listOf(), bank) return GetAccountDataResponse(ErrorCode.NoneOfTheAccountsSupportsRetrievingData, errorMessage, mapper.map(bank), previousJobMessageLog ?: listOf(), bank)
} }
for (account in accountsSupportingRetrievingTransactions) { accountsSupportingRetrievingTransactions.forEach { account ->
val response = getAccountTransactions(param, bank, account) retrievedTransactionsResponses.add(getAccountTransactions(param, bank, account))
retrievedTransactionsResponses.add(response)
if (response.tanRequiredButWeWereToldToAbortIfSo || response.userCancelledAction) { // if user cancelled action or TAN is required but we were told to abort then, then don't continue with next account
break
}
} }
val unsuccessfulJob = retrievedTransactionsResponses.firstOrNull { it.successful == false } val unsuccessfulJob = retrievedTransactionsResponses.firstOrNull { it.successful == false }
@ -80,7 +75,7 @@ open class FinTsClient(
} }
protected open suspend fun getAccountTransactions(param: GetAccountDataParameter, bank: BankData, account: AccountData): GetAccountTransactionsResponse { protected open suspend fun getAccountTransactions(param: GetAccountDataParameter, bank: BankData, account: AccountData): GetAccountTransactionsResponse {
val context = JobContext(JobContextType.GetTransactions, this.callback, config, bank, account, param.preferredTanMethods, param.tanMethodsNotSupportedByApplication, param.preferredTanMedium) val context = JobContext(JobContextType.GetTransactions, this.callback, config, bank, account)
return config.jobExecutor.getTransactionsAsync(context, mapper.toGetAccountTransactionsParameter(param, bank, account)) return config.jobExecutor.getTransactionsAsync(context, mapper.toGetAccountTransactionsParameter(param, bank, account))
} }
@ -138,7 +133,7 @@ open class FinTsClient(
accountToUse = selectedAccount accountToUse = selectedAccount
} }
val context = JobContext(JobContextType.TransferMoney, this.callback, config, bank, accountToUse, param.preferredTanMethods, param.tanMethodsNotSupportedByApplication, param.preferredTanMedium) val context = JobContext(JobContextType.TransferMoney, this.callback, config, bank, accountToUse)
val response = config.jobExecutor.transferMoneyAsync(context, BankTransferData(param.recipientName, param.recipientAccountIdentifier, recipientBankIdentifier, val response = config.jobExecutor.transferMoneyAsync(context, BankTransferData(param.recipientName, param.recipientAccountIdentifier, recipientBankIdentifier,
param.amount, param.reference, param.instantPayment)) param.amount, param.reference, param.instantPayment))
@ -184,14 +179,12 @@ open class FinTsClient(
return net.dankito.banking.client.model.response.FinTsClientResponse(null, null, emptyList(), param.finTsModel) return net.dankito.banking.client.model.response.FinTsClientResponse(null, null, emptyList(), param.finTsModel)
} }
val defaultValues = (param as? GetAccountDataParameter)?.defaultBankValues val finTsServerAddress = config.finTsServerAddressFinder.findFinTsServerAddress(param.bankCode)
val finTsServerAddress = defaultValues?.finTs3ServerAddress ?: config.finTsServerAddressFinder.findFinTsServerAddress(param.bankCode)
if (finTsServerAddress.isNullOrBlank()) { if (finTsServerAddress.isNullOrBlank()) {
return net.dankito.banking.client.model.response.FinTsClientResponse(ErrorCode.BankDoesNotSupportFinTs3, "Either bank does not support FinTS 3.0 or we don't know its FinTS server address", emptyList(), null) return net.dankito.banking.client.model.response.FinTsClientResponse(ErrorCode.BankDoesNotSupportFinTs3, "Either bank does not support FinTS 3.0 or we don't know its FinTS server address", emptyList(), null)
} }
val bank = mapper.mapToBankData(param, finTsServerAddress, defaultValues) val bank = mapper.mapToBankData(param, finTsServerAddress)
val getAccountInfoResponse = getAccountInfo(param, bank) val getAccountInfoResponse = getAccountInfo(param, bank)
@ -205,11 +198,11 @@ open class FinTsClient(
// return GetAccountInfoResponse(it) // return GetAccountInfoResponse(it)
} }
val context = JobContext(JobContextType.GetAccountInfo, this.callback, config, bank, null, param.preferredTanMethods, param.tanMethodsNotSupportedByApplication, param.preferredTanMedium) val context = JobContext(JobContextType.GetAccountInfo, this.callback, config, bank)
/* First dialog: Get user's basic data like BPD, customer system ID and her TAN methods */ /* First dialog: Get user's basic data like BPD, customer system ID and her TAN methods */
val newUserInfoResponse = config.jobExecutor.retrieveBasicDataLikeUsersTanMethods(context) val newUserInfoResponse = config.jobExecutor.retrieveBasicDataLikeUsersTanMethods(context, param.preferredTanMethods, param.preferredTanMedium)
/* Second dialog, executed in retrieveBasicDataLikeUsersTanMethods() if required: some banks require that in order to initialize a dialog with /* Second dialog, executed in retrieveBasicDataLikeUsersTanMethods() if required: some banks require that in order to initialize a dialog with
strong customer authorization TAN media is required */ strong customer authorization TAN media is required */

View File

@ -1,5 +1,7 @@
package net.codinux.banking.fints package net.codinux.banking.fints
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.datetime.* import kotlinx.datetime.*
import net.codinux.banking.fints.callback.FinTsClientCallback import net.codinux.banking.fints.callback.FinTsClientCallback
import net.codinux.banking.fints.config.FinTsClientConfiguration import net.codinux.banking.fints.config.FinTsClientConfiguration
@ -37,13 +39,13 @@ open class FinTsClientDeprecated(
} }
open suspend fun addAccountAsync(param: AddAccountParameter): AddAccountResponse { open suspend fun addAccountAsync(parameter: AddAccountParameter): AddAccountResponse {
val bank = param.bank val bank = parameter.bank
val context = JobContext(JobContextType.AddAccount, this.callback, config, bank, null, param.preferredTanMethods, param.tanMethodsNotSupportedByApplication, param.preferredTanMedium) val context = JobContext(JobContextType.AddAccount, this.callback, config, bank)
/* First dialog: Get user's basic data like BPD, customer system ID and her TAN methods */ /* First dialog: Get user's basic data like BPD, customer system ID and her TAN methods */
val newUserInfoResponse = config.jobExecutor.retrieveBasicDataLikeUsersTanMethods(context) val newUserInfoResponse = config.jobExecutor.retrieveBasicDataLikeUsersTanMethods(context, parameter.preferredTanMethods, parameter.preferredTanMedium)
if (newUserInfoResponse.successful == false) { // bank parameter (FinTS server address, ...) already seem to be wrong if (newUserInfoResponse.successful == false) { // bank parameter (FinTS server address, ...) already seem to be wrong
return AddAccountResponse(context, newUserInfoResponse) return AddAccountResponse(context, newUserInfoResponse)
@ -52,7 +54,7 @@ open class FinTsClientDeprecated(
/* Second dialog, executed in retrieveBasicDataLikeUsersTanMethods() if required: some banks require that in order to initialize a dialog with /* Second dialog, executed in retrieveBasicDataLikeUsersTanMethods() if required: some banks require that in order to initialize a dialog with
strong customer authorization TAN media is required */ strong customer authorization TAN media is required */
return addAccountGetAccountsAndTransactions(context, param) return addAccountGetAccountsAndTransactions(context, parameter)
} }
protected open suspend fun addAccountGetAccountsAndTransactions(context: JobContext, parameter: AddAccountParameter): AddAccountResponse { protected open suspend fun addAccountGetAccountsAndTransactions(context: JobContext, parameter: AddAccountParameter): AddAccountResponse {
@ -118,11 +120,11 @@ open class FinTsClientDeprecated(
return GetAccountTransactionsParameter(bank, account, account.supportsRetrievingBalance, ninetyDaysAgo, abortIfTanIsRequired = true) return GetAccountTransactionsParameter(bank, account, account.supportsRetrievingBalance, ninetyDaysAgo, abortIfTanIsRequired = true)
} }
open suspend fun getAccountTransactionsAsync(param: GetAccountTransactionsParameter): GetAccountTransactionsResponse { open suspend fun getAccountTransactionsAsync(parameter: GetAccountTransactionsParameter): GetAccountTransactionsResponse {
val context = JobContext(JobContextType.GetTransactions, this.callback, config, param.bank, param.account) val context = JobContext(JobContextType.GetTransactions, this.callback, config, parameter.bank, parameter.account)
return config.jobExecutor.getTransactionsAsync(context, param) return config.jobExecutor.getTransactionsAsync(context, parameter)
} }

View File

@ -1,9 +1,8 @@
package net.codinux.banking.fints package net.codinux.banking.fints
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.datetime.Instant import kotlinx.datetime.Clock
import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDate
import net.codinux.banking.fints.extensions.*
import net.codinux.log.logger import net.codinux.log.logger
import net.codinux.banking.fints.messages.MessageBuilder import net.codinux.banking.fints.messages.MessageBuilder
import net.codinux.banking.fints.messages.MessageBuilderResult import net.codinux.banking.fints.messages.MessageBuilderResult
@ -20,7 +19,9 @@ import net.codinux.banking.fints.response.segments.*
import net.codinux.banking.fints.tan.FlickerCodeDecoder import net.codinux.banking.fints.tan.FlickerCodeDecoder
import net.codinux.banking.fints.tan.TanImageDecoder import net.codinux.banking.fints.tan.TanImageDecoder
import net.codinux.banking.fints.util.TanMethodSelector import net.codinux.banking.fints.util.TanMethodSelector
import net.codinux.log.Log import net.codinux.banking.fints.extensions.minusDays
import net.codinux.banking.fints.extensions.todayAtEuropeBerlin
import net.codinux.banking.fints.extensions.todayAtSystemDefaultTimeZone
import kotlin.math.max import kotlin.math.max
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@ -74,7 +75,8 @@ open class FinTsJobExecutor(
* *
* Be aware this method resets BPD, UPD and selected TAN method! * Be aware this method resets BPD, UPD and selected TAN method!
*/ */
open suspend fun retrieveBasicDataLikeUsersTanMethods(context: JobContext): BankResponse { open suspend fun retrieveBasicDataLikeUsersTanMethods(context: JobContext, preferredTanMethods: List<TanMethodType>? = null, preferredTanMedium: String? = null,
closeDialog: Boolean = false): BankResponse {
val bank = context.bank val bank = context.bank
// just to ensure settings are in its initial state and that bank sends us bank parameter (BPD), // just to ensure settings are in its initial state and that bank sends us bank parameter (BPD),
@ -90,7 +92,7 @@ open class FinTsJobExecutor(
bank.resetSelectedTanMethod() bank.resetSelectedTanMethod()
// this is the only case where Einschritt-TAN-Verfahren is accepted: to get user's TAN methods // this is the only case where Einschritt-TAN-Verfahren is accepted: to get user's TAN methods
context.startNewDialog(versionOfSecurityProcedure = VersionDesSicherheitsverfahrens.Version_1) context.startNewDialog(closeDialog, versionOfSecurityProcedure = VersionDesSicherheitsverfahrens.Version_1)
val message = messageBuilder.createInitDialogMessage(context) val message = messageBuilder.createInitDialogMessage(context)
@ -103,10 +105,12 @@ open class FinTsJobExecutor(
if (bank.tanMethodsAvailableForUser.isEmpty()) { // could not retrieve supported tan methods for user if (bank.tanMethodsAvailableForUser.isEmpty()) { // could not retrieve supported tan methods for user
return getTanMethodsResponse return getTanMethodsResponse
} else { } else {
getUsersTanMethod(context) getUsersTanMethod(context, preferredTanMethods)
if (bank.isTanMethodSelected && bank.tanMedia.isEmpty() && bank.tanMethodsAvailableForUser.any { it.nameOfTanMediumRequired } && isJobSupported(bank, CustomerSegmentId.TanMediaList)) { // tan media not retrieved yet if (bank.isTanMethodSelected == false) {
getTanMediaList(context, TanMedienArtVersion.Alle, TanMediumKlasse.AlleMedien, context.preferredTanMedium) return getTanMethodsResponse
} else if (bank.tanMedia.isEmpty() && isJobSupported(bank, CustomerSegmentId.TanMediaList)) { // tan media not retrieved yet
getTanMediaList(context, TanMedienArtVersion.Alle, TanMediumKlasse.AlleMedien, preferredTanMedium)
return getTanMethodsResponse // TODO: judge if bank requires selecting TAN media and if though evaluate getTanMediaListResponse return getTanMethodsResponse // TODO: judge if bank requires selecting TAN media and if though evaluate getTanMediaListResponse
} else { } else {
@ -145,7 +149,6 @@ open class FinTsJobExecutor(
return BankResponse(true, internalError = "Die TAN Verfahren der Bank konnten nicht ermittelt werden") // TODO: translate return BankResponse(true, internalError = "Die TAN Verfahren der Bank konnten nicht ermittelt werden") // TODO: translate
} else { } else {
bank.tanMethodsAvailableForUser = bank.tanMethodsSupportedByBank bank.tanMethodsAvailableForUser = bank.tanMethodsSupportedByBank
.filterNot { context.tanMethodsNotSupportedByApplication.contains(it.type) }
val didSelectTanMethod = getUsersTanMethod(context) val didSelectTanMethod = getUsersTanMethod(context)
@ -224,11 +227,11 @@ open class FinTsJobExecutor(
response.getFirstSegmentById<ReceivedCreditCardTransactionsAndBalance>(InstituteSegmentId.CreditCardTransactions)?.let { creditCardTransactionsSegment -> response.getFirstSegmentById<ReceivedCreditCardTransactionsAndBalance>(InstituteSegmentId.CreditCardTransactions)?.let { creditCardTransactionsSegment ->
balance = Money(creditCardTransactionsSegment.balance.amount, creditCardTransactionsSegment.balance.currency ?: "EUR") balance = Money(creditCardTransactionsSegment.balance.amount, creditCardTransactionsSegment.balance.currency ?: "EUR")
bookedTransactions.addAll(creditCardTransactionsSegment.transactions.map { AccountTransaction(parameter.account, it.amount, it.description, it.bookingDate, it.valueDate, it.transactionDescriptionBase ?: "", null, null) }) bookedTransactions.addAll(creditCardTransactionsSegment.transactions.map { AccountTransaction(parameter.account, it.amount, it.description, it.bookingDate, it.transactionDescriptionBase ?: "", null, null, "", it.valueDate) })
} }
} }
val startTime = Instant.nowExt() val startTime = Clock.System.now()
val response = getAndHandleResponseForMessage(context, message) val response = getAndHandleResponseForMessage(context, message)
@ -237,7 +240,7 @@ open class FinTsJobExecutor(
val successful = response.tanRequiredButWeWereToldToAbortIfSo val successful = response.tanRequiredButWeWereToldToAbortIfSo
|| (response.successful && (parameter.alsoRetrieveBalance == false || balance != null)) || (response.successful && (parameter.alsoRetrieveBalance == false || balance != null))
val fromDate = parameter.fromDate val fromDate = parameter.fromDate
?: parameter.account.serverTransactionsRetentionDays?.let { LocalDate.todayAtSystemDefaultTimeZone().minusDays(it) } ?: parameter.account.countDaysForWhichTransactionsAreKept?.let { LocalDate.todayAtSystemDefaultTimeZone().minusDays(it) }
?: bookedTransactions.minByOrNull { it.valueDate }?.valueDate ?: bookedTransactions.minByOrNull { it.valueDate }?.valueDate
val retrievedData = RetrievedAccountData(parameter.account, successful, balance, bookedTransactions, unbookedTransactions, startTime, fromDate, parameter.toDate ?: LocalDate.todayAtEuropeBerlin(), response.internalError) val retrievedData = RetrievedAccountData(parameter.account, successful, balance, bookedTransactions, unbookedTransactions, startTime, fromDate, parameter.toDate ?: LocalDate.todayAtEuropeBerlin(), response.internalError)
@ -381,32 +384,16 @@ open class FinTsJobExecutor(
context.callback.enterTan(tanChallenge) context.callback.enterTan(tanChallenge)
while (tanChallenge.enterTanResult == null) {
delay(250)
mayRetrieveAutomaticallyIfUserEnteredDecoupledTan(context, tanChallenge, tanResponse) mayRetrieveAutomaticallyIfUserEnteredDecoupledTan(context, tanChallenge, tanResponse)
var invocationCount = 0 // TODO: remove again // TODO: add a timeout of e.g. 30 min
while (tanChallenge.isEnteringTanDone == false) {
delay(500)
if (++invocationCount % 10 == 0) {
Log.info { "Waiting for TAN input invocation count: $invocationCount" }
}
val now = Instant.nowExt()
if ((tanChallenge.tanExpirationTime != null && now > tanChallenge.tanExpirationTime) ||
// most TANs a valid 5 - 15 minutes. So terminate wait process after that time
(tanChallenge.tanExpirationTime == null && now > tanChallenge.challengeCreationTimestamp.plusMinutes(15))) {
if (tanChallenge.isEnteringTanDone == false) {
Log.info { "Terminating waiting for TAN input" } // TODO: remove again
tanChallenge.tanExpired()
}
break
}
} }
val enteredTanResult = tanChallenge.enterTanResult!! val enteredTanResult = tanChallenge.enterTanResult!!
// }
return handleEnterTanResult(context, enteredTanResult, tanResponse, response) return handleEnterTanResult(context, enteredTanResult, tanResponse, response)
} }
@ -421,13 +408,13 @@ Log.info { "Terminating waiting for TAN input" } // TODO: remove again
TanMethodType.ChipTanFlickercode -> TanMethodType.ChipTanFlickercode ->
FlickerCodeTanChallenge( FlickerCodeTanChallenge(
FlickerCodeDecoder().decodeChallenge(challenge, tanMethod.hhdVersion ?: HHDVersion.HHD_1_4), // HHD 1.4 is currently the most used version FlickerCodeDecoder().decodeChallenge(challenge, tanMethod.hhdVersion ?: HHDVersion.HHD_1_4), // HHD 1.4 is currently the most used version
forAction, messageToShowToUser, challenge, tanMethod, tanResponse.tanMediaIdentifier, bank, account, tanResponse.tanExpirationTime) forAction, messageToShowToUser, challenge, tanMethod, tanResponse.tanMediaIdentifier, bank, account)
TanMethodType.ChipTanQrCode, TanMethodType.ChipTanPhotoTanMatrixCode, TanMethodType.ChipTanQrCode, TanMethodType.ChipTanPhotoTanMatrixCode,
TanMethodType.QrCode, TanMethodType.photoTan -> TanMethodType.QrCode, TanMethodType.photoTan ->
ImageTanChallenge(TanImageDecoder().decodeChallenge(challenge), forAction, messageToShowToUser, challenge, tanMethod, tanResponse.tanMediaIdentifier, bank, account, tanResponse.tanExpirationTime) ImageTanChallenge(TanImageDecoder().decodeChallenge(challenge), forAction, messageToShowToUser, challenge, tanMethod, tanResponse.tanMediaIdentifier, bank, account)
else -> TanChallenge(forAction, messageToShowToUser, challenge, tanMethod, tanResponse.tanMediaIdentifier, bank, account, tanResponse.tanExpirationTime) else -> TanChallenge(forAction, messageToShowToUser, challenge, tanMethod, tanResponse.tanMediaIdentifier, bank, account)
} }
} }
@ -485,8 +472,6 @@ Log.info { "Terminating waiting for TAN input" } // TODO: remove again
} }
} }
tanChallenge.tanExpired()
return null return null
} }
@ -631,7 +616,7 @@ Log.info { "Terminating waiting for TAN input" } // TODO: remove again
protected open suspend fun initDialogWithStrongCustomerAuthenticationAfterSuccessfulPreconditionChecks(context: JobContext): BankResponse { protected open suspend fun initDialogWithStrongCustomerAuthenticationAfterSuccessfulPreconditionChecks(context: JobContext): BankResponse {
context.startNewDialog() // don't know if it's ok for all invocations of this method to set closeDialog to false (was actually only set in getAccounts()) context.startNewDialog(false) // don't know if it's ok for all invocations of this method to set closeDialog to false (was actually only set in getAccounts())
val message = messageBuilder.createInitDialogMessage(context) val message = messageBuilder.createInitDialogMessage(context)
@ -707,7 +692,7 @@ Log.info { "Terminating waiting for TAN input" } // TODO: remove again
return BankResponse(true, noTanMethodSelected = noTanMethodSelected, internalError = errorMessage) return BankResponse(true, noTanMethodSelected = noTanMethodSelected, internalError = errorMessage)
} }
open suspend fun getUsersTanMethod(context: JobContext): Boolean { open suspend fun getUsersTanMethod(context: JobContext, preferredTanMethods: List<TanMethodType>? = null): Boolean {
val bank = context.bank val bank = context.bank
if (bank.tanMethodsAvailableForUser.size == 1) { // user has only one TAN method -> set it and we're done if (bank.tanMethodsAvailableForUser.size == 1) { // user has only one TAN method -> set it and we're done
@ -715,13 +700,13 @@ Log.info { "Terminating waiting for TAN input" } // TODO: remove again
return true return true
} }
else { else {
tanMethodSelector.findPreferredTanMethod(bank.tanMethodsAvailableForUser, context.preferredTanMethods, context.tanMethodsNotSupportedByApplication)?.let { tanMethodSelector.findPreferredTanMethod(bank.tanMethodsAvailableForUser, preferredTanMethods)?.let {
bank.selectedTanMethod = it bank.selectedTanMethod = it
return true return true
} }
// we know user's supported tan methods, now ask user which one to select // we know user's supported tan methods, now ask user which one to select
val suggestedTanMethod = tanMethodSelector.getSuggestedTanMethod(bank.tanMethodsAvailableForUser, context.tanMethodsNotSupportedByApplication) val suggestedTanMethod = tanMethodSelector.getSuggestedTanMethod(bank.tanMethodsAvailableForUser)
val selectedTanMethod = context.callback.askUserForTanMethod(bank.tanMethodsAvailableForUser, suggestedTanMethod) val selectedTanMethod = context.callback.askUserForTanMethod(bank.tanMethodsAvailableForUser, suggestedTanMethod)
@ -742,14 +727,14 @@ Log.info { "Terminating waiting for TAN input" } // TODO: remove again
protected open fun updateBankAndCustomerDataIfResponseSuccessful(context: JobContext, response: BankResponse) { protected open fun updateBankAndCustomerDataIfResponseSuccessful(context: JobContext, response: BankResponse) {
if (response.successful) { if (response.successful) {
updateBankAndCustomerData(context.bank, response, context) updateBankAndCustomerData(context.bank, response)
} }
} }
protected open fun updateBankAndCustomerData(bank: BankData, response: BankResponse, context: JobContext) { protected open fun updateBankAndCustomerData(bank: BankData, response: BankResponse) {
updateBankData(bank, response) updateBankData(bank, response)
modelMapper.updateCustomerData(bank, response, context) modelMapper.updateCustomerData(bank, response)
} }

View File

@ -26,9 +26,6 @@ data class FinTsClientOptions(
* Defaults to true. * Defaults to true.
*/ */
val removeSensitiveDataFromMessageLog: Boolean = true, val removeSensitiveDataFromMessageLog: Boolean = true,
val closeDialogs: Boolean = true,
val version: String = "1.0.0", // TODO: get version dynamically val version: String = "1.0.0", // TODO: get version dynamically
val productName: String = "15E53C26816138699C7B6A3E8" val productName: String = "15E53C26816138699C7B6A3E8"
) { ) {

View File

@ -1,12 +0,0 @@
package net.codinux.banking.fints.extensions
import kotlinx.datetime.Clock
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.Instant
import kotlinx.datetime.plus
// should actually be named `now()`, but that name is already shadowed by deprecated Instant.Companion.now() method
fun Instant.Companion.nowExt(): Instant = Clock.System.now()
fun Instant.plusMinutes(minutes: Int) = this.plus(minutes, DateTimeUnit.MINUTE)

View File

@ -13,5 +13,5 @@ fun LocalDateTime.Companion.nowAtEuropeBerlin(): LocalDateTime {
} }
fun LocalDateTime.Companion.nowAt(timeZone: TimeZone): LocalDateTime { fun LocalDateTime.Companion.nowAt(timeZone: TimeZone): LocalDateTime {
return Instant.nowExt().toLocalDateTime(timeZone) return Clock.System.now().toLocalDateTime(timeZone)
} }

View File

@ -1,11 +1,11 @@
package net.codinux.banking.fints.extensions package net.codinux.banking.fints.extensions
import kotlinx.datetime.Instant import kotlinx.datetime.Clock
import kotlin.random.Random import kotlin.random.Random
fun randomWithSeed(): Random = Random(randomSeed()) fun randomWithSeed(): Random = Random(randomSeed())
fun randomSeed(): Long { fun randomSeed(): Long {
return Instant.nowExt().nanosecondsOfSecond.toLong() + Instant.nowExt().toEpochMilliseconds() return Clock.System.now().nanosecondsOfSecond.toLong() + Clock.System.now().toEpochMilliseconds()
} }

View File

@ -26,11 +26,8 @@ open class FinTsModelMapper {
protected open val bicFinder = BicFinder() protected open val bicFinder = BicFinder()
open fun mapToBankData(param: FinTsClientParameter, finTsServerAddress: String, defaultValues: BankData? = null): BankData { open fun mapToBankData(param: FinTsClientParameter, finTsServerAddress: String): BankData {
return BankData( return BankData(param.bankCode, param.loginName, param.password, finTsServerAddress, bicFinder.findBic(param.bankCode) ?: "")
param.bankCode, param.loginName, param.password, finTsServerAddress,
defaultValues?.bic ?: bicFinder.findBic(param.bankCode) ?: "", defaultValues?.bankName ?: ""
)
} }
open fun mapToAccountData(credentials: BankAccountIdentifier, param: FinTsClientParameter): AccountData { open fun mapToAccountData(credentials: BankAccountIdentifier, param: FinTsClientParameter): AccountData {
@ -58,7 +55,7 @@ open class FinTsModelMapper {
open fun map(account: AccountData): BankAccount { open fun map(account: AccountData): BankAccount {
return BankAccount(account.accountIdentifier, account.subAccountAttribute, account.iban, account.accountHolderName, map(account.accountType), account.productName, return BankAccount(account.accountIdentifier, account.subAccountAttribute, account.iban, account.accountHolderName, map(account.accountType), account.productName,
account.currency ?: Currency.DefaultCurrencyCode, account.accountLimit, account.serverTransactionsRetentionDays, account.isAccountTypeSupportedByApplication, account.currency ?: Currency.DefaultCurrencyCode, account.accountLimit, account.countDaysForWhichTransactionsAreKept, account.isAccountTypeSupportedByApplication,
account.supportsRetrievingAccountTransactions, account.supportsRetrievingBalance, account.supportsTransferringMoney, account.supportsRealTimeTransfer) account.supportsRetrievingAccountTransactions, account.supportsRetrievingBalance, account.supportsTransferringMoney, account.supportsRealTimeTransfer)
} }
@ -114,28 +111,14 @@ open class FinTsModelMapper {
} }
open fun map(transaction: net.codinux.banking.fints.model.AccountTransaction): AccountTransaction { open fun map(transaction: net.codinux.banking.fints.model.AccountTransaction): AccountTransaction {
return AccountTransaction( return AccountTransaction(transaction.amount, transaction.unparsedReference, transaction.bookingDate,
transaction.amount, transaction.reference, transaction.otherPartyName, transaction.otherPartyBankCode, transaction.otherPartyAccountId, transaction.bookingText, transaction.valueDate,
transaction.bookingDate, transaction.valueDate, transaction.statementNumber, transaction.sequenceNumber, transaction.openingBalance, transaction.closingBalance,
transaction.otherPartyName, transaction.otherPartyBankId, transaction.otherPartyAccountId, transaction.endToEndReference, transaction.customerReference, transaction.mandateReference, transaction.creditorIdentifier, transaction.originatorsIdentificationCode,
transaction.compensationAmount, transaction.originalAmount, transaction.sepaReference, transaction.deviantOriginator, transaction.deviantRecipient,
transaction.postingText, transaction.referenceWithNoSpecialType, transaction.primaNotaNumber, transaction.textKeySupplement,
transaction.currencyType, transaction.bookingKey, transaction.referenceForTheAccountOwner, transaction.referenceOfTheAccountServicingInstitution, transaction.supplementaryDetails,
transaction.openingBalance, transaction.closingBalance, transaction.transactionReferenceNumber, transaction.relatedReferenceNumber)
transaction.statementNumber, transaction.sheetNumber,
transaction.customerReference, transaction.bankReference, transaction.furtherInformation,
transaction.endToEndReference, transaction.mandateReference, transaction.creditorIdentifier, transaction.originatorsIdentificationCode,
transaction.compensationAmount, transaction.originalAmount, transaction.deviantOriginator, transaction.deviantRecipient,
transaction.referenceWithNoSpecialType,
transaction.journalNumber, transaction.textKeyAddition,
transaction.orderReferenceNumber, transaction.referenceNumber,
transaction.isReversal
)
} }

View File

@ -32,11 +32,7 @@ open class AccountData(
|| allowedJobNames.contains(CustomerSegmentId.AccountTransactionsMt940.id) || allowedJobNames.contains(CustomerSegmentId.AccountTransactionsMt940.id)
open var countDaysForWhichTransactionsAreKept: Int? = null
/**
* Count days for which transactions are stored on bank server (if available).
*/
open var serverTransactionsRetentionDays: Int? = null
protected open val _supportedFeatures = mutableSetOf<AccountFeature>() protected open val _supportedFeatures = mutableSetOf<AccountFeature>()

View File

@ -7,124 +7,59 @@ import net.codinux.banking.fints.extensions.UnixEpochStart
open class AccountTransaction( open class AccountTransaction(
val account: AccountData, val account: AccountData,
val amount: Money, val amount: Money,
val reference: String?, // that was also new to me that reference may is null val isReversal: Boolean,
val unparsedReference: String,
val bookingDate: LocalDate, val bookingDate: LocalDate,
val valueDate: LocalDate,
/**
* Name des Überweisenden oder Zahlungsempfängers
*/
val otherPartyName: String?, val otherPartyName: String?,
/** val otherPartyBankCode: String?,
* BIC des Überweisenden / Zahlungsempfängers
*/
val otherPartyBankId: String?,
/**
* IBAN des Überweisenden oder Zahlungsempfängers
*/
val otherPartyAccountId: String?, val otherPartyAccountId: String?,
val bookingText: String?,
/** val valueDate: LocalDate,
* Buchungstext, z. B. DAUERAUFTRAG, BARGELDAUSZAHLUNG, ONLINE-UEBERWEISUNG, FOLGELASTSCHRIFT, ... val statementNumber: Int,
*/ val sequenceNumber: Int?,
val postingText: String?,
val openingBalance: Money?, val openingBalance: Money?,
val closingBalance: Money?, val closingBalance: Money?,
/**
* Auszugsnummer
*/
val statementNumber: Int,
/**
* Blattnummer
*/
val sheetNumber: Int?,
/**
* Kundenreferenz.
*/
val customerReference: String?,
/**
* Bankreferenz
*/
val bankReference: String?,
/**
* Währungsart und Umsatzbetrag in Ursprungswährung
*/
val furtherInformation: String?,
/* Remittance information */
val endToEndReference: String?, val endToEndReference: String?,
val customerReference: String?,
val mandateReference: String?, val mandateReference: String?,
val creditorIdentifier: String?, val creditorIdentifier: String?,
val originatorsIdentificationCode: String?, val originatorsIdentificationCode: String?,
/**
* Summe aus Auslagenersatz und Bearbeitungsprovision bei einer nationalen Rücklastschrift
* sowie optionalem Zinsausgleich.
*/
val compensationAmount: String?, val compensationAmount: String?,
/**
* Betrag der ursprünglichen Lastschrift
*/
val originalAmount: String?, val originalAmount: String?,
/** val sepaReference: String?,
* Abweichender Überweisender oder Zahlungsempfänger
*/
val deviantOriginator: String?, val deviantOriginator: String?,
/**
* Abweichender Zahlungsempfänger oder Zahlungspflichtiger
*/
val deviantRecipient: String?, val deviantRecipient: String?,
val referenceWithNoSpecialType: String?, val referenceWithNoSpecialType: String?,
val primaNotaNumber: String?,
val textKeySupplement: String?,
/** val currencyType: String?,
* Primanoten-Nr. val bookingKey: String,
*/ val referenceForTheAccountOwner: String,
val journalNumber: String?, val referenceOfTheAccountServicingInstitution: String?,
/** val supplementaryDetails: String?,
* Bei R-Transaktionen siehe Tabelle der
* SEPA-Rückgabecodes, bei SEPALastschriften siehe optionale Belegung
* bei GVC 104 und GVC 105 (GVC = Geschäftsvorfallcode)
*/
val textKeyAddition: String?,
/** val transactionReferenceNumber: String,
* Referenznummer, die vom Sender als eindeutige Kennung für die Nachricht vergeben wurde val relatedReferenceNumber: String?
* (z.B. als Referenz auf stornierte Nachrichten).
*/
val orderReferenceNumber: String?,
/**
* Bezugsreferenz
*/
val referenceNumber: String?,
/**
* Storno, ob die Buchung storniert wurde(?).
* Aus:
* RC = Storno Haben
* RD = Storno Soll
*/
val isReversal: Boolean
) { ) {
// for object deserializers // for object deserializers
internal constructor() : this(AccountData(), Money(Amount.Zero, ""), "", UnixEpochStart, UnixEpochStart, null, null, null, null) internal constructor() : this(AccountData(), Money(Amount.Zero, ""), "", UnixEpochStart, null, null, null, null, UnixEpochStart)
constructor(account: AccountData, amount: Money, unparsedReference: String, bookingDate: LocalDate, valueDate: LocalDate, otherPartyName: String?, otherPartyBankId: String?, otherPartyAccountId: String?, postingText: String? = null) constructor(account: AccountData, amount: Money, unparsedReference: String, bookingDate: LocalDate, otherPartyName: String?, otherPartyBankCode: String?, otherPartyAccountId: String?, bookingText: String?, valueDate: LocalDate)
: this(account, amount, unparsedReference, bookingDate, valueDate, otherPartyName, otherPartyBankId, otherPartyAccountId, postingText, : this(account, amount, false, unparsedReference, bookingDate, otherPartyName, otherPartyBankCode, otherPartyAccountId, bookingText, valueDate,
null, null, 0, null, 0, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
"", null, null, "", null, false) null, "", "", null, null, "", null)
open val showOtherPartyName: Boolean open val showOtherPartyName: Boolean
get() = otherPartyName.isNullOrBlank() == false /* && type != "ENTGELTABSCHLUSS" && type != "AUSZAHLUNG" */ // TODO get() = otherPartyName.isNullOrBlank() == false /* && type != "ENTGELTABSCHLUSS" && type != "AUSZAHLUNG" */ // TODO
val reference: String
get() = sepaReference ?: unparsedReference
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
@ -132,12 +67,12 @@ open class AccountTransaction(
if (account != other.account) return false if (account != other.account) return false
if (amount != other.amount) return false if (amount != other.amount) return false
if (reference != other.reference) return false if (unparsedReference != other.unparsedReference) return false
if (bookingDate != other.bookingDate) return false if (bookingDate != other.bookingDate) return false
if (otherPartyName != other.otherPartyName) return false if (otherPartyName != other.otherPartyName) return false
if (otherPartyBankId != other.otherPartyBankId) return false if (otherPartyBankCode != other.otherPartyBankCode) return false
if (otherPartyAccountId != other.otherPartyAccountId) return false if (otherPartyAccountId != other.otherPartyAccountId) return false
if (postingText != other.postingText) return false if (bookingText != other.bookingText) return false
if (valueDate != other.valueDate) return false if (valueDate != other.valueDate) return false
return true return true
@ -146,19 +81,19 @@ open class AccountTransaction(
override fun hashCode(): Int { override fun hashCode(): Int {
var result = account.hashCode() var result = account.hashCode()
result = 31 * result + amount.hashCode() result = 31 * result + amount.hashCode()
result = 31 * result + reference.hashCode() result = 31 * result + unparsedReference.hashCode()
result = 31 * result + bookingDate.hashCode() result = 31 * result + bookingDate.hashCode()
result = 31 * result + (otherPartyName?.hashCode() ?: 0) result = 31 * result + (otherPartyName?.hashCode() ?: 0)
result = 31 * result + (otherPartyBankId?.hashCode() ?: 0) result = 31 * result + (otherPartyBankCode?.hashCode() ?: 0)
result = 31 * result + (otherPartyAccountId?.hashCode() ?: 0) result = 31 * result + (otherPartyAccountId?.hashCode() ?: 0)
result = 31 * result + (postingText?.hashCode() ?: 0) result = 31 * result + (bookingText?.hashCode() ?: 0)
result = 31 * result + valueDate.hashCode() result = 31 * result + valueDate.hashCode()
return result return result
} }
override fun toString(): String { override fun toString(): String {
return "$valueDate $amount $otherPartyName: $reference" return "$valueDate $amount $otherPartyName: $unparsedReference"
} }
} }

View File

@ -7,7 +7,6 @@ open class AddAccountParameter @JvmOverloads constructor(
open val bank: BankData, open val bank: BankData,
open val fetchBalanceAndTransactions: Boolean = true, open val fetchBalanceAndTransactions: Boolean = true,
open val preferredTanMethods: List<TanMethodType>? = null, open val preferredTanMethods: List<TanMethodType>? = null,
open val tanMethodsNotSupportedByApplication: List<TanMethodType>? = null,
open val preferredTanMedium: String? = null open val preferredTanMedium: String? = null
) { ) {

View File

@ -1,6 +1,5 @@
package net.codinux.banking.fints.model package net.codinux.banking.fints.model
import kotlinx.datetime.Instant
import net.codinux.banking.fints.tan.FlickerCode import net.codinux.banking.fints.tan.FlickerCode
@ -12,9 +11,8 @@ open class FlickerCodeTanChallenge(
tanMethod: TanMethod, tanMethod: TanMethod,
tanMediaIdentifier: String?, tanMediaIdentifier: String?,
bank: BankData, bank: BankData,
account: AccountData? = null, account: AccountData? = null
tanExpirationTime: Instant? = null ) : TanChallenge(forAction, messageToShowToUser, challenge, tanMethod, tanMediaIdentifier, bank, account) {
) : TanChallenge(forAction, messageToShowToUser, challenge, tanMethod, tanMediaIdentifier, bank, account, tanExpirationTime) {
override fun toString(): String { override fun toString(): String {
return "$tanMethod (medium: $tanMediaIdentifier) $flickerCode: $messageToShowToUser" return "$tanMethod (medium: $tanMediaIdentifier) $flickerCode: $messageToShowToUser"

View File

@ -1,6 +1,5 @@
package net.codinux.banking.fints.model package net.codinux.banking.fints.model
import kotlinx.datetime.Instant
import net.codinux.banking.fints.tan.TanImage import net.codinux.banking.fints.tan.TanImage
@ -12,9 +11,8 @@ open class ImageTanChallenge(
tanMethod: TanMethod, tanMethod: TanMethod,
tanMediaIdentifier: String?, tanMediaIdentifier: String?,
bank: BankData, bank: BankData,
account: AccountData? = null, account: AccountData? = null
tanExpirationTime: Instant? = null ) : TanChallenge(forAction, messageToShowToUser, challenge, tanMethod, tanMediaIdentifier, bank, account) {
) : TanChallenge(forAction, messageToShowToUser, challenge, tanMethod, tanMediaIdentifier, bank, account, tanExpirationTime) {
override fun toString(): String { override fun toString(): String {
return "$tanMethod (medium: $tanMediaIdentifier) $image: $messageToShowToUser" return "$tanMethod (medium: $tanMediaIdentifier) $image: $messageToShowToUser"

View File

@ -25,9 +25,6 @@ open class JobContext(
* Only set if the current context is for a specific account (like get account's transactions). * Only set if the current context is for a specific account (like get account's transactions).
*/ */
open val account: AccountData? = null, open val account: AccountData? = null,
open val preferredTanMethods: List<TanMethodType>? = null,
tanMethodsNotSupportedByApplication: List<TanMethodType>? = null,
open val preferredTanMedium: String? = null,
protected open val messageLogCollector: MessageLogCollector = MessageLogCollector(callback, config.options) protected open val messageLogCollector: MessageLogCollector = MessageLogCollector(callback, config.options)
) : MessageBaseData(bank, config.options.product), IMessageLogAppender { ) : MessageBaseData(bank, config.options.product), IMessageLogAppender {
@ -38,8 +35,6 @@ open class JobContext(
protected open val _dialogs = mutableListOf<DialogContext>() protected open val _dialogs = mutableListOf<DialogContext>()
open val tanMethodsNotSupportedByApplication: List<TanMethodType> = tanMethodsNotSupportedByApplication ?: emptyList()
open val mt940Parser: IAccountTransactionsParser = Mt940AccountTransactionsParser(Mt940Parser(this), this) open val mt940Parser: IAccountTransactionsParser = Mt940AccountTransactionsParser(Mt940Parser(this), this)
open val responseParser: ResponseParser = ResponseParser(logAppender = this) open val responseParser: ResponseParser = ResponseParser(logAppender = this)
@ -60,7 +55,7 @@ open class JobContext(
protected open var dialogNumber: Int = 0 protected open var dialogNumber: Int = 0
open fun startNewDialog(closeDialog: Boolean = config.options.closeDialogs, dialogId: String = DialogContext.InitialDialogId, open fun startNewDialog(closeDialog: Boolean = true, dialogId: String = DialogContext.InitialDialogId,
versionOfSecurityProcedure: VersionDesSicherheitsverfahrens = VersionDesSicherheitsverfahrens.Version_2, versionOfSecurityProcedure: VersionDesSicherheitsverfahrens = VersionDesSicherheitsverfahrens.Version_2,
chunkedResponseHandler: ((BankResponse) -> Unit)? = dialog.chunkedResponseHandler) : DialogContext { chunkedResponseHandler: ((BankResponse) -> Unit)? = dialog.chunkedResponseHandler) : DialogContext {

View File

@ -1,7 +1,7 @@
package net.codinux.banking.fints.model package net.codinux.banking.fints.model
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import net.codinux.banking.fints.extensions.nowExt
import net.codinux.banking.fints.log.MessageContext import net.codinux.banking.fints.log.MessageContext
import net.codinux.banking.fints.response.segments.ReceivedSegment import net.codinux.banking.fints.response.segments.ReceivedSegment
@ -18,7 +18,7 @@ open class MessageLogEntry(
* Is only set if [type] is set to [MessageLogEntryType.Received] and response parsing was successful. * Is only set if [type] is set to [MessageLogEntryType.Received] and response parsing was successful.
*/ */
open val parsedSegments: List<ReceivedSegment> = emptyList(), open val parsedSegments: List<ReceivedSegment> = emptyList(),
open val time: Instant = Instant.nowExt() open val time: Instant = Clock.System.now()
) { ) {
val messageIncludingMessageTrace: String val messageIncludingMessageTrace: String

View File

@ -1,11 +1,8 @@
package net.codinux.banking.fints.model package net.codinux.banking.fints.model
import kotlinx.datetime.Instant
import net.codinux.banking.fints.extensions.nowExt
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanMedium import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanMedium
import net.codinux.banking.fints.response.BankResponse import net.codinux.banking.fints.response.BankResponse
import net.codinux.banking.fints.response.client.FinTsClientResponse import net.codinux.banking.fints.response.client.FinTsClientResponse
import net.codinux.log.Log
open class TanChallenge( open class TanChallenge(
@ -15,14 +12,7 @@ open class TanChallenge(
val tanMethod: TanMethod, val tanMethod: TanMethod,
val tanMediaIdentifier: String?, val tanMediaIdentifier: String?,
val bank: BankData, val bank: BankData,
val account: AccountData? = null, val account: AccountData? = null
/**
* Datum und Uhrzeit, bis zu welchem Zeitpunkt eine TAN auf Basis der gesendeten Challenge gültig ist. Nach Ablauf der Gültigkeitsdauer wird die entsprechende TAN entwertet.
*
* In server's time zone, that is Europe/Berlin.
*/
val tanExpirationTime: Instant? = null,
val challengeCreationTimestamp: Instant = Instant.nowExt()
) { ) {
var enterTanResult: EnterTanResult? = null var enterTanResult: EnterTanResult? = null
@ -31,8 +21,6 @@ open class TanChallenge(
open val isEnteringTanDone: Boolean open val isEnteringTanDone: Boolean
get() = enterTanResult != null get() = enterTanResult != null
private val tanExpiredCallbacks = mutableListOf<() -> Unit>()
private val userApprovedDecoupledTanCallbacks = mutableListOf<() -> Unit>() private val userApprovedDecoupledTanCallbacks = mutableListOf<() -> Unit>()
@ -43,59 +31,23 @@ open class TanChallenge(
internal fun userApprovedDecoupledTan(responseAfterApprovingDecoupledTan: BankResponse) { internal fun userApprovedDecoupledTan(responseAfterApprovingDecoupledTan: BankResponse) {
this.enterTanResult = EnterTanResult(null, true, responseAfterApprovingDecoupledTan) this.enterTanResult = EnterTanResult(null, true, responseAfterApprovingDecoupledTan)
userApprovedDecoupledTanCallbacks.toTypedArray().forEach { // copy to avoid ConcurrentModificationException userApprovedDecoupledTanCallbacks.forEach { it.invoke() }
try { userApprovedDecoupledTanCallbacks.clear()
it.invoke()
} catch (e: Throwable) {
Log.error(e) { "Could not call userApprovedDecoupledTanCallback" }
}
}
clearUserApprovedDecoupledTanCallbacks()
} }
fun userDidNotEnterTan() { fun userDidNotEnterTan() {
clearUserApprovedDecoupledTanCallbacks()
this.enterTanResult = EnterTanResult(null) this.enterTanResult = EnterTanResult(null)
} }
internal fun tanExpired() {
tanExpiredCallbacks.toTypedArray().forEach {
try {
it.invoke()
} catch (e: Throwable) {
Log.error(e) { "Could not call tanExpiredCallback" }
}
}
clearTanExpiredCallbacks()
userDidNotEnterTan()
}
fun userAsksToChangeTanMethod(changeTanMethodTo: TanMethod) { fun userAsksToChangeTanMethod(changeTanMethodTo: TanMethod) {
clearUserApprovedDecoupledTanCallbacks()
this.enterTanResult = EnterTanResult(null, changeTanMethodTo = changeTanMethodTo) this.enterTanResult = EnterTanResult(null, changeTanMethodTo = changeTanMethodTo)
} }
fun userAsksToChangeTanMedium(changeTanMediumTo: TanMedium, changeTanMediumResultCallback: ((FinTsClientResponse) -> Unit)?) { fun userAsksToChangeTanMedium(changeTanMediumTo: TanMedium, changeTanMediumResultCallback: ((FinTsClientResponse) -> Unit)?) {
clearUserApprovedDecoupledTanCallbacks()
this.enterTanResult = EnterTanResult(null, changeTanMediumTo = changeTanMediumTo, changeTanMediumResultCallback = changeTanMediumResultCallback) this.enterTanResult = EnterTanResult(null, changeTanMediumTo = changeTanMediumTo, changeTanMediumResultCallback = changeTanMediumResultCallback)
} }
fun addTanExpiredCallback(callback: () -> Unit) {
if (isEnteringTanDone == false) {
this.tanExpiredCallbacks.add(callback)
}
}
protected open fun clearTanExpiredCallbacks() {
tanExpiredCallbacks.clear()
}
fun addUserApprovedDecoupledTanCallback(callback: () -> Unit) { fun addUserApprovedDecoupledTanCallback(callback: () -> Unit) {
if (isEnteringTanDone == false) { if (isEnteringTanDone == false) {
this.userApprovedDecoupledTanCallbacks.add(callback) this.userApprovedDecoupledTanCallbacks.add(callback)
@ -104,10 +56,6 @@ open class TanChallenge(
} }
} }
protected open fun clearUserApprovedDecoupledTanCallbacks() {
userApprovedDecoupledTanCallbacks.clear()
}
override fun toString(): String { override fun toString(): String {
return "$tanMethod (medium: $tanMediaIdentifier): $messageToShowToUser" return "$tanMethod (medium: $tanMediaIdentifier): $messageToShowToUser"

View File

@ -63,7 +63,7 @@ open class ModelMapper(
} }
} }
open fun updateCustomerData(bank: BankData, response: BankResponse, context: JobContext) { open fun updateCustomerData(bank: BankData, response: BankResponse) {
response.getFirstSegmentById<BankParameters>(InstituteSegmentId.BankParameters)?.let { bankParameters -> response.getFirstSegmentById<BankParameters>(InstituteSegmentId.BankParameters)?.let { bankParameters ->
// TODO: ask user if there is more than one supported language? But it seems that almost all banks only support German. // TODO: ask user if there is more than one supported language? But it seems that almost all banks only support German.
if (bank.selectedLanguage == Dialogsprache.Default && bankParameters.supportedLanguages.isNotEmpty()) { if (bank.selectedLanguage == Dialogsprache.Default && bankParameters.supportedLanguages.isNotEmpty()) {
@ -102,7 +102,7 @@ open class ModelMapper(
accountInfo.accountLimit, accountInfo.allowedJobNames) accountInfo.accountLimit, accountInfo.allowedJobNames)
bank.supportedJobs.filterIsInstance<RetrieveAccountTransactionsParameters>().sortedByDescending { it.segmentVersion }.firstOrNull { newAccount.allowedJobNames.contains(it.jobName) }?.let { transactionsParameters -> bank.supportedJobs.filterIsInstance<RetrieveAccountTransactionsParameters>().sortedByDescending { it.segmentVersion }.firstOrNull { newAccount.allowedJobNames.contains(it.jobName) }?.let { transactionsParameters ->
newAccount.serverTransactionsRetentionDays = transactionsParameters.serverTransactionsRetentionDays newAccount.countDaysForWhichTransactionsAreKept = transactionsParameters.countDaysForWhichTransactionsAreKept
} }
bank.addAccount(newAccount) bank.addAccount(newAccount)
@ -146,7 +146,6 @@ open class ModelMapper(
if (response.supportedTanMethodsForUser.isNotEmpty()) { if (response.supportedTanMethodsForUser.isNotEmpty()) {
bank.tanMethodsAvailableForUser = response.supportedTanMethodsForUser.mapNotNull { findTanMethod(it, bank) } bank.tanMethodsAvailableForUser = response.supportedTanMethodsForUser.mapNotNull { findTanMethod(it, bank) }
.filterNot { context.tanMethodsNotSupportedByApplication.contains(it.type) }
if (bank.tanMethodsAvailableForUser.firstOrNull { it.securityFunction == bank.selectedTanMethod.securityFunction } == null) { // supportedTanMethods don't contain selectedTanMethod anymore if (bank.tanMethodsAvailableForUser.firstOrNull { it.securityFunction == bank.selectedTanMethod.securityFunction } == null) { // supportedTanMethods don't contain selectedTanMethod anymore
bank.resetSelectedTanMethod() bank.resetSelectedTanMethod()

View File

@ -1,7 +1,9 @@
package net.codinux.banking.fints.response package net.codinux.banking.fints.response
import kotlinx.datetime.* import kotlinx.datetime.LocalDate
import net.codinux.banking.fints.extensions.EuropeBerlin import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.LocalTime
import kotlinx.datetime.atTime
import net.codinux.log.logger import net.codinux.log.logger
import net.codinux.banking.fints.log.IMessageLogAppender import net.codinux.banking.fints.log.IMessageLogAppender
import net.codinux.banking.fints.messages.Separators import net.codinux.banking.fints.messages.Separators
@ -575,7 +577,7 @@ open class ResponseParser(
if (dataElementGroups.size > 3) parseStringToNullIfEmpty(dataElementGroups[3]) else null, if (dataElementGroups.size > 3) parseStringToNullIfEmpty(dataElementGroups[3]) else null,
if (dataElementGroups.size > 4) parseStringToNullIfEmpty(dataElementGroups[4]) else null, if (dataElementGroups.size > 4) parseStringToNullIfEmpty(dataElementGroups[4]) else null,
binaryChallengeHHD_UC?.let { extractBinaryData(it) }, binaryChallengeHHD_UC?.let { extractBinaryData(it) },
if (dataElementGroups.size > 6) parseNullableDateTime(dataElementGroups[6])?.toInstant(TimeZone.EuropeBerlin) else null, if (dataElementGroups.size > 6) parseNullableDateTime(dataElementGroups[6]) else null,
if (dataElementGroups.size > 7) parseStringToNullIfEmpty(dataElementGroups[7]) else null, if (dataElementGroups.size > 7) parseStringToNullIfEmpty(dataElementGroups[7]) else null,
segment segment
) )
@ -746,11 +748,11 @@ open class ResponseParser(
val transactionsParameterIndex = if (jobParameters.segmentVersion >= 6) 4 else 3 val transactionsParameterIndex = if (jobParameters.segmentVersion >= 6) 4 else 3
val dataElements = getDataElements(dataElementGroups[transactionsParameterIndex]) val dataElements = getDataElements(dataElementGroups[transactionsParameterIndex])
val serverTransactionsRetentionDays = parseInt(dataElements[0]) val countDaysForWhichTransactionsAreKept = parseInt(dataElements[0])
val settingCountEntriesAllowed = parseBoolean(dataElements[1]) val settingCountEntriesAllowed = parseBoolean(dataElements[1])
val settingAllAccountAllowed = if (dataElements.size > 2) parseBoolean(dataElements[2]) else false val settingAllAccountAllowed = if (dataElements.size > 2) parseBoolean(dataElements[2]) else false
return RetrieveAccountTransactionsParameters(jobParameters, serverTransactionsRetentionDays, settingCountEntriesAllowed, settingAllAccountAllowed) return RetrieveAccountTransactionsParameters(jobParameters, countDaysForWhichTransactionsAreKept, settingCountEntriesAllowed, settingAllAccountAllowed)
} }
@ -803,11 +805,11 @@ open class ResponseParser(
val transactionsParameterIndex = if (jobParameters.segmentVersion >= 2) 4 else 3 // TODO: check if at segment version 1 the transactions parameter are the third data elements group val transactionsParameterIndex = if (jobParameters.segmentVersion >= 2) 4 else 3 // TODO: check if at segment version 1 the transactions parameter are the third data elements group
val dataElements = getDataElements(dataElementGroups[transactionsParameterIndex]) val dataElements = getDataElements(dataElementGroups[transactionsParameterIndex])
val serverTransactionsRetentionDays = parseInt(dataElements[0]) val countDaysForWhichTransactionsAreKept = parseInt(dataElements[0])
val settingCountEntriesAllowed = parseBoolean(dataElements[1]) val settingCountEntriesAllowed = parseBoolean(dataElements[1])
val settingAllAccountAllowed = if (dataElements.size > 2) parseBoolean(dataElements[2]) else false val settingAllAccountAllowed = if (dataElements.size > 2) parseBoolean(dataElements[2]) else false
return RetrieveAccountTransactionsParameters(jobParameters, serverTransactionsRetentionDays, settingCountEntriesAllowed, settingAllAccountAllowed) return RetrieveAccountTransactionsParameters(jobParameters, countDaysForWhichTransactionsAreKept, settingCountEntriesAllowed, settingAllAccountAllowed)
} }

View File

@ -3,7 +3,7 @@ package net.codinux.banking.fints.response.segments
open class RetrieveAccountTransactionsParameters( open class RetrieveAccountTransactionsParameters(
parameters: JobParameters, parameters: JobParameters,
open val serverTransactionsRetentionDays: Int, open val countDaysForWhichTransactionsAreKept: Int,
open val settingCountEntriesAllowed: Boolean, open val settingCountEntriesAllowed: Boolean,
open val settingAllAccountAllowed: Boolean open val settingAllAccountAllowed: Boolean
) : JobParameters(parameters) { ) : JobParameters(parameters) {

View File

@ -1,6 +1,6 @@
package net.codinux.banking.fints.response.segments package net.codinux.banking.fints.response.segments
import kotlinx.datetime.Instant import kotlinx.datetime.LocalDateTime
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanProcess import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanProcess
@ -31,13 +31,7 @@ open class TanResponse(
val challenge: String?, // M: bei TAN-Prozess=1, 3, 4. O: bei TAN-Prozess=2 val challenge: String?, // M: bei TAN-Prozess=1, 3, 4. O: bei TAN-Prozess=2
val challengeHHD_UC: String?, val challengeHHD_UC: String?,
val validityDateTimeForChallenge: LocalDateTime?,
/**
* Datum und Uhrzeit, bis zu welchem Zeitpunkt eine TAN auf Basis der gesendeten Challenge gültig ist. Nach Ablauf der Gültigkeitsdauer wird die entsprechende TAN entwertet.
*
* In server's time zone, that is Europe/Berlin.
*/
val tanExpirationTime: Instant?,
val tanMediaIdentifier: String? = null, // M: bei TAN-Prozess=1, 3, 4 und „Anzahl unterstützter aktiver TAN-Medien“ nicht vorhanden. O: sonst val tanMediaIdentifier: String? = null, // M: bei TAN-Prozess=1, 3, 4 und „Anzahl unterstützter aktiver TAN-Medien“ nicht vorhanden. O: sonst
segmentString: String segmentString: String

View File

@ -44,50 +44,44 @@ open class Mt940AccountTransactionsParser(
protected open fun mapToAccountTransaction(statement: AccountStatement, transaction: Transaction, account: AccountData): AccountTransaction { protected open fun mapToAccountTransaction(statement: AccountStatement, transaction: Transaction, account: AccountData): AccountTransaction {
val currency = statement.closingBalance.currency val currency = statement.closingBalance.currency
// may parse postingKey to postingText (Buchungstext) according to table in 8.2.3 Buchungsschlüssel (Feld 61), S. 654 ff.
return AccountTransaction( return AccountTransaction(
account, account,
Money(mapAmount(transaction.statementLine), currency), Money(mapAmount(transaction.statementLine), currency),
// either field :86: contains structured information, then sepaReference is a mandatory field, or :86: is unstructured, then the whole field value is the reference transaction.statementLine.isReversal,
transaction.information?.sepaReference ?: transaction.information?.unparsedReference ?: "", transaction.information?.unparsedReference ?: "",
transaction.statementLine.bookingDate ?: statement.closingBalance.bookingDate, transaction.statementLine.bookingDate ?: statement.closingBalance.bookingDate,
transaction.statementLine.valueDate,
transaction.information?.otherPartyName, transaction.information?.otherPartyName,
transaction.information?.otherPartyBankId, transaction.information?.otherPartyBankCode,
transaction.information?.otherPartyAccountId, transaction.information?.otherPartyAccountId,
transaction.information?.bookingText,
transaction.information?.postingText, transaction.statementLine.valueDate,
Money(mapAmount(statement.openingBalance), currency),
Money(mapAmount(statement.closingBalance), currency),
statement.statementNumber, statement.statementNumber,
statement.sheetNumber, statement.sequenceNumber,
Money(mapAmount(statement.openingBalance), currency), // TODO: that's not true, these are the opening and closing balance of
// :60: customer reference: Wenn „KREF+“ eingestellt ist, dann erfolgt die Angabe der Referenznummer in Tag :86: . Money(mapAmount(statement.closingBalance), currency), // all transactions of this day, not this specific transaction's ones
transaction.information?.customerReference ?: transaction.statementLine.customerReference,
transaction.statementLine.bankReference,
transaction.statementLine.furtherInformationOriginalAmountAndCharges,
transaction.information?.endToEndReference, transaction.information?.endToEndReference,
transaction.information?.customerReference,
transaction.information?.mandateReference, transaction.information?.mandateReference,
transaction.information?.creditorIdentifier, transaction.information?.creditorIdentifier,
transaction.information?.originatorsIdentificationCode, transaction.information?.originatorsIdentificationCode,
transaction.information?.compensationAmount, transaction.information?.compensationAmount,
transaction.information?.originalAmount, transaction.information?.originalAmount,
transaction.information?.sepaReference,
transaction.information?.deviantOriginator, transaction.information?.deviantOriginator,
transaction.information?.deviantRecipient, transaction.information?.deviantRecipient,
transaction.information?.referenceWithNoSpecialType, transaction.information?.referenceWithNoSpecialType,
transaction.information?.journalNumber, transaction.information?.primaNotaNumber,
transaction.information?.textKeyAddition, transaction.information?.textKeySupplement,
statement.orderReferenceNumber, transaction.statementLine.currencyType,
statement.referenceNumber, transaction.statementLine.bookingKey,
transaction.statementLine.referenceForTheAccountOwner,
transaction.statementLine.referenceOfTheAccountServicingInstitution,
transaction.statementLine.supplementaryDetails,
transaction.statementLine.isReversal, statement.transactionReferenceNumber,
statement.relatedReferenceNumber
) )
} }

View File

@ -34,9 +34,9 @@ open class Mt940Parser(
val AccountStatementFieldSeparatorRegex = Regex("(?<!T\\d\\d(:\\d\\d)?):\\d\\d\\w?:") val AccountStatementFieldSeparatorRegex = Regex("(?<!T\\d\\d(:\\d\\d)?):\\d\\d\\w?:")
const val OrderReferenceNumberCode = "20" const val TransactionReferenceNumberCode = "20"
const val ReferenceNumberCode = "21" const val RelatedReferenceNumberCode = "21"
const val AccountIdentificationCode = "25" const val AccountIdentificationCode = "25"
@ -46,7 +46,7 @@ open class Mt940Parser(
const val StatementLineCode = "61" const val StatementLineCode = "61"
const val RemittanceInformationFieldCode = "86" const val InformationToAccountOwnerCode = "86"
const val ClosingBalanceCode = "62" const val ClosingBalanceCode = "62"
@ -61,7 +61,7 @@ open class Mt940Parser(
val ReferenceTypeRegex = Regex("[A-Z]{4}\\+") val ReferenceTypeRegex = Regex("[A-Z]{4}\\+")
val RemittanceInformationSubFieldRegex = Regex("\\?\\d\\d") val InformationToAccountOwnerSubFieldRegex = Regex("\\?\\d\\d")
const val EndToEndReferenceKey = "EREF+" const val EndToEndReferenceKey = "EREF+"
@ -169,8 +169,8 @@ open class Mt940Parser(
val closingBalancePair = fieldsByCode.first { it.first.startsWith(ClosingBalanceCode) } val closingBalancePair = fieldsByCode.first { it.first.startsWith(ClosingBalanceCode) }
return AccountStatement( return AccountStatement(
getFieldValue(fieldsByCode, OrderReferenceNumberCode), getFieldValue(fieldsByCode, TransactionReferenceNumberCode),
getOptionalFieldValue(fieldsByCode, ReferenceNumberCode), getOptionalFieldValue(fieldsByCode, RelatedReferenceNumberCode),
accountIdentification[0], accountIdentification[0],
if (accountIdentification.size > 1) accountIdentification[1] else null, if (accountIdentification.size > 1) accountIdentification[1] else null,
statementAndMaySequenceNumber[0].toInt(), statementAndMaySequenceNumber[0].toInt(),
@ -210,7 +210,7 @@ open class Mt940Parser(
val statementLine = parseStatementLine(pair.second) val statementLine = parseStatementLine(pair.second)
val nextPair = if (index < fieldsByCode.size - 1) fieldsByCode.get(index + 1) else null val nextPair = if (index < fieldsByCode.size - 1) fieldsByCode.get(index + 1) else null
val information = if (nextPair?.first == RemittanceInformationFieldCode) parseNullableRemittanceInformationField(nextPair.second) else null val information = if (nextPair?.first == InformationToAccountOwnerCode) parseNullableInformationToAccountOwner(nextPair.second) else null
transactions.add(Transaction(statementLine, information)) transactions.add(Transaction(statementLine, information))
} }
@ -269,11 +269,11 @@ open class Mt940Parser(
*/ */
val transactionType = fieldValue.substring(amountEndIndex, amountEndIndex + 1) // transaction type is 'N', 'S' or 'F' val transactionType = fieldValue.substring(amountEndIndex, amountEndIndex + 1) // transaction type is 'N', 'S' or 'F'
val postingKeyStart = amountEndIndex + 1 val bookingKeyStart = amountEndIndex + 1
val postingKey = fieldValue.substring(postingKeyStart, postingKeyStart + 3) // TODO: parse codes, p. 178 val bookingKey = fieldValue.substring(bookingKeyStart, bookingKeyStart + 3) // TODO: parse codes, p. 178
val customerAndBankReference = fieldValue.substring(postingKeyStart + 3).split("//") val customerAndBankReference = fieldValue.substring(bookingKeyStart + 3).split("//")
val customerReference = customerAndBankReference[0].takeIf { it != "NONREF" } val customerReference = customerAndBankReference[0]
/** /**
* The content of this subfield is the account servicing institution's own reference for the transaction. * The content of this subfield is the account servicing institution's own reference for the transaction.
@ -281,58 +281,58 @@ open class Mt940Parser(
* reference may be identical to subfield 7, Reference for the Account Owner. If this is * reference may be identical to subfield 7, Reference for the Account Owner. If this is
* the case, Reference of the Account Servicing Institution, subfield 8 may be omitted. * the case, Reference of the Account Servicing Institution, subfield 8 may be omitted.
*/ */
var bankReference = if (customerAndBankReference.size > 1) customerAndBankReference[1] else null var bankReference = if (customerAndBankReference.size > 1) customerAndBankReference[1] else customerReference // TODO: or use null?
var furtherInformation: String? = null var supplementaryDetails: String? = null
if (bankReference != null && bankReference.contains('\n')) { val bankReferenceAndSupplementaryDetails = bankReference.split("\n")
val bankReferenceAndFurtherInformation = bankReference.split("\n") if (bankReferenceAndSupplementaryDetails.size > 1) {
bankReference = bankReferenceAndFurtherInformation[0].trim() bankReference = bankReferenceAndSupplementaryDetails[0].trim()
// TODO: parse /OCMT/ and /CHGS/, see page 518 // TODO: parse /OCMT/ and /CHGS/, see page 518
furtherInformation = bankReferenceAndFurtherInformation[1].trim() supplementaryDetails = bankReferenceAndSupplementaryDetails[1].trim()
} }
return StatementLine(!!!isDebit, isCancellation, valueDate, bookingDate, null, amount, postingKey, return StatementLine(!!!isDebit, isCancellation, valueDate, bookingDate, null, amount, bookingKey,
customerReference, bankReference, furtherInformation) customerReference, bankReference, supplementaryDetails)
} }
protected open fun parseNullableRemittanceInformationField(remittanceInformationFieldString: String): RemittanceInformationField? { protected open fun parseNullableInformationToAccountOwner(informationToAccountOwnerString: String): InformationToAccountOwner? {
try { try {
val information = parseRemittanceInformationField(remittanceInformationFieldString) val information = parseInformationToAccountOwner(informationToAccountOwnerString)
mapReference(information) mapReference(information)
return information return information
} catch (e: Exception) { } catch (e: Exception) {
logError("Could not parse RemittanceInformationField from field value '$remittanceInformationFieldString'", e) logError("Could not parse InformationToAccountOwner from field value '$informationToAccountOwnerString'", e)
} }
return null return null
} }
protected open fun parseRemittanceInformationField(remittanceInformationFieldString: String): RemittanceInformationField { protected open fun parseInformationToAccountOwner(informationToAccountOwnerString: String): InformationToAccountOwner {
// e. g. starts with 0 -> Inlandszahlungsverkehr, starts with '3' -> Wertpapiergeschäft // e. g. starts with 0 -> Inlandszahlungsverkehr, starts with '3' -> Wertpapiergeschäft
// see Finanzdatenformate p. 209 - 215 // see Finanzdatenformate p. 209 - 215
val geschaeftsvorfallCode = remittanceInformationFieldString.substring(0, 2) // TODO: may map val geschaeftsvorfallCode = informationToAccountOwnerString.substring(0, 2) // TODO: may map
val referenceParts = mutableListOf<String>() val referenceParts = mutableListOf<String>()
val otherPartyName = StringBuilder() val otherPartyName = StringBuilder()
var otherPartyBankId: String? = null var otherPartyBankCode: String? = null
var otherPartyAccountId: String? = null var otherPartyAccountId: String? = null
var bookingText: String? = null var bookingText: String? = null
var primaNotaNumber: String? = null var primaNotaNumber: String? = null
var textKeySupplement: String? = null var textKeySupplement: String? = null
val subFieldMatches = RemittanceInformationSubFieldRegex.findAll(remittanceInformationFieldString).toList() val subFieldMatches = InformationToAccountOwnerSubFieldRegex.findAll(informationToAccountOwnerString).toList()
subFieldMatches.forEachIndexed { index, matchResult -> subFieldMatches.forEachIndexed { index, matchResult ->
val fieldCode = matchResult.value.substring(1, 3).toInt() val fieldCode = matchResult.value.substring(1, 3).toInt()
val endIndex = if (index + 1 < subFieldMatches.size) subFieldMatches[index + 1].range.start else remittanceInformationFieldString.length val endIndex = if (index + 1 < subFieldMatches.size) subFieldMatches[index + 1].range.start else informationToAccountOwnerString.length
val fieldValue = remittanceInformationFieldString.substring(matchResult.range.last + 1, endIndex) val fieldValue = informationToAccountOwnerString.substring(matchResult.range.last + 1, endIndex)
when (fieldCode) { when (fieldCode) {
0 -> bookingText = fieldValue 0 -> bookingText = fieldValue
10 -> primaNotaNumber = fieldValue 10 -> primaNotaNumber = fieldValue
in 20..29 -> referenceParts.add(fieldValue) in 20..29 -> referenceParts.add(fieldValue)
30 -> otherPartyBankId = fieldValue 30 -> otherPartyBankCode = fieldValue
31 -> otherPartyAccountId = fieldValue 31 -> otherPartyAccountId = fieldValue
32, 33 -> otherPartyName.append(fieldValue) 32, 33 -> otherPartyName.append(fieldValue)
34 -> textKeySupplement = fieldValue 34 -> textKeySupplement = fieldValue
@ -345,8 +345,8 @@ open class Mt940Parser(
val otherPartyNameString = if (otherPartyName.isBlank()) null else otherPartyName.toString() val otherPartyNameString = if (otherPartyName.isBlank()) null else otherPartyName.toString()
return RemittanceInformationField( return InformationToAccountOwner(
reference, otherPartyNameString, otherPartyBankId, otherPartyAccountId, reference, otherPartyNameString, otherPartyBankCode, otherPartyAccountId,
bookingText, primaNotaNumber, textKeySupplement bookingText, primaNotaNumber, textKeySupplement
) )
} }
@ -396,7 +396,7 @@ open class Mt940Parser(
* *
* Weitere 4 Verwendungszwecke können zu den Feldschlüsseln 60 bis 63 eingestellt werden. * Weitere 4 Verwendungszwecke können zu den Feldschlüsseln 60 bis 63 eingestellt werden.
*/ */
protected open fun mapReference(information: RemittanceInformationField) { protected open fun mapReference(information: InformationToAccountOwner) {
val referenceParts = getReferenceParts(information.unparsedReference) val referenceParts = getReferenceParts(information.unparsedReference)
referenceParts.forEach { entry -> referenceParts.forEach { entry ->
@ -431,7 +431,7 @@ open class Mt940Parser(
} }
// TODO: there are more. See .pdf from Deutsche Bank // TODO: there are more. See .pdf from Deutsche Bank
protected open fun setReferenceLineValue(information: RemittanceInformationField, referenceType: String, typeValue: String) { protected open fun setReferenceLineValue(information: InformationToAccountOwner, referenceType: String, typeValue: String) {
when (referenceType) { when (referenceType) {
EndToEndReferenceKey -> information.endToEndReference = typeValue EndToEndReferenceKey -> information.endToEndReference = typeValue
CustomerReferenceKey -> information.customerReference = typeValue CustomerReferenceKey -> information.customerReference = typeValue

View File

@ -11,7 +11,7 @@ open class AccountStatement(
* *
* Max length = 16 * Max length = 16
*/ */
val orderReferenceNumber: String, val transactionReferenceNumber: String,
/** /**
* Bezugsreferenz oder NONREF. * Bezugsreferenz oder NONREF.
@ -20,7 +20,7 @@ open class AccountStatement(
* *
* Max length = 16 * Max length = 16
*/ */
val referenceNumber: String?, val relatedReferenceNumber: String?,
/** /**
* xxxxxxxxxxx/Konto-Nr. oder yyyyyyyy/Konto-Nr. * xxxxxxxxxxx/Konto-Nr. oder yyyyyyyy/Konto-Nr.
@ -50,7 +50,7 @@ open class AccountStatement(
* *
* Max length = 5 * Max length = 5
*/ */
val sheetNumber: Int?, val sequenceNumber: Int?,
val openingBalance: Balance, val openingBalance: Balance,
@ -72,7 +72,7 @@ open class AccountStatement(
* *
* Max length = 65 * Max length = 65
*/ */
val remittanceInformationField: String? = null val multipurposeField: String? = null
) { ) {

View File

@ -20,7 +20,7 @@ open class Balance(
val isCredit: Boolean, val isCredit: Boolean,
/** /**
* JJMMTT = Buchungsdatum des Saldos oder '000000' beim ersten Auszug * JJMMTT = Buchungsdatum des Saldos oder '0' beim ersten Auszug
* *
* Max length = 6 * Max length = 6
*/ */

View File

@ -0,0 +1,41 @@
package net.codinux.banking.fints.transactions.mt940.model
open class InformationToAccountOwner(
val unparsedReference: String,
val otherPartyName: String?,
val otherPartyBankCode: String?,
val otherPartyAccountId: String?,
val bookingText: String?,
val primaNotaNumber: String?,
val textKeySupplement: String?
) {
var endToEndReference: String? = null
var customerReference: String? = null
var mandateReference: String? = null
var creditorIdentifier: String? = null
var originatorsIdentificationCode: String? = null
var compensationAmount: String? = null
var originalAmount: String? = null
var sepaReference: String? = null
var deviantOriginator: String? = null
var deviantRecipient: String? = null
var referenceWithNoSpecialType: String? = null
override fun toString(): String {
return "$otherPartyName $unparsedReference"
}
}

View File

@ -1,99 +0,0 @@
package net.codinux.banking.fints.transactions.mt940.model
open class RemittanceInformationField(
val unparsedReference: String,
/**
* AT 02 Name des Überweisenden
* AT 03 Name des Zahlungsempfängers (bei mehr als 54 Zeichen wird der Name gekürzt)
*/
val otherPartyName: String?,
/**
* BLZ Überweisender / Zahlungsempfänger
* Bei SEPA-Zahlungen BIC des Überweisenden / Zahlungsempfängers.
*/
val otherPartyBankId: String?,
/**
* AT 01 IBAN des Überweisenden (Zahlungseingang Überweisung)
* AT 04 IBAN des Zahlungsempfängers (Eingang Lastschrift)
*/
val otherPartyAccountId: String?,
/**
* Buchungstext, z. B. DAUERAUFTRAG, BARGELDAUSZAHLUNG, ONLINE-UEBERWEISUNG, FOLGELASTSCHRIFT, ...
*/
val postingText: String?,
/**
* Primanoten-Nr.
*/
val journalNumber: String?,
/**
* Bei R-Transaktionen siehe Tabelle der
* SEPA-Rückgabecodes, bei SEPALastschriften siehe optionale Belegung
* bei GVC 104 und GVC 105 (GVC = Geschäftsvorfallcode)
*/
val textKeyAddition: String?
) {
/**
* (DDAT10; CT-AT41 - Angabe verpflichtend)
* (NOTPROVIDED wird nicht eingestellt.
* Im Falle von Schecks wird hinter EREF+ die Konstante SCHECK-NR. , gefolgt von der Schecknummer angegeben (erst
* nach Migration Scheckvordruck auf ISO 20022; November 2016, entspricht dem Inhalt der EndToEndId des
* entsprechenden Scheckumsatzes).
*/
var endToEndReference: String? = null
var customerReference: String? = null
/**
* (DD-AT01 - Angabe verpflichtend)
*/
var mandateReference: String? = null
/**
* (DD-AT02 - Angabe verpflichtend bei SEPALastschriften, nicht jedoch bei SEPARücklastschriften)
*/
var creditorIdentifier: String? = null
/**
* (CT-AT10- Angabe verpflichtend,)
* Entweder CRED oder DEBT
*/
var originatorsIdentificationCode: String? = null
/**
* Summe aus Auslagenersatz und Bearbeitungsprovision bei einer nationalen Rücklastschrift
* sowie optionalem Zinsausgleich.
*/
var compensationAmount: String? = null
/**
* Betrag der ursprünglichen Lastschrift
*/
var originalAmount: String? = null
/**
* (DD-AT22; CT-AT05 -Angabe verpflichtend, nicht jedoch bei RTransaktionen52)
*/
var sepaReference: String? = null
/**
* Abweichender Überweisender (CT-AT08) / Abweichender Zahlungsempfänger (DD-AT38)
* (optional)53
*/
var deviantOriginator: String? = null
/**
* Abweichender Zahlungsempfänger (CT-AT28) /
* Abweichender Zahlungspflichtiger ((DDAT15)
* (optional)53
*/
var deviantRecipient: String? = null
var referenceWithNoSpecialType: String? = null
override fun toString(): String {
return "$otherPartyName $unparsedReference"
}
}

View File

@ -42,57 +42,26 @@ open class StatementLine(
val currencyType: String?, val currencyType: String?,
/** /**
* in Kontowährung * Codes see p. 177 bottom - 179
*
* After constant N
* *
* Max length = 15 * Max length = 15
*/ */
val amount: Amount, val amount: Amount,
/** /**
* Codes see p. 177 bottom - 179 * in Kontowährung
*
* After constant N
* *
* Length = 3 * Length = 3
*/ */
val postingKey: String, val bookingKey: String,
/** val referenceForTheAccountOwner: String,
* Kundenreferenz.
* Bei Nichtbelegung wird NONREF eingestellt, zum Beispiel bei Schecknummer
* Wenn KREF+ eingestellt ist, dann erfolgt die Angabe der Referenznummer in Tag :86: .
*/
val customerReference: String?,
/** val referenceOfTheAccountServicingInstitution: String?,
* Bankreferenz
*/
val bankReference: String?,
/** val supplementaryDetails: String? = null
* Währungsart und Umsatzbetrag in Ursprungswährung (original currency
* amount) in folgendem
* Format:
* /OCMT/3a..15d/
* sowie Währungsart und
* Gebührenbetrag
* (charges) in folgendem
* Format:
* /CHGS/3a..15d/
* 3a = 3-stelliger
* Währungscode gemäß
* ISO 4217
* ..15d = Betrag mit Komma
* als Dezimalzeichen (gemäß SWIFT-Konvention).
* Im Falle von SEPALastschriftrückgaben ist
* das Feld /OCMT/ mit dem
* Originalbetrag und das
* Feld /CHGS/ mit der
* Summe aus Entgelten
* sowie Zinsausgleich zu
* belegen.
*/
val furtherInformationOriginalAmountAndCharges: String? = null
) { ) {

View File

@ -4,7 +4,7 @@ package net.codinux.banking.fints.transactions.mt940.model
open class Transaction( open class Transaction(
val statementLine: StatementLine, val statementLine: StatementLine,
val information: RemittanceInformationField? = null val information: InformationToAccountOwner? = null
) { ) {

View File

@ -12,36 +12,64 @@ open class TanMethodSelector {
val ImageBased = listOf(TanMethodType.QrCode, TanMethodType.ChipTanQrCode, TanMethodType.photoTan, TanMethodType.ChipTanPhotoTanMatrixCode) val ImageBased = listOf(TanMethodType.QrCode, TanMethodType.ChipTanQrCode, TanMethodType.photoTan, TanMethodType.ChipTanPhotoTanMatrixCode)
/**
* NonVisualOrImageBased is a good default for most users as it lists the most simplistic ones (which also work with
* the command line) first and then continues with image based TAN methods, which for UI applications are easily to display.
*/
val NonVisualOrImageBased = buildList {
// decoupled TAN method is the most simplistic TAN method, user only has to confirm the action in her TAN app, no manual TAN entering required
// AppTan is the second most simplistic TAN method: user has to confirm action in her TAN app and then enter the displayed TAN
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 as last
}
} }
open fun getSuggestedTanMethod(tanMethods: List<TanMethod>, tanMethodsNotSupportedByApplication: List<TanMethodType> = emptyList()): TanMethod? { open fun getSuggestedTanMethod(tanMethods: List<TanMethod>): TanMethod? {
return findPreferredTanMethod(tanMethods, NonVisualOrImageBased, tanMethodsNotSupportedByApplication) // we use NonVisualOrImageBased as it provides a good default for most users return tanMethods.firstOrNull { it.type == TanMethodType.DecoupledPushTan || it.type == TanMethodType.DecoupledTan } // decoupled TAN method is the most simplistic TAN method, user only has to confirm the action in her TAN app, no manual TAN entering required
?: tanMethods.firstOrNull { it.type !in tanMethodsNotSupportedByApplication } ?: tanMethods.firstOrNull { it.type == TanMethodType.AppTan } // that's the second most simplistic TAN method: user has to confirm action in her TAN app and then enter the displayed TAN
?: tanMethods.firstOrNull { it.type != TanMethodType.ChipTanUsb && it.type != TanMethodType.SmsTan && it.type != TanMethodType.ChipTanManuell }
?: tanMethods.firstOrNull { it.type != TanMethodType.ChipTanUsb && it.type != TanMethodType.SmsTan }
?: tanMethods.firstOrNull { it.type != TanMethodType.ChipTanUsb }
?: first(tanMethods)
} }
open fun findPreferredTanMethod(tanMethods: List<TanMethod>, preferredTanMethods: List<TanMethodType>?, tanMethodsNotSupportedByApplication: List<TanMethodType> = emptyList()): TanMethod? { open fun findPreferredTanMethod(tanMethods: List<TanMethod>, preferredTanMethods: List<TanMethodType>?): TanMethod? {
preferredTanMethods?.forEach { preferredTanMethodType -> preferredTanMethods?.forEach { preferredTanMethodType ->
if (preferredTanMethodType !in tanMethodsNotSupportedByApplication) {
tanMethods.firstOrNull { it.type == preferredTanMethodType }?.let { tanMethods.firstOrNull { it.type == preferredTanMethodType }?.let {
return it return it
} }
} }
}
return null return null
} }
open fun nonVisual(tanMethods: List<TanMethod>): TanMethod? {
return findPreferredTanMethod(tanMethods, NonVisual)
?: tanMethods.firstOrNull { it.displayName.contains("manuell", true) }
}
open fun nonVisualOrFirst(tanMethods: List<TanMethod>): TanMethod? {
return nonVisual(tanMethods)
?: first(tanMethods)
}
open fun imageBased(tanMethods: List<TanMethod>): TanMethod? {
return findPreferredTanMethod(tanMethods, ImageBased)
}
open fun imageBasedOrFirst(tanMethods: List<TanMethod>): TanMethod? {
return imageBased(tanMethods)
?: first(tanMethods)
}
open fun nonVisualOrImageBased(tanMethods: List<TanMethod>): TanMethod? {
return nonVisual(tanMethods)
?: imageBased(tanMethods)
}
open fun nonVisualOrImageBasedOrFirst(tanMethods: List<TanMethod>): TanMethod? {
return nonVisual(tanMethods)
?: imageBased(tanMethods)
?: first(tanMethods)
}
open fun first(tanMethods: List<TanMethod>): TanMethod? {
return tanMethods.firstOrNull()
}
} }

View File

@ -10,72 +10,70 @@ import net.codinux.banking.fints.extensions.UnixEpochStart
@Serializable @Serializable
open class AccountTransaction( open class AccountTransaction(
val amount: Money, // TODO: if we decide to stick with Money, create own type, don't use that one from fints.model (or move over from) val amount: Money, // TODO: if we decide to stick with Money, create own type, don't use that one from fints.model (or move over from)
val reference: String?, // alternative names: purpose, reason val unparsedReference: String, // alternative names: purpose, reason
val bookingDate: LocalDate, val bookingDate: LocalDate,
val valueDate: LocalDate,
val otherPartyName: String?, val otherPartyName: String?,
val otherPartyBankId: String?, val otherPartyBankCode: String?,
val otherPartyAccountId: String?, val otherPartyAccountId: String?,
val bookingText: String?,
val postingText: String?, val valueDate: LocalDate,
val statementNumber: Int,
val sequenceNumber: Int?,
val openingBalance: Money?, val openingBalance: Money?,
val closingBalance: Money?, val closingBalance: Money?,
val statementNumber: Int,
val sheetNumber: Int?,
val customerReference: String?,
val bankReference: String?,
val furtherInformation: String?,
val endToEndReference: String?, val endToEndReference: String?,
val customerReference: String?,
val mandateReference: String?, val mandateReference: String?,
val creditorIdentifier: String?, val creditorIdentifier: String?,
val originatorsIdentificationCode: String?, val originatorsIdentificationCode: String?,
val compensationAmount: String?, val compensationAmount: String?,
val originalAmount: String?, val originalAmount: String?,
val sepaReference: String?,
val deviantOriginator: String?, val deviantOriginator: String?,
val deviantRecipient: String?, val deviantRecipient: String?,
val referenceWithNoSpecialType: String?, val referenceWithNoSpecialType: String?,
val primaNotaNumber: String?,
val textKeySupplement: String?,
val journalNumber: String?, val currencyType: String?,
val textKeyAddition: String?, val bookingKey: String,
val referenceForTheAccountOwner: String,
val referenceOfTheAccountServicingInstitution: String?,
val supplementaryDetails: String?,
val orderReferenceNumber: String?, val transactionReferenceNumber: String,
val referenceNumber: String?, val relatedReferenceNumber: String?
val isReversal: Boolean
) { ) {
// for object deserializers // for object deserializers
internal constructor() : this(Money(Amount.Zero, ""), "", UnixEpochStart, UnixEpochStart, null, null, null, null) internal constructor() : this(Money(Amount.Zero, ""), "", UnixEpochStart, null, null, null, null, UnixEpochStart)
constructor(amount: Money, unparsedReference: String, bookingDate: LocalDate, valueDate: LocalDate, otherPartyName: String?, otherPartyBankId: String?, otherPartyAccountId: String?, postingText: String?) constructor(amount: Money, unparsedReference: String, bookingDate: LocalDate, otherPartyName: String?, otherPartyBankCode: String?, otherPartyAccountId: String?, bookingText: String?, valueDate: LocalDate)
: this(amount, unparsedReference, bookingDate, valueDate, otherPartyName, otherPartyBankId, otherPartyAccountId, postingText, : this(amount, unparsedReference, bookingDate, otherPartyName, otherPartyBankCode, otherPartyAccountId, bookingText, valueDate,
null, null, 0, null, 0, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, false) null, "", "", null, null, "", null)
open val showOtherPartyName: Boolean open val showOtherPartyName: Boolean
get() = otherPartyName.isNullOrBlank() == false /* && type != "ENTGELTABSCHLUSS" && type != "AUSZAHLUNG" */ // TODO get() = otherPartyName.isNullOrBlank() == false /* && type != "ENTGELTABSCHLUSS" && type != "AUSZAHLUNG" */ // TODO
val reference: String
get() = sepaReference ?: unparsedReference
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other !is AccountTransaction) return false if (other !is AccountTransaction) return false
if (amount != other.amount) return false if (amount != other.amount) return false
if (reference != other.reference) return false if (unparsedReference != other.unparsedReference) return false
if (bookingDate != other.bookingDate) return false if (bookingDate != other.bookingDate) return false
if (otherPartyName != other.otherPartyName) return false if (otherPartyName != other.otherPartyName) return false
if (otherPartyBankId != other.otherPartyBankId) return false if (otherPartyBankCode != other.otherPartyBankCode) return false
if (otherPartyAccountId != other.otherPartyAccountId) return false if (otherPartyAccountId != other.otherPartyAccountId) return false
if (postingText != other.postingText) return false if (bookingText != other.bookingText) return false
if (valueDate != other.valueDate) return false if (valueDate != other.valueDate) return false
return true return true
@ -83,19 +81,19 @@ open class AccountTransaction(
override fun hashCode(): Int { override fun hashCode(): Int {
var result = amount.hashCode() var result = amount.hashCode()
result = 31 * result + reference.hashCode() result = 31 * result + unparsedReference.hashCode()
result = 31 * result + bookingDate.hashCode() result = 31 * result + bookingDate.hashCode()
result = 31 * result + (otherPartyName?.hashCode() ?: 0) result = 31 * result + (otherPartyName?.hashCode() ?: 0)
result = 31 * result + (otherPartyBankId?.hashCode() ?: 0) result = 31 * result + (otherPartyBankCode?.hashCode() ?: 0)
result = 31 * result + (otherPartyAccountId?.hashCode() ?: 0) result = 31 * result + (otherPartyAccountId?.hashCode() ?: 0)
result = 31 * result + (postingText?.hashCode() ?: 0) result = 31 * result + (bookingText?.hashCode() ?: 0)
result = 31 * result + valueDate.hashCode() result = 31 * result + valueDate.hashCode()
return result return result
} }
override fun toString(): String { override fun toString(): String {
return "$valueDate $amount $otherPartyName: $reference" return "$valueDate $amount $otherPartyName: $unparsedReference"
} }
} }

View File

@ -18,7 +18,7 @@ open class BankAccount(
open val currency: String = Currency.DefaultCurrencyCode, // TODO: may parse to a value object open val currency: String = Currency.DefaultCurrencyCode, // TODO: may parse to a value object
open val accountLimit: String? = null, open val accountLimit: String? = null,
open val serverTransactionsRetentionDays: Int? = null, open val countDaysForWhichTransactionsAreKept: Int? = null,
open val isAccountTypeSupportedByApplication: Boolean = false, open val isAccountTypeSupportedByApplication: Boolean = false,
// TODO: create an enum AccountCapabilities [ RetrieveBalance, RetrieveTransactions, TransferMoney / MoneyTransfer(?), InstantPayment ] // TODO: create an enum AccountCapabilities [ RetrieveBalance, RetrieveTransactions, TransferMoney / MoneyTransfer(?), InstantPayment ]
open val supportsRetrievingTransactions: Boolean = false, open val supportsRetrievingTransactions: Boolean = false,

View File

@ -12,7 +12,6 @@ open class FinTsClientParameter(
password: String, password: String,
open val preferredTanMethods: List<TanMethodType>? = null, open val preferredTanMethods: List<TanMethodType>? = null,
open val tanMethodsNotSupportedByApplication: List<TanMethodType>? = null,
open val preferredTanMedium: String? = null, // the ID of the medium open val preferredTanMedium: String? = null, // the ID of the medium
open val abortIfTanIsRequired: Boolean = false, open val abortIfTanIsRequired: Boolean = false,
open val finTsModel: BankData? = null open val finTsModel: BankData? = null

View File

@ -21,12 +21,10 @@ open class GetAccountDataParameter(
open val retrieveTransactionsTo: LocalDate? = null, open val retrieveTransactionsTo: LocalDate? = null,
preferredTanMethods: List<TanMethodType>? = null, preferredTanMethods: List<TanMethodType>? = null,
tanMethodsNotSupportedByApplication: List<TanMethodType>? = null,
preferredTanMedium: String? = null, preferredTanMedium: String? = null,
abortIfTanIsRequired: Boolean = false, abortIfTanIsRequired: Boolean = false,
finTsModel: BankData? = null, finTsModel: BankData? = null
open val defaultBankValues: BankData? = null ) : FinTsClientParameter(bankCode, loginName, password, preferredTanMethods, preferredTanMedium, abortIfTanIsRequired, finTsModel) {
) : FinTsClientParameter(bankCode, loginName, password, preferredTanMethods, tanMethodsNotSupportedByApplication, preferredTanMedium, abortIfTanIsRequired, finTsModel) {
open val retrieveOnlyAccountInfo: Boolean open val retrieveOnlyAccountInfo: Boolean
get() = retrieveBalance == false && retrieveTransactions == RetrieveTransactions.No get() = retrieveBalance == false && retrieveTransactions == RetrieveTransactions.No

View File

@ -34,11 +34,10 @@ open class TransferMoneyParameter(
open val instantPayment: Boolean = false, open val instantPayment: Boolean = false,
preferredTanMethods: List<TanMethodType>? = null, preferredTanMethods: List<TanMethodType>? = null,
tanMethodsNotSupportedByApplication: List<TanMethodType>? = null,
preferredTanMedium: String? = null, preferredTanMedium: String? = null,
abortIfTanIsRequired: Boolean = false, abortIfTanIsRequired: Boolean = false,
finTsModel: BankData? = null, finTsModel: BankData? = null,
open val selectAccountToUseForTransfer: ((List<AccountData>) -> AccountData?)? = null // TODO: use BankAccount instead of AccountData open val selectAccountToUseForTransfer: ((List<AccountData>) -> AccountData?)? = null // TODO: use BankAccount instead of AccountData
) : FinTsClientParameter(bankCode, loginName, password, preferredTanMethods, tanMethodsNotSupportedByApplication, preferredTanMedium, abortIfTanIsRequired, finTsModel) ) : FinTsClientParameter(bankCode, loginName, password, preferredTanMethods, preferredTanMedium, abortIfTanIsRequired, finTsModel)

View File

@ -966,7 +966,7 @@ class ResponseParserTest : FinTsTestBase() {
assertEquals(TanResponse.NoJobReferenceResponse, segment.jobReference) assertEquals(TanResponse.NoJobReferenceResponse, segment.jobReference)
assertEquals(TanResponse.NoChallengeResponse, segment.challenge) assertEquals(TanResponse.NoChallengeResponse, segment.challenge)
assertNull(segment.challengeHHD_UC) assertNull(segment.challengeHHD_UC)
assertEquals(null, segment.tanExpirationTime) assertEquals(null, segment.validityDateTimeForChallenge)
assertEquals(null, segment.tanMediaIdentifier) assertEquals(null, segment.tanMediaIdentifier)
} }
?: run { fail("No segment of type TanResponse found in ${result.receivedSegments}") } ?: run { fail("No segment of type TanResponse found in ${result.receivedSegments}") }
@ -995,7 +995,7 @@ class ResponseParserTest : FinTsTestBase() {
assertEquals(jobReference, segment.jobReference) assertEquals(jobReference, segment.jobReference)
assertEquals(unmaskString(challenge), segment.challenge) assertEquals(unmaskString(challenge), segment.challenge)
assertEquals(challengeHHD_UC, segment.challengeHHD_UC) assertEquals(challengeHHD_UC, segment.challengeHHD_UC)
assertEquals(null, segment.tanExpirationTime) assertEquals(null, segment.validityDateTimeForChallenge)
assertEquals(tanMediaIdentifier, segment.tanMediaIdentifier) assertEquals(tanMediaIdentifier, segment.tanMediaIdentifier)
} }
?: run { fail("No segment of type TanResponse found in ${result.receivedSegments}") } ?: run { fail("No segment of type TanResponse found in ${result.receivedSegments}") }
@ -1189,16 +1189,16 @@ class ResponseParserTest : FinTsTestBase() {
fun parseAccountTransactionsMt940Parameters_Version4() { fun parseAccountTransactionsMt940Parameters_Version4() {
// given // given
val serverTransactionsRetentionDays = 90 val countDaysForWhichTransactionsAreKept = 90
// when // when
val result = underTest.parse("HIKAZS:21:4:4+20+1+$serverTransactionsRetentionDays:N'") val result = underTest.parse("HIKAZS:21:4:4+20+1+$countDaysForWhichTransactionsAreKept:N'")
// then // then
assertSuccessfullyParsedSegment(result, InstituteSegmentId.AccountTransactionsMt940Parameters, 21, 4, 4) assertSuccessfullyParsedSegment(result, InstituteSegmentId.AccountTransactionsMt940Parameters, 21, 4, 4)
result.getFirstSegmentById<RetrieveAccountTransactionsParameters>(InstituteSegmentId.AccountTransactionsMt940Parameters)?.let { segment -> result.getFirstSegmentById<RetrieveAccountTransactionsParameters>(InstituteSegmentId.AccountTransactionsMt940Parameters)?.let { segment ->
assertEquals(serverTransactionsRetentionDays, segment.serverTransactionsRetentionDays) assertEquals(countDaysForWhichTransactionsAreKept, segment.countDaysForWhichTransactionsAreKept)
assertFalse(segment.settingCountEntriesAllowed) assertFalse(segment.settingCountEntriesAllowed)
assertFalse(segment.settingAllAccountAllowed) assertFalse(segment.settingAllAccountAllowed)
} }
@ -1209,16 +1209,16 @@ class ResponseParserTest : FinTsTestBase() {
fun parseAccountTransactionsMt940Parameters_Version6() { fun parseAccountTransactionsMt940Parameters_Version6() {
// given // given
val serverTransactionsRetentionDays = 90 val countDaysForWhichTransactionsAreKept = 90
// when // when
val result = underTest.parse("HIKAZS:23:6:4+20+1+1+$serverTransactionsRetentionDays:N:N'") val result = underTest.parse("HIKAZS:23:6:4+20+1+1+$countDaysForWhichTransactionsAreKept:N:N'")
// then // then
assertSuccessfullyParsedSegment(result, InstituteSegmentId.AccountTransactionsMt940Parameters, 23, 6, 4) assertSuccessfullyParsedSegment(result, InstituteSegmentId.AccountTransactionsMt940Parameters, 23, 6, 4)
result.getFirstSegmentById<RetrieveAccountTransactionsParameters>(InstituteSegmentId.AccountTransactionsMt940Parameters)?.let { segment -> result.getFirstSegmentById<RetrieveAccountTransactionsParameters>(InstituteSegmentId.AccountTransactionsMt940Parameters)?.let { segment ->
assertEquals(serverTransactionsRetentionDays, segment.serverTransactionsRetentionDays) assertEquals(countDaysForWhichTransactionsAreKept, segment.countDaysForWhichTransactionsAreKept)
assertFalse(segment.settingCountEntriesAllowed) assertFalse(segment.settingCountEntriesAllowed)
assertFalse(segment.settingAllAccountAllowed) assertFalse(segment.settingAllAccountAllowed)
} }
@ -1290,16 +1290,16 @@ class ResponseParserTest : FinTsTestBase() {
fun parseCreditCardAccountTransactionsParameters() { fun parseCreditCardAccountTransactionsParameters() {
// given // given
val serverTransactionsRetentionDays = 9999 val countDaysForWhichTransactionsAreKept = 9999
// when // when
val result = underTest.parse("DIKKUS:15:2:4+999+1+0+$serverTransactionsRetentionDays:J:J'") val result = underTest.parse("DIKKUS:15:2:4+999+1+0+$countDaysForWhichTransactionsAreKept:J:J'")
// then // then
assertSuccessfullyParsedSegment(result, InstituteSegmentId.CreditCardTransactionsParameters, 15, 2, 4) assertSuccessfullyParsedSegment(result, InstituteSegmentId.CreditCardTransactionsParameters, 15, 2, 4)
result.getFirstSegmentById<RetrieveAccountTransactionsParameters>(InstituteSegmentId.CreditCardTransactionsParameters)?.let { segment -> result.getFirstSegmentById<RetrieveAccountTransactionsParameters>(InstituteSegmentId.CreditCardTransactionsParameters)?.let { segment ->
assertEquals(serverTransactionsRetentionDays, segment.serverTransactionsRetentionDays) assertEquals(countDaysForWhichTransactionsAreKept, segment.countDaysForWhichTransactionsAreKept)
assertTrue(segment.settingCountEntriesAllowed) assertTrue(segment.settingCountEntriesAllowed)
assertTrue(segment.settingAllAccountAllowed) assertTrue(segment.settingAllAccountAllowed)
} }

View File

@ -3,7 +3,7 @@ package net.codinux.banking.fints.transactions
import net.codinux.banking.fints.FinTsTestBase import net.codinux.banking.fints.FinTsTestBase
import net.codinux.banking.fints.transactions.mt940.Mt940Parser import net.codinux.banking.fints.transactions.mt940.Mt940Parser
import net.codinux.banking.fints.transactions.mt940.model.Balance import net.codinux.banking.fints.transactions.mt940.model.Balance
import net.codinux.banking.fints.transactions.mt940.model.RemittanceInformationField import net.codinux.banking.fints.transactions.mt940.model.InformationToAccountOwner
import net.codinux.banking.fints.transactions.mt940.model.StatementLine import net.codinux.banking.fints.transactions.mt940.model.StatementLine
import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDate
import net.codinux.banking.fints.extensions.* import net.codinux.banking.fints.extensions.*
@ -26,12 +26,12 @@ class Mt940ParserTest : FinTsTestBase() {
val AccountStatement1Transaction1Amount = Amount("1234,56") val AccountStatement1Transaction1Amount = Amount("1234,56")
val AccountStatement1Transaction1OtherPartyName = "Sender1" val AccountStatement1Transaction1OtherPartyName = "Sender1"
val AccountStatement1Transaction1OtherPartyBankId = "AAAADE12" val AccountStatement1Transaction1OtherPartyBankCode = "AAAADE12"
val AccountStatement1Transaction1OtherPartyAccountId = "DE99876543210987654321" val AccountStatement1Transaction1OtherPartyAccountId = "DE99876543210987654321"
val AccountStatement1Transaction2Amount = Amount("432,10") val AccountStatement1Transaction2Amount = Amount("432,10")
val AccountStatement1Transaction2OtherPartyName = "Receiver2" val AccountStatement1Transaction2OtherPartyName = "Receiver2"
val AccountStatement1Transaction2OtherPartyBankId = "BBBBDE56" val AccountStatement1Transaction2OtherPartyBankCode = "BBBBDE56"
val AccountStatement1Transaction2OtherPartyAccountId = "DE77987654321234567890" val AccountStatement1Transaction2OtherPartyAccountId = "DE77987654321234567890"
val AccountStatement1ClosingBalanceAmount = Amount("13580,23") val AccountStatement1ClosingBalanceAmount = Amount("13580,23")
@ -67,7 +67,7 @@ class Mt940ParserTest : FinTsTestBase() {
val transaction = statement.transactions.first() val transaction = statement.transactions.first()
assertTurnover(transaction.statementLine, AccountStatement1BookingDate, AccountStatement1Transaction1Amount) assertTurnover(transaction.statementLine, AccountStatement1BookingDate, AccountStatement1Transaction1Amount)
assertTransactionDetails(transaction.information, AccountStatement1Transaction1OtherPartyName, assertTransactionDetails(transaction.information, AccountStatement1Transaction1OtherPartyName,
AccountStatement1Transaction1OtherPartyBankId, AccountStatement1Transaction1OtherPartyAccountId) AccountStatement1Transaction1OtherPartyBankCode, AccountStatement1Transaction1OtherPartyAccountId)
} }
@Test @Test
@ -90,7 +90,7 @@ class Mt940ParserTest : FinTsTestBase() {
assertEquals(BankCode, statement.bankCodeBicOrIban) assertEquals(BankCode, statement.bankCodeBicOrIban)
assertEquals(CustomerId, statement.accountIdentifier) assertEquals(CustomerId, statement.accountIdentifier)
assertEquals(0, statement.statementNumber) assertEquals(0, statement.statementNumber)
assertNull(statement.sheetNumber) assertNull(statement.sequenceNumber)
assertBalance(statement.openingBalance, true, bookingDate, Amount("0,00")) assertBalance(statement.openingBalance, true, bookingDate, Amount("0,00"))
assertBalance(statement.closingBalance, isCredit, bookingDate, amount) assertBalance(statement.closingBalance, isCredit, bookingDate, amount)
@ -124,12 +124,12 @@ class Mt940ParserTest : FinTsTestBase() {
val firstTransaction = statement.transactions.first() val firstTransaction = statement.transactions.first()
assertTurnover(firstTransaction.statementLine, AccountStatement1BookingDate, AccountStatement1Transaction1Amount) assertTurnover(firstTransaction.statementLine, AccountStatement1BookingDate, AccountStatement1Transaction1Amount)
assertTransactionDetails(firstTransaction.information, AccountStatement1Transaction1OtherPartyName, assertTransactionDetails(firstTransaction.information, AccountStatement1Transaction1OtherPartyName,
AccountStatement1Transaction1OtherPartyBankId, AccountStatement1Transaction1OtherPartyAccountId) AccountStatement1Transaction1OtherPartyBankCode, AccountStatement1Transaction1OtherPartyAccountId)
val secondTransaction = statement.transactions[1] val secondTransaction = statement.transactions[1]
assertTurnover(secondTransaction.statementLine, AccountStatement1BookingDate, AccountStatement1Transaction2Amount, false) assertTurnover(secondTransaction.statementLine, AccountStatement1BookingDate, AccountStatement1Transaction2Amount, false)
assertTransactionDetails(secondTransaction.information, AccountStatement1Transaction2OtherPartyName, assertTransactionDetails(secondTransaction.information, AccountStatement1Transaction2OtherPartyName,
AccountStatement1Transaction2OtherPartyBankId, AccountStatement1Transaction2OtherPartyAccountId) AccountStatement1Transaction2OtherPartyBankCode, AccountStatement1Transaction2OtherPartyAccountId)
} }
@Test @Test
@ -306,8 +306,8 @@ class Mt940ParserTest : FinTsTestBase() {
assertSize(1, result.first().transactions) assertSize(1, result.first().transactions)
result.first().transactions[0].information?.apply { result.first().transactions[0].information?.apply {
assertEquals("BASISLASTSCHRIFT", postingText) assertEquals("BASISLASTSCHRIFT", bookingText)
assertEquals("TUBDDEDD", otherPartyBankId) assertEquals("TUBDDEDD", otherPartyBankCode)
assertEquals("DE87300308801234567890", otherPartyAccountId) assertEquals("DE87300308801234567890", otherPartyAccountId)
assertEquals("6MKL2OT30QENNLIU", endToEndReference) assertEquals("6MKL2OT30QENNLIU", endToEndReference)
assertEquals("?,3SQNdUbxm9z7dB)+gKYDJAKzCM0G", mandateReference) assertEquals("?,3SQNdUbxm9z7dB)+gKYDJAKzCM0G", mandateReference)
@ -362,13 +362,13 @@ class Mt940ParserTest : FinTsTestBase() {
assertEquals(amount, statementLine.amount) assertEquals(amount, statementLine.amount)
} }
private fun assertTransactionDetails(details: RemittanceInformationField?, otherPartyName: String, private fun assertTransactionDetails(details: InformationToAccountOwner?, otherPartyName: String,
otherPartyBankId: String, otherPartyAccountId: String) { otherPartyBankCode: String, otherPartyAccountId: String) {
assertNotNull(details) assertNotNull(details)
assertEquals(otherPartyName, details.otherPartyName) assertEquals(otherPartyName, details.otherPartyName)
assertEquals(otherPartyBankId, details.otherPartyBankId) assertEquals(otherPartyBankCode, details.otherPartyBankCode)
assertEquals(otherPartyAccountId, details.otherPartyAccountId) assertEquals(otherPartyAccountId, details.otherPartyAccountId)
} }
@ -380,7 +380,7 @@ class Mt940ParserTest : FinTsTestBase() {
:60F:C${convertMt940Date(AccountStatement1PreviousStatementBookingDate)}EUR$AccountStatement1OpeningBalanceAmount :60F:C${convertMt940Date(AccountStatement1PreviousStatementBookingDate)}EUR$AccountStatement1OpeningBalanceAmount
:61:${convertMt940Date(AccountStatement1BookingDate)}${convertToShortBookingDate(AccountStatement1BookingDate)}CR${AccountStatement1Transaction1Amount}N062NONREF :61:${convertMt940Date(AccountStatement1BookingDate)}${convertToShortBookingDate(AccountStatement1BookingDate)}CR${AccountStatement1Transaction1Amount}N062NONREF
:86:166?00GUTSCHR. UEBERWEISUNG?109249?20EREF+674?21SVWZ+1908301/ :86:166?00GUTSCHR. UEBERWEISUNG?109249?20EREF+674?21SVWZ+1908301/
EUR ${AccountStatement1Transaction1Amount}/20?2219-10-02/...?30$AccountStatement1Transaction1OtherPartyBankId?31$AccountStatement1Transaction1OtherPartyAccountId EUR ${AccountStatement1Transaction1Amount}/20?2219-10-02/...?30$AccountStatement1Transaction1OtherPartyBankCode?31$AccountStatement1Transaction1OtherPartyAccountId
?32$AccountStatement1Transaction1OtherPartyName ?32$AccountStatement1Transaction1OtherPartyName
:62F:C${convertMt940Date(AccountStatement1BookingDate)}EUR$AccountStatement1ClosingBalanceAmount :62F:C${convertMt940Date(AccountStatement1BookingDate)}EUR$AccountStatement1ClosingBalanceAmount
- -
@ -393,11 +393,11 @@ class Mt940ParserTest : FinTsTestBase() {
:60F:C${convertMt940Date(AccountStatement1PreviousStatementBookingDate)}EUR$AccountStatement1OpeningBalanceAmount :60F:C${convertMt940Date(AccountStatement1PreviousStatementBookingDate)}EUR$AccountStatement1OpeningBalanceAmount
:61:${convertMt940Date(AccountStatement1BookingDate)}${convertToShortBookingDate(AccountStatement1BookingDate)}CR${AccountStatement1Transaction1Amount}N062NONREF :61:${convertMt940Date(AccountStatement1BookingDate)}${convertToShortBookingDate(AccountStatement1BookingDate)}CR${AccountStatement1Transaction1Amount}N062NONREF
:86:166?00GUTSCHR. UEBERWEISUNG?109249?20EREF+674?21SVWZ+1908301/ :86:166?00GUTSCHR. UEBERWEISUNG?109249?20EREF+674?21SVWZ+1908301/
EUR ${AccountStatement1Transaction1Amount}/20?2219-10-02/...?30$AccountStatement1Transaction1OtherPartyBankId?31$AccountStatement1Transaction1OtherPartyAccountId EUR ${AccountStatement1Transaction1Amount}/20?2219-10-02/...?30$AccountStatement1Transaction1OtherPartyBankCode?31$AccountStatement1Transaction1OtherPartyAccountId
?32$AccountStatement1Transaction1OtherPartyName ?32$AccountStatement1Transaction1OtherPartyName
:61:${convertMt940Date(AccountStatement1BookingDate)}${convertToShortBookingDate(AccountStatement1BookingDate)}DR${AccountStatement1Transaction2Amount}N062NONREF :61:${convertMt940Date(AccountStatement1BookingDate)}${convertToShortBookingDate(AccountStatement1BookingDate)}DR${AccountStatement1Transaction2Amount}N062NONREF
:86:166?00ONLINE-UEBERWEISUNG?109249?20EREF+674?21SVWZ+1908301/ :86:166?00ONLINE-UEBERWEISUNG?109249?20EREF+674?21SVWZ+1908301/
EUR ${AccountStatement1Transaction2Amount}/20?2219-10-02/...?30$AccountStatement1Transaction2OtherPartyBankId?31$AccountStatement1Transaction2OtherPartyAccountId EUR ${AccountStatement1Transaction2Amount}/20?2219-10-02/...?30$AccountStatement1Transaction2OtherPartyBankCode?31$AccountStatement1Transaction2OtherPartyAccountId
?32$AccountStatement1Transaction2OtherPartyName ?32$AccountStatement1Transaction2OtherPartyName
:62F:C${convertMt940Date(AccountStatement1BookingDate)}EUR${AccountStatement1With2TransactionsClosingBalanceAmount} :62F:C${convertMt940Date(AccountStatement1BookingDate)}EUR${AccountStatement1With2TransactionsClosingBalanceAmount}
- -