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) {
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 ?: ""),
ensureNotNull(transaction.otherPartyName), ensureNotNull(transaction.otherPartyBankId), ensureNotNull(transaction.otherPartyAccountId)).joinToString(valueSeparator))
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(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)
}
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
}
accountsSupportingRetrievingTransactions.forEach { account ->
retrievedTransactionsResponses.add(getAccountTransactions(param, bank, account))
}
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 {
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))
}
@ -138,7 +133,7 @@ open class FinTsClient(
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,
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)
}
val defaultValues = (param as? GetAccountDataParameter)?.defaultBankValues
val finTsServerAddress = defaultValues?.finTs3ServerAddress ?: config.finTsServerAddressFinder.findFinTsServerAddress(param.bankCode)
val finTsServerAddress = 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, defaultValues)
val bank = mapper.mapToBankData(param, finTsServerAddress)
val getAccountInfoResponse = getAccountInfo(param, bank)
@ -205,11 +198,11 @@ open class FinTsClient(
// 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 */
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
strong customer authorization TAN media is required */

View File

@ -1,5 +1,7 @@
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
@ -37,13 +39,13 @@ open class FinTsClientDeprecated(
}
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)
open suspend fun addAccountAsync(parameter: AddAccountParameter): AddAccountResponse {
val bank = parameter.bank
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 */
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
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
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 {
@ -118,11 +120,11 @@ open class FinTsClientDeprecated(
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
import kotlinx.coroutines.delay
import kotlinx.datetime.Instant
import kotlinx.datetime.Clock
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
@ -20,7 +19,9 @@ 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.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.time.Duration.Companion.seconds
@ -74,7 +75,8 @@ open class FinTsJobExecutor(
*
* 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
// 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()
// 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)
@ -103,10 +105,12 @@ open class FinTsJobExecutor(
if (bank.tanMethodsAvailableForUser.isEmpty()) { // could not retrieve supported tan methods for user
return getTanMethodsResponse
} 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
getTanMediaList(context, TanMedienArtVersion.Alle, TanMediumKlasse.AlleMedien, context.preferredTanMedium)
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)
return getTanMethodsResponse // TODO: judge if bank requires selecting TAN media and if though evaluate getTanMediaListResponse
} else {
@ -145,7 +149,6 @@ 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)
@ -224,11 +227,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.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)
@ -237,7 +240,7 @@ open class FinTsJobExecutor(
val successful = response.tanRequiredButWeWereToldToAbortIfSo
|| (response.successful && (parameter.alsoRetrieveBalance == false || balance != null))
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
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)
while (tanChallenge.enterTanResult == null) {
delay(250)
mayRetrieveAutomaticallyIfUserEnteredDecoupledTan(context, tanChallenge, tanResponse)
var invocationCount = 0 // TODO: remove again
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
}
// TODO: add a timeout of e.g. 30 min
}
val enteredTanResult = tanChallenge.enterTanResult!!
// }
return handleEnterTanResult(context, enteredTanResult, tanResponse, response)
}
@ -421,13 +408,13 @@ Log.info { "Terminating waiting for TAN input" } // TODO: remove again
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, tanResponse.tanExpirationTime)
forAction, messageToShowToUser, challenge, tanMethod, tanResponse.tanMediaIdentifier, bank, account)
TanMethodType.ChipTanQrCode, TanMethodType.ChipTanPhotoTanMatrixCode,
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
}
@ -631,7 +616,7 @@ Log.info { "Terminating waiting for TAN input" } // TODO: remove again
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)
@ -707,7 +692,7 @@ Log.info { "Terminating waiting for TAN input" } // TODO: remove again
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
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
}
else {
tanMethodSelector.findPreferredTanMethod(bank.tanMethodsAvailableForUser, context.preferredTanMethods, context.tanMethodsNotSupportedByApplication)?.let {
tanMethodSelector.findPreferredTanMethod(bank.tanMethodsAvailableForUser, preferredTanMethods)?.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, context.tanMethodsNotSupportedByApplication)
val suggestedTanMethod = tanMethodSelector.getSuggestedTanMethod(bank.tanMethodsAvailableForUser)
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) {
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)
modelMapper.updateCustomerData(bank, response, context)
modelMapper.updateCustomerData(bank, response)
}

View File

@ -26,9 +26,6 @@ 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

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

View File

@ -1,11 +1,11 @@
package net.codinux.banking.fints.extensions
import kotlinx.datetime.Instant
import kotlinx.datetime.Clock
import kotlin.random.Random
fun randomWithSeed(): Random = Random(randomSeed())
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()
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 mapToBankData(param: FinTsClientParameter, finTsServerAddress: String): BankData {
return BankData(param.bankCode, param.loginName, param.password, finTsServerAddress, bicFinder.findBic(param.bankCode) ?: "")
}
open fun mapToAccountData(credentials: BankAccountIdentifier, param: FinTsClientParameter): AccountData {
@ -58,7 +55,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.serverTransactionsRetentionDays, account.isAccountTypeSupportedByApplication,
account.currency ?: Currency.DefaultCurrencyCode, account.accountLimit, account.countDaysForWhichTransactionsAreKept, account.isAccountTypeSupportedByApplication,
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 {
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
)
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)
}

View File

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

View File

@ -7,124 +7,59 @@ import net.codinux.banking.fints.extensions.UnixEpochStart
open class AccountTransaction(
val account: AccountData,
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 valueDate: LocalDate,
/**
* Name des Überweisenden oder Zahlungsempfängers
*/
val otherPartyName: String?,
/**
* BIC des Überweisenden / Zahlungsempfängers
*/
val otherPartyBankId: String?,
/**
* IBAN des Überweisenden oder Zahlungsempfängers
*/
val otherPartyBankCode: String?,
val otherPartyAccountId: String?,
/**
* Buchungstext, z. B. DAUERAUFTRAG, BARGELDAUSZAHLUNG, ONLINE-UEBERWEISUNG, FOLGELASTSCHRIFT, ...
*/
val postingText: String?,
val bookingText: String?,
val valueDate: LocalDate,
val statementNumber: Int,
val sequenceNumber: Int?,
val openingBalance: 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 customerReference: 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?,
/**
* Abweichender Überweisender oder Zahlungsempfänger
*/
val sepaReference: String?,
val deviantOriginator: String?,
/**
* Abweichender Zahlungsempfänger oder Zahlungspflichtiger
*/
val deviantRecipient: String?,
val referenceWithNoSpecialType: String?,
val primaNotaNumber: String?,
val textKeySupplement: 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 currencyType: String?,
val bookingKey: String,
val referenceForTheAccountOwner: String,
val referenceOfTheAccountServicingInstitution: String?,
val supplementaryDetails: 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
val transactionReferenceNumber: String,
val relatedReferenceNumber: String?
) {
// 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)
: 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)
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)
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
@ -132,12 +67,12 @@ open class AccountTransaction(
if (account != other.account) 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 (otherPartyName != other.otherPartyName) return false
if (otherPartyBankId != other.otherPartyBankId) return false
if (otherPartyBankCode != other.otherPartyBankCode) 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
return true
@ -146,19 +81,19 @@ open class AccountTransaction(
override fun hashCode(): Int {
var result = account.hashCode()
result = 31 * result + amount.hashCode()
result = 31 * result + reference.hashCode()
result = 31 * result + unparsedReference.hashCode()
result = 31 * result + bookingDate.hashCode()
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 + (postingText?.hashCode() ?: 0)
result = 31 * result + (bookingText?.hashCode() ?: 0)
result = 31 * result + valueDate.hashCode()
return result
}
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 fetchBalanceAndTransactions: Boolean = true,
open val preferredTanMethods: List<TanMethodType>? = null,
open val tanMethodsNotSupportedByApplication: List<TanMethodType>? = null,
open val preferredTanMedium: String? = null
) {

View File

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

View File

@ -1,6 +1,5 @@
package net.codinux.banking.fints.model
import kotlinx.datetime.Instant
import net.codinux.banking.fints.tan.TanImage
@ -12,9 +11,8 @@ open class ImageTanChallenge(
tanMethod: TanMethod,
tanMediaIdentifier: String?,
bank: BankData,
account: AccountData? = null,
tanExpirationTime: Instant? = null
) : TanChallenge(forAction, messageToShowToUser, challenge, tanMethod, tanMediaIdentifier, bank, account, tanExpirationTime) {
account: AccountData? = null
) : TanChallenge(forAction, messageToShowToUser, challenge, tanMethod, tanMediaIdentifier, bank, account) {
override fun toString(): String {
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).
*/
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 {
@ -38,8 +35,6 @@ 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)
@ -60,7 +55,7 @@ open class JobContext(
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,
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 = Instant.nowExt()
open val time: Instant = Clock.System.now()
) {
val messageIncludingMessageTrace: String

View File

@ -1,11 +1,8 @@
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(
@ -15,14 +12,7 @@ open class TanChallenge(
val tanMethod: TanMethod,
val tanMediaIdentifier: String?,
val bank: BankData,
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()
val account: AccountData? = null
) {
var enterTanResult: EnterTanResult? = null
@ -31,8 +21,6 @@ open class TanChallenge(
open val isEnteringTanDone: Boolean
get() = enterTanResult != null
private val tanExpiredCallbacks = mutableListOf<() -> Unit>()
private val userApprovedDecoupledTanCallbacks = mutableListOf<() -> Unit>()
@ -43,59 +31,23 @@ open class TanChallenge(
internal fun userApprovedDecoupledTan(responseAfterApprovingDecoupledTan: BankResponse) {
this.enterTanResult = EnterTanResult(null, true, responseAfterApprovingDecoupledTan)
userApprovedDecoupledTanCallbacks.toTypedArray().forEach { // copy to avoid ConcurrentModificationException
try {
it.invoke()
} catch (e: Throwable) {
Log.error(e) { "Could not call userApprovedDecoupledTanCallback" }
}
}
clearUserApprovedDecoupledTanCallbacks()
userApprovedDecoupledTanCallbacks.forEach { it.invoke() }
userApprovedDecoupledTanCallbacks.clear()
}
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)
@ -104,10 +56,6 @@ 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, context: JobContext) {
open fun updateCustomerData(bank: BankData, response: BankResponse) {
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.serverTransactionsRetentionDays = transactionsParameters.serverTransactionsRetentionDays
newAccount.countDaysForWhichTransactionsAreKept = transactionsParameters.countDaysForWhichTransactionsAreKept
}
bank.addAccount(newAccount)
@ -146,7 +146,6 @@ 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,7 +1,9 @@
package net.codinux.banking.fints.response
import kotlinx.datetime.*
import net.codinux.banking.fints.extensions.EuropeBerlin
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.LocalTime
import kotlinx.datetime.atTime
import net.codinux.log.logger
import net.codinux.banking.fints.log.IMessageLogAppender
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 > 4) parseStringToNullIfEmpty(dataElementGroups[4]) else null,
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,
segment
)
@ -746,11 +748,11 @@ open class ResponseParser(
val transactionsParameterIndex = if (jobParameters.segmentVersion >= 6) 4 else 3
val dataElements = getDataElements(dataElementGroups[transactionsParameterIndex])
val serverTransactionsRetentionDays = parseInt(dataElements[0])
val countDaysForWhichTransactionsAreKept = parseInt(dataElements[0])
val settingCountEntriesAllowed = parseBoolean(dataElements[1])
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 dataElements = getDataElements(dataElementGroups[transactionsParameterIndex])
val serverTransactionsRetentionDays = parseInt(dataElements[0])
val countDaysForWhichTransactionsAreKept = parseInt(dataElements[0])
val settingCountEntriesAllowed = parseBoolean(dataElements[1])
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(
parameters: JobParameters,
open val serverTransactionsRetentionDays: Int,
open val countDaysForWhichTransactionsAreKept: 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.Instant
import kotlinx.datetime.LocalDateTime
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 challengeHHD_UC: String?,
/**
* 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 validityDateTimeForChallenge: LocalDateTime?,
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,50 +44,44 @@ 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),
// 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.isReversal,
transaction.information?.unparsedReference ?: "",
transaction.statementLine.bookingDate ?: statement.closingBalance.bookingDate,
transaction.statementLine.valueDate,
transaction.information?.otherPartyName,
transaction.information?.otherPartyBankId,
transaction.information?.otherPartyBankCode,
transaction.information?.otherPartyAccountId,
transaction.information?.postingText,
Money(mapAmount(statement.openingBalance), currency),
Money(mapAmount(statement.closingBalance), currency),
transaction.information?.bookingText,
transaction.statementLine.valueDate,
statement.statementNumber,
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,
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
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?.journalNumber,
transaction.information?.textKeyAddition,
transaction.information?.primaNotaNumber,
transaction.information?.textKeySupplement,
statement.orderReferenceNumber,
statement.referenceNumber,
transaction.statementLine.currencyType,
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?:")
const val OrderReferenceNumberCode = "20"
const val TransactionReferenceNumberCode = "20"
const val ReferenceNumberCode = "21"
const val RelatedReferenceNumberCode = "21"
const val AccountIdentificationCode = "25"
@ -46,7 +46,7 @@ open class Mt940Parser(
const val StatementLineCode = "61"
const val RemittanceInformationFieldCode = "86"
const val InformationToAccountOwnerCode = "86"
const val ClosingBalanceCode = "62"
@ -61,7 +61,7 @@ open class Mt940Parser(
val ReferenceTypeRegex = Regex("[A-Z]{4}\\+")
val RemittanceInformationSubFieldRegex = Regex("\\?\\d\\d")
val InformationToAccountOwnerSubFieldRegex = 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, OrderReferenceNumberCode),
getOptionalFieldValue(fieldsByCode, ReferenceNumberCode),
getFieldValue(fieldsByCode, TransactionReferenceNumberCode),
getOptionalFieldValue(fieldsByCode, RelatedReferenceNumberCode),
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 == RemittanceInformationFieldCode) parseNullableRemittanceInformationField(nextPair.second) else null
val information = if (nextPair?.first == InformationToAccountOwnerCode) parseNullableInformationToAccountOwner(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 postingKeyStart = amountEndIndex + 1
val postingKey = fieldValue.substring(postingKeyStart, postingKeyStart + 3) // TODO: parse codes, p. 178
val bookingKeyStart = amountEndIndex + 1
val bookingKey = fieldValue.substring(bookingKeyStart, bookingKeyStart + 3) // TODO: parse codes, p. 178
val customerAndBankReference = fieldValue.substring(postingKeyStart + 3).split("//")
val customerReference = customerAndBankReference[0].takeIf { it != "NONREF" }
val customerAndBankReference = fieldValue.substring(bookingKeyStart + 3).split("//")
val customerReference = customerAndBankReference[0]
/**
* 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 null
var furtherInformation: String? = null
var bankReference = if (customerAndBankReference.size > 1) customerAndBankReference[1] else customerReference // TODO: or use null?
var supplementaryDetails: String? = null
if (bankReference != null && bankReference.contains('\n')) {
val bankReferenceAndFurtherInformation = bankReference.split("\n")
bankReference = bankReferenceAndFurtherInformation[0].trim()
val bankReferenceAndSupplementaryDetails = bankReference.split("\n")
if (bankReferenceAndSupplementaryDetails.size > 1) {
bankReference = bankReferenceAndSupplementaryDetails[0].trim()
// 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,
customerReference, bankReference, furtherInformation)
return StatementLine(!!!isDebit, isCancellation, valueDate, bookingDate, null, amount, bookingKey,
customerReference, bankReference, supplementaryDetails)
}
protected open fun parseNullableRemittanceInformationField(remittanceInformationFieldString: String): RemittanceInformationField? {
protected open fun parseNullableInformationToAccountOwner(informationToAccountOwnerString: String): InformationToAccountOwner? {
try {
val information = parseRemittanceInformationField(remittanceInformationFieldString)
val information = parseInformationToAccountOwner(informationToAccountOwnerString)
mapReference(information)
return information
} 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
}
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
// 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 otherPartyName = StringBuilder()
var otherPartyBankId: String? = null
var otherPartyBankCode: String? = null
var otherPartyAccountId: String? = null
var bookingText: String? = null
var primaNotaNumber: String? = null
var textKeySupplement: String? = null
val subFieldMatches = RemittanceInformationSubFieldRegex.findAll(remittanceInformationFieldString).toList()
val subFieldMatches = InformationToAccountOwnerSubFieldRegex.findAll(informationToAccountOwnerString).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 remittanceInformationFieldString.length
val fieldValue = remittanceInformationFieldString.substring(matchResult.range.last + 1, endIndex)
val endIndex = if (index + 1 < subFieldMatches.size) subFieldMatches[index + 1].range.start else informationToAccountOwnerString.length
val fieldValue = informationToAccountOwnerString.substring(matchResult.range.last + 1, endIndex)
when (fieldCode) {
0 -> bookingText = fieldValue
10 -> primaNotaNumber = fieldValue
in 20..29 -> referenceParts.add(fieldValue)
30 -> otherPartyBankId = fieldValue
30 -> otherPartyBankCode = 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 RemittanceInformationField(
reference, otherPartyNameString, otherPartyBankId, otherPartyAccountId,
return InformationToAccountOwner(
reference, otherPartyNameString, otherPartyBankCode, 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: RemittanceInformationField) {
protected open fun mapReference(information: InformationToAccountOwner) {
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: RemittanceInformationField, referenceType: String, typeValue: String) {
protected open fun setReferenceLineValue(information: InformationToAccountOwner, 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 orderReferenceNumber: String,
val transactionReferenceNumber: String,
/**
* Bezugsreferenz oder NONREF.
@ -20,7 +20,7 @@ open class AccountStatement(
*
* Max length = 16
*/
val referenceNumber: String?,
val relatedReferenceNumber: String?,
/**
* xxxxxxxxxxx/Konto-Nr. oder yyyyyyyy/Konto-Nr.
@ -50,7 +50,7 @@ open class AccountStatement(
*
* Max length = 5
*/
val sheetNumber: Int?,
val sequenceNumber: Int?,
val openingBalance: Balance,
@ -72,7 +72,7 @@ open class AccountStatement(
*
* Max length = 65
*/
val remittanceInformationField: String? = null
val multipurposeField: String? = null
) {

View File

@ -20,7 +20,7 @@ open class Balance(
val isCredit: Boolean,
/**
* JJMMTT = Buchungsdatum des Saldos oder '000000' beim ersten Auszug
* JJMMTT = Buchungsdatum des Saldos oder '0' beim ersten Auszug
*
* 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?,
/**
* in Kontowährung
* Codes see p. 177 bottom - 179
*
* After constant N
*
* Max length = 15
*/
val amount: Amount,
/**
* Codes see p. 177 bottom - 179
*
* After constant N
* in Kontowährung
*
* Length = 3
*/
val postingKey: String,
val bookingKey: 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 referenceForTheAccountOwner: String,
/**
* Bankreferenz
*/
val bankReference: String?,
val referenceOfTheAccountServicingInstitution: String?,
/**
* 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
val supplementaryDetails: String? = null
) {

View File

@ -4,7 +4,7 @@ package net.codinux.banking.fints.transactions.mt940.model
open class Transaction(
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)
/**
* 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? {
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 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 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 ->
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,72 +10,70 @@ 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 reference: String?, // alternative names: purpose, reason
val unparsedReference: String, // alternative names: purpose, reason
val bookingDate: LocalDate,
val valueDate: LocalDate,
val otherPartyName: String?,
val otherPartyBankId: String?,
val otherPartyBankCode: String?,
val otherPartyAccountId: String?,
val postingText: String?,
val bookingText: String?,
val valueDate: LocalDate,
val statementNumber: Int,
val sequenceNumber: Int?,
val openingBalance: Money?,
val closingBalance: Money?,
val statementNumber: Int,
val sheetNumber: Int?,
val customerReference: String?,
val bankReference: String?,
val furtherInformation: String?,
val endToEndReference: String?,
val customerReference: 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 journalNumber: String?,
val textKeyAddition: String?,
val currencyType: String?,
val bookingKey: String,
val referenceForTheAccountOwner: String,
val referenceOfTheAccountServicingInstitution: String?,
val supplementaryDetails: String?,
val orderReferenceNumber: String?,
val referenceNumber: String?,
val isReversal: Boolean
val transactionReferenceNumber: String,
val relatedReferenceNumber: String?
) {
// 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?)
: 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)
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)
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 (reference != other.reference) return false
if (unparsedReference != other.unparsedReference) return false
if (bookingDate != other.bookingDate) 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 (postingText != other.postingText) return false
if (bookingText != other.bookingText) return false
if (valueDate != other.valueDate) return false
return true
@ -83,19 +81,19 @@ open class AccountTransaction(
override fun hashCode(): Int {
var result = amount.hashCode()
result = 31 * result + reference.hashCode()
result = 31 * result + unparsedReference.hashCode()
result = 31 * result + bookingDate.hashCode()
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 + (postingText?.hashCode() ?: 0)
result = 31 * result + (bookingText?.hashCode() ?: 0)
result = 31 * result + valueDate.hashCode()
return result
}
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 accountLimit: String? = null,
open val serverTransactionsRetentionDays: Int? = null,
open val countDaysForWhichTransactionsAreKept: 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,7 +12,6 @@ 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,12 +21,10 @@ 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,
open val defaultBankValues: BankData? = null
) : FinTsClientParameter(bankCode, loginName, password, preferredTanMethods, tanMethodsNotSupportedByApplication, preferredTanMedium, abortIfTanIsRequired, finTsModel) {
finTsModel: BankData? = null
) : FinTsClientParameter(bankCode, loginName, password, preferredTanMethods, preferredTanMedium, abortIfTanIsRequired, finTsModel) {
open val retrieveOnlyAccountInfo: Boolean
get() = retrieveBalance == false && retrieveTransactions == RetrieveTransactions.No

View File

@ -34,11 +34,10 @@ 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, 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.NoChallengeResponse, segment.challenge)
assertNull(segment.challengeHHD_UC)
assertEquals(null, segment.tanExpirationTime)
assertEquals(null, segment.validityDateTimeForChallenge)
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.tanExpirationTime)
assertEquals(null, segment.validityDateTimeForChallenge)
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 serverTransactionsRetentionDays = 90
val countDaysForWhichTransactionsAreKept = 90
// 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
assertSuccessfullyParsedSegment(result, InstituteSegmentId.AccountTransactionsMt940Parameters, 21, 4, 4)
result.getFirstSegmentById<RetrieveAccountTransactionsParameters>(InstituteSegmentId.AccountTransactionsMt940Parameters)?.let { segment ->
assertEquals(serverTransactionsRetentionDays, segment.serverTransactionsRetentionDays)
assertEquals(countDaysForWhichTransactionsAreKept, segment.countDaysForWhichTransactionsAreKept)
assertFalse(segment.settingCountEntriesAllowed)
assertFalse(segment.settingAllAccountAllowed)
}
@ -1209,16 +1209,16 @@ class ResponseParserTest : FinTsTestBase() {
fun parseAccountTransactionsMt940Parameters_Version6() {
// given
val serverTransactionsRetentionDays = 90
val countDaysForWhichTransactionsAreKept = 90
// 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
assertSuccessfullyParsedSegment(result, InstituteSegmentId.AccountTransactionsMt940Parameters, 23, 6, 4)
result.getFirstSegmentById<RetrieveAccountTransactionsParameters>(InstituteSegmentId.AccountTransactionsMt940Parameters)?.let { segment ->
assertEquals(serverTransactionsRetentionDays, segment.serverTransactionsRetentionDays)
assertEquals(countDaysForWhichTransactionsAreKept, segment.countDaysForWhichTransactionsAreKept)
assertFalse(segment.settingCountEntriesAllowed)
assertFalse(segment.settingAllAccountAllowed)
}
@ -1290,16 +1290,16 @@ class ResponseParserTest : FinTsTestBase() {
fun parseCreditCardAccountTransactionsParameters() {
// given
val serverTransactionsRetentionDays = 9999
val countDaysForWhichTransactionsAreKept = 9999
// 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
assertSuccessfullyParsedSegment(result, InstituteSegmentId.CreditCardTransactionsParameters, 15, 2, 4)
result.getFirstSegmentById<RetrieveAccountTransactionsParameters>(InstituteSegmentId.CreditCardTransactionsParameters)?.let { segment ->
assertEquals(serverTransactionsRetentionDays, segment.serverTransactionsRetentionDays)
assertEquals(countDaysForWhichTransactionsAreKept, segment.countDaysForWhichTransactionsAreKept)
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.RemittanceInformationField
import net.codinux.banking.fints.transactions.mt940.model.InformationToAccountOwner
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 AccountStatement1Transaction1OtherPartyBankId = "AAAADE12"
val AccountStatement1Transaction1OtherPartyBankCode = "AAAADE12"
val AccountStatement1Transaction1OtherPartyAccountId = "DE99876543210987654321"
val AccountStatement1Transaction2Amount = Amount("432,10")
val AccountStatement1Transaction2OtherPartyName = "Receiver2"
val AccountStatement1Transaction2OtherPartyBankId = "BBBBDE56"
val AccountStatement1Transaction2OtherPartyBankCode = "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,
AccountStatement1Transaction1OtherPartyBankId, AccountStatement1Transaction1OtherPartyAccountId)
AccountStatement1Transaction1OtherPartyBankCode, AccountStatement1Transaction1OtherPartyAccountId)
}
@Test
@ -90,7 +90,7 @@ class Mt940ParserTest : FinTsTestBase() {
assertEquals(BankCode, statement.bankCodeBicOrIban)
assertEquals(CustomerId, statement.accountIdentifier)
assertEquals(0, statement.statementNumber)
assertNull(statement.sheetNumber)
assertNull(statement.sequenceNumber)
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,
AccountStatement1Transaction1OtherPartyBankId, AccountStatement1Transaction1OtherPartyAccountId)
AccountStatement1Transaction1OtherPartyBankCode, AccountStatement1Transaction1OtherPartyAccountId)
val secondTransaction = statement.transactions[1]
assertTurnover(secondTransaction.statementLine, AccountStatement1BookingDate, AccountStatement1Transaction2Amount, false)
assertTransactionDetails(secondTransaction.information, AccountStatement1Transaction2OtherPartyName,
AccountStatement1Transaction2OtherPartyBankId, AccountStatement1Transaction2OtherPartyAccountId)
AccountStatement1Transaction2OtherPartyBankCode, AccountStatement1Transaction2OtherPartyAccountId)
}
@Test
@ -306,8 +306,8 @@ class Mt940ParserTest : FinTsTestBase() {
assertSize(1, result.first().transactions)
result.first().transactions[0].information?.apply {
assertEquals("BASISLASTSCHRIFT", postingText)
assertEquals("TUBDDEDD", otherPartyBankId)
assertEquals("BASISLASTSCHRIFT", bookingText)
assertEquals("TUBDDEDD", otherPartyBankCode)
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: RemittanceInformationField?, otherPartyName: String,
otherPartyBankId: String, otherPartyAccountId: String) {
private fun assertTransactionDetails(details: InformationToAccountOwner?, otherPartyName: String,
otherPartyBankCode: String, otherPartyAccountId: String) {
assertNotNull(details)
assertEquals(otherPartyName, details.otherPartyName)
assertEquals(otherPartyBankId, details.otherPartyBankId)
assertEquals(otherPartyBankCode, details.otherPartyBankCode)
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$AccountStatement1Transaction1OtherPartyBankId?31$AccountStatement1Transaction1OtherPartyAccountId
EUR ${AccountStatement1Transaction1Amount}/20?2219-10-02/...?30$AccountStatement1Transaction1OtherPartyBankCode?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$AccountStatement1Transaction1OtherPartyBankId?31$AccountStatement1Transaction1OtherPartyAccountId
EUR ${AccountStatement1Transaction1Amount}/20?2219-10-02/...?30$AccountStatement1Transaction1OtherPartyBankCode?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$AccountStatement1Transaction2OtherPartyBankId?31$AccountStatement1Transaction2OtherPartyAccountId
EUR ${AccountStatement1Transaction2Amount}/20?2219-10-02/...?30$AccountStatement1Transaction2OtherPartyBankCode?31$AccountStatement1Transaction2OtherPartyAccountId
?32$AccountStatement1Transaction2OtherPartyName
:62F:C${convertMt940Date(AccountStatement1BookingDate)}EUR${AccountStatement1With2TransactionsClosingBalanceAmount}
-