Implemented reading user's tan media list

This commit is contained in:
dankl 2019-12-29 15:07:05 +01:00 committed by dankito
parent 677f360855
commit 553b55e090
17 changed files with 415 additions and 1 deletions

View File

@ -6,12 +6,15 @@ import net.dankito.fints.messages.datenelemente.implementierte.Dialogsprache
import net.dankito.fints.messages.datenelemente.implementierte.KundensystemID
import net.dankito.fints.messages.datenelemente.implementierte.KundensystemStatusWerte
import net.dankito.fints.messages.datenelemente.implementierte.signatur.Sicherheitsfunktion
import net.dankito.fints.messages.datenelemente.implementierte.tan.TanMedienArtVersion
import net.dankito.fints.messages.datenelemente.implementierte.tan.TanMediumKlasseVersion
import net.dankito.fints.model.*
import net.dankito.fints.response.InstituteSegmentId
import net.dankito.fints.response.Response
import net.dankito.fints.response.ResponseParser
import net.dankito.fints.response.client.AddAccountResponse
import net.dankito.fints.response.client.FinTsClientResponse
import net.dankito.fints.response.client.GetTanMediaListResponse
import net.dankito.fints.response.client.GetTransactionsResponse
import net.dankito.fints.response.segments.*
import net.dankito.fints.util.IBase64Service
@ -300,6 +303,42 @@ open class FinTsClient @JvmOverloads constructor(
}
open fun getTanMediaListAsync(bank: BankData, customer: CustomerData,
tanMediaKind: TanMedienArtVersion = TanMedienArtVersion.Alle,
tanMediumClass: TanMediumKlasseVersion = TanMediumKlasseVersion.AlleMedien,
callback: (GetTanMediaListResponse) -> Unit) {
threadPool.runAsync {
callback(getTanMediaList(bank, customer))
}
}
open fun getTanMediaList(bank: BankData, customer: CustomerData, tanMediaKind: TanMedienArtVersion = TanMedienArtVersion.Alle,
tanMediumClass: TanMediumKlasseVersion = TanMediumKlasseVersion.AlleMedien): GetTanMediaListResponse {
val dialogData = DialogData()
val initDialogResponse = initDialog(bank, customer, dialogData)
if (initDialogResponse.successful == false) {
return GetTanMediaListResponse(initDialogResponse, null)
}
dialogData.increaseMessageNumber()
val message = messageBuilder.createGetTanMediaListMessage(bank, customer, dialogData, tanMediaKind, tanMediumClass)
val response = getAndHandleResponseForMessageThatMayRequiresTan(message, bank, customer, dialogData)
closeDialog(bank, customer, dialogData)
val tanMediaList = response.getFirstSegmentById<TanMediaList>(InstituteSegmentId.TanMediaList)
return GetTanMediaListResponse(response, tanMediaList)
}
open fun doBankTransferAsync(bankTransferData: BankTransferData, bank: BankData,
customer: CustomerData, callback: (FinTsClientResponse) -> Unit) {

View File

@ -3,6 +3,8 @@ 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.TanMedienArtVersion
import net.dankito.fints.messages.datenelemente.implementierte.tan.TanMediumKlasseVersion
import net.dankito.fints.messages.datenelemente.implementierte.tan.TanProcess
import net.dankito.fints.messages.segmente.ISegmentNumberGenerator
import net.dankito.fints.messages.segmente.Segment
@ -11,6 +13,7 @@ import net.dankito.fints.messages.segmente.Synchronisierung
import net.dankito.fints.messages.segmente.id.CustomerSegmentId
import net.dankito.fints.messages.segmente.implementierte.*
import net.dankito.fints.messages.segmente.implementierte.sepa.SepaEinzelueberweisung
import net.dankito.fints.messages.segmente.implementierte.tan.TanGeneratorListeAnzeigen
import net.dankito.fints.messages.segmente.implementierte.umsaetze.KontoumsaetzeZeitraumMt940Version5
import net.dankito.fints.messages.segmente.implementierte.umsaetze.KontoumsaetzeZeitraumMt940Version6
import net.dankito.fints.messages.segmente.implementierte.umsaetze.KontoumsaetzeZeitraumMt940Version7
@ -136,6 +139,24 @@ open class MessageBuilder(protected val generator: ISegmentNumberGenerator = Seg
}
open fun createGetTanMediaListMessage(bank: BankData, customer: CustomerData, dialogData: DialogData,
tanMediaKind: TanMedienArtVersion = TanMedienArtVersion.Alle,
tanMediumClass: TanMediumKlasseVersion = TanMediumKlasseVersion.AlleMedien): MessageBuilderResult {
val result = getSupportedVersionsOfJob(CustomerSegmentId.TanMediaList, customer, listOf(2, 3, 4, 5))
if (result.isJobVersionSupported) {
val segments = listOf(
TanGeneratorListeAnzeigen(result.getHighestAllowedVersion!!,
generator.resetSegmentNumber(2), tanMediaKind, tanMediumClass)
)
return createMessageBuilderResult(bank, customer, dialogData, segments)
}
return result
}
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

View File

@ -22,4 +22,7 @@ open class MessageBuilderResult(
return allowedVersions.contains(version)
}
open val getHighestAllowedVersion: Int?
get() = allowedVersions.max()
}

View File

@ -0,0 +1,18 @@
package net.dankito.fints.messages.datenelemente.implementierte.tan
import net.dankito.fints.messages.datenelemente.implementierte.ICodeEnum
/**
* Es werden die Möglichkeiten festgelegt, die ein Kunde hat, wenn er für
* PIN/TAN parallel mehrere TAN-Medien zur Verfügung hat.
*/
enum class TanEinsatzOption(override val code: String) : ICodeEnum {
KundeKannAlleAktivenMedienParallelNutzen("0"),
KundeKannGenauEinMediumZuEinerZeitNutzen("1"),
KundeKannEinMobiltelefonUndEinenTanGeneratorParallelNutzen("2")
}

View File

@ -0,0 +1,51 @@
package net.dankito.fints.messages.datenelemente.implementierte.tan
import java.util.*
class TanGeneratorTanMedium(
mediumClass: TanMediumKlasseVersion,
status: TanMediumStatus,
val cardNumber: String,
val followUpCardNumber: String?,
val cardType: String?,
val validFrom: Date?,
val validTo: Date?,
val mediaName: String?
) : TanMedium(mediumClass, status) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as TanGeneratorTanMedium
if (cardNumber != other.cardNumber) return false
if (followUpCardNumber != other.followUpCardNumber) return false
if (cardType != other.cardType) return false
if (validFrom != other.validFrom) return false
if (validTo != other.validTo) return false
if (mediaName != other.mediaName) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + cardNumber.hashCode()
result = 31 * result + followUpCardNumber.hashCode()
result = 31 * result + (cardType?.hashCode() ?: 0)
result = 31 * result + (validFrom?.hashCode() ?: 0)
result = 31 * result + (validTo?.hashCode() ?: 0)
result = 31 * result + (mediaName?.hashCode() ?: 0)
return result
}
override fun toString(): String {
return super.toString() + " $mediaName $cardNumber (follow up: ${followUpCardNumber ?: "-"})"
}
}

View File

@ -0,0 +1,19 @@
package net.dankito.fints.messages.datenelemente.implementierte.tan
import net.dankito.fints.messages.datenelemente.implementierte.ICodeEnum
/**
* dient der Klassifizierung der gesamten dem Kunden zugeordneten TAN-Medien. Bei
* Geschäftsvorfällen zum Management des TAN-Generators kann aus diesen nach folgender
* Codierung selektiert werden.
*/
enum class TanMedienArtVersion(override val code: String) : ICodeEnum {
Alle("0"),
Aktiv("1"),
Verfuegbar("2")
}

View File

@ -0,0 +1,40 @@
package net.dankito.fints.messages.datenelemente.implementierte.tan
/**
* Informationen zu Art und Parametrisierung von TAN-Medien. Als TAN-Medien werden sowohl
* TAN-Listen als auch DK-TAN-Generatoren / Karten oder Mobiltelefone sowie bilateral
* vereinbarte Medien bezeichnet.
*
* Wird das Datenelement TAN-Medium-Klasse mit B (bilateral vereinbart) belegt, so muss im Element Sicherheitsfunktion, kodiert die entsprechende Sicherheitsfunktion in der DEG Verfahrensparameter Zwei-Schritt-Verfahren referenziert werden.
*/
open class TanMedium(
val mediumClass: TanMediumKlasseVersion,
val status: TanMediumStatus
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as TanMedium
if (mediumClass != other.mediumClass) return false
if (status != other.status) return false
return true
}
override fun hashCode(): Int {
var result = mediumClass.hashCode()
result = 31 * result + status.hashCode()
return result
}
override fun toString(): String {
return "$mediumClass $status"
}
}

View File

@ -0,0 +1,24 @@
package net.dankito.fints.messages.datenelemente.implementierte.tan
import net.dankito.fints.messages.datenelemente.implementierte.ICodeEnum
/**
* dient der Klassifizierung der möglichen TAN-Medien. Bei Geschäftsvorfällen zum
* Management der TAN-Medien kann aus diesen nach folgender Codierung selektiert werden.
*/
enum class TanMediumKlasseVersion(override val code: String, val supportedHkTabVersions: List<Int>) : ICodeEnum {
AlleMedien("A", listOf(4, 5)),
Liste("L", listOf(1, 2, 3, 4, 5)),
TanGenerator("G", listOf(1, 2, 3, 4, 5)),
MobiltelefonMitMobileTan("M", listOf(2, 3, 4, 5)),
Secoder("S", listOf(3, 4, 5)),
BilateralVereinbart("B", listOf(5))
}

View File

@ -0,0 +1,33 @@
package net.dankito.fints.messages.datenelemente.implementierte.tan
import net.dankito.fints.messages.datenelemente.implementierte.ICodeEnum
/**
* Gibt an, in welchem Status sich ein TAN-Medium befindet.
*
*/
enum class TanMediumStatus(override val code: String) : ICodeEnum {
/**
* Die Bank zeigt an, dass es eine TAN-Verifikation gegen dieses Medium vornimmt.
*/
Aktiv("1"),
/**
* Das Medium kann genutzt werden, muss aber zuvor mit TAN-Generator an- bzw. ummelden (HKTAU) aktiv gemeldet werden.
*/
Verfuegbar("2"),
/**
* Mit der ersten Nutzung der Folgekarte wird die zur Zeit aktive Karte gesperrt.
*/
AktivFolgekarte("3"),
/**
* Das Medium kann mit dem Geschäftsvorfall TAN-Medium an- bzw. ummelden (HKTAU) aktiv gemeldet werden.
* Die aktuelle Karte kann dann nicht mehr genutzt werden.
*/
VerfuegbarFolgekarte("4")
}

View File

@ -13,6 +13,8 @@ enum class CustomerSegmentId(override val id: String) : ISegmentId {
Tan("HKTAN"),
TanMediaList("HKTAB"),
Balance("HKSAL"),
AccountTransactionsMt940("HKKAZ"),

View File

@ -0,0 +1,34 @@
package net.dankito.fints.messages.segmente.implementierte.tan
import net.dankito.fints.messages.Existenzstatus
import net.dankito.fints.messages.datenelemente.abgeleiteteformate.Code
import net.dankito.fints.messages.datenelemente.implementierte.allCodes
import net.dankito.fints.messages.datenelemente.implementierte.tan.TanMedienArtVersion
import net.dankito.fints.messages.datenelemente.implementierte.tan.TanMediumKlasseVersion
import net.dankito.fints.messages.datenelementgruppen.implementierte.Segmentkopf
import net.dankito.fints.messages.segmente.Segment
import net.dankito.fints.messages.segmente.id.CustomerSegmentId
open class TanGeneratorListeAnzeigen(
segmentVersion: Int,
segmentNumber: Int,
tanMediaKind: TanMedienArtVersion = TanMedienArtVersion.Alle,
tanMediumClass: TanMediumKlasseVersion = TanMediumKlasseVersion.AlleMedien
)
: Segment(listOf(
Segmentkopf(CustomerSegmentId.TanMediaList, segmentVersion, segmentNumber),
Code(tanMediaKind, allCodes<TanMedienArtVersion>(), Existenzstatus.Mandatory),
Code(tanMediumClass, allCodes<TanMediumKlasseVersion>(), Existenzstatus.Mandatory)
)) {
init {
val supportedMediaClasses = TanMediumKlasseVersion.values().filter { it.supportedHkTabVersions.contains(segmentVersion) }
if (supportedMediaClasses.contains(tanMediumClass) == false) {
throw UnsupportedOperationException("Value $tanMediumClass for TAN medium class is not valid for HKTAB version $segmentVersion. " +
"Supported values are: " + TanMediumKlasseVersion.values().filter { it.supportedHkTabVersions.contains(segmentVersion) }.map { it.code })
}
}
}

View File

@ -29,6 +29,8 @@ enum class InstituteSegmentId(override val id: String) : ISegmentId {
Tan("HITAN"),
TanMediaList("HITAB"),
Balance("HISAL"),
AccountTransactionsMt940("HIKAZ")

View File

@ -93,6 +93,7 @@ open class ResponseParser @JvmOverloads constructor(
InstituteSegmentId.TanInfo.id -> parseTanInfo(segment, segmentId, dataElementGroups)
InstituteSegmentId.Tan.id -> parseTanResponse(segment, dataElementGroups)
InstituteSegmentId.TanMediaList.id -> parseTanMediaList(segment, dataElementGroups)
InstituteSegmentId.Balance.id -> parseBalanceSegment(segment, dataElementGroups)
InstituteSegmentId.AccountTransactionsMt940.id -> parseMt940AccountTransactions(segment, dataElementGroups)
@ -395,6 +396,52 @@ open class ResponseParser @JvmOverloads constructor(
)
}
protected open fun parseTanMediaList(segment: String, dataElementGroups: List<String>): TanMediaList {
val usageOption = parseCodeEnum(dataElementGroups[1], TanEinsatzOption.values())
val segmentVersion = parseInt(getDataElements(dataElementGroups[0])[2])
return TanMediaList(usageOption,
parseTanMedia(segmentVersion, dataElementGroups.subList(2, dataElementGroups.size)),
segment)
}
protected open fun parseTanMedia(hitabVersion: Int, dataElementGroups: List<String>): List<TanMedium> {
return dataElementGroups.map { getDataElements(it) }.map { parseTanMedium(hitabVersion, it) }
}
protected open fun parseTanMedium(hitabVersion: Int, dataElements: List<String>): TanMedium {
val mediumClassCode = dataElements[0]
val mediumClass = parseCodeEnum(mediumClassCode, TanMediumKlasseVersion.values())
if (mediumClass.supportedHkTabVersions.contains(hitabVersion) == false) {
throw UnsupportedOperationException("$mediumClassCode is not a valid medium class for HITAB version $hitabVersion. " +
"Supported values are: " + TanMediumKlasseVersion.values().filter { it.supportedHkTabVersions.contains(hitabVersion) }.map { it.code })
}
val status = parseCodeEnum(dataElements[1], TanMediumStatus.values())
// TODO: may also parse 'Letzte Benutzung' (second last element) and 'Freigeschaltet am' (last element)
val remainingDataElements = dataElements.subList(2, dataElements.size - 2)
return when (mediumClass) {
TanMediumKlasseVersion.TanGenerator -> parseTanGeneratorTanMedium(mediumClass, status, hitabVersion, remainingDataElements)
else -> TanMedium(mediumClass, status)
}
}
protected open fun parseTanGeneratorTanMedium(mediumClass: TanMediumKlasseVersion, status: TanMediumStatus,
hitabVersion: Int, dataElements: List<String>): TanGeneratorTanMedium {
val cardType = if (hitabVersion < 2) null else parseStringToNullIfEmpty(dataElements[2]) // TODO: may parse to number
// TODO: may also parse account info
val validFrom = if (hitabVersion < 2) null else parseNullableDate(dataElements[8])
val validTo = if (hitabVersion < 2) null else parseNullableDate(dataElements[9])
val mediaName = if (hitabVersion < 2) null else parseStringToNullIfEmpty(dataElements[10])
return TanGeneratorTanMedium(mediumClass, status, parseString(dataElements[0]), parseStringToNullIfEmpty(dataElements[1]),
cardType, validFrom, validTo, mediaName)
}
protected open fun parseBalanceSegment(segment: String, dataElementGroups: List<String>): BalanceSegment {
// dataElementGroups[1] is account details

View File

@ -0,0 +1,11 @@
package net.dankito.fints.response.client
import net.dankito.fints.response.Response
import net.dankito.fints.response.segments.TanMediaList
open class GetTanMediaListResponse(
response: Response,
val tanMediaList: TanMediaList?
)
: FinTsClientResponse(response)

View File

@ -0,0 +1,12 @@
package net.dankito.fints.response.segments
import net.dankito.fints.messages.datenelemente.implementierte.tan.TanEinsatzOption
import net.dankito.fints.messages.datenelemente.implementierte.tan.TanMedium
open class TanMediaList(
val usageOption: TanEinsatzOption,
val tanMedia: List<TanMedium>,
segmentString: String
)
: ReceivedSegment(segmentString)

View File

@ -5,6 +5,9 @@ import net.dankito.fints.messages.datenelemente.abgeleiteteformate.Laenderkennze
import net.dankito.fints.messages.datenelemente.implementierte.Dialogsprache
import net.dankito.fints.messages.datenelemente.implementierte.KundensystemStatus
import net.dankito.fints.messages.datenelemente.implementierte.KundensystemStatusWerte
import net.dankito.fints.messages.datenelemente.implementierte.tan.TanEinsatzOption
import net.dankito.fints.messages.datenelemente.implementierte.tan.TanMedienArtVersion
import net.dankito.fints.messages.datenelemente.implementierte.tan.TanMediumKlasseVersion
import net.dankito.fints.model.*
import net.dankito.fints.model.mapper.BankDataMapper
import net.dankito.fints.response.client.FinTsClientResponse
@ -132,6 +135,34 @@ class FinTsClientTest {
}
@Test
fun getTanMediaList() {
// when
val result = underTest.getTanMediaList(Bank, Customer, TanMedienArtVersion.Alle, TanMediumKlasseVersion.AlleMedien)
// then
assertThat(result.isSuccessful).isTrue()
assertThat(result.tanMediaList).isNotNull()
assertThat(result.tanMediaList!!.usageOption).isEqualByComparingTo(TanEinsatzOption.KundeKannGenauEinMediumZuEinerZeitNutzen) // TODO: may adjust to your value
assertThat(result.tanMediaList!!.tanMedia).isNotEmpty()
}
@Ignore // only works with banks that don't support HKTAB version 5
@Test(expected = UnsupportedOperationException::class)
fun getTanMediaList_UnsupportedTanMediumClass() {
// when
underTest.getTanMediaList(Bank, Customer, TanMedienArtVersion.Alle, TanMediumKlasseVersion.BilateralVereinbart)
// then
// exception gets thrown
}
@Test
fun testBankTransfer() {

View File

@ -6,7 +6,7 @@ import net.dankito.fints.messages.datenelemente.implementierte.HbciVersion
import net.dankito.fints.messages.datenelemente.implementierte.signatur.Sicherheitsfunktion
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.datenelemente.implementierte.tan.*
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
@ -683,6 +683,33 @@ class ResponseParserTest : FinTsTestBase() {
}
@Test
fun parseTanMediaListResponse() {
// given
val oldCardNumber = "5109972878"
val followUpCardNumber = "5200310149"
val mediaName = "EC-Card (Debitkarte)"
// when
val result = underTest.parse("HITAB:5:4:3+1+G:3:$oldCardNumber:$followUpCardNumber:::::::::$mediaName::::::::+G:2:$followUpCardNumber::::::::::$mediaName::::::::'")
// then
assertSuccessfullyParsedSegment(result, InstituteSegmentId.TanMediaList, 5, 4, 3)
assertThat(result.isStrongAuthenticationRequired).isFalse()
result.getFirstSegmentById<TanMediaList>(InstituteSegmentId.TanMediaList)?.let { segment ->
assertThat(segment.usageOption).isEqualTo(TanEinsatzOption.KundeKannGenauEinMediumZuEinerZeitNutzen)
assertThat(segment.tanMedia).containsOnly(
TanGeneratorTanMedium(TanMediumKlasseVersion.TanGenerator, TanMediumStatus.AktivFolgekarte, oldCardNumber, followUpCardNumber, null, null, null, mediaName),
TanGeneratorTanMedium(TanMediumKlasseVersion.TanGenerator, TanMediumStatus.Verfuegbar, followUpCardNumber, null, null, null, null, mediaName)
)
}
?: run { Assert.fail("No segment of type TanMediaList found in ${result.receivedSegments}") }
}
@Test
fun parseBalance() {