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 13047de8..341d0ddc 100644 --- a/fints4javaLib/src/main/kotlin/net/dankito/fints/response/InstituteSegmentId.kt +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/InstituteSegmentId.kt @@ -17,6 +17,8 @@ enum class InstituteSegmentId(override val id: String) : ISegmentId { TanInfo("HITANS"), + Balance("HISAL"), + 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 4e794b9e..2562e78f 100644 --- a/fints4javaLib/src/main/kotlin/net/dankito/fints/response/ResponseParser.kt +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/ResponseParser.kt @@ -1,6 +1,8 @@ package net.dankito.fints.response import net.dankito.fints.messages.Separators +import net.dankito.fints.messages.datenelemente.abgeleiteteformate.Datum +import net.dankito.fints.messages.datenelemente.abgeleiteteformate.Uhrzeit import net.dankito.fints.messages.datenelemente.implementierte.Dialogsprache import net.dankito.fints.messages.datenelemente.implementierte.HbciVersion import net.dankito.fints.messages.datenelemente.implementierte.ICodeEnum @@ -15,6 +17,8 @@ import net.dankito.fints.response.segments.* import net.dankito.fints.transactions.IAccountTransactionsParser import net.dankito.fints.transactions.Mt940AccountTransactionsParser import org.slf4j.LoggerFactory +import java.math.BigDecimal +import java.util.* import java.util.regex.Matcher import java.util.regex.Pattern @@ -82,6 +86,7 @@ open class ResponseParser @JvmOverloads constructor( InstituteSegmentId.AccountInfo.id -> parseAccountInfo(segment, dataElementGroups) InstituteSegmentId.TanInfo.id -> parseTanInfo(segment, dataElementGroups) + InstituteSegmentId.Balance.id -> parseBalanceSegment(segment, dataElementGroups) InstituteSegmentId.AccountTransactionsMt940.id -> parseMt940AccountTransactions(segment, dataElementGroups) else -> UnparsedSegment(segment) @@ -244,6 +249,41 @@ open class ResponseParser @JvmOverloads constructor( } + protected open fun parseBalanceSegment(segment: String, dataElementGroups: List): BalanceSegment { + // dataElementGroups[1] is account details + + val balance = parseBalance(dataElementGroups[4]) + val balanceOfPreBookedTransactions = parseBalance(dataElementGroups[5]) + + return BalanceSegment( + balance.amount, + parseString(dataElementGroups[3]), + balance.date, + parseString(dataElementGroups[2]), + if (balanceOfPreBookedTransactions.amount.equals(BigDecimal.ZERO)) null else balanceOfPreBookedTransactions.amount, + segment + ) + } + + protected open fun parseBalance(dataElementGroup: String): Balance { + val dataElements = getDataElements(dataElementGroup) + + val isCredit = parseString(dataElements[0]) == "C" + + var dateIndex = 2 + var date: Date? = parseNullableDate(dataElements[dateIndex]) // in older versions dateElements[2] was the currency + if (date == null) { + date = parseDate(dataElements[++dateIndex]) + } + + return Balance( + parseAmount(dataElements[1], isCredit), + date, + if (dataElements.size > dateIndex + 1) parseTime(dataElements[dateIndex + 1]) else null + ) + } + + protected open fun parseMt940AccountTransactions(segment: String, dataElementGroups: List): ReceivedAccountTransactions { val bookedTransactionsString = extractBinaryData(dataElementGroups[1]) @@ -398,17 +438,6 @@ open class ResponseParser @JvmOverloads constructor( return indices } - protected open fun parseInt(string: String): Int { - return parseString(string).toInt() - } - - protected open fun parseNullableInt(mayInt: String): Int? { - try { - return parseInt(mayInt) - } catch (ignored: Exception) { } - - return null - } protected open fun parseStringToNullIfEmpty(string: String): String? { val parsedString = parseString(string) @@ -433,6 +462,47 @@ open class ResponseParser @JvmOverloads constructor( return false } + protected open fun parseInt(string: String): Int { + return parseString(string).toInt() + } + + protected open fun parseNullableInt(mayInt: String): Int? { + try { + return parseInt(mayInt) + } catch (ignored: Exception) { } + + return null + } + + @JvmOverloads + protected open fun parseAmount(amountString: String, isPositive: Boolean = true): BigDecimal { + val adjustedAmountString = amountString.replace(',', '.') // Hbci amount format uses comma instead dot as decimal separator + + val amount = adjustedAmountString.toBigDecimal() + + if (isPositive == false) { + return amount.negate() + } + + return amount + } + + protected open fun parseDate(dateString: String): Date { + return Datum.HbciDateFormat.parse(dateString) + } + + protected open fun parseNullableDate(dateString: String): Date? { + try { + return Datum.HbciDateFormat.parse(dateString) + } catch (ignored: Exception) { } + + return null + } + + protected open fun parseTime(timeString: String): Date { + return Uhrzeit.HbciTimeFormat.parse(timeString) + } + protected open fun extractBinaryData(binaryData: String): String { if (binaryData.startsWith('@')) { val headerEndIndex = binaryData.indexOf('@', 2) diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/Balance.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/Balance.kt new file mode 100644 index 00000000..dd6f89ce --- /dev/null +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/Balance.kt @@ -0,0 +1,17 @@ +package net.dankito.fints.response.segments + +import java.math.BigDecimal +import java.util.* + + +open class Balance( + val amount: BigDecimal, + val date: Date, + val time: Date? +) { + + override fun toString(): String { + return "$amount ($date)" + } + +} \ No newline at end of file diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/BalanceSegment.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/BalanceSegment.kt new file mode 100644 index 00000000..eb61c24c --- /dev/null +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/BalanceSegment.kt @@ -0,0 +1,15 @@ +package net.dankito.fints.response.segments + +import java.math.BigDecimal +import java.util.* + + +open class BalanceSegment( + val balance: BigDecimal, + val currency: String, + val date: Date, + val accountProductName: String, + val balanceOfPreBookedTransactions: BigDecimal?, + segmentString: String +) + : ReceivedSegment(segmentString) \ No newline at end of file diff --git a/fints4javaLib/src/test/kotlin/net/dankito/fints/FinTsTestBase.kt b/fints4javaLib/src/test/kotlin/net/dankito/fints/FinTsTestBase.kt index 3e06db80..e756625d 100644 --- a/fints4javaLib/src/test/kotlin/net/dankito/fints/FinTsTestBase.kt +++ b/fints4javaLib/src/test/kotlin/net/dankito/fints/FinTsTestBase.kt @@ -1,9 +1,11 @@ package net.dankito.fints +import net.dankito.fints.messages.datenelemente.abgeleiteteformate.Datum import net.dankito.fints.messages.datenelemente.abgeleiteteformate.Laenderkennzeichen import net.dankito.fints.messages.datenelemente.implementierte.Dialogsprache import net.dankito.fints.messages.datenelemente.implementierte.signatur.Sicherheitsfunktion import net.dankito.fints.model.* +import java.math.BigDecimal import java.util.* @@ -42,6 +44,14 @@ abstract class FinTsTestBase { return UUID.randomUUID().toString().replace("-", "") } + protected open fun convertAmount(amount: BigDecimal): String { + return amount.toString().replace('.', ',') + } + + protected open fun convertDate(date: Date): String { + return Datum.HbciDateFormat.format(date) + } + protected open fun normalizeBinaryData(message: String): String { return message.replace(0.toChar(), ' ') } diff --git a/fints4javaLib/src/test/kotlin/net/dankito/fints/response/ResponseParserTest.kt b/fints4javaLib/src/test/kotlin/net/dankito/fints/response/ResponseParserTest.kt index 5069fe70..0b62d918 100644 --- a/fints4javaLib/src/test/kotlin/net/dankito/fints/response/ResponseParserTest.kt +++ b/fints4javaLib/src/test/kotlin/net/dankito/fints/response/ResponseParserTest.kt @@ -1,5 +1,6 @@ package net.dankito.fints.response +import net.dankito.fints.FinTsTestBase import net.dankito.fints.messages.datenelemente.implementierte.Dialogsprache import net.dankito.fints.messages.datenelemente.implementierte.HbciVersion import net.dankito.fints.messages.datenelemente.implementierte.signatur.Sicherheitsverfahren @@ -8,12 +9,14 @@ import net.dankito.fints.messages.datenelementgruppen.implementierte.signatur.Si import net.dankito.fints.messages.segmente.id.ISegmentId import net.dankito.fints.messages.segmente.id.MessageSegmentId import net.dankito.fints.response.segments.* +import net.dankito.utils.datetime.asUtilDate import org.assertj.core.api.Assertions.assertThat import org.junit.Assert import org.junit.Test +import java.time.LocalDate -class ResponseParserTest { +class ResponseParserTest : FinTsTestBase() { private val underTest = ResponseParser() @@ -211,6 +214,34 @@ class ResponseParserTest { } + @Test + fun parseBalance() { + + // given + val balance = 1234.56.toBigDecimal() + val date = LocalDate.of(1988, 3, 27).asUtilDate() + val bankCode = "12345678" + val accountId = "0987654321" + val accountProductName = "Sichteinlagen" + + // when + val result = underTest.parse("HISAL:8:5:3+$accountId::280:$bankCode+$accountProductName+EUR+" + + "C:${convertAmount(balance)}:EUR:${convertDate(date)}+C:0,:EUR:20191006++${convertAmount(balance)}:EUR") + + // then + assertSuccessfullyParsedSegment(result, InstituteSegmentId.Balance, 8, 5, 3) + + result.getFirstSegmentById(InstituteSegmentId.Balance)?.let { segment -> + assertThat(segment.balance).isEqualTo(balance) + assertThat(segment.currency).isEqualTo("EUR") + assertThat(segment.date).isEqualTo(date) + assertThat(segment.accountProductName).isEqualTo(accountProductName) + assertThat(segment.balanceOfPreBookedTransactions).isNull() + } + ?: run { Assert.fail("No segment of type Balance found in ${result.receivedSegments}") } + } + + @Test fun parseTanInfo() { diff --git a/fints4javaLib/src/test/kotlin/net/dankito/fints/transactions/Mt940ParserTest.kt b/fints4javaLib/src/test/kotlin/net/dankito/fints/transactions/Mt940ParserTest.kt index 09f06cdb..c1294d87 100644 --- a/fints4javaLib/src/test/kotlin/net/dankito/fints/transactions/Mt940ParserTest.kt +++ b/fints4javaLib/src/test/kotlin/net/dankito/fints/transactions/Mt940ParserTest.kt @@ -1,5 +1,6 @@ package net.dankito.fints.transactions +import net.dankito.fints.FinTsTestBase import net.dankito.fints.transactions.mt940.Mt940Parser import net.dankito.fints.transactions.mt940.model.Balance import net.dankito.fints.transactions.mt940.model.TransactionDetails @@ -11,17 +12,13 @@ import java.text.SimpleDateFormat import java.util.* -class Mt940ParserTest { +class Mt940ParserTest : FinTsTestBase() { companion object { const val TestFilesFolderName = "test_files/" const val TransactionsMt940FileRelativePath = TestFilesFolderName + "TransactionsMt940.txt" - const val BankCode = "12345678" - - const val CustomerId = "0987654321" - const val Currency = "EUR" val AccountStatement1PreviousStatementBookingDate = Date(88, 2, 26) @@ -150,12 +147,12 @@ class Mt940ParserTest { :20:STARTUMSE :25:$BankCode/$CustomerId :28C:00000/001 - :60F:C${convertDate(AccountStatement1PreviousStatementBookingDate)}EUR${convertAmount(AccountStatement1OpeningBalanceAmount)} - :61:${convertDate(AccountStatement1BookingDate)}${convertToShortBookingDate(AccountStatement1BookingDate)}CR${convertAmount(AccountStatement1Transaction1Amount)}N062NONREF + :60F:C${convertMt940Date(AccountStatement1PreviousStatementBookingDate)}EUR${convertAmount(AccountStatement1OpeningBalanceAmount)} + :61:${convertMt940Date(AccountStatement1BookingDate)}${convertToShortBookingDate(AccountStatement1BookingDate)}CR${convertAmount(AccountStatement1Transaction1Amount)}N062NONREF :86:166?00GUTSCHR. UEBERWEISUNG?109249?20EREF+674?21SVWZ+1908301/ EUR ${convertAmount(AccountStatement1Transaction1Amount)}/20?2219-10-02/...?30$AccountStatement1Transaction1OtherPartyBankCode?31$AccountStatement1Transaction1OtherPartyAccountId ?32$AccountStatement1Transaction1OtherPartyName - :62F:C${convertDate(AccountStatement1BookingDate)}EUR${convertAmount(AccountStatement1ClosingBalanceAmount)} + :62F:C${convertMt940Date(AccountStatement1BookingDate)}EUR${convertAmount(AccountStatement1ClosingBalanceAmount)} - """.trimIndent() @@ -163,25 +160,21 @@ class Mt940ParserTest { :20:STARTUMSE :25:$BankCode/$CustomerId :28C:00000/001 - :60F:C${convertDate(AccountStatement1PreviousStatementBookingDate)}EUR${convertAmount(AccountStatement1OpeningBalanceAmount)} - :61:${convertDate(AccountStatement1BookingDate)}${convertToShortBookingDate(AccountStatement1BookingDate)}CR${convertAmount(AccountStatement1Transaction1Amount)}N062NONREF + :60F:C${convertMt940Date(AccountStatement1PreviousStatementBookingDate)}EUR${convertAmount(AccountStatement1OpeningBalanceAmount)} + :61:${convertMt940Date(AccountStatement1BookingDate)}${convertToShortBookingDate(AccountStatement1BookingDate)}CR${convertAmount(AccountStatement1Transaction1Amount)}N062NONREF :86:166?00GUTSCHR. UEBERWEISUNG?109249?20EREF+674?21SVWZ+1908301/ EUR ${convertAmount(AccountStatement1Transaction1Amount)}/20?2219-10-02/...?30$AccountStatement1Transaction1OtherPartyBankCode?31$AccountStatement1Transaction1OtherPartyAccountId ?32$AccountStatement1Transaction1OtherPartyName - :61:${convertDate(AccountStatement1BookingDate)}${convertToShortBookingDate(AccountStatement1BookingDate)}DR${convertAmount(AccountStatement1Transaction2Amount)}N062NONREF + :61:${convertMt940Date(AccountStatement1BookingDate)}${convertToShortBookingDate(AccountStatement1BookingDate)}DR${convertAmount(AccountStatement1Transaction2Amount)}N062NONREF :86:166?00ONLINE-UEBERWEISUNG?109249?20EREF+674?21SVWZ+1908301/ EUR ${convertAmount(AccountStatement1Transaction2Amount)}/20?2219-10-02/...?30$AccountStatement1Transaction2OtherPartyBankCode?31$AccountStatement1Transaction2OtherPartyAccountId ?32$AccountStatement1Transaction2OtherPartyName - :62F:C${convertDate(AccountStatement1BookingDate)}EUR${convertAmount(AccountStatement1With2TransactionsClosingBalanceAmount)} + :62F:C${convertMt940Date(AccountStatement1BookingDate)}EUR${convertAmount(AccountStatement1With2TransactionsClosingBalanceAmount)} - """.trimIndent() - private fun convertAmount(amount: BigDecimal): String { - return amount.toString().replace('.', ',') - } - - private fun convertDate(date: Date): String { + private fun convertMt940Date(date: Date): String { return Mt940Parser.DateFormat.format(date) }