Implemented parsing balance
This commit is contained in:
parent
8cf57d1c35
commit
89d44beab9
|
@ -17,6 +17,8 @@ enum class InstituteSegmentId(override val id: String) : ISegmentId {
|
|||
|
||||
TanInfo("HITANS"),
|
||||
|
||||
Balance("HISAL"),
|
||||
|
||||
AccountTransactionsMt940("HIKAZ")
|
||||
|
||||
}
|
|
@ -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<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 {
|
||||
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)
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
|
@ -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(), ' ')
|
||||
}
|
||||
|
|
|
@ -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<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
|
||||
fun parseTanInfo() {
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue