Implemented parsing balance

This commit is contained in:
dankl 2019-10-13 00:49:49 +02:00 committed by dankito
parent 8cf57d1c35
commit 89d44beab9
7 changed files with 167 additions and 29 deletions

View File

@ -17,6 +17,8 @@ enum class InstituteSegmentId(override val id: String) : ISegmentId {
TanInfo("HITANS"), TanInfo("HITANS"),
Balance("HISAL"),
AccountTransactionsMt940("HIKAZ") AccountTransactionsMt940("HIKAZ")
} }

View File

@ -1,6 +1,8 @@
package net.dankito.fints.response package net.dankito.fints.response
import net.dankito.fints.messages.Separators 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.Dialogsprache
import net.dankito.fints.messages.datenelemente.implementierte.HbciVersion import net.dankito.fints.messages.datenelemente.implementierte.HbciVersion
import net.dankito.fints.messages.datenelemente.implementierte.ICodeEnum 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.IAccountTransactionsParser
import net.dankito.fints.transactions.Mt940AccountTransactionsParser import net.dankito.fints.transactions.Mt940AccountTransactionsParser
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.math.BigDecimal
import java.util.*
import java.util.regex.Matcher import java.util.regex.Matcher
import java.util.regex.Pattern import java.util.regex.Pattern
@ -82,6 +86,7 @@ open class ResponseParser @JvmOverloads constructor(
InstituteSegmentId.AccountInfo.id -> parseAccountInfo(segment, dataElementGroups) InstituteSegmentId.AccountInfo.id -> parseAccountInfo(segment, dataElementGroups)
InstituteSegmentId.TanInfo.id -> parseTanInfo(segment, dataElementGroups) InstituteSegmentId.TanInfo.id -> parseTanInfo(segment, dataElementGroups)
InstituteSegmentId.Balance.id -> parseBalanceSegment(segment, dataElementGroups)
InstituteSegmentId.AccountTransactionsMt940.id -> parseMt940AccountTransactions(segment, dataElementGroups) InstituteSegmentId.AccountTransactionsMt940.id -> parseMt940AccountTransactions(segment, dataElementGroups)
else -> UnparsedSegment(segment) else -> UnparsedSegment(segment)
@ -244,6 +249,41 @@ open class ResponseParser @JvmOverloads constructor(
} }
protected open fun parseBalanceSegment(segment: String, dataElementGroups: List<String>): 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<String>): ReceivedAccountTransactions { protected open fun parseMt940AccountTransactions(segment: String, dataElementGroups: List<String>): ReceivedAccountTransactions {
val bookedTransactionsString = extractBinaryData(dataElementGroups[1]) val bookedTransactionsString = extractBinaryData(dataElementGroups[1])
@ -398,17 +438,6 @@ open class ResponseParser @JvmOverloads constructor(
return indices 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? { protected open fun parseStringToNullIfEmpty(string: String): String? {
val parsedString = parseString(string) val parsedString = parseString(string)
@ -433,6 +462,47 @@ open class ResponseParser @JvmOverloads constructor(
return false 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 { protected open fun extractBinaryData(binaryData: String): String {
if (binaryData.startsWith('@')) { if (binaryData.startsWith('@')) {
val headerEndIndex = binaryData.indexOf('@', 2) val headerEndIndex = binaryData.indexOf('@', 2)

View File

@ -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)"
}
}

View File

@ -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)

View File

@ -1,9 +1,11 @@
package net.dankito.fints 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.abgeleiteteformate.Laenderkennzeichen
import net.dankito.fints.messages.datenelemente.implementierte.Dialogsprache import net.dankito.fints.messages.datenelemente.implementierte.Dialogsprache
import net.dankito.fints.messages.datenelemente.implementierte.signatur.Sicherheitsfunktion import net.dankito.fints.messages.datenelemente.implementierte.signatur.Sicherheitsfunktion
import net.dankito.fints.model.* import net.dankito.fints.model.*
import java.math.BigDecimal
import java.util.* import java.util.*
@ -42,6 +44,14 @@ abstract class FinTsTestBase {
return UUID.randomUUID().toString().replace("-", "") 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 { protected open fun normalizeBinaryData(message: String): String {
return message.replace(0.toChar(), ' ') return message.replace(0.toChar(), ' ')
} }

View File

@ -1,5 +1,6 @@
package net.dankito.fints.response 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.Dialogsprache
import net.dankito.fints.messages.datenelemente.implementierte.HbciVersion import net.dankito.fints.messages.datenelemente.implementierte.HbciVersion
import net.dankito.fints.messages.datenelemente.implementierte.signatur.Sicherheitsverfahren 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.ISegmentId
import net.dankito.fints.messages.segmente.id.MessageSegmentId import net.dankito.fints.messages.segmente.id.MessageSegmentId
import net.dankito.fints.response.segments.* import net.dankito.fints.response.segments.*
import net.dankito.utils.datetime.asUtilDate
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.Assert import org.junit.Assert
import org.junit.Test import org.junit.Test
import java.time.LocalDate
class ResponseParserTest { class ResponseParserTest : FinTsTestBase() {
private val underTest = ResponseParser() 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<BalanceSegment>(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 @Test
fun parseTanInfo() { fun parseTanInfo() {

View File

@ -1,5 +1,6 @@
package net.dankito.fints.transactions package net.dankito.fints.transactions
import net.dankito.fints.FinTsTestBase
import net.dankito.fints.transactions.mt940.Mt940Parser import net.dankito.fints.transactions.mt940.Mt940Parser
import net.dankito.fints.transactions.mt940.model.Balance import net.dankito.fints.transactions.mt940.model.Balance
import net.dankito.fints.transactions.mt940.model.TransactionDetails import net.dankito.fints.transactions.mt940.model.TransactionDetails
@ -11,17 +12,13 @@ import java.text.SimpleDateFormat
import java.util.* import java.util.*
class Mt940ParserTest { class Mt940ParserTest : FinTsTestBase() {
companion object { companion object {
const val TestFilesFolderName = "test_files/" const val TestFilesFolderName = "test_files/"
const val TransactionsMt940FileRelativePath = TestFilesFolderName + "TransactionsMt940.txt" const val TransactionsMt940FileRelativePath = TestFilesFolderName + "TransactionsMt940.txt"
const val BankCode = "12345678"
const val CustomerId = "0987654321"
const val Currency = "EUR" const val Currency = "EUR"
val AccountStatement1PreviousStatementBookingDate = Date(88, 2, 26) val AccountStatement1PreviousStatementBookingDate = Date(88, 2, 26)
@ -150,12 +147,12 @@ class Mt940ParserTest {
:20:STARTUMSE :20:STARTUMSE
:25:$BankCode/$CustomerId :25:$BankCode/$CustomerId
:28C:00000/001 :28C:00000/001
:60F:C${convertDate(AccountStatement1PreviousStatementBookingDate)}EUR${convertAmount(AccountStatement1OpeningBalanceAmount)} :60F:C${convertMt940Date(AccountStatement1PreviousStatementBookingDate)}EUR${convertAmount(AccountStatement1OpeningBalanceAmount)}
:61:${convertDate(AccountStatement1BookingDate)}${convertToShortBookingDate(AccountStatement1BookingDate)}CR${convertAmount(AccountStatement1Transaction1Amount)}N062NONREF :61:${convertMt940Date(AccountStatement1BookingDate)}${convertToShortBookingDate(AccountStatement1BookingDate)}CR${convertAmount(AccountStatement1Transaction1Amount)}N062NONREF
:86:166?00GUTSCHR. UEBERWEISUNG?109249?20EREF+674?21SVWZ+1908301/ :86:166?00GUTSCHR. UEBERWEISUNG?109249?20EREF+674?21SVWZ+1908301/
EUR ${convertAmount(AccountStatement1Transaction1Amount)}/20?2219-10-02/...?30$AccountStatement1Transaction1OtherPartyBankCode?31$AccountStatement1Transaction1OtherPartyAccountId EUR ${convertAmount(AccountStatement1Transaction1Amount)}/20?2219-10-02/...?30$AccountStatement1Transaction1OtherPartyBankCode?31$AccountStatement1Transaction1OtherPartyAccountId
?32$AccountStatement1Transaction1OtherPartyName ?32$AccountStatement1Transaction1OtherPartyName
:62F:C${convertDate(AccountStatement1BookingDate)}EUR${convertAmount(AccountStatement1ClosingBalanceAmount)} :62F:C${convertMt940Date(AccountStatement1BookingDate)}EUR${convertAmount(AccountStatement1ClosingBalanceAmount)}
- -
""".trimIndent() """.trimIndent()
@ -163,25 +160,21 @@ class Mt940ParserTest {
:20:STARTUMSE :20:STARTUMSE
:25:$BankCode/$CustomerId :25:$BankCode/$CustomerId
:28C:00000/001 :28C:00000/001
:60F:C${convertDate(AccountStatement1PreviousStatementBookingDate)}EUR${convertAmount(AccountStatement1OpeningBalanceAmount)} :60F:C${convertMt940Date(AccountStatement1PreviousStatementBookingDate)}EUR${convertAmount(AccountStatement1OpeningBalanceAmount)}
:61:${convertDate(AccountStatement1BookingDate)}${convertToShortBookingDate(AccountStatement1BookingDate)}CR${convertAmount(AccountStatement1Transaction1Amount)}N062NONREF :61:${convertMt940Date(AccountStatement1BookingDate)}${convertToShortBookingDate(AccountStatement1BookingDate)}CR${convertAmount(AccountStatement1Transaction1Amount)}N062NONREF
:86:166?00GUTSCHR. UEBERWEISUNG?109249?20EREF+674?21SVWZ+1908301/ :86:166?00GUTSCHR. UEBERWEISUNG?109249?20EREF+674?21SVWZ+1908301/
EUR ${convertAmount(AccountStatement1Transaction1Amount)}/20?2219-10-02/...?30$AccountStatement1Transaction1OtherPartyBankCode?31$AccountStatement1Transaction1OtherPartyAccountId EUR ${convertAmount(AccountStatement1Transaction1Amount)}/20?2219-10-02/...?30$AccountStatement1Transaction1OtherPartyBankCode?31$AccountStatement1Transaction1OtherPartyAccountId
?32$AccountStatement1Transaction1OtherPartyName ?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/ :86:166?00ONLINE-UEBERWEISUNG?109249?20EREF+674?21SVWZ+1908301/
EUR ${convertAmount(AccountStatement1Transaction2Amount)}/20?2219-10-02/...?30$AccountStatement1Transaction2OtherPartyBankCode?31$AccountStatement1Transaction2OtherPartyAccountId EUR ${convertAmount(AccountStatement1Transaction2Amount)}/20?2219-10-02/...?30$AccountStatement1Transaction2OtherPartyBankCode?31$AccountStatement1Transaction2OtherPartyAccountId
?32$AccountStatement1Transaction2OtherPartyName ?32$AccountStatement1Transaction2OtherPartyName
:62F:C${convertDate(AccountStatement1BookingDate)}EUR${convertAmount(AccountStatement1With2TransactionsClosingBalanceAmount)} :62F:C${convertMt940Date(AccountStatement1BookingDate)}EUR${convertAmount(AccountStatement1With2TransactionsClosingBalanceAmount)}
- -
""".trimIndent() """.trimIndent()
private fun convertAmount(amount: BigDecimal): String { private fun convertMt940Date(date: Date): String {
return amount.toString().replace('.', ',')
}
private fun convertDate(date: Date): String {
return Mt940Parser.DateFormat.format(date) return Mt940Parser.DateFormat.format(date)
} }