diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/model/AccountTransaction.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/model/AccountTransaction.kt index 2e9b742f..96bfadc9 100644 --- a/fints4javaLib/src/main/kotlin/net/dankito/fints/model/AccountTransaction.kt +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/model/AccountTransaction.kt @@ -1,14 +1,31 @@ package net.dankito.fints.model +import java.math.BigDecimal +import java.text.DateFormat +import java.util.* + open class AccountTransaction( - var referenceNumber: String = "", - var bezugsReferenceNumber: String? = null, - var accountIdentification: String = "", - var statementNumber: String = "", - var openingBalance: String = "", - var statement: String = "", - var accountOwner: String = "", - var closingBalance: String = "" + val amount: BigDecimal, + val currency: String, + val usage: String, + val bookingDate: Date, + val otherPartyName: String?, + val otherPartyBankCode: String?, + val otherPartyAccountId: String?, + val bookingText: String?, + val valueDate: Date?, + val openingBalance: BigDecimal?, + val closingBalance: BigDecimal? + // TODO: may also add other values from parsed usage lines ) { + + // for object deserializers + private constructor() : this(0.toBigDecimal(),"", "", Date(), null, null, null, null, null, null, null) + + + override fun toString(): String { + return "${DateFormat.getDateInstance(DateFormat.MEDIUM).format(bookingDate)} $amount $otherPartyName: $usage" + } + } \ No newline at end of file diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/response/InstituteSegmentId.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/InstituteSegmentId.kt index a378c4c5..111e1ef0 100644 --- a/fints4javaLib/src/main/kotlin/net/dankito/fints/response/InstituteSegmentId.kt +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/InstituteSegmentId.kt @@ -13,6 +13,8 @@ enum class InstituteSegmentId(override val id: String) : ISegmentId { UserParameters("HIUPA"), - AccountInfo("HIUPD") + AccountInfo("HIUPD"), + + AccountTransactionsMt940("HIKAZ") } \ No newline at end of file diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/response/ResponseParser.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/ResponseParser.kt index 2d5517ce..b5c98686 100644 --- a/fints4javaLib/src/main/kotlin/net/dankito/fints/response/ResponseParser.kt +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/ResponseParser.kt @@ -10,12 +10,24 @@ import net.dankito.fints.messages.datenelementgruppen.implementierte.Kreditinsti import net.dankito.fints.messages.datenelementgruppen.implementierte.signatur.Sicherheitsprofil import net.dankito.fints.messages.segmente.id.MessageSegmentId import net.dankito.fints.response.segments.* +import net.dankito.fints.transactions.IAccountTransactionsParser +import net.dankito.fints.transactions.Mt940AccountTransactionsParser import org.slf4j.LoggerFactory +import java.util.regex.Matcher +import java.util.regex.Pattern -open class ResponseParser { +open class ResponseParser @JvmOverloads constructor( + protected val mt940Parser: IAccountTransactionsParser = Mt940AccountTransactionsParser() +) { companion object { + val BinaryDataHeaderPatternString = "@\\d+@" + + val BinaryDataHeaderPattern = Pattern.compile(BinaryDataHeaderPatternString) + + val EncryptionDataSegmentHeaderPattern = Pattern.compile("${MessageSegmentId.EncryptionData.id}:\\d{1,3}:\\d{1,3}\\+") + private val log = LoggerFactory.getLogger(ResponseParser::class.java) } @@ -67,6 +79,8 @@ open class ResponseParser { InstituteSegmentId.UserParameters.id -> parseUserParameters(segment, dataElementGroups) InstituteSegmentId.AccountInfo.id -> parseAccountInfo(segment, dataElementGroups) + InstituteSegmentId.AccountTransactionsMt940.id -> parseMt940AccountTransactions(segment, dataElementGroups) + else -> UnparsedSegment(segment) } } @@ -151,6 +165,16 @@ open class ResponseParser { } + protected open fun parseMt940AccountTransactions(segment: String, dataElementGroups: List): ReceivedAccountTransactions { + val bookedTransactionsString = extractBinaryData(dataElementGroups[1]) + + // TODO: implement parsing MT942 + val unbookedTransactionsString = if (dataElementGroups.size > 2) extractBinaryData(dataElementGroups[2]) else null + + return ReceivedAccountTransactions(mt940Parser.parseTransactions(bookedTransactionsString), listOf(), segment) + } + + protected open fun parseBankDetails(dataElementsGroup: String): Kreditinstitutskennung { val detailsStrings = getDataElements(dataElementsGroup) @@ -216,14 +240,83 @@ open class ResponseParser { * Also binary data shouldn't be taken into account (TODO: really?). */ protected open fun splitIntoPartsAndUnmask(dataString: String, separator: String): List { - val separatorMask = Separators.MaskingCharacter + separator - val maskedSymbolsGuard = Separators.MaskingCharacter + "ยง" + val binaryDataRanges = mutableListOf() + val binaryDataMatcher = BinaryDataHeaderPattern.matcher(dataString) - val maskedDataString = dataString.replace(separatorMask, maskedSymbolsGuard) + while (binaryDataMatcher.find()) { + if (isEncryptionDataSegment(dataString, binaryDataMatcher) == false) { + val startIndex = binaryDataMatcher.end() + val length = binaryDataMatcher.group().replace("@", "").toInt() - val elements = maskedDataString.split(separator) + binaryDataRanges.add(IntRange(startIndex, startIndex + length - 1)) + } + } - return elements.map { it.replace(maskedSymbolsGuard, separator) } + val separatorIndices = allIndicesOf(dataString, separator) + .filter { isCharacterMasked(it, dataString) == false } + .filter { isInRange(it, binaryDataRanges) == false } + + var startIndex = 0 + val elements = separatorIndices.map { endIndex -> + val element = dataString.substring(startIndex, endIndex) + startIndex = endIndex + 1 + element + }.toMutableList() + + if (startIndex < dataString.length) { + elements.add(dataString.substring(startIndex)) + } + + return elements + } + + protected open fun isEncryptionDataSegment(dataString: String, binaryDataMatcher: Matcher): Boolean { + val binaryDataHeaderStartIndex = binaryDataMatcher.start() + + if (binaryDataHeaderStartIndex > 15) { + val encryptionDataSegmentMatcher = EncryptionDataSegmentHeaderPattern.matcher(dataString) + + if (encryptionDataSegmentMatcher.find(binaryDataHeaderStartIndex - 15)) { + return encryptionDataSegmentMatcher.start() < binaryDataHeaderStartIndex + } + } + + return false + } + + protected open fun isCharacterMasked(characterIndex: Int, wholeString: String): Boolean { + if (characterIndex > 0) { + val previousChar = wholeString[characterIndex - 1] + + return previousChar.toString() == Separators.MaskingCharacter + } + + return false + } + + protected open fun isInRange(index: Int, ranges: List): Boolean { + for (range in ranges) { + if (range.contains(index)) { + return true + } + } + + return false + } + + protected open fun allIndicesOf(string: String, toFind: String): List { + val indices = mutableListOf() + var index = -1 + + do { + index = string.indexOf(toFind, index + 1) + + if (index > -1) { + indices.add(index) + } + } while (index > -1) + + return indices } protected open fun parseInt(string: String): Int { @@ -253,4 +346,16 @@ open class ResponseParser { return false } + protected open fun extractBinaryData(binaryData: String): String { + if (binaryData.startsWith('@')) { + val headerEndIndex = binaryData.indexOf('@', 2) + + if (headerEndIndex > -1) { + return binaryData.substring(headerEndIndex + 1) + } + } + + return binaryData + } + } \ No newline at end of file diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/ReceivedAccountTransactions.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/ReceivedAccountTransactions.kt new file mode 100644 index 00000000..ea8c8dd6 --- /dev/null +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/ReceivedAccountTransactions.kt @@ -0,0 +1,12 @@ +package net.dankito.fints.response.segments + +import net.dankito.fints.model.AccountTransaction + + +open class ReceivedAccountTransactions( + val bookedTransactions: List, + val unbookedTransactions: List, // TODO + segmentString: String + +) + : ReceivedSegment(segmentString) \ No newline at end of file