Implemented continueing at Aufsetzpunkt

This commit is contained in:
dankl 2019-10-26 18:22:06 +02:00 committed by dankito
parent 14bc302c6d
commit ee3cd937df
14 changed files with 266 additions and 31 deletions

View File

@ -6,8 +6,11 @@ ext {
kotlinVersion = '1.3.41'
javaUtilsVersion = '1.0.8'
androidUtilsVersion = '1.1.0'
javaFxUtilsVersion = '1.0.3'
junitVersion = '4.12'
assertJVersion = '3.12.2'

View File

@ -31,4 +31,6 @@ dependencies {
testCompile "ch.qos.logback:logback-core:$logbackVersion"
testCompile "ch.qos.logback:logback-classic:$logbackVersion"
testCompile "net.dankito.utils:java-fx-utils:$javaFxUtilsVersion"
}

View File

@ -255,15 +255,36 @@ open class FinTsClient @JvmOverloads constructor(
tryGetTransactionsOfLast90DaysWithoutTan(bank, customer)
}
val bookedAndUnbookedTransactions = getTransactionsFromResponse(response, transactions)
return GetTransactionsResponse(response,
transactions.bookedTransactions.sortedByDescending { it.bookingDate },
transactions.unbookedTransactions,
bookedAndUnbookedTransactions.first.sortedByDescending { it.bookingDate },
bookedAndUnbookedTransactions.second,
balance)
}
return GetTransactionsResponse(response)
}
protected open fun getTransactionsFromResponse(response: Response, transactions: ReceivedAccountTransactions): Pair<List<AccountTransaction>, List<Any>> {
val bookedTransactions = mutableListOf<AccountTransaction>()
val unbookedTransactions = mutableListOf<Any>()
bookedTransactions.addAll(transactions.bookedTransactions)
unbookedTransactions.addAll(transactions.unbookedTransactions)
response.followUpResponse?.let { followUpResponse ->
followUpResponse.getFirstSegmentById<ReceivedAccountTransactions>(InstituteSegmentId.AccountTransactionsMt940)?.let { followUpTransactions ->
val followUpBookedAndUnbookedTransactions = getTransactionsFromResponse(followUpResponse, followUpTransactions)
bookedTransactions.addAll(followUpBookedAndUnbookedTransactions.first)
unbookedTransactions.addAll(followUpBookedAndUnbookedTransactions.second)
}
}
return Pair(bookedTransactions, unbookedTransactions)
}
protected open fun getBalanceAfterDialogInit(bank: BankData, customer: CustomerData,
dialogData: DialogData): Response {
@ -433,6 +454,32 @@ open class FinTsClient @JvmOverloads constructor(
customer: CustomerData, dialogData: DialogData): Response {
val response = getAndHandleResponseForMessage(message, bank)
val handledResponse = handleMayRequiredTan(response, bank, customer, dialogData)
// if there's a Aufsetzpunkt (continuationId) set, then response is not complete yet, there's more information to fetch by sending this Aufsetzpunkt
handledResponse.aufsetzpunkt?.let { continuationId ->
handledResponse.followUpResponse = getFollowUpMessageForContinuationId(handledResponse, continuationId, message, bank, customer, dialogData)
handledResponse.hasFollowUpMessageButCouldNotReceiveIt = handledResponse.followUpResponse == null
}
return handledResponse
}
protected open fun getFollowUpMessageForContinuationId(response: Response, continuationId: String, message: MessageBuilderResult,
bank: BankData, customer: CustomerData, dialogData: DialogData): Response? {
messageBuilder.rebuildMessageWithContinuationId(message, continuationId, bank, customer, dialogData)?.let { followUpMessage ->
return getAndHandleResponseForMessageThatMayRequiresTan(followUpMessage, bank, customer, dialogData)
}
return null
}
protected open fun getAndHandleResponseForMessageThatMayRequiresTan(message: String, bank: BankData,
customer: CustomerData, dialogData: DialogData): Response {
val response = getAndHandleResponseForMessage(message, bank)
return handleMayRequiredTan(response, bank, customer, dialogData)
}

View File

@ -1,6 +1,7 @@
package net.dankito.fints.messages
import net.dankito.fints.extensions.containsAny
import net.dankito.fints.messages.datenelemente.implementierte.Aufsetzpunkt
import net.dankito.fints.messages.datenelemente.implementierte.Synchronisierungsmodus
import net.dankito.fints.messages.datenelemente.implementierte.tan.TanProcess
import net.dankito.fints.messages.segmente.ISegmentNumberGenerator
@ -107,11 +108,12 @@ open class MessageBuilder(protected val generator: ISegmentNumberGenerator = Seg
else if (result.isAllowed(6)) KontoumsaetzeZeitraumMt940Version6(generator.resetSegmentNumber(2), parameter, bank, customer)
else KontoumsaetzeZeitraumMt940Version5(generator.resetSegmentNumber(2), parameter, bank, customer)
return MessageBuilderResult(createSignedMessage(bank, customer, dialogData, listOf(
val segments = listOf(
transactionsJob,
ZweiSchrittTanEinreichung(generator.getNextSegmentNumber(), TanProcess.TanProcess4, CustomerSegmentId.AccountTransactionsMt940)
)))
)
return createMessageBuilderResult(bank, customer, dialogData, segments)
}
return result
@ -122,24 +124,26 @@ open class MessageBuilder(protected val generator: ISegmentNumberGenerator = Seg
val result = getSupportedVersionsOfJob(CustomerSegmentId.Balance, customer, listOf(5))
if (result.isJobVersionSupported) {
return MessageBuilderResult(createSignedMessage(bank, customer, dialogData, listOf(
val segments = listOf(
Saldenabfrage(generator.resetSegmentNumber(2), bank, customer),
ZweiSchrittTanEinreichung(generator.getNextSegmentNumber(), TanProcess.TanProcess4, CustomerSegmentId.Balance)
)))
)
return createMessageBuilderResult(bank, customer, dialogData, segments)
}
return result
}
open fun createSendEnteredTanMessage(enteredTan: String, tanResponse: TanResponse, bank: BankData, customer: CustomerData, dialogData: DialogData): MessageBuilderResult {
open fun createSendEnteredTanMessage(enteredTan: String, tanResponse: TanResponse, bank: BankData, customer: CustomerData, dialogData: DialogData): String {
val tanProcess = if (tanResponse.tanProcess == TanProcess.TanProcess1) TanProcess.TanProcess1 else TanProcess.TanProcess2
return MessageBuilderResult(createSignedMessage(bank, customer, dialogData, enteredTan, listOf(
return createSignedMessage(bank, customer, dialogData, enteredTan, listOf(
ZweiSchrittTanEinreichung(generator.resetSegmentNumber(2), tanProcess, null,
tanResponse.jobHashValue, tanResponse.jobReference, false, null, tanResponse.tanMediaIdentifier)
)))
))
}
@ -150,10 +154,12 @@ open class MessageBuilder(protected val generator: ISegmentNumberGenerator = Seg
if (result.isJobVersionSupported) {
getSepaUrnFor(CustomerSegmentId.SepaAccountInfoParameters, customer, "pain.001.001.03")?.let { urn ->
return MessageBuilderResult(createSignedMessage(bank, customer, dialogData, listOf(
val segments = listOf(
SepaEinzelueberweisung(generator.resetSegmentNumber(2), urn, customer, bank.bic, bankTransferData),
ZweiSchrittTanEinreichung(generator.getNextSegmentNumber(), TanProcess.TanProcess4, CustomerSegmentId.SepaBankTransfer)
)))
)
return createMessageBuilderResult(bank, customer, dialogData, segments)
}
return MessageBuilderResult(true, false, result.allowedVersions, result.supportedVersions, null) // TODO: how to tell that we don't support required SEPA pain version?
@ -163,6 +169,29 @@ open class MessageBuilder(protected val generator: ISegmentNumberGenerator = Seg
}
open fun rebuildMessageWithContinuationId(message: MessageBuilderResult, continuationId: String, bank: BankData,
customer: CustomerData, dialogData: DialogData): MessageBuilderResult? {
// val copiedSegments = message.messageBodySegments.map { }
val aufsetzpunkte = message.messageBodySegments.flatMap { it.dataElementsAndGroups }.filterIsInstance<Aufsetzpunkt>()
if (aufsetzpunkte.isEmpty()) {
// return MessageBuilderResult(message.isJobAllowed, message.isJobVersionSupported, message.allowedVersions, message.supportedVersions, null)
return null
}
aufsetzpunkte.forEach { it.resetContinuationId(continuationId) }
dialogData.increaseMessageNumber()
return createMessageBuilderResult(bank, customer, dialogData, message.messageBodySegments)
}
protected open fun createMessageBuilderResult(bank: BankData, customer: CustomerData, dialogData: DialogData, segments: List<Segment>): MessageBuilderResult {
return MessageBuilderResult(createSignedMessage(bank, customer, dialogData, segments), segments)
}
open fun createSignedMessage(bank: BankData, customer: CustomerData, dialogData: DialogData,
payloadSegments: List<Segment>): String {

View File

@ -1,17 +1,21 @@
package net.dankito.fints.messages
import net.dankito.fints.messages.segmente.Segment
open class MessageBuilderResult(
val isJobAllowed: Boolean,
val isJobVersionSupported: Boolean,
val allowedVersions: List<Int>,
val supportedVersions: List<Int>,
val createdMessage: String?
val createdMessage: String?,
val messageBodySegments: List<Segment> = listOf()
) {
constructor(isJobAllowed: Boolean) : this(isJobAllowed, false, listOf(), listOf(), null)
constructor(createdMessage: String) : this(true, true, listOf(), listOf(), createdMessage)
constructor(createdMessage: String, messageBodySegments: List<Segment>)
: this(true, true, listOf(), listOf(), createdMessage, messageBodySegments)
open fun isAllowed(version: Int): Boolean {

View File

@ -14,16 +14,18 @@ abstract class AlphanumerischesDatenelement @JvmOverloads constructor(
override fun validate() {
super.validate()
if (writeToOutput && value != null) { // if value is null and value has to be written to output then validation already fails above
if (value.contains("\r") || value.contains("\n")) {
throwValidationException("Alphanumerischer Wert '$value' darf kein Carriage Return (\r) oder " +
"Line Feed (\n) enthalten.")
}
if (writeToOutput) {
value?.let { value -> // if value is null and value has to be written to output then validation already fails above
if (value.contains("\r") || value.contains("\n")) {
throwValidationException("Alphanumerischer Wert '$value' darf kein Carriage Return (\r) oder " +
"Line Feed (\n) enthalten.")
}
maxLength?.let {
if (value.length > maxLength) {
throwValidationException("Wert '$value' darf maximal $maxLength Zeichen lang sein, " +
"hat aber ${value.length} Zeichen.")
maxLength?.let {
if (value.length > maxLength) {
throwValidationException("Wert '$value' darf maximal $maxLength Zeichen lang sein, " +
"hat aber ${value.length} Zeichen.")
}
}
}
}

View File

@ -37,7 +37,7 @@ open class BinaerDatenelement @JvmOverloads constructor(data: String?, existenzs
if (writeToOutput) {
checkIfMandatoryValueIsSet()
value?.let { // if value is null and value has to be written to output then validation already fails above
value?.let { value -> // if value is null and value has to be written to output then validation already fails above
maxLength?.let {
if (value.length > maxLength) {
throwValidationException("Binäre Daten dürfen nur eine maximale Größe von $maxLength Bytes " +

View File

@ -9,10 +9,11 @@ import net.dankito.fints.messages.datenelemente.Datenelement
/**
* Es gilt der vollständige FinTS-Basiszeichensatz.
*/
abstract class TextDatenelement(val value: String?, existenzstatus: Existenzstatus) : Datenelement(existenzstatus) {
abstract class TextDatenelement(var value: String?, existenzstatus: Existenzstatus) : Datenelement(existenzstatus) {
override val isValueSet = value != null
override val isValueSet
get() = value != null
override fun format(): String {
if (writeToOutput) {

View File

@ -10,4 +10,10 @@ import net.dankito.fints.messages.datenelemente.basisformate.AlphanumerischesDat
* einzigen Auftragssegment erfolgen kann (s. [Formals]).
*/
open class Aufsetzpunkt(continuationId: String?, existenzstatus: Existenzstatus)
: AlphanumerischesDatenelement(continuationId, existenzstatus, 35)
: AlphanumerischesDatenelement(continuationId, existenzstatus, 35) {
open fun resetContinuationId(continuationId: String?) {
value = continuationId
}
}

View File

@ -49,6 +49,9 @@ open class Response constructor(
open val segmentFeedbacks: List<SegmentFeedback>
get() = getSegmentsById(InstituteSegmentId.SegmentFeedback)
open val aufsetzpunkt: String? // TODO: what to do if there are multiple Aufsetzpunkte?
get() = segmentFeedbacks.flatMap { it.feedbacks }.filterIsInstance<AufsetzpunktFeedback>().firstOrNull()?.aufsetzpunkt
open val errorsToShowToUser: List<String>
get() {
val errorMessages = segmentFeedbacks
@ -66,6 +69,11 @@ open class Response constructor(
}
open var followUpResponse: Response? = null
open var hasFollowUpMessageButCouldNotReceiveIt: Boolean? = false
/**
* Returns an empty list of response didn't contain any job parameters.
*

View File

@ -29,13 +29,15 @@ open class ResponseParser @JvmOverloads constructor(
) {
companion object {
val EncryptionDataSegmentHeaderPattern = Pattern.compile("${MessageSegmentId.EncryptionData.id}:\\d{1,3}:\\d{1,3}\\+")
val EncryptionDataSegmentHeaderPattern: Pattern = Pattern.compile("${MessageSegmentId.EncryptionData.id}:\\d{1,3}:\\d{1,3}\\+")
val JobParametersSegmentPattern = Pattern.compile("HI[A-Z]{3}S")
val JobParametersSegmentPattern: Pattern = Pattern.compile("HI[A-Z]{3}S")
val FeedbackParametersSeparator = "; "
const val FeedbackParametersSeparator = "; "
val SupportedTanProceduresForUserResponseCode = 3920
const val AufsetzpunktResponseCode = 3040
const val SupportedTanProceduresForUserResponseCode = 3920
private val log = LoggerFactory.getLogger(ResponseParser::class.java)
}
@ -138,6 +140,9 @@ open class ResponseParser @JvmOverloads constructor(
val supportedProcedures = parseCodeEnum(dataElements.subList(3, dataElements.size), Sicherheitsfunktion.values())
return SupportedTanProceduresForUserFeedback(supportedProcedures, message)
}
else if (responseCode == AufsetzpunktResponseCode) {
return AufsetzpunktFeedback(parseString(dataElements[3]), message)
}
val parameter = if (dataElements.size > 3) dataElements.subList(3, dataElements.size).joinToString(FeedbackParametersSeparator) else null

View File

@ -0,0 +1,10 @@
package net.dankito.fints.response.segments
import net.dankito.fints.response.ResponseParser
open class AufsetzpunktFeedback(
val aufsetzpunkt: String,
message: String
)
: Feedback(ResponseParser.AufsetzpunktResponseCode, message)

View File

@ -1,10 +1,18 @@
package net.dankito.fints.messages
import net.dankito.fints.FinTsTestBase
import net.dankito.fints.model.AccountData
import net.dankito.fints.model.DialogData
import net.dankito.fints.model.GetTransactionsParameter
import net.dankito.fints.response.segments.AccountType
import net.dankito.fints.response.segments.JobParameters
import net.dankito.fints.util.FinTsUtils
import net.dankito.utils.datetime.asUtilDate
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Test
import java.time.LocalDate
import java.time.Month
import java.util.*
@ -27,6 +35,13 @@ class MessageBuilderTest : FinTsTestBase() {
}
@After
fun tearDown() {
Bank.supportedJobs = listOf()
Customer.accounts = listOf()
}
@Test
fun createAnonymousDialogInitMessage() {
@ -102,4 +117,94 @@ class MessageBuilderTest : FinTsTestBase() {
))
}
@Test
fun createGetTransactionsMessage_JobIsNotAllowed() {
// when
val result = underTest.createGetTransactionsMessage(GetTransactionsParameter(), Bank, Customer, Product, DialogData.DialogInitDialogData)
// then
assertThat(result.isJobAllowed).isFalse()
}
@Test
fun createGetTransactionsMessage_JobVersionIsNotSupported() {
// given
val getTransactionsJob = JobParameters("HKKAZ", 1, 1, null, "HKKAZ:73:5")
val getTransactionsJobWithPreviousVersion = JobParameters("HKKAZ", 1, 1, null, "HKKAZ:72:4")
Bank.supportedJobs = listOf(getTransactionsJob)
val account = AccountData(CustomerId, null, BankCountryCode, BankCode, null, CustomerId, AccountType.Girokonto, "EUR", "", null, null, listOf(getTransactionsJob.jobName), listOf(getTransactionsJobWithPreviousVersion))
Customer.accounts = listOf(account)
// when
val result = underTest.createGetTransactionsMessage(GetTransactionsParameter(), Bank, Customer, Product, DialogData.DialogInitDialogData)
// then
assertThat(result.isJobAllowed).isTrue()
assertThat(result.isJobVersionSupported).isFalse()
}
@Test
fun createGetTransactionsMessage() {
// given
val getTransactionsJob = JobParameters("HKKAZ", 1, 1, null, "HKKAZ:73:5")
Bank.supportedJobs = listOf(getTransactionsJob)
val account = AccountData(CustomerId, null, BankCountryCode, BankCode, null, CustomerId, AccountType.Girokonto, "EUR", "", null, null, listOf(getTransactionsJob.jobName), listOf(getTransactionsJob))
Customer.accounts = listOf(account)
val fromDate = LocalDate.of(2019, Month.AUGUST, 6).asUtilDate()
val toDate = LocalDate.of(2019, Month.OCTOBER, 21).asUtilDate()
val maxCountEntries = 99
// when
val result = underTest.createGetTransactionsMessage(GetTransactionsParameter(false, fromDate, toDate, maxCountEntries), Bank, Customer, Product, DialogData.DialogInitDialogData)
// then
assertThat(result.createdMessage).isNotNull()
assertThat(normalizeBinaryData(result.createdMessage!!)).isEqualTo(normalizeBinaryData(
"HNHBK:1:3+000000000362+300+0+1'" +
"HNVSK:998:3+PIN:2+998+1+1::0+1:$Date:$Time+2:16:14:@8@ :5:1+280:$BankCode:$CustomerId:V:0:0+0'" +
"HNVSD:999:1+@198@" + "HNSHK:2:4+PIN:2+${SecurityFunction.code}+$ControlReference+1+1+1::0+1+1:$Date:$Time+1:999:1+6:10:16+280:$BankCode:$CustomerId:S:0:0'" +
"HKKAZ:3:${getTransactionsJob.segmentVersion}+$CustomerId::280:$BankCode+N+${convertDate(fromDate)}+${convertDate(toDate)}+$maxCountEntries'" +
"HKTAN:4:6+4+HKKAZ'" +
"HNSHA:5:2+$ControlReference++$Pin''" +
"HNHBS:6:1+1'"
))
}
@Test
fun createGetTransactionsMessage_WithContinuationIdSet() {
// given
val getTransactionsJob = JobParameters("HKKAZ", 1, 1, null, "HKKAZ:73:5")
Bank.supportedJobs = listOf(getTransactionsJob)
val account = AccountData(CustomerId, null, BankCountryCode, BankCode, null, CustomerId, AccountType.Girokonto, "EUR", "", null, null, listOf(getTransactionsJob.jobName), listOf(getTransactionsJob))
Customer.accounts = listOf(account)
val fromDate = LocalDate.of(2019, Month.AUGUST, 6).asUtilDate()
val toDate = LocalDate.of(2019, Month.OCTOBER, 21).asUtilDate()
val maxCountEntries = 99
val continuationId = "9345-10-26-11.52.15.693455"
// when
val result = underTest.createGetTransactionsMessage(GetTransactionsParameter(false, fromDate, toDate, maxCountEntries, false, continuationId), Bank, Customer, Product, DialogData.DialogInitDialogData)
// then
assertThat(result.createdMessage).isNotNull()
assertThat(normalizeBinaryData(result.createdMessage!!)).isEqualTo(normalizeBinaryData(
"HNHBK:1:3+000000000389+300+0+1'" +
"HNVSK:998:3+PIN:2+998+1+1::0+1:$Date:$Time+2:16:14:@8@ :5:1+280:$BankCode:$CustomerId:V:0:0+0'" +
"HNVSD:999:1+@225@" + "HNSHK:2:4+PIN:2+${SecurityFunction.code}+$ControlReference+1+1+1::0+1+1:$Date:$Time+1:999:1+6:10:16+280:$BankCode:$CustomerId:S:0:0'" +
"HKKAZ:3:${getTransactionsJob.segmentVersion}+$CustomerId::280:$BankCode+N+${convertDate(fromDate)}+${convertDate(toDate)}+$maxCountEntries+$continuationId'" +
"HKTAN:4:6+4+HKKAZ'" +
"HNSHA:5:2+$ControlReference++$Pin''" +
"HNHBS:6:1+1'"
))
}
}

View File

@ -208,6 +208,19 @@ class ResponseParserTest : FinTsTestBase() {
}
@Test
fun parseSegmentFeedback_Aufsetzpunkt() {
// when
val result = underTest.parse("HIRMS:4:2:3+0020::Der Auftrag wurde ausgeführt.+0020::Die gebuchten Umsätze wurden übermittelt.+3040::Es liegen weitere Informationen vor.:9345-10-26-11.52.15.693455")
// then
assertCouldParseSegment(result, InstituteSegmentId.SegmentFeedback, 4, 2, 3)
assertThat(result.aufsetzpunkt).isEqualTo("9345-10-26-11.52.15.693455")
}
@Test
fun parseSegmentFeedback_AllowedUserTanProcedures() {