Implemented parsing Message- and SegmentFeedback

This commit is contained in:
dankl 2019-10-13 13:17:01 +02:00 committed by dankito
parent 41a343421d
commit 031629e0c9
8 changed files with 281 additions and 19 deletions

View File

@ -184,7 +184,7 @@ open class FinTsClient(
return responseParser.parse(decodedResponse)
}
return Response(false, false, error = webResponse.error)
return Response(false, error = webResponse.error)
}
protected open fun decodeBase64Response(responseBody: String): String {

View File

@ -5,6 +5,10 @@ import net.dankito.fints.messages.segmente.id.ISegmentId
enum class InstituteSegmentId(override val id: String) : ISegmentId {
MessageFeedback("HIRMG"),
SegmentFeedback("HIRMS"),
Synchronization("HISYN"),
BankParameters("HIBPA"),

View File

@ -3,21 +3,21 @@ package net.dankito.fints.response
import net.dankito.fints.messages.Separators
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
import net.dankito.fints.response.segments.*
open class Response(
open class Response constructor(
val didReceiveResponse: Boolean,
val didResponseContainErrors: Boolean,
val receivedResponse: String? = null,
val receivedSegments: List<ReceivedSegment> = listOf(),
val error: Exception? = null
) {
open val responseContainsErrors: Boolean
get() = error == null && messageFeedback?.isError == true
open val successful: Boolean
get() = didReceiveResponse && didResponseContainErrors == false
get() = didReceiveResponse && responseContainsErrors == false
open val isStrongAuthenticationRequired: Boolean
get() {
@ -32,6 +32,29 @@ open class Response(
open val messageHeader: ReceivedMessageHeader?
get() = getFirstSegmentById(MessageSegmentId.MessageHeader)
open val messageFeedback: MessageFeedback?
get() = getFirstSegmentById(InstituteSegmentId.MessageFeedback)
open val segmentFeedbacks: List<SegmentFeedback>
get() = getSegmentsById(InstituteSegmentId.SegmentFeedback)
open val errorsToShowToUser: List<String>
get() {
val errorMessages = segmentFeedbacks
.flatMap { it.feedbacks }
.mapNotNull { if (it.isError) it.message else null }
.toMutableList()
messageFeedback?.let { messageFeedback ->
if (messageFeedback.isError) {
errorMessages.addAll(0, messageFeedback.feedbacks.mapNotNull { if (it.isError) it.message else null })
}
}
return errorMessages
}
open fun <T : ReceivedSegment> getFirstSegmentById(id: ISegmentId): T? {
return getFirstSegmentById(id.id)
}
@ -40,12 +63,12 @@ open class Response(
return receivedSegments.firstOrNull { it.segmentId == id } as T?
}
open fun getSegmentsById(id: ISegmentId): List<ReceivedSegment> {
open fun <T : ReceivedSegment> getSegmentsById(id: ISegmentId): List<T> {
return getSegmentsById(id.id)
}
open fun getSegmentsById(id: String): List<ReceivedSegment> {
return receivedSegments.filter { it.segmentId == id }
open fun <T : ReceivedSegment> getSegmentsById(id: String): List<T> {
return receivedSegments.filter { it.segmentId == id }.mapNotNull { it as? T }
}

View File

@ -44,20 +44,15 @@ open class ResponseParser @JvmOverloads constructor(
val parsedSegments = segments.mapNotNull { parseSegment(it) }
return Response(true, determineContainsErrors(parsedSegments), response, parsedSegments)
return Response(true, response, parsedSegments)
} catch (e: Exception) {
log.error("Could not parse response '$response'", e)
return Response(true, true, response, error = e)
return Response(true, response, error = e)
}
}
protected open fun determineContainsErrors(parsedSegments: List<ReceivedSegment>): Boolean {
return false // TODO
}
protected open fun parseSegment(segment: String): ReceivedSegment? {
try {
if (segment.isNotEmpty()) { // filter out empty lines
@ -78,6 +73,9 @@ open class ResponseParser @JvmOverloads constructor(
return when (segmentId) {
MessageSegmentId.MessageHeader.id -> parseMessageHeaderSegment(segment, dataElementGroups)
InstituteSegmentId.MessageFeedback.id -> parseMessageFeedback(segment, dataElementGroups)
InstituteSegmentId.SegmentFeedback.id -> parseSegmentFeedback(segment, dataElementGroups)
InstituteSegmentId.Synchronization.id -> parseSynchronization(segment, dataElementGroups)
InstituteSegmentId.BankParameters.id -> parseBankParameters(segment, dataElementGroups)
InstituteSegmentId.SecurityMethods.id -> parseSecurityMethods(segment, dataElementGroups)
@ -111,6 +109,29 @@ open class ResponseParser @JvmOverloads constructor(
return ReceivedMessageHeader(messageSize, finTsVersion, dialogId, messageNumber, segment)
}
protected open fun parseMessageFeedback(segment: String, dataElementGroups: List<String>): MessageFeedback {
val feedbacks = dataElementGroups.subList(1, dataElementGroups.size).map { parseFeedback(it) }
return MessageFeedback(feedbacks, segment)
}
protected open fun parseSegmentFeedback(segment: String, dataElementGroups: List<String>): SegmentFeedback {
val feedbacks = dataElementGroups.subList(1, dataElementGroups.size).map { parseFeedback(it) }
return SegmentFeedback(feedbacks, segment)
}
protected open fun parseFeedback(dataElementGroup: String): Feedback {
val dataElements = getDataElements(dataElementGroup)
return Feedback(
parseInt(dataElements[0]),
parseString(dataElements[2]),
parseStringToNullIfEmpty(dataElements[1]),
if (dataElements.size > 3) parseStringToNullIfEmpty(dataElements[3]) else null
)
}
protected open fun parseSynchronization(segment: String, dataElementGroups: List<String>): ReceivedSynchronization {
val customerSystemId = parseString(dataElementGroups[1])
val lastMessageNumber = if (dataElementGroups.size > 2) parseString(dataElementGroups[2]) else null

View File

@ -0,0 +1,57 @@
package net.dankito.fints.response.segments
open class Feedback(
val responseCode: Int,
val message: String,
/**
* Die Position des Datenelements bzw. Gruppendatenelements, auf das sich der Rückmeldungscode bezieht
* (z. B. die Position des fehlerhaften Elementes bei Syntaxfehlern).
*
* Bei Rückmeldecodes, die sich auf eine Nachricht oder ein Segment (Auftrag) beziehen, darf dieses DE nicht
* belegt werden.
*
* Die Angabe des Bezugsdatenelement erlaubt u.U. eine automatische Reaktion des Kundenproduktes. So kann bspw.
* bei fehlerhaften Eingaben des Kunden direkt auf das betreffende Eingabefeld positioniert werden.
*
* Die Referenzierung erfolgt
* - bei DE durch die Position
* - bei GD durch die Position der DEG und die Position des GD (die beiden Werte sind durch Komma getrennt)
*
* Position des DE:
* Position des DE = Anzahl der vorstehenden DE-Trennzeichen + 1.
* Die Anzahl der vorstehenden DE-Trennzeichen ist gleich der Anzahl der vorstehenden DE + Anzahl der
* vorstehenden DEGs (GD sind nicht separat zuzählen, sondern gehen in die DEGs ein). Entwertete Pluszeichen
* sind nicht zu zählen.
*
* Position des GD innerhalb einer DEG:
* Position des GD = Anzahl der vorstehenden GD-Trennzeichen innerhalb der DEG + 1
*
* Beispiele (Nummern beziehen sich auf das mit |xy| gekennzeichnete Element):
* Segmentkopf+DE+GD:GD:GD:GD+|DE|+GD:GD : 4
* Segmentkopf+DE+GD:GD:GD:|GD|+DE+GD:GD : 3,4
* Segmentkopf+DE+GD:GD:GD:GD+DE+GD:|GD| : 5,2
*/
val referencedDataElement: String?,
val parameter: String?
) {
val isSuccess: Boolean
get() = responseCode in 1000..1999
val isWarning: Boolean
get() = responseCode in 3000..3999
val isError: Boolean
get() = responseCode in 9000..9999
override fun toString(): String {
return "${if (isSuccess) "Success" else if (isWarning) "Warning" else "Error"}: $responseCode $message " +
if (parameter != null) "($parameter)" else ""
}
}

View File

@ -0,0 +1,29 @@
package net.dankito.fints.response.segments
/**
* In diesem Segment werden Rückmeldungen übermittelt, die sich auf die gesamte Nachricht und nicht auf ein
* spezifisches Segment beziehen (z. B. Nachricht entgegengenommen, "Elektronische Signatur gesperrt").
*/
open class MessageFeedback(
val feedbacks: List<Feedback>,
segmentString: String
)
: ReceivedSegment(segmentString) {
val isSuccess: Boolean
get() = isError == false && isWarning == false
val isWarning: Boolean
get() = isError == false && feedbacks.firstOrNull { it.isWarning } != null
val isError: Boolean
get() = feedbacks.firstOrNull { it.isError } != null
override fun toString(): String {
return feedbacks.joinToString("\n")
}
}

View File

@ -0,0 +1,15 @@
package net.dankito.fints.response.segments
/**
* Dieses Segment ist genau einmal für jedes Segment der Kundennachricht einzustellen. Hier sind sämtliche
* Rückmeldungscodes aufzuführen, die sich auf das Kundensegment bzw. die zugehörigen Datenelemente und
* Datenelementgruppen beziehen. Falls für das jeweilige Kundensegment keine Rückmeldungscodes erzeugt wurden,
* kann das zugehörige Rückmeldesegment entfallen. Ist das jeweilige Kundensegment fehlerhaft, dann dürfen keine
* Datensegmente (s.u.) rückgemeldet werden.
*/
open class SegmentFeedback(
feedbacks: List<Feedback>,
segmentString: String
)
: MessageFeedback(feedbacks, segmentString)

View File

@ -102,6 +102,111 @@ class ResponseParserTest : FinTsTestBase() {
}
@Test
fun parseMessageFeedback_Warning() {
// when
val result = underTest.parse("HIRMG:3:2+3060::Bitte beachten Sie die enthaltenen Warnungen/Hinweise.")
// then
assertSuccessfullyParsedSegment(result, InstituteSegmentId.MessageFeedback, 3, 2)
assertThat(result.messageFeedback).isNotNull()
assertThat(result.messageFeedback?.feedbacks).hasSize(1)
val firstFeedback = result.messageFeedback?.feedbacks?.get(0)!!
assertThat(firstFeedback.responseCode).isEqualTo(3060)
assertThat(firstFeedback.isSuccess).isFalse()
assertThat(firstFeedback.isWarning).isTrue()
assertThat(firstFeedback.isError).isFalse()
assertThat(firstFeedback.message).isEqualTo("Bitte beachten Sie die enthaltenen Warnungen/Hinweise.")
assertThat(firstFeedback.parameter).isNull()
}
@Test
fun parseMessageFeedback_Error() {
// when
val result = underTest.parse("HIRMG:3:2+9050::Die Nachricht enthält Fehler.")
// then
assertCouldParseSegment(result, InstituteSegmentId.MessageFeedback, 3, 2)
assertThat(result.messageFeedback).isNotNull()
assertThat(result.messageFeedback?.feedbacks).hasSize(1)
val firstFeedback = result.messageFeedback?.feedbacks?.get(0)!!
assertThat(firstFeedback.responseCode).isEqualTo(9050)
assertThat(firstFeedback.isSuccess).isFalse()
assertThat(firstFeedback.isWarning).isFalse()
assertThat(firstFeedback.isError).isTrue()
assertThat(firstFeedback.message).isEqualTo("Die Nachricht enthält Fehler.")
assertThat(firstFeedback.parameter).isNull()
}
@Test
fun parseMessageFeedback_MultipleFeedback() {
// when
val result = underTest.parse("HIRMG:3:2+9050::Die Nachricht enthält Fehler.+3905::Es wurde keine Challenge erzeugt.")
// then
assertCouldParseSegment(result, InstituteSegmentId.MessageFeedback, 3, 2)
assertThat(result.messageFeedback).isNotNull()
assertThat(result.messageFeedback?.feedbacks).hasSize(2)
val firstFeedback = result.messageFeedback?.feedbacks?.get(0)!!
assertThat(firstFeedback.responseCode).isEqualTo(9050)
assertThat(firstFeedback.isSuccess).isFalse()
assertThat(firstFeedback.isWarning).isFalse()
assertThat(firstFeedback.isError).isTrue()
assertThat(firstFeedback.message).isEqualTo("Die Nachricht enthält Fehler.")
assertThat(firstFeedback.parameter).isNull()
val secondFeedback = result.messageFeedback?.feedbacks?.get(1)!!
assertThat(secondFeedback.responseCode).isEqualTo(3905)
assertThat(secondFeedback.isSuccess).isFalse()
assertThat(secondFeedback.isWarning).isTrue()
assertThat(secondFeedback.isError).isFalse()
assertThat(secondFeedback.message).isEqualTo("Es wurde keine Challenge erzeugt.")
assertThat(secondFeedback.parameter).isNull()
}
@Test
fun parseSegmentFeedback_MultipleFeedback() {
// when
val result = underTest.parse("HIRMG:3:2+9050::Die Nachricht enthält Fehler.+3905::Es wurde keine Challenge erzeugt.")
// then
assertCouldParseSegment(result, InstituteSegmentId.MessageFeedback, 3, 2)
assertThat(result.messageFeedback).isNotNull()
assertThat(result.messageFeedback?.feedbacks).hasSize(2)
val firstFeedback = result.messageFeedback?.feedbacks?.get(0)!!
assertThat(firstFeedback.responseCode).isEqualTo(9050)
assertThat(firstFeedback.isSuccess).isFalse()
assertThat(firstFeedback.isWarning).isFalse()
assertThat(firstFeedback.isError).isTrue()
assertThat(firstFeedback.message).isEqualTo("Die Nachricht enthält Fehler.")
assertThat(firstFeedback.parameter).isNull()
val secondFeedback = result.messageFeedback?.feedbacks?.get(1)!!
assertThat(secondFeedback.responseCode).isEqualTo(3905)
assertThat(secondFeedback.isSuccess).isFalse()
assertThat(secondFeedback.isWarning).isTrue()
assertThat(secondFeedback.isError).isFalse()
assertThat(secondFeedback.message).isEqualTo("Es wurde keine Challenge erzeugt.")
assertThat(secondFeedback.parameter).isNull()
}
@Test
fun parseSynchronization() {
@ -199,7 +304,7 @@ class ResponseParserTest : FinTsTestBase() {
assertSuccessfullyParsedSegment(result, InstituteSegmentId.AccountInfo, 7, 6, 4)
result.getFirstSegmentById<AccountInfo>(InstituteSegmentId.AccountInfo)?.let { segment ->
assertThat(segment.accountNumber).isEqualTo("0987654321")
assertThat(segment.accountIdentifier).isEqualTo("0987654321")
assertThat(segment.subAccountAttribute).isNull()
assertThat(segment.bankCountryCode).isEqualTo(280)
assertThat(segment.bankCode).isEqualTo("12345678")
@ -227,7 +332,7 @@ class ResponseParserTest : FinTsTestBase() {
assertSuccessfullyParsedSegment(result, InstituteSegmentId.AccountInfo, 74, 6, 3)
result.getFirstSegmentById<AccountInfo>(InstituteSegmentId.AccountInfo)?.let { segment ->
assertThat(segment.accountNumber).isEqualTo("9999999999")
assertThat(segment.accountIdentifier).isEqualTo("9999999999")
assertThat(segment.subAccountAttribute).isNull()
assertThat(segment.bankCountryCode).isEqualTo(280)
assertThat(segment.bankCode).isEqualTo("10070000")
@ -465,9 +570,17 @@ class ResponseParserTest : FinTsTestBase() {
segmentVersion: Int, referenceSegmentNumber: Int? = null) {
assertThat(result.successful).isTrue()
assertThat(result.responseContainsErrors).isFalse()
assertThat(result.error).isNull()
assertThat(result.errorsToShowToUser).isEmpty()
assertThat(result.receivedResponse).isNotNull()
assertCouldParseSegment(result, segmentId, segmentNumber, segmentVersion, referenceSegmentNumber)
}
private fun assertCouldParseSegment(result: Response, segmentId: ISegmentId, segmentNumber: Int,
segmentVersion: Int, referenceSegmentNumber: Int? = null) {
val segment = result.getFirstSegmentById<ReceivedSegment>(segmentId)
assertThat(segment).isNotNull()