Compare commits

...

30 Commits

Author SHA1 Message Date
dankito c158097d3a Added tanMethodsNotSupportedByApplication to filter out TAN methods that client application does not support (e.g. chipTanUsb) 2024-09-10 03:20:50 +02:00
dankito 6908f52e48 Using now NonVisualOrImageBased as default to determine user's (suggested) TAN method as it provides a good default for most users 2024-09-10 02:47:23 +02:00
dankito 61d8f2c342 Added preferredTanMethods and preferredTanMedium to JobContext 2024-09-10 02:46:35 +02:00
dankito 6bf7fdcb44 Implemented passing default bank data to FinTsClient as e.g. bank names returned from bank server are often quite bad, e.g. DB24 for Deutsche Bank 2024-09-09 23:01:06 +02:00
dankito fbafbb62e3 Added option closeDialogs 2024-09-09 17:06:29 +02:00
dankito 9372d17313 Avoiding concurrent modification exception 2024-09-09 17:02:47 +02:00
dankito 9b1a5fa929 Fixed not continuing to next account if user cancelled process 2024-09-09 17:01:32 +02:00
dankito 42bf002626 Added tanExpiredCallback, so that UI can react to when TAN expired 2024-09-09 03:36:23 +02:00
dankito 20f06387c5 Added check if tanExpirationTime is exceeded if set 2024-09-09 02:56:34 +02:00
dankito 75320da2be Changed type of tanExpirationTime to Instant so that UI can better convert it to user's time zone 2024-09-09 02:55:37 +02:00
dankito be2908517f Fixed that validityDateTimeForChallenge has been renamed to tanExpirationTime 2024-09-09 00:45:30 +02:00
dankito c4f504dd0a Added tanExpirationTime to TanChallenge 2024-09-08 22:40:05 +02:00
dankito 0848586894 Added timestamp at which TanChallenge was created 2024-09-08 22:36:39 +02:00
dankito 83c2882567 Added isReversal 2024-09-08 22:20:59 +02:00
dankito f069f9155c Adjusted names to fints4k names 2024-09-08 22:19:38 +02:00
dankito bf5ee4890e Renamed otherPartyBankCode to otherPartyBankId 2024-09-08 22:17:16 +02:00
dankito ed4214fd49 Fixed calling the right Instant.now() method 2024-09-08 22:03:37 +02:00
dankito b8fe9e78e1 Renamed transactionsRetentionDays to serverTransactionsRetentionDays 2024-09-08 22:01:28 +02:00
dankito da2bf8d469 Terminate waiting for TAN input after a timeout 2024-09-08 20:38:20 +02:00
dankito 113b817627 Extracted Instant.nowExt() 2024-09-08 20:31:12 +02:00
dankito bd18644c0d Calling mayRetrieveAutomaticallyIfUserEnteredDecoupledTan() out of loop. Should make no difference but should sound more logic 2024-09-08 20:22:18 +02:00
dankito b32cf94e25 Using now isEnteringTanDone 2024-09-08 20:20:31 +02:00
dankito 8cc2f3bdcd Added timestamp at which TanChallenge was created 2024-09-08 18:31:02 +02:00
dankito 59b8213163 Extracted clearUserApprovedDecoupledTanCallbacks() and clearing callbacks also when user did not enter TAN or requested to change TAN method or medium to avoid memory leaks 2024-09-08 18:14:35 +02:00
dankito cb34c86665 Changed order of opening and closing balance 2024-09-05 23:31:02 +02:00
dankito 70c1082531 Renamed countDaysForWhichTransactionsAreKept to transactionsRetentionDays 2024-09-05 21:53:00 +02:00
dankito 30e9a57b96 Fixed setting either sepaReference - in case of structured information - or unparsedReference - in case of unstructured reference. And that reference may is null 2024-09-05 19:36:03 +02:00
dankito bf76de4f23 Applied adjusted values from MT 940 to AccountTransaction 2024-09-05 19:16:15 +02:00
dankito 47e2b851b9 Adjusted names according to English Translation of DFÜ-Abkommen Anlage_3_Datenformate_V3.8.pdf (Appendix_3-Data_Formats_V3-8.pdf) 2024-09-05 18:20:56 +02:00
dankito f90e280b74 Adjusted names according to English Translation of DFÜ-Abkommen Anlage_3_Datenformate_V3.8.pdf (Appendix_3-Data_Formats_V3-8.pdf) 2024-09-05 18:15:42 +02:00
38 changed files with 633 additions and 346 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) {
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.bookingText), wrap(transaction.reference),
ensureNotNull(transaction.otherPartyName), ensureNotNull(transaction.otherPartyBankCode), ensureNotNull(transaction.otherPartyAccountId)).joinToString(valueSeparator))
stream.writeString(listOf(customer.bankName, account.identifier, transaction.valueDate, amount, transaction.amount.currency, ensureNotNull(transaction.postingText), wrap(transaction.reference ?: ""),
ensureNotNull(transaction.otherPartyName), ensureNotNull(transaction.otherPartyBankId), ensureNotNull(transaction.otherPartyAccountId)).joinToString(valueSeparator))
stream.writeString(NewLine)
}

26
docs/Vokabular.md Normal file
View File

@ -0,0 +1,26 @@
| | |
|--------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------|
| 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,8 +63,13 @@ open class FinTsClient(
return GetAccountDataResponse(ErrorCode.NoneOfTheAccountsSupportsRetrievingData, errorMessage, mapper.map(bank), previousJobMessageLog ?: listOf(), bank)
}
accountsSupportingRetrievingTransactions.forEach { account ->
retrievedTransactionsResponses.add(getAccountTransactions(param, bank, account))
for (account in accountsSupportingRetrievingTransactions) {
val response = 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 }
@ -75,7 +80,7 @@ open class FinTsClient(
}
protected open suspend fun getAccountTransactions(param: GetAccountDataParameter, bank: BankData, account: AccountData): GetAccountTransactionsResponse {
val context = JobContext(JobContextType.GetTransactions, this.callback, config, bank, account)
val context = JobContext(JobContextType.GetTransactions, this.callback, config, bank, account, param.preferredTanMethods, param.tanMethodsNotSupportedByApplication, param.preferredTanMedium)
return config.jobExecutor.getTransactionsAsync(context, mapper.toGetAccountTransactionsParameter(param, bank, account))
}
@ -133,7 +138,7 @@ open class FinTsClient(
accountToUse = selectedAccount
}
val context = JobContext(JobContextType.TransferMoney, this.callback, config, bank, accountToUse)
val context = JobContext(JobContextType.TransferMoney, this.callback, config, bank, accountToUse, param.preferredTanMethods, param.tanMethodsNotSupportedByApplication, param.preferredTanMedium)
val response = config.jobExecutor.transferMoneyAsync(context, BankTransferData(param.recipientName, param.recipientAccountIdentifier, recipientBankIdentifier,
param.amount, param.reference, param.instantPayment))
@ -179,12 +184,14 @@ open class FinTsClient(
return net.dankito.banking.client.model.response.FinTsClientResponse(null, null, emptyList(), param.finTsModel)
}
val finTsServerAddress = config.finTsServerAddressFinder.findFinTsServerAddress(param.bankCode)
val defaultValues = (param as? GetAccountDataParameter)?.defaultBankValues
val finTsServerAddress = defaultValues?.finTs3ServerAddress ?: config.finTsServerAddressFinder.findFinTsServerAddress(param.bankCode)
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)
}
val bank = mapper.mapToBankData(param, finTsServerAddress)
val bank = mapper.mapToBankData(param, finTsServerAddress, defaultValues)
val getAccountInfoResponse = getAccountInfo(param, bank)
@ -198,11 +205,11 @@ open class FinTsClient(
// return GetAccountInfoResponse(it)
}
val context = JobContext(JobContextType.GetAccountInfo, this.callback, config, bank)
val context = JobContext(JobContextType.GetAccountInfo, this.callback, config, bank, null, param.preferredTanMethods, param.tanMethodsNotSupportedByApplication, param.preferredTanMedium)
/* First dialog: Get user's basic data like BPD, customer system ID and her TAN methods */
val newUserInfoResponse = config.jobExecutor.retrieveBasicDataLikeUsersTanMethods(context, param.preferredTanMethods, param.preferredTanMedium)
val newUserInfoResponse = config.jobExecutor.retrieveBasicDataLikeUsersTanMethods(context)
/* 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 */

View File

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

View File

@ -1,8 +1,9 @@
package net.codinux.banking.fints
import kotlinx.coroutines.delay
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import net.codinux.banking.fints.extensions.*
import net.codinux.log.logger
import net.codinux.banking.fints.messages.MessageBuilder
import net.codinux.banking.fints.messages.MessageBuilderResult
@ -19,9 +20,7 @@ import net.codinux.banking.fints.response.segments.*
import net.codinux.banking.fints.tan.FlickerCodeDecoder
import net.codinux.banking.fints.tan.TanImageDecoder
import net.codinux.banking.fints.util.TanMethodSelector
import net.codinux.banking.fints.extensions.minusDays
import net.codinux.banking.fints.extensions.todayAtEuropeBerlin
import net.codinux.banking.fints.extensions.todayAtSystemDefaultTimeZone
import net.codinux.log.Log
import kotlin.math.max
import kotlin.time.Duration.Companion.seconds
@ -75,8 +74,7 @@ open class FinTsJobExecutor(
*
* Be aware this method resets BPD, UPD and selected TAN method!
*/
open suspend fun retrieveBasicDataLikeUsersTanMethods(context: JobContext, preferredTanMethods: List<TanMethodType>? = null, preferredTanMedium: String? = null,
closeDialog: Boolean = false): BankResponse {
open suspend fun retrieveBasicDataLikeUsersTanMethods(context: JobContext): BankResponse {
val bank = context.bank
// just to ensure settings are in its initial state and that bank sends us bank parameter (BPD),
@ -92,7 +90,7 @@ open class FinTsJobExecutor(
bank.resetSelectedTanMethod()
// this is the only case where Einschritt-TAN-Verfahren is accepted: to get user's TAN methods
context.startNewDialog(closeDialog, versionOfSecurityProcedure = VersionDesSicherheitsverfahrens.Version_1)
context.startNewDialog(versionOfSecurityProcedure = VersionDesSicherheitsverfahrens.Version_1)
val message = messageBuilder.createInitDialogMessage(context)
@ -105,12 +103,10 @@ open class FinTsJobExecutor(
if (bank.tanMethodsAvailableForUser.isEmpty()) { // could not retrieve supported tan methods for user
return getTanMethodsResponse
} else {
getUsersTanMethod(context, preferredTanMethods)
getUsersTanMethod(context)
if (bank.isTanMethodSelected == false) {
return getTanMethodsResponse
} else if (bank.tanMedia.isEmpty() && isJobSupported(bank, CustomerSegmentId.TanMediaList)) { // tan media not retrieved yet
getTanMediaList(context, TanMedienArtVersion.Alle, TanMediumKlasse.AlleMedien, preferredTanMedium)
if (bank.isTanMethodSelected && bank.tanMedia.isEmpty() && bank.tanMethodsAvailableForUser.any { it.nameOfTanMediumRequired } && isJobSupported(bank, CustomerSegmentId.TanMediaList)) { // tan media not retrieved yet
getTanMediaList(context, TanMedienArtVersion.Alle, TanMediumKlasse.AlleMedien, context.preferredTanMedium)
return getTanMethodsResponse // TODO: judge if bank requires selecting TAN media and if though evaluate getTanMediaListResponse
} else {
@ -149,6 +145,7 @@ open class FinTsJobExecutor(
return BankResponse(true, internalError = "Die TAN Verfahren der Bank konnten nicht ermittelt werden") // TODO: translate
} else {
bank.tanMethodsAvailableForUser = bank.tanMethodsSupportedByBank
.filterNot { context.tanMethodsNotSupportedByApplication.contains(it.type) }
val didSelectTanMethod = getUsersTanMethod(context)
@ -227,11 +224,11 @@ open class FinTsJobExecutor(
response.getFirstSegmentById<ReceivedCreditCardTransactionsAndBalance>(InstituteSegmentId.CreditCardTransactions)?.let { creditCardTransactionsSegment ->
balance = Money(creditCardTransactionsSegment.balance.amount, creditCardTransactionsSegment.balance.currency ?: "EUR")
bookedTransactions.addAll(creditCardTransactionsSegment.transactions.map { AccountTransaction(parameter.account, it.amount, it.description, it.bookingDate, it.transactionDescriptionBase ?: "", null, null, "", it.valueDate) })
bookedTransactions.addAll(creditCardTransactionsSegment.transactions.map { AccountTransaction(parameter.account, it.amount, it.description, it.bookingDate, it.valueDate, it.transactionDescriptionBase ?: "", null, null) })
}
}
val startTime = Clock.System.now()
val startTime = Instant.nowExt()
val response = getAndHandleResponseForMessage(context, message)
@ -240,7 +237,7 @@ open class FinTsJobExecutor(
val successful = response.tanRequiredButWeWereToldToAbortIfSo
|| (response.successful && (parameter.alsoRetrieveBalance == false || balance != null))
val fromDate = parameter.fromDate
?: parameter.account.countDaysForWhichTransactionsAreKept?.let { LocalDate.todayAtSystemDefaultTimeZone().minusDays(it) }
?: parameter.account.serverTransactionsRetentionDays?.let { LocalDate.todayAtSystemDefaultTimeZone().minusDays(it) }
?: bookedTransactions.minByOrNull { it.valueDate }?.valueDate
val retrievedData = RetrievedAccountData(parameter.account, successful, balance, bookedTransactions, unbookedTransactions, startTime, fromDate, parameter.toDate ?: LocalDate.todayAtEuropeBerlin(), response.internalError)
@ -380,20 +377,36 @@ open class FinTsJobExecutor(
protected open suspend fun handleEnteringTanRequired(context: JobContext, tanResponse: TanResponse, response: BankResponse): BankResponse {
// on all platforms run on Dispatchers.Main, but on iOS skip this (or wrap in withContext(Dispatchers.IO) )
// val enteredTanResult = GlobalScope.async {
val tanChallenge = createTanChallenge(tanResponse, modelMapper.mapToActionRequiringTan(context.type), context.bank, context.account)
val tanChallenge = createTanChallenge(tanResponse, modelMapper.mapToActionRequiringTan(context.type), context.bank, context.account)
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!!
// }
return handleEnterTanResult(context, enteredTanResult, tanResponse, response)
}
@ -408,13 +421,13 @@ open class FinTsJobExecutor(
TanMethodType.ChipTanFlickercode ->
FlickerCodeTanChallenge(
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)
forAction, messageToShowToUser, challenge, tanMethod, tanResponse.tanMediaIdentifier, bank, account, tanResponse.tanExpirationTime)
TanMethodType.ChipTanQrCode, TanMethodType.ChipTanPhotoTanMatrixCode,
TanMethodType.QrCode, TanMethodType.photoTan ->
ImageTanChallenge(TanImageDecoder().decodeChallenge(challenge), forAction, messageToShowToUser, challenge, tanMethod, tanResponse.tanMediaIdentifier, bank, account)
ImageTanChallenge(TanImageDecoder().decodeChallenge(challenge), forAction, messageToShowToUser, challenge, tanMethod, tanResponse.tanMediaIdentifier, bank, account, tanResponse.tanExpirationTime)
else -> TanChallenge(forAction, messageToShowToUser, challenge, tanMethod, tanResponse.tanMediaIdentifier, bank, account)
else -> TanChallenge(forAction, messageToShowToUser, challenge, tanMethod, tanResponse.tanMediaIdentifier, bank, account, tanResponse.tanExpirationTime)
}
}
@ -472,6 +485,8 @@ open class FinTsJobExecutor(
}
}
tanChallenge.tanExpired()
return null
}
@ -616,7 +631,7 @@ open class FinTsJobExecutor(
protected open suspend fun initDialogWithStrongCustomerAuthenticationAfterSuccessfulPreconditionChecks(context: JobContext): BankResponse {
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())
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())
val message = messageBuilder.createInitDialogMessage(context)
@ -692,7 +707,7 @@ open class FinTsJobExecutor(
return BankResponse(true, noTanMethodSelected = noTanMethodSelected, internalError = errorMessage)
}
open suspend fun getUsersTanMethod(context: JobContext, preferredTanMethods: List<TanMethodType>? = null): Boolean {
open suspend fun getUsersTanMethod(context: JobContext): Boolean {
val bank = context.bank
if (bank.tanMethodsAvailableForUser.size == 1) { // user has only one TAN method -> set it and we're done
@ -700,13 +715,13 @@ open class FinTsJobExecutor(
return true
}
else {
tanMethodSelector.findPreferredTanMethod(bank.tanMethodsAvailableForUser, preferredTanMethods)?.let {
tanMethodSelector.findPreferredTanMethod(bank.tanMethodsAvailableForUser, context.preferredTanMethods, context.tanMethodsNotSupportedByApplication)?.let {
bank.selectedTanMethod = it
return true
}
// we know user's supported tan methods, now ask user which one to select
val suggestedTanMethod = tanMethodSelector.getSuggestedTanMethod(bank.tanMethodsAvailableForUser)
val suggestedTanMethod = tanMethodSelector.getSuggestedTanMethod(bank.tanMethodsAvailableForUser, context.tanMethodsNotSupportedByApplication)
val selectedTanMethod = context.callback.askUserForTanMethod(bank.tanMethodsAvailableForUser, suggestedTanMethod)
@ -727,14 +742,14 @@ open class FinTsJobExecutor(
protected open fun updateBankAndCustomerDataIfResponseSuccessful(context: JobContext, response: BankResponse) {
if (response.successful) {
updateBankAndCustomerData(context.bank, response)
updateBankAndCustomerData(context.bank, response, context)
}
}
protected open fun updateBankAndCustomerData(bank: BankData, response: BankResponse) {
protected open fun updateBankAndCustomerData(bank: BankData, response: BankResponse, context: JobContext) {
updateBankData(bank, response)
modelMapper.updateCustomerData(bank, response)
modelMapper.updateCustomerData(bank, response, context)
}

View File

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

View File

@ -0,0 +1,12 @@
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 {
return Clock.System.now().toLocalDateTime(timeZone)
return Instant.nowExt().toLocalDateTime(timeZone)
}

View File

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

View File

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

View File

@ -7,59 +7,124 @@ import net.codinux.banking.fints.extensions.UnixEpochStart
open class AccountTransaction(
val account: AccountData,
val amount: Money,
val isReversal: Boolean,
val unparsedReference: String,
val reference: String?, // that was also new to me that reference may is null
val bookingDate: LocalDate,
val otherPartyName: String?,
val otherPartyBankCode: String?,
val otherPartyAccountId: String?,
val bookingText: String?,
val valueDate: LocalDate,
val statementNumber: Int,
val sequenceNumber: Int?,
/**
* Name des Überweisenden oder Zahlungsempfängers
*/
val otherPartyName: String?,
/**
* BIC des Überweisenden / Zahlungsempfängers
*/
val otherPartyBankId: String?,
/**
* IBAN des Überweisenden oder Zahlungsempfängers
*/
val otherPartyAccountId: String?,
/**
* Buchungstext, z. B. DAUERAUFTRAG, BARGELDAUSZAHLUNG, ONLINE-UEBERWEISUNG, FOLGELASTSCHRIFT, ...
*/
val postingText: String?,
val openingBalance: Money?,
val closingBalance: Money?,
val endToEndReference: String?,
/**
* 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 mandateReference: String?,
val creditorIdentifier: String?,
val originatorsIdentificationCode: String?,
/**
* Summe aus Auslagenersatz und Bearbeitungsprovision bei einer nationalen Rücklastschrift
* sowie optionalem Zinsausgleich.
*/
val compensationAmount: String?,
/**
* Betrag der ursprünglichen Lastschrift
*/
val originalAmount: String?,
val sepaReference: String?,
/**
* Abweichender Überweisender oder Zahlungsempfänger
*/
val deviantOriginator: String?,
/**
* Abweichender Zahlungsempfänger oder Zahlungspflichtiger
*/
val deviantRecipient: String?,
val referenceWithNoSpecialType: String?,
val primaNotaNumber: String?,
val textKeySupplement: String?,
val currencyType: String?,
val bookingKey: String,
val referenceForTheAccountOwner: String,
val referenceOfTheAccountServicingInstitution: String?,
val supplementaryDetails: 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?,
val transactionReferenceNumber: String,
val relatedReferenceNumber: String?
/**
* Referenznummer, die vom Sender als eindeutige Kennung für die Nachricht vergeben wurde
* (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
internal constructor() : this(AccountData(), Money(Amount.Zero, ""), "", UnixEpochStart, null, null, null, null, UnixEpochStart)
internal constructor() : this(AccountData(), Money(Amount.Zero, ""), "", UnixEpochStart, UnixEpochStart, null, null, null, null)
constructor(account: AccountData, amount: Money, unparsedReference: String, bookingDate: LocalDate, otherPartyName: String?, otherPartyBankCode: String?, otherPartyAccountId: String?, bookingText: String?, valueDate: LocalDate)
: this(account, amount, false, unparsedReference, bookingDate, otherPartyName, otherPartyBankCode, otherPartyAccountId, bookingText, valueDate,
0, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null, null,
null, "", "", null, null, "", null)
constructor(account: AccountData, amount: Money, unparsedReference: String, bookingDate: LocalDate, valueDate: LocalDate, otherPartyName: String?, otherPartyBankId: String?, otherPartyAccountId: String?, postingText: String? = null)
: this(account, amount, unparsedReference, bookingDate, valueDate, otherPartyName, otherPartyBankId, otherPartyAccountId, postingText,
null, null, 0, null,
null, null, null, null, null, null, null, null, null, null, null,
"", null, null, "", null, false)
open val showOtherPartyName: Boolean
get() = otherPartyName.isNullOrBlank() == false /* && type != "ENTGELTABSCHLUSS" && type != "AUSZAHLUNG" */ // TODO
val reference: String
get() = sepaReference ?: unparsedReference
override fun equals(other: Any?): Boolean {
if (this === other) return true
@ -67,12 +132,12 @@ open class AccountTransaction(
if (account != other.account) return false
if (amount != other.amount) return false
if (unparsedReference != other.unparsedReference) return false
if (reference != other.reference) return false
if (bookingDate != other.bookingDate) return false
if (otherPartyName != other.otherPartyName) return false
if (otherPartyBankCode != other.otherPartyBankCode) return false
if (otherPartyBankId != other.otherPartyBankId) return false
if (otherPartyAccountId != other.otherPartyAccountId) return false
if (bookingText != other.bookingText) return false
if (postingText != other.postingText) return false
if (valueDate != other.valueDate) return false
return true
@ -81,19 +146,19 @@ open class AccountTransaction(
override fun hashCode(): Int {
var result = account.hashCode()
result = 31 * result + amount.hashCode()
result = 31 * result + unparsedReference.hashCode()
result = 31 * result + reference.hashCode()
result = 31 * result + bookingDate.hashCode()
result = 31 * result + (otherPartyName?.hashCode() ?: 0)
result = 31 * result + (otherPartyBankCode?.hashCode() ?: 0)
result = 31 * result + (otherPartyBankId?.hashCode() ?: 0)
result = 31 * result + (otherPartyAccountId?.hashCode() ?: 0)
result = 31 * result + (bookingText?.hashCode() ?: 0)
result = 31 * result + (postingText?.hashCode() ?: 0)
result = 31 * result + valueDate.hashCode()
return result
}
override fun toString(): String {
return "$valueDate $amount $otherPartyName: $unparsedReference"
return "$valueDate $amount $otherPartyName: $reference"
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
package net.codinux.banking.fints.model
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import net.codinux.banking.fints.extensions.nowExt
import net.codinux.banking.fints.log.MessageContext
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.
*/
open val parsedSegments: List<ReceivedSegment> = emptyList(),
open val time: Instant = Clock.System.now()
open val time: Instant = Instant.nowExt()
) {
val messageIncludingMessageTrace: String

View File

@ -1,8 +1,11 @@
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.response.BankResponse
import net.codinux.banking.fints.response.client.FinTsClientResponse
import net.codinux.log.Log
open class TanChallenge(
@ -12,7 +15,14 @@ open class TanChallenge(
val tanMethod: TanMethod,
val tanMediaIdentifier: String?,
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
@ -21,6 +31,8 @@ open class TanChallenge(
open val isEnteringTanDone: Boolean
get() = enterTanResult != null
private val tanExpiredCallbacks = mutableListOf<() -> Unit>()
private val userApprovedDecoupledTanCallbacks = mutableListOf<() -> Unit>()
@ -31,23 +43,59 @@ open class TanChallenge(
internal fun userApprovedDecoupledTan(responseAfterApprovingDecoupledTan: BankResponse) {
this.enterTanResult = EnterTanResult(null, true, responseAfterApprovingDecoupledTan)
userApprovedDecoupledTanCallbacks.forEach { it.invoke() }
userApprovedDecoupledTanCallbacks.clear()
userApprovedDecoupledTanCallbacks.toTypedArray().forEach { // copy to avoid ConcurrentModificationException
try {
it.invoke()
} catch (e: Throwable) {
Log.error(e) { "Could not call userApprovedDecoupledTanCallback" }
}
}
clearUserApprovedDecoupledTanCallbacks()
}
fun userDidNotEnterTan() {
clearUserApprovedDecoupledTanCallbacks()
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) {
clearUserApprovedDecoupledTanCallbacks()
this.enterTanResult = EnterTanResult(null, changeTanMethodTo = changeTanMethodTo)
}
fun userAsksToChangeTanMedium(changeTanMediumTo: TanMedium, changeTanMediumResultCallback: ((FinTsClientResponse) -> Unit)?) {
clearUserApprovedDecoupledTanCallbacks()
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) {
if (isEnteringTanDone == false) {
this.userApprovedDecoupledTanCallbacks.add(callback)
@ -56,6 +104,10 @@ open class TanChallenge(
}
}
protected open fun clearUserApprovedDecoupledTanCallbacks() {
userApprovedDecoupledTanCallbacks.clear()
}
override fun toString(): String {
return "$tanMethod (medium: $tanMediaIdentifier): $messageToShowToUser"

View File

@ -63,7 +63,7 @@ open class ModelMapper(
}
}
open fun updateCustomerData(bank: BankData, response: BankResponse) {
open fun updateCustomerData(bank: BankData, response: BankResponse, context: JobContext) {
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.
if (bank.selectedLanguage == Dialogsprache.Default && bankParameters.supportedLanguages.isNotEmpty()) {
@ -102,7 +102,7 @@ open class ModelMapper(
accountInfo.accountLimit, accountInfo.allowedJobNames)
bank.supportedJobs.filterIsInstance<RetrieveAccountTransactionsParameters>().sortedByDescending { it.segmentVersion }.firstOrNull { newAccount.allowedJobNames.contains(it.jobName) }?.let { transactionsParameters ->
newAccount.countDaysForWhichTransactionsAreKept = transactionsParameters.countDaysForWhichTransactionsAreKept
newAccount.serverTransactionsRetentionDays = transactionsParameters.serverTransactionsRetentionDays
}
bank.addAccount(newAccount)
@ -146,6 +146,7 @@ open class ModelMapper(
if (response.supportedTanMethodsForUser.isNotEmpty()) {
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
bank.resetSelectedTanMethod()

View File

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

View File

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

View File

@ -1,6 +1,6 @@
package net.codinux.banking.fints.response.segments
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.Instant
import net.codinux.banking.fints.messages.datenelemente.implementierte.tan.TanProcess
@ -31,7 +31,13 @@ open class TanResponse(
val challenge: String?, // M: bei TAN-Prozess=1, 3, 4. O: bei TAN-Prozess=2
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
segmentString: String

View File

@ -44,44 +44,50 @@ open class Mt940AccountTransactionsParser(
protected open fun mapToAccountTransaction(statement: AccountStatement, transaction: Transaction, account: AccountData): AccountTransaction {
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(
account,
Money(mapAmount(transaction.statementLine), currency),
transaction.statementLine.isReversal,
transaction.information?.unparsedReference ?: "",
// 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.information?.sepaReference ?: transaction.information?.unparsedReference ?: "",
transaction.statementLine.bookingDate ?: statement.closingBalance.bookingDate,
transaction.information?.otherPartyName,
transaction.information?.otherPartyBankCode,
transaction.information?.otherPartyAccountId,
transaction.information?.bookingText,
transaction.statementLine.valueDate,
transaction.information?.otherPartyName,
transaction.information?.otherPartyBankId,
transaction.information?.otherPartyAccountId,
transaction.information?.postingText,
Money(mapAmount(statement.openingBalance), currency),
Money(mapAmount(statement.closingBalance), currency),
statement.statementNumber,
statement.sequenceNumber,
Money(mapAmount(statement.openingBalance), currency), // TODO: that's not true, these are the opening and closing balance of
Money(mapAmount(statement.closingBalance), currency), // all transactions of this day, not this specific transaction's ones
statement.sheetNumber,
// :60: customer reference: Wenn „KREF+“ eingestellt ist, dann erfolgt die Angabe der Referenznummer in Tag :86: .
transaction.information?.customerReference ?: transaction.statementLine.customerReference,
transaction.statementLine.bankReference,
transaction.statementLine.furtherInformationOriginalAmountAndCharges,
transaction.information?.endToEndReference,
transaction.information?.customerReference,
transaction.information?.mandateReference,
transaction.information?.creditorIdentifier,
transaction.information?.originatorsIdentificationCode,
transaction.information?.compensationAmount,
transaction.information?.originalAmount,
transaction.information?.sepaReference,
transaction.information?.deviantOriginator,
transaction.information?.deviantRecipient,
transaction.information?.referenceWithNoSpecialType,
transaction.information?.primaNotaNumber,
transaction.information?.textKeySupplement,
transaction.information?.journalNumber,
transaction.information?.textKeyAddition,
transaction.statementLine.currencyType,
transaction.statementLine.bookingKey,
transaction.statementLine.referenceForTheAccountOwner,
transaction.statementLine.referenceOfTheAccountServicingInstitution,
transaction.statementLine.supplementaryDetails,
statement.orderReferenceNumber,
statement.referenceNumber,
statement.transactionReferenceNumber,
statement.relatedReferenceNumber
transaction.statementLine.isReversal,
)
}

View File

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

View File

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

View File

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

View File

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

@ -0,0 +1,99 @@
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,26 +42,57 @@ open class StatementLine(
val currencyType: String?,
/**
* Codes see p. 177 bottom - 179
*
* After constant N
* in Kontowährung
*
* Max length = 15
*/
val amount: Amount,
/**
* in Kontowährung
* Codes see p. 177 bottom - 179
*
* After constant N
*
* Length = 3
*/
val bookingKey: String,
val postingKey: 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(
val statementLine: StatementLine,
val information: InformationToAccountOwner? = null
val information: RemittanceInformationField? = null
) {

View File

@ -12,64 +12,36 @@ open class TanMethodSelector {
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>): TanMethod? {
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 == 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 getSuggestedTanMethod(tanMethods: List<TanMethod>, tanMethodsNotSupportedByApplication: List<TanMethodType> = emptyList()): TanMethod? {
return findPreferredTanMethod(tanMethods, NonVisualOrImageBased, tanMethodsNotSupportedByApplication) // we use NonVisualOrImageBased as it provides a good default for most users
?: tanMethods.firstOrNull { it.type !in tanMethodsNotSupportedByApplication }
}
open fun findPreferredTanMethod(tanMethods: List<TanMethod>, preferredTanMethods: List<TanMethodType>?): TanMethod? {
open fun findPreferredTanMethod(tanMethods: List<TanMethod>, preferredTanMethods: List<TanMethodType>?, tanMethodsNotSupportedByApplication: List<TanMethodType> = emptyList()): TanMethod? {
preferredTanMethods?.forEach { preferredTanMethodType ->
tanMethods.firstOrNull { it.type == preferredTanMethodType }?.let {
return it
if (preferredTanMethodType !in tanMethodsNotSupportedByApplication) {
tanMethods.firstOrNull { it.type == preferredTanMethodType }?.let {
return it
}
}
}
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,70 +10,72 @@ import net.codinux.banking.fints.extensions.UnixEpochStart
@Serializable
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 unparsedReference: String, // alternative names: purpose, reason
val reference: String?, // alternative names: purpose, reason
val bookingDate: LocalDate,
val otherPartyName: String?,
val otherPartyBankCode: String?,
val otherPartyAccountId: String?,
val bookingText: String?,
val valueDate: LocalDate,
val statementNumber: Int,
val sequenceNumber: Int?,
val otherPartyName: String?,
val otherPartyBankId: String?,
val otherPartyAccountId: String?,
val postingText: String?,
val openingBalance: Money?,
val closingBalance: Money?,
val endToEndReference: String?,
val statementNumber: Int,
val sheetNumber: Int?,
val customerReference: String?,
val bankReference: String?,
val furtherInformation: String?,
val endToEndReference: String?,
val mandateReference: String?,
val creditorIdentifier: String?,
val originatorsIdentificationCode: String?,
val compensationAmount: String?,
val originalAmount: String?,
val sepaReference: String?,
val deviantOriginator: String?,
val deviantRecipient: String?,
val referenceWithNoSpecialType: String?,
val primaNotaNumber: String?,
val textKeySupplement: String?,
val currencyType: String?,
val bookingKey: String,
val referenceForTheAccountOwner: String,
val referenceOfTheAccountServicingInstitution: String?,
val supplementaryDetails: String?,
val journalNumber: String?,
val textKeyAddition: String?,
val transactionReferenceNumber: String,
val relatedReferenceNumber: String?
val orderReferenceNumber: String?,
val referenceNumber: String?,
val isReversal: Boolean
) {
// for object deserializers
internal constructor() : this(Money(Amount.Zero, ""), "", UnixEpochStart, null, null, null, null, UnixEpochStart)
internal constructor() : this(Money(Amount.Zero, ""), "", UnixEpochStart, UnixEpochStart, null, null, null, null)
constructor(amount: Money, unparsedReference: String, bookingDate: LocalDate, otherPartyName: String?, otherPartyBankCode: String?, otherPartyAccountId: String?, bookingText: String?, valueDate: LocalDate)
: this(amount, unparsedReference, bookingDate, otherPartyName, otherPartyBankCode, otherPartyAccountId, bookingText, valueDate,
0, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null, null,
null, "", "", null, null, "", null)
constructor(amount: Money, unparsedReference: String, bookingDate: LocalDate, valueDate: LocalDate, otherPartyName: String?, otherPartyBankId: String?, otherPartyAccountId: String?, postingText: String?)
: this(amount, unparsedReference, bookingDate, valueDate, otherPartyName, otherPartyBankId, otherPartyAccountId, postingText,
null, null, 0, null,
null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, false)
open val showOtherPartyName: Boolean
get() = otherPartyName.isNullOrBlank() == false /* && type != "ENTGELTABSCHLUSS" && type != "AUSZAHLUNG" */ // TODO
val reference: String
get() = sepaReference ?: unparsedReference
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is AccountTransaction) return false
if (amount != other.amount) return false
if (unparsedReference != other.unparsedReference) return false
if (reference != other.reference) return false
if (bookingDate != other.bookingDate) return false
if (otherPartyName != other.otherPartyName) return false
if (otherPartyBankCode != other.otherPartyBankCode) return false
if (otherPartyBankId != other.otherPartyBankId) return false
if (otherPartyAccountId != other.otherPartyAccountId) return false
if (bookingText != other.bookingText) return false
if (postingText != other.postingText) return false
if (valueDate != other.valueDate) return false
return true
@ -81,19 +83,19 @@ open class AccountTransaction(
override fun hashCode(): Int {
var result = amount.hashCode()
result = 31 * result + unparsedReference.hashCode()
result = 31 * result + reference.hashCode()
result = 31 * result + bookingDate.hashCode()
result = 31 * result + (otherPartyName?.hashCode() ?: 0)
result = 31 * result + (otherPartyBankCode?.hashCode() ?: 0)
result = 31 * result + (otherPartyBankId?.hashCode() ?: 0)
result = 31 * result + (otherPartyAccountId?.hashCode() ?: 0)
result = 31 * result + (bookingText?.hashCode() ?: 0)
result = 31 * result + (postingText?.hashCode() ?: 0)
result = 31 * result + valueDate.hashCode()
return result
}
override fun toString(): String {
return "$valueDate $amount $otherPartyName: $unparsedReference"
return "$valueDate $amount $otherPartyName: $reference"
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -966,7 +966,7 @@ class ResponseParserTest : FinTsTestBase() {
assertEquals(TanResponse.NoJobReferenceResponse, segment.jobReference)
assertEquals(TanResponse.NoChallengeResponse, segment.challenge)
assertNull(segment.challengeHHD_UC)
assertEquals(null, segment.validityDateTimeForChallenge)
assertEquals(null, segment.tanExpirationTime)
assertEquals(null, segment.tanMediaIdentifier)
}
?: run { fail("No segment of type TanResponse found in ${result.receivedSegments}") }
@ -995,7 +995,7 @@ class ResponseParserTest : FinTsTestBase() {
assertEquals(jobReference, segment.jobReference)
assertEquals(unmaskString(challenge), segment.challenge)
assertEquals(challengeHHD_UC, segment.challengeHHD_UC)
assertEquals(null, segment.validityDateTimeForChallenge)
assertEquals(null, segment.tanExpirationTime)
assertEquals(tanMediaIdentifier, segment.tanMediaIdentifier)
}
?: run { fail("No segment of type TanResponse found in ${result.receivedSegments}") }
@ -1189,16 +1189,16 @@ class ResponseParserTest : FinTsTestBase() {
fun parseAccountTransactionsMt940Parameters_Version4() {
// given
val countDaysForWhichTransactionsAreKept = 90
val serverTransactionsRetentionDays = 90
// when
val result = underTest.parse("HIKAZS:21:4:4+20+1+$countDaysForWhichTransactionsAreKept:N'")
val result = underTest.parse("HIKAZS:21:4:4+20+1+$serverTransactionsRetentionDays:N'")
// then
assertSuccessfullyParsedSegment(result, InstituteSegmentId.AccountTransactionsMt940Parameters, 21, 4, 4)
result.getFirstSegmentById<RetrieveAccountTransactionsParameters>(InstituteSegmentId.AccountTransactionsMt940Parameters)?.let { segment ->
assertEquals(countDaysForWhichTransactionsAreKept, segment.countDaysForWhichTransactionsAreKept)
assertEquals(serverTransactionsRetentionDays, segment.serverTransactionsRetentionDays)
assertFalse(segment.settingCountEntriesAllowed)
assertFalse(segment.settingAllAccountAllowed)
}
@ -1209,16 +1209,16 @@ class ResponseParserTest : FinTsTestBase() {
fun parseAccountTransactionsMt940Parameters_Version6() {
// given
val countDaysForWhichTransactionsAreKept = 90
val serverTransactionsRetentionDays = 90
// when
val result = underTest.parse("HIKAZS:23:6:4+20+1+1+$countDaysForWhichTransactionsAreKept:N:N'")
val result = underTest.parse("HIKAZS:23:6:4+20+1+1+$serverTransactionsRetentionDays:N:N'")
// then
assertSuccessfullyParsedSegment(result, InstituteSegmentId.AccountTransactionsMt940Parameters, 23, 6, 4)
result.getFirstSegmentById<RetrieveAccountTransactionsParameters>(InstituteSegmentId.AccountTransactionsMt940Parameters)?.let { segment ->
assertEquals(countDaysForWhichTransactionsAreKept, segment.countDaysForWhichTransactionsAreKept)
assertEquals(serverTransactionsRetentionDays, segment.serverTransactionsRetentionDays)
assertFalse(segment.settingCountEntriesAllowed)
assertFalse(segment.settingAllAccountAllowed)
}
@ -1290,16 +1290,16 @@ class ResponseParserTest : FinTsTestBase() {
fun parseCreditCardAccountTransactionsParameters() {
// given
val countDaysForWhichTransactionsAreKept = 9999
val serverTransactionsRetentionDays = 9999
// when
val result = underTest.parse("DIKKUS:15:2:4+999+1+0+$countDaysForWhichTransactionsAreKept:J:J'")
val result = underTest.parse("DIKKUS:15:2:4+999+1+0+$serverTransactionsRetentionDays:J:J'")
// then
assertSuccessfullyParsedSegment(result, InstituteSegmentId.CreditCardTransactionsParameters, 15, 2, 4)
result.getFirstSegmentById<RetrieveAccountTransactionsParameters>(InstituteSegmentId.CreditCardTransactionsParameters)?.let { segment ->
assertEquals(countDaysForWhichTransactionsAreKept, segment.countDaysForWhichTransactionsAreKept)
assertEquals(serverTransactionsRetentionDays, segment.serverTransactionsRetentionDays)
assertTrue(segment.settingCountEntriesAllowed)
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.transactions.mt940.Mt940Parser
import net.codinux.banking.fints.transactions.mt940.model.Balance
import net.codinux.banking.fints.transactions.mt940.model.InformationToAccountOwner
import net.codinux.banking.fints.transactions.mt940.model.RemittanceInformationField
import net.codinux.banking.fints.transactions.mt940.model.StatementLine
import kotlinx.datetime.LocalDate
import net.codinux.banking.fints.extensions.*
@ -26,12 +26,12 @@ class Mt940ParserTest : FinTsTestBase() {
val AccountStatement1Transaction1Amount = Amount("1234,56")
val AccountStatement1Transaction1OtherPartyName = "Sender1"
val AccountStatement1Transaction1OtherPartyBankCode = "AAAADE12"
val AccountStatement1Transaction1OtherPartyBankId = "AAAADE12"
val AccountStatement1Transaction1OtherPartyAccountId = "DE99876543210987654321"
val AccountStatement1Transaction2Amount = Amount("432,10")
val AccountStatement1Transaction2OtherPartyName = "Receiver2"
val AccountStatement1Transaction2OtherPartyBankCode = "BBBBDE56"
val AccountStatement1Transaction2OtherPartyBankId = "BBBBDE56"
val AccountStatement1Transaction2OtherPartyAccountId = "DE77987654321234567890"
val AccountStatement1ClosingBalanceAmount = Amount("13580,23")
@ -67,7 +67,7 @@ class Mt940ParserTest : FinTsTestBase() {
val transaction = statement.transactions.first()
assertTurnover(transaction.statementLine, AccountStatement1BookingDate, AccountStatement1Transaction1Amount)
assertTransactionDetails(transaction.information, AccountStatement1Transaction1OtherPartyName,
AccountStatement1Transaction1OtherPartyBankCode, AccountStatement1Transaction1OtherPartyAccountId)
AccountStatement1Transaction1OtherPartyBankId, AccountStatement1Transaction1OtherPartyAccountId)
}
@Test
@ -90,7 +90,7 @@ class Mt940ParserTest : FinTsTestBase() {
assertEquals(BankCode, statement.bankCodeBicOrIban)
assertEquals(CustomerId, statement.accountIdentifier)
assertEquals(0, statement.statementNumber)
assertNull(statement.sequenceNumber)
assertNull(statement.sheetNumber)
assertBalance(statement.openingBalance, true, bookingDate, Amount("0,00"))
assertBalance(statement.closingBalance, isCredit, bookingDate, amount)
@ -124,12 +124,12 @@ class Mt940ParserTest : FinTsTestBase() {
val firstTransaction = statement.transactions.first()
assertTurnover(firstTransaction.statementLine, AccountStatement1BookingDate, AccountStatement1Transaction1Amount)
assertTransactionDetails(firstTransaction.information, AccountStatement1Transaction1OtherPartyName,
AccountStatement1Transaction1OtherPartyBankCode, AccountStatement1Transaction1OtherPartyAccountId)
AccountStatement1Transaction1OtherPartyBankId, AccountStatement1Transaction1OtherPartyAccountId)
val secondTransaction = statement.transactions[1]
assertTurnover(secondTransaction.statementLine, AccountStatement1BookingDate, AccountStatement1Transaction2Amount, false)
assertTransactionDetails(secondTransaction.information, AccountStatement1Transaction2OtherPartyName,
AccountStatement1Transaction2OtherPartyBankCode, AccountStatement1Transaction2OtherPartyAccountId)
AccountStatement1Transaction2OtherPartyBankId, AccountStatement1Transaction2OtherPartyAccountId)
}
@Test
@ -306,8 +306,8 @@ class Mt940ParserTest : FinTsTestBase() {
assertSize(1, result.first().transactions)
result.first().transactions[0].information?.apply {
assertEquals("BASISLASTSCHRIFT", bookingText)
assertEquals("TUBDDEDD", otherPartyBankCode)
assertEquals("BASISLASTSCHRIFT", postingText)
assertEquals("TUBDDEDD", otherPartyBankId)
assertEquals("DE87300308801234567890", otherPartyAccountId)
assertEquals("6MKL2OT30QENNLIU", endToEndReference)
assertEquals("?,3SQNdUbxm9z7dB)+gKYDJAKzCM0G", mandateReference)
@ -362,13 +362,13 @@ class Mt940ParserTest : FinTsTestBase() {
assertEquals(amount, statementLine.amount)
}
private fun assertTransactionDetails(details: InformationToAccountOwner?, otherPartyName: String,
otherPartyBankCode: String, otherPartyAccountId: String) {
private fun assertTransactionDetails(details: RemittanceInformationField?, otherPartyName: String,
otherPartyBankId: String, otherPartyAccountId: String) {
assertNotNull(details)
assertEquals(otherPartyName, details.otherPartyName)
assertEquals(otherPartyBankCode, details.otherPartyBankCode)
assertEquals(otherPartyBankId, details.otherPartyBankId)
assertEquals(otherPartyAccountId, details.otherPartyAccountId)
}
@ -380,7 +380,7 @@ class Mt940ParserTest : FinTsTestBase() {
:60F:C${convertMt940Date(AccountStatement1PreviousStatementBookingDate)}EUR$AccountStatement1OpeningBalanceAmount
:61:${convertMt940Date(AccountStatement1BookingDate)}${convertToShortBookingDate(AccountStatement1BookingDate)}CR${AccountStatement1Transaction1Amount}N062NONREF
:86:166?00GUTSCHR. UEBERWEISUNG?109249?20EREF+674?21SVWZ+1908301/
EUR ${AccountStatement1Transaction1Amount}/20?2219-10-02/...?30$AccountStatement1Transaction1OtherPartyBankCode?31$AccountStatement1Transaction1OtherPartyAccountId
EUR ${AccountStatement1Transaction1Amount}/20?2219-10-02/...?30$AccountStatement1Transaction1OtherPartyBankId?31$AccountStatement1Transaction1OtherPartyAccountId
?32$AccountStatement1Transaction1OtherPartyName
:62F:C${convertMt940Date(AccountStatement1BookingDate)}EUR$AccountStatement1ClosingBalanceAmount
-
@ -393,11 +393,11 @@ class Mt940ParserTest : FinTsTestBase() {
:60F:C${convertMt940Date(AccountStatement1PreviousStatementBookingDate)}EUR$AccountStatement1OpeningBalanceAmount
:61:${convertMt940Date(AccountStatement1BookingDate)}${convertToShortBookingDate(AccountStatement1BookingDate)}CR${AccountStatement1Transaction1Amount}N062NONREF
:86:166?00GUTSCHR. UEBERWEISUNG?109249?20EREF+674?21SVWZ+1908301/
EUR ${AccountStatement1Transaction1Amount}/20?2219-10-02/...?30$AccountStatement1Transaction1OtherPartyBankCode?31$AccountStatement1Transaction1OtherPartyAccountId
EUR ${AccountStatement1Transaction1Amount}/20?2219-10-02/...?30$AccountStatement1Transaction1OtherPartyBankId?31$AccountStatement1Transaction1OtherPartyAccountId
?32$AccountStatement1Transaction1OtherPartyName
:61:${convertMt940Date(AccountStatement1BookingDate)}${convertToShortBookingDate(AccountStatement1BookingDate)}DR${AccountStatement1Transaction2Amount}N062NONREF
:86:166?00ONLINE-UEBERWEISUNG?109249?20EREF+674?21SVWZ+1908301/
EUR ${AccountStatement1Transaction2Amount}/20?2219-10-02/...?30$AccountStatement1Transaction2OtherPartyBankCode?31$AccountStatement1Transaction2OtherPartyAccountId
EUR ${AccountStatement1Transaction2Amount}/20?2219-10-02/...?30$AccountStatement1Transaction2OtherPartyBankId?31$AccountStatement1Transaction2OtherPartyAccountId
?32$AccountStatement1Transaction2OtherPartyName
:62F:C${convertMt940Date(AccountStatement1BookingDate)}EUR${AccountStatement1With2TransactionsClosingBalanceAmount}
-