Implemented parsing TAN response (HITAN)
This commit is contained in:
parent
3ccef79596
commit
c20bf13c5c
|
@ -17,6 +17,8 @@ enum class InstituteSegmentId(override val id: String) : ISegmentId {
|
|||
|
||||
TanInfo("HITANS"),
|
||||
|
||||
Tan("HITAN"),
|
||||
|
||||
Balance("HISAL"),
|
||||
|
||||
AccountTransactionsMt940("HIKAZ")
|
||||
|
|
|
@ -5,6 +5,7 @@ import net.dankito.fints.messages.segmente.id.ISegmentId
|
|||
import net.dankito.fints.messages.segmente.id.MessageSegmentId
|
||||
import net.dankito.fints.response.segments.ReceivedMessageHeader
|
||||
import net.dankito.fints.response.segments.ReceivedSegment
|
||||
import net.dankito.fints.response.segments.TanResponse
|
||||
|
||||
|
||||
open class Response(
|
||||
|
@ -18,6 +19,15 @@ open class Response(
|
|||
open val successful: Boolean
|
||||
get() = didReceiveResponse && didResponseContainErrors == false
|
||||
|
||||
open val isStrongAuthenticationRequired: Boolean
|
||||
get() {
|
||||
getFirstSegmentById<TanResponse>(InstituteSegmentId.Tan)?.let { tanResponse ->
|
||||
return tanResponse.isStrongAuthenticationRequired
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
open val messageHeader: ReceivedMessageHeader?
|
||||
get() = getFirstSegmentById(MessageSegmentId.MessageHeader)
|
||||
|
|
|
@ -84,7 +84,9 @@ open class ResponseParser @JvmOverloads constructor(
|
|||
|
||||
InstituteSegmentId.UserParameters.id -> parseUserParameters(segment, dataElementGroups)
|
||||
InstituteSegmentId.AccountInfo.id -> parseAccountInfo(segment, dataElementGroups)
|
||||
|
||||
InstituteSegmentId.TanInfo.id -> parseTanInfo(segment, dataElementGroups)
|
||||
InstituteSegmentId.Tan.id -> parseTanResponse(segment, dataElementGroups)
|
||||
|
||||
InstituteSegmentId.Balance.id -> parseBalanceSegment(segment, dataElementGroups)
|
||||
InstituteSegmentId.AccountTransactionsMt940.id -> parseMt940AccountTransactions(segment, dataElementGroups)
|
||||
|
@ -249,6 +251,23 @@ open class ResponseParser @JvmOverloads constructor(
|
|||
}
|
||||
|
||||
|
||||
protected open fun parseTanResponse(segment: String, dataElementGroups: List<String>): TanResponse {
|
||||
val binaryJobHashValue = if (dataElementGroups.size > 2) parseStringToNullIfEmpty(dataElementGroups[2]) else null
|
||||
val binaryChallengeHHD_UC = if (dataElementGroups.size > 5) parseStringToNullIfEmpty(dataElementGroups[5]) else null
|
||||
|
||||
return TanResponse(
|
||||
parseCodeEnum(dataElementGroups[1], TanProcess.values()),
|
||||
binaryJobHashValue?.let { extractBinaryData(it) },
|
||||
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]) else null,
|
||||
if (dataElementGroups.size > 7) parseStringToNullIfEmpty(dataElementGroups[7]) else null,
|
||||
segment
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
protected open fun parseBalanceSegment(segment: String, dataElementGroups: List<String>): BalanceSegment {
|
||||
// dataElementGroups[1] is account details
|
||||
|
||||
|
@ -487,13 +506,27 @@ open class ResponseParser @JvmOverloads constructor(
|
|||
return amount
|
||||
}
|
||||
|
||||
protected open fun parseNullableDateTime(dataElementGroup: String): Date? {
|
||||
val dataElements = getDataElements(dataElementGroup)
|
||||
|
||||
if (dataElements.size >= 2) {
|
||||
parseNullableDate(dataElements[0])?.let { date ->
|
||||
parseNullableTime(dataElements[1])?.let { time ->
|
||||
return Date(date.time + time.time)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
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)
|
||||
return parseDate(dateString)
|
||||
} catch (ignored: Exception) { }
|
||||
|
||||
return null
|
||||
|
@ -503,6 +536,14 @@ open class ResponseParser @JvmOverloads constructor(
|
|||
return Uhrzeit.HbciTimeFormat.parse(timeString)
|
||||
}
|
||||
|
||||
protected open fun parseNullableTime(timeString: String): Date? {
|
||||
try {
|
||||
return parseTime(timeString)
|
||||
} catch (ignored: Exception) { }
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
protected open fun extractBinaryData(binaryData: String): String {
|
||||
if (binaryData.startsWith('@')) {
|
||||
val headerEndIndex = binaryData.indexOf('@', 2)
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
package net.dankito.fints.response.segments
|
||||
|
||||
import net.dankito.fints.messages.datenelemente.implementierte.tan.TanProcess
|
||||
import java.util.*
|
||||
|
||||
|
||||
open class TanResponse(
|
||||
val tanProcess: TanProcess,
|
||||
val jobHashValue: String?, // M: bei Auftrags-Hashwertverfahren<>0 und TAN-Prozess=1. N: sonst
|
||||
val jobReference: String?, // M: bei TAN-Prozess=2, 3, 4. O: bei TAN-Prozess=1
|
||||
|
||||
/**
|
||||
* Dieses Datenelement enthält im Falle des Zwei-Schritt-TAN-Verfahrens die Challenge zu einem
|
||||
* eingereichten Auftrag. Aus der Challenge wird vom Kunden die eigentliche TAN ermittelt.
|
||||
* Die Challenge wird unabhängig vom Prozessvariante 1 oder 2 in der Kreditinstitutsantwort im
|
||||
* Segment HITAN übermittelt.
|
||||
*
|
||||
* Ist der BPD-Parameter „Challenge strukturiert“ mit „J“ belegt, so können im Text folgende
|
||||
* Formatsteuerzeichen enthalten sein, die kundenseitig entsprechend zu interpretieren sind.
|
||||
* Eine Kaskadierung von Steuerzeichen ist nicht erlaubt.
|
||||
*
|
||||
* <br> Zeilenumbruch
|
||||
* <p> Neuer Absatz
|
||||
* <b> ... </b> Fettdruck
|
||||
* <i> ... </i> Kursivdruck
|
||||
* <u> ... </u> Unterstreichen
|
||||
* <ul> ... </ul> Beginn / Ende Aufzählung
|
||||
* <ol> ... </ol> Beginn / Ende Nummerierte Liste
|
||||
* <li> ... </li> Listenelement einer Aufzählung / Nummerierten Liste
|
||||
*/
|
||||
val challenge: String?, // M: bei TAN-Prozess=1, 3, 4. O: bei TAN-Prozess=2
|
||||
|
||||
val challengeHHD_UC: String?,
|
||||
val validityDateTimeForChallenge: Date?,
|
||||
val tanMediaIdentifier: String? = null, // M: bei TAN-Prozess=1, 3, 4 und „Anzahl unterstützter aktiver TAN-Medien“ nicht vorhanden. O: sonst
|
||||
|
||||
segmentString: String
|
||||
) :
|
||||
ReceivedSegment(segmentString) {
|
||||
|
||||
companion object {
|
||||
const val NoChallengeResponse = "nochallenge"
|
||||
const val NoJobReferenceResponse = "noref"
|
||||
}
|
||||
|
||||
open val isStrongAuthenticationRequired: Boolean
|
||||
get() = challenge != NoChallengeResponse
|
||||
|
||||
}
|
|
@ -52,6 +52,10 @@ abstract class FinTsTestBase {
|
|||
return Datum.HbciDateFormat.format(date)
|
||||
}
|
||||
|
||||
protected open fun unmaskString(string: String): String {
|
||||
return string.replace("?'", "'").replace("?+", "+").replace("?:", ":")
|
||||
}
|
||||
|
||||
protected open fun normalizeBinaryData(message: String): String {
|
||||
return message.replace(0.toChar(), ' ')
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ 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
|
||||
import net.dankito.fints.messages.datenelemente.implementierte.signatur.VersionDesSicherheitsverfahrens
|
||||
import net.dankito.fints.messages.datenelemente.implementierte.tan.TanProcess
|
||||
import net.dankito.fints.messages.datenelementgruppen.implementierte.signatur.Sicherheitsprofil
|
||||
import net.dankito.fints.messages.segmente.id.ISegmentId
|
||||
import net.dankito.fints.messages.segmente.id.MessageSegmentId
|
||||
|
@ -214,34 +215,6 @@ class ResponseParserTest : FinTsTestBase() {
|
|||
}
|
||||
|
||||
|
||||
@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() {
|
||||
|
||||
|
@ -267,6 +240,86 @@ class ResponseParserTest : FinTsTestBase() {
|
|||
?: run { Assert.fail("No segment of type TanInfo found in ${result.receivedSegments}") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseTanResponse_NoStrongAuthenticationRequired() {
|
||||
|
||||
// when
|
||||
val result = underTest.parse("HITAN:6:6:5+4++noref+nochallenge")
|
||||
|
||||
// then
|
||||
assertSuccessfullyParsedSegment(result, InstituteSegmentId.Tan, 6, 6, 5)
|
||||
|
||||
assertThat(result.isStrongAuthenticationRequired).isFalse()
|
||||
|
||||
result.getFirstSegmentById<TanResponse>(InstituteSegmentId.Tan)?.let { segment ->
|
||||
assertThat(segment.tanProcess).isEqualTo(TanProcess.TanProcess4)
|
||||
assertThat(segment.jobHashValue).isNull()
|
||||
assertThat(segment.jobReference).isEqualTo(TanResponse.NoJobReferenceResponse)
|
||||
assertThat(segment.challenge).isEqualTo(TanResponse.NoChallengeResponse)
|
||||
assertThat(segment.challengeHHD_UC).isNull()
|
||||
assertThat(segment.validityDateTimeForChallenge).isNull()
|
||||
assertThat(segment.tanMediaIdentifier).isNull()
|
||||
}
|
||||
?: run { Assert.fail("No segment of type TanResponse found in ${result.receivedSegments}") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseTanResponse_StrongAuthenticationRequired() {
|
||||
|
||||
// given
|
||||
val jobReference = "4937-10-13-02.30.03.700259"
|
||||
val challenge = "Sie möchten eine \"Umsatzabfrage\" freigeben?: Bitte bestätigen Sie den \"Startcode 80085335\" mit der Taste \"OK\"."
|
||||
val challengeHHD_UC = "100880085335"
|
||||
val tanMediaIdentifier = "Kartennummer ******0892"
|
||||
|
||||
// when
|
||||
val result = underTest.parse("'HITAN:5:6:4+4++$jobReference+$challenge+@12@$challengeHHD_UC++$tanMediaIdentifier'")
|
||||
|
||||
// then
|
||||
assertSuccessfullyParsedSegment(result, InstituteSegmentId.Tan, 5, 6, 4)
|
||||
|
||||
assertThat(result.isStrongAuthenticationRequired).isTrue()
|
||||
|
||||
result.getFirstSegmentById<TanResponse>(InstituteSegmentId.Tan)?.let { segment ->
|
||||
assertThat(segment.tanProcess).isEqualTo(TanProcess.TanProcess4)
|
||||
assertThat(segment.jobHashValue).isNull()
|
||||
assertThat(segment.jobReference).isEqualTo(jobReference)
|
||||
assertThat(segment.challenge).isEqualTo(unmaskString(challenge))
|
||||
assertThat(segment.challengeHHD_UC).isEqualTo(challengeHHD_UC)
|
||||
assertThat(segment.validityDateTimeForChallenge).isNull()
|
||||
assertThat(segment.tanMediaIdentifier).isEqualTo(tanMediaIdentifier)
|
||||
}
|
||||
?: run { Assert.fail("No segment of type TanResponse found in ${result.receivedSegments}") }
|
||||
}
|
||||
|
||||
|
||||
@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}") }
|
||||
}
|
||||
|
||||
|
||||
private fun assertSuccessfullyParsedSegment(result: Response, segmentId: ISegmentId, segmentNumber: Int,
|
||||
segmentVersion: Int, referenceSegmentNumber: Int? = null) {
|
||||
|
|
Loading…
Reference in New Issue