diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/FinTsClient.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/FinTsClient.kt index aead256e..e3ef71ad 100644 --- a/fints4javaLib/src/main/kotlin/net/dankito/fints/FinTsClient.kt +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/FinTsClient.kt @@ -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 { 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 e1df0d1d..968ed315 100644 --- a/fints4javaLib/src/main/kotlin/net/dankito/fints/response/InstituteSegmentId.kt +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/InstituteSegmentId.kt @@ -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"), diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/response/Response.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/Response.kt index bc862962..b2b803a0 100644 --- a/fints4javaLib/src/main/kotlin/net/dankito/fints/response/Response.kt +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/Response.kt @@ -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 = 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 + get() = getSegmentsById(InstituteSegmentId.SegmentFeedback) + + open val errorsToShowToUser: List + 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 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 { + open fun getSegmentsById(id: ISegmentId): List { return getSegmentsById(id.id) } - open fun getSegmentsById(id: String): List { - return receivedSegments.filter { it.segmentId == id } + open fun getSegmentsById(id: String): List { + return receivedSegments.filter { it.segmentId == id }.mapNotNull { it as? T } } 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 7f337071..5f0fb4e4 100644 --- a/fints4javaLib/src/main/kotlin/net/dankito/fints/response/ResponseParser.kt +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/ResponseParser.kt @@ -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): 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): MessageFeedback { + val feedbacks = dataElementGroups.subList(1, dataElementGroups.size).map { parseFeedback(it) } + + return MessageFeedback(feedbacks, segment) + } + + protected open fun parseSegmentFeedback(segment: String, dataElementGroups: List): 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): ReceivedSynchronization { val customerSystemId = parseString(dataElementGroups[1]) val lastMessageNumber = if (dataElementGroups.size > 2) parseString(dataElementGroups[2]) else null diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/Feedback.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/Feedback.kt new file mode 100644 index 00000000..e21a089c --- /dev/null +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/Feedback.kt @@ -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 "" + } + +} \ No newline at end of file diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/MessageFeedback.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/MessageFeedback.kt new file mode 100644 index 00000000..3819a47a --- /dev/null +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/MessageFeedback.kt @@ -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, + 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") + } + +} \ No newline at end of file diff --git a/fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/SegmentFeedback.kt b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/SegmentFeedback.kt new file mode 100644 index 00000000..833e24e1 --- /dev/null +++ b/fints4javaLib/src/main/kotlin/net/dankito/fints/response/segments/SegmentFeedback.kt @@ -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, + segmentString: String +) + : MessageFeedback(feedbacks, segmentString) \ No newline at end of file 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 ff66535b..801c0360 100644 --- a/fints4javaLib/src/test/kotlin/net/dankito/fints/response/ResponseParserTest.kt +++ b/fints4javaLib/src/test/kotlin/net/dankito/fints/response/ResponseParserTest.kt @@ -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(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(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(segmentId) assertThat(segment).isNotNull()