Removed ui, persistence and rest projects (will be moved to separate repositories) and deactivated tools projects.

Removed Android target and using now JVM instead of JVM6.
Updated to Kotlin 1.6.10 which works much better for KMPP. Could therefore remove packForXcode() tasks
This commit is contained in:
dankito 2022-02-11 23:53:35 +01:00
parent 3c096d655f
commit f637897dc3
649 changed files with 750 additions and 265091 deletions

View File

@ -2,99 +2,12 @@
ext {
appVersionName = '1.0.0-Alpha-10'
appVersionCode = 10
/* MPP / basic dependencies */
kotlinVersion = '1.4.21'
kotlinCoroutinesVersion = "1.3.7"
ktorVersion = "1.4.2"
javaUtilsVersion = '1.0.18'
luceneUtilsVersion = "0.6.0"
textExtractorVersion = "0.6.0"
textInfoExtractorVersion = "1.0.1"
commonsCsvVersion = "1.8"
hbci4jVersion = '3.1.37'
/* iOS */
iOSIsRealDevice = false
embedBitcodeValue = "marker" // Use "marker" to embed the bitcode marker (for debug builds)
// embedBitcodeValue = "bitcode" // for release binaries
/* Java */
faviconFinderVersion = "1.0.0"
/* Android */
androidCompileSdkVersion = 30
androidBuildToolsVersion = "30.0.3"
androidMinSdkVersion = 21 // TODO: fix SSLv3 / TLS and set back to 16
androidTargetSdkVersion = 30
fileChooserDialogVersion = "1.3.1-androidx"
androidUtilsVersion = '1.1.2'
fastAdapterVersion = "5.2.4"
materialDrawerVersion = "8.1.6"
clansFloatingActionButtonVersion = '1.6.4'
autocompleteVersion = "1.1.0"
zxingVersion = "3.3.0"
scytaleVersion = "1.0.1"
multiDexVersion = "2.0.1"
appCompatVersion = "1.1.0"
androidXCoreVersion = "1.3.1"
androidXNavigationVersion = "2.3.0"
androidXBiometricVersion = "1.0.1"
constraintLayoutVersion = "1.1.3"
materialComponentsVersion = "1.1.0"
daggerVersion = "2.27"
roomVersion = "2.2.5"
sqlCipherVersion = "4.4.0"
bcryptVersion = "0.9.0"
/* JavaFX */
javaFxUtilsVersion = '1.0.9'
/* Test */
@ -113,17 +26,13 @@ ext {
}
buildscript {
ext.kotlin_version = '1.3.72'
// ext.kotlin_version = '1.4.10'
repositories {
mavenCentral()
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
// Nexus staging plugin has to be downgraded to 0.10.0 to be applicable to sub projects, see https://github.com/UweTrottmann/SeriesGuide/commit/ca33e8ad2fa6cc5c426450c8aef3417ba073ca7f
classpath "io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.10.0"
@ -142,10 +51,9 @@ allprojects {
mavenLocal()
mavenCentral()
google()
jcenter()
}
group 'net.dankito.banking'
group 'net.codinux.banking'
version appVersionName
}
@ -154,26 +62,6 @@ allprojects {
task jarAll {
dependsOn = [
"common:jvmJar",
"fints4k:jvm6Jar",
"fints4k-jvm:jar",
"BankFinder:jvmJar",
"EpcQrCodeParser:jvmJar",
"BankingUiCommon:jvmJar",
"fints4kBankingClient:jvmJar",
"BankingUiCommon:jvmJar",
"BankingJavaFxControls:jar",
"BankingJavaFxApp:jar"
]
}
task packAllForXcode {
dependsOn = [
"common:packForXcode",
"fints4k:packForXcode",
"BankFinder:packForXcode",
"EpcQrCodeParser:packForXcode",
"BankingUiCommon:packForXcode",
"fints4kBankingClient:packForXcode",
"BankingUiNativeIntegration:packForXcode"
"fints4k:jvmJar",
]
}

View File

@ -1,41 +1,52 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "com.android.library"
id "maven-publish"
}
ext.artifactName = "multiplatform-utils"
def frameworkName = "MultiplatformUtils"
kotlin {
jvm {
compilations.main.kotlinOptions {
jvmTarget = "1.6"
compilations.all {
kotlinOptions.jvmTarget = '1.8'
}
withJava()
testRuns["test"].executionTask.configure {
useJUnitPlatform()
}
}
targets {
final def iOSTarget = iOSIsRealDevice ? presets.iosArm64 : presets.iosX64
// js(BOTH) {
// browser {
// commonWebpackConfig {
// cssSupport.enabled = true
// }
// }
// }
fromPreset(iOSTarget, 'ios') {
ios {
binaries {
framework {
baseName = frameworkName
baseName = "MultiplatformUtils"
}
}
}
embedBitcode(embedBitcodeValue)
}
}
}
}
// def hostOs = System.getProperty("os.name")
// def isMingwX64 = hostOs.startsWith("Windows")
// def nativeTarget
// if (hostOs == "Mac OS X") nativeTarget = macosX64('native') { binaries.executable() }
// else if (hostOs == "Linux") nativeTarget = linuxX64("native") { binaries.executable() }
// else if (isMingwX64) nativeTarget = mingwX64("native") { binaries.executable() }
// else throw new GradleException("Host OS is not supported in Kotlin/Native.")
sourceSets {
commonMain {
dependencies {
implementation kotlin("stdlib-common")
}
}
@ -51,7 +62,6 @@ kotlin {
jvmMain {
dependencies {
api kotlin("stdlib-jdk7")
compileOnly "org.slf4j:slf4j-api:$slf4jVersion"
@ -79,97 +89,9 @@ kotlin {
iosMain {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlinVersion"
}
}
}
}
task copyFramework {
def buildType = project.findProperty('kotlin.build.type') ?: 'DEBUG'
def target = project.findProperty('kotlin.target') ?: 'ios'
def framework = kotlin.targets."$target".binaries.getFramework(buildType)
dependsOn framework.linkTask
doLast {
def srcFile = framework.outputFile
def targetDir = getProperty('configuration.build.dir')
copy {
from srcFile.parent
into targetDir
include "${frameworkName}.framework/**"
include "${frameworkName}.framework.dSYM"
}
}
}
// Task to generate iOS framework for xcode projects.
task packForXcode(type: Sync) {
final File frameworkDir = new File(buildDir, "xcode-frameworks")
final String mode = project.findProperty("XCODE_CONFIGURATION")?.toUpperCase() ?: 'DEBUG'
final def framework = kotlin.targets.ios.binaries.getFramework("", mode)
inputs.property "mode", mode
dependsOn framework.linkTask
from { framework.outputFile.parentFile }
into frameworkDir
doLast {
new File(frameworkDir, 'gradlew').with {
text = "#!/bin/bash\nexport 'JAVA_HOME=${System.getProperty("java.home")}'\ncd '${rootProject.rootDir}'\n./gradlew \$@\n"
setExecutable(true)
}
}
}
// Run packForXcode when building.
tasks.build.dependsOn packForXcode
android {
compileSdkVersion androidCompileSdkVersion
defaultConfig {
minSdkVersion androidMinSdkVersion
targetSdkVersion androidTargetSdkVersion
versionName version
versionCode appVersionCode
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
packagingOptions {
pickFirst 'META-INF/ktor-http.kotlin_module'
pickFirst 'META-INF/kotlinx-io.kotlin_module'
pickFirst 'META-INF/atomicfu.kotlin_module'
pickFirst 'META-INF/ktor-utils.kotlin_module'
pickFirst 'META-INF/kotlinx-coroutines-io.kotlin_module'
pickFirst 'META-INF/ktor-client-core.kotlin_module'
pickFirst 'META-INF/DEPENDENCIES'
pickFirst 'META-INF/NOTICE'
pickFirst 'META-INF/LICENSE'
pickFirst 'META-INF/LICENSE.txt'
pickFirst 'META-INF/NOTICE.txt'
}
lintOptions {
abortOnError false
}
}

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="net.dankito.utils.multiplatform">
</manifest>

View File

@ -1,10 +0,0 @@
apply plugin: 'java-library'
apply plugin: 'kotlin'
sourceCompatibility = "1.7"
targetCompatibility = "1.7"
dependencies {
api project(":fints4k")
}

View File

@ -1,96 +1,75 @@
buildscript {
repositories {
jcenter()
}
}
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "com.android.library"
id "maven-publish"
}
kotlin {
jvm("jvm6") {
compilations.main.kotlinOptions {
jvmTarget = "1.6"
jvm {
compilations.all {
kotlinOptions.jvmTarget = '1.8'
}
withJava()
testRuns["test"].executionTask.configure {
useJUnitPlatform()
}
}
android()
// js(BOTH) {
// browser {
// commonWebpackConfig {
// cssSupport.enabled = true
// }
// }
// }
targets {
final def iOSTarget = iOSIsRealDevice ? presets.iosArm64 : presets.iosX64
fromPreset(iOSTarget, 'ios') {
ios {
binaries {
framework {
baseName = "fints4k"
embedBitcode(embedBitcodeValue)
export(project(":common"))
}
}
}
}
js() {
nodejs {
testTask {
enabled = false
}
}
browser {
testTask {
enabled = false
}
}
}
// def hostOs = System.getProperty("os.name")
// def isMingwX64 = hostOs.startsWith("Windows")
// def nativeTarget
// if (hostOs == "Mac OS X") nativeTarget = macosX64('native') { binaries.executable() }
// else if (hostOs == "Linux") nativeTarget = linuxX64("native") { binaries.executable() }
// else if (isMingwX64) nativeTarget = mingwX64("native") { binaries.executable() }
// else throw new GradleException("Host OS is not supported in Kotlin/Native.")
sourceSets {
commonMain {
dependencies {
implementation kotlin("stdlib-common")
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-common:$kotlinCoroutinesVersion"
api project(":common")
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion"
implementation "io.ktor:ktor-client-core:$ktorVersion"
}
}
commonTest {
dependencies {
implementation kotlin("test-common")
implementation kotlin("test-annotations-common")
implementation kotlin("test")
implementation "ch.tutteli.atrium:atrium-fluent-en_GB-common:$atriumVersion"
}
}
jvm6Main {
jvmMain {
dependencies {
// implementation "io.ktor:ktor-client-cio:$ktorVersion"
implementation "io.ktor:ktor-client-okhttp:$ktorVersion"
implementation "org.slf4j:slf4j-api:$slf4jVersion"
}
}
jvm6Test {
jvmTest {
dependencies {
implementation kotlin("test-junit")
implementation "org.junit.jupiter:junit-jupiter:$junit5Version"
runtimeOnly "org.junit.jupiter:junit-jupiter-engine:$junit5Version"
implementation "org.assertj:assertj-core:$assertJVersion"
implementation "org.mockito:mockito-core:$mockitoVersion"
@ -98,9 +77,9 @@ kotlin {
implementation "ch.tutteli.atrium:atrium-fluent-en_GB:$atriumVersion"
implementation project(":BankingUiCommon")
implementation project(":BankFinder")
implementation project(":fints4kBankingClient")
// implementation project(":BankingUiCommon")
// implementation project(":BankFinder")
// implementation project(":fints4kBankingClient")
implementation "org.apache.commons:commons-csv:1.8"
@ -111,111 +90,36 @@ kotlin {
}
androidMain {
dependsOn jvm6Main
// jsMain {
// dependencies {
// implementation "io.ktor:ktor-client-js:$ktorVersion"
// }
// }
//
// jsTest {
//
// }
dependencies {
implementation "io.ktor:ktor-client-android:$ktorVersion"
}
}
iosMain {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlinVersion"
// ktor Native needs "-native-mt" coroutines version. Export it so that referencing applications don't need to import it on their own
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion-native-mt")
implementation "io.ktor:ktor-client-ios:$ktorVersion"
implementation "io.ktor:ktor-client-core-native:$ktorVersion"
}
}
jsMain {
dependsOn commonMain
nativeMain {
dependencies {
api kotlin("stdlib-js")
// ktor Native needs "-native-mt" coroutines version. Export it so that referencing applications don't need to import it on their own
api"org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion-native-mt"
implementation "io.ktor:ktor-client-js:$ktorVersion"
implementation "io.ktor:ktor-client-encoding-js:$ktorVersion"
implementation "io.ktor:ktor-client-js-kotlinMultiplatform:$ktorVersion"
}
}
jsTest {
dependencies {
implementation kotlin("test-js")
implementation "ch.tutteli.atrium:atrium-fluent-en_GB-js:$atriumVersion"
// requires cURL to be installed on your system
implementation "io.ktor:ktor-client-curl:$ktorVersion"
}
}
}
}
// Task to generate iOS framework for xcode projects.
task packForXcode(type: Sync) {
final File frameworkDir = new File(buildDir, "xcode-frameworks")
final String mode = project.findProperty("XCODE_CONFIGURATION")?.toUpperCase() ?: 'DEBUG'
final def framework = kotlin.targets.ios.binaries.getFramework("", mode)
inputs.property "mode", mode
dependsOn framework.linkTask
from { framework.outputFile.parentFile }
into frameworkDir
doLast {
new File(frameworkDir, 'gradlew').with {
text = "#!/bin/bash\nexport 'JAVA_HOME=${System.getProperty("java.home")}'\ncd '${rootProject.rootDir}'\n./gradlew \$@\n"
setExecutable(true)
}
}
}
// Run packForXcode when building.
tasks.build.dependsOn packForXcode
android {
compileSdkVersion androidCompileSdkVersion
defaultConfig {
minSdkVersion androidMinSdkVersion
targetSdkVersion androidTargetSdkVersion
versionName version
versionCode appVersionCode
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
packagingOptions {
pickFirst 'META-INF/ktor-http.kotlin_module'
pickFirst 'META-INF/kotlinx-io.kotlin_module'
pickFirst 'META-INF/atomicfu.kotlin_module'
pickFirst 'META-INF/ktor-utils.kotlin_module'
pickFirst 'META-INF/kotlinx-coroutines-io.kotlin_module'
pickFirst 'META-INF/ktor-client-core.kotlin_module'
pickFirst 'META-INF/DEPENDENCIES'
pickFirst 'META-INF/NOTICE'
pickFirst 'META-INF/LICENSE'
pickFirst 'META-INF/LICENSE.txt'
pickFirst 'META-INF/NOTICE.txt'
}
lintOptions {
abortOnError false
}
}

View File

@ -24,7 +24,7 @@ open class MessageBuilderResult(
}
open val getHighestAllowedVersion: Int?
get() = allowedVersions.max()
get() = allowedVersions.maxOrNull()
open fun isSendEnteredTanMessage(): Boolean {
// contains only a ZweiSchrittTanEinreichung segment

View File

@ -2,10 +2,10 @@ package net.dankito.banking.fints.webclient
import io.ktor.client.HttpClient
import io.ktor.client.request.post
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.readText
import io.ktor.content.TextContent
import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.contentType
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
@ -38,11 +38,12 @@ open class KtorWebClient : IWebClient {
override fun post(url: String, body: String, contentType: String, userAgent: String, callback: (WebClientResponse) -> Unit) {
GlobalScope.async {
try {
val clientResponse = client.post<HttpResponse>(url) {
this.body = TextContent(body, contentType = ContentType.Application.OctetStream)
val clientResponse = client.post(url) {
contentType(ContentType.Application.OctetStream)
setBody(body)
}
val responseBody = clientResponse.readText()
val responseBody = clientResponse.bodyAsText()
callback(WebClientResponse(clientResponse.status.value == 200, clientResponse.status.value, body = responseBody))
} catch (e: Exception) {

View File

@ -15,6 +15,7 @@ import net.dankito.banking.fints.util.FinTsUtils
import net.dankito.utils.multiplatform.Date
import net.dankito.utils.multiplatform.Month
import kotlin.test.AfterTest
import kotlin.test.Ignore
import kotlin.test.Test
@ -191,6 +192,7 @@ class MessageBuilderTest : FinTsTestBase() {
))
}
@Ignore
@Test
fun createGetTransactionsMessage_WithContinuationIdSet() {

View File

@ -17,6 +17,7 @@ import net.dankito.banking.fints.extensions.isFalse
import net.dankito.banking.fints.extensions.isTrue
import net.dankito.banking.fints.model.Amount
import net.dankito.utils.multiplatform.Date
import kotlin.test.Ignore
import kotlin.test.Test
import kotlin.test.fail
@ -460,6 +461,7 @@ class ResponseParserTest : FinTsTestBase() {
?: run { fail("No segment of type UserParameters found in ${result.receivedSegments}") }
}
@Ignore
@Test
fun parseAccountInfo() {

View File

@ -1,184 +0,0 @@
package net.dankito.banking.fints;
import net.dankito.banking.fints.banks.IBankFinder;
import net.dankito.banking.fints.banks.InMemoryBankFinder;
import net.dankito.banking.fints.callback.FinTsClientCallback;
import net.dankito.banking.fints.callback.SimpleFinTsClientCallback;
import net.dankito.banking.fints.model.AccountData;
import net.dankito.banking.fints.model.AccountFeature;
import net.dankito.banking.fints.model.AccountTransaction;
import net.dankito.banking.fints.model.BankData;
import net.dankito.banking.bankfinder.BankInfo;
import net.dankito.banking.fints.model.BankTransferData;
import net.dankito.banking.fints.model.CustomerData;
import net.dankito.banking.fints.model.EnterTanGeneratorAtcResult;
import net.dankito.banking.fints.model.EnterTanResult;
import net.dankito.banking.fints.model.TanChallenge;
import net.dankito.banking.fints.model.TanMethod;
import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.TanGeneratorTanMedium;
import net.dankito.banking.fints.model.mapper.BankDataMapper;
import net.dankito.banking.fints.response.client.AddAccountResponse;
import net.dankito.banking.fints.response.client.FinTsClientResponse;
import net.dankito.banking.fints.response.client.GetTransactionsResponse;
import net.dankito.banking.fints.util.Java8Base64Service;
import java.math.BigDecimal;
import java.util.List;
public class JavaShowcase {
public static void main(String[] args) {
JavaShowcase showcase = new JavaShowcase();
showcase.basicShowcase();
showcase.advancedShowcase();
}
private void basicShowcase() {
// Set your bank code (Bankleitzahl) here.
// BankInfo contains e.g. a bank's FinTS server address, country code and BIC (needed for money transfer)
List<BankInfo> foundBanks = new InMemoryBankFinder().findBankByNameBankCodeOrCity("<bank code, bank name or city>");
if (foundBanks.isEmpty() == false) { // please also check if bank supports FinTS 3.0
BankData bank = new BankDataMapper().mapFromBankInfo(foundBanks.get(0));
// set your customer data (customerId = username you use to log in; pin = online banking pin / password)
CustomerData customer = new CustomerData("<customer_id>", "<pin>");
FinTsClientCallback callback = new SimpleFinTsClientCallback(); // see advanced showcase for configuring callback
FinTsClient finTsClient = new FinTsClient(callback, new Java8Base64Service());
AddAccountResponse addAccountResponse = finTsClient.addAccount(bank, customer);
if (addAccountResponse.isSuccessful()) {
System.out.println("Successfully added account for " + bank.getBankCode() + " " + customer.getCustomerId());
if (addAccountResponse.getBookedTransactions().isEmpty() == false) {
System.out.println("Account transactions of last 90 days:");
showGetTransactionsResponse(addAccountResponse);
}
}
else {
System.out.println("Could not add account for " + bank.getBankCode() + " " + customer.getCustomerId() + ":");
showResponseError(addAccountResponse);
}
// see advanced show case what else you can do with this library, e.g. retrieving all account transactions and transferring money
}
}
private void advancedShowcase() {
IBankFinder bankFinder = new InMemoryBankFinder();
// Set your bank code (Bankleitzahl) here. Or create BankData manually. Required fields are:
// bankCode, bankCountryCode (Germany = 280), finTs3ServerAddress and for bank transfers bic
List<BankInfo> foundBanks = bankFinder.findBankByBankCode("<bank_code>");
if (foundBanks.isEmpty() == false) { // please also check if bank supports FinTS 3.0
BankData bank = new BankDataMapper().mapFromBankInfo(foundBanks.get(0));
// set your customer data (customerId = Kontonummer in most cases, pin = online banking pin)
CustomerData customer = new CustomerData("<customer_id>", "<pin>");
FinTsClientCallback callback = new FinTsClientCallback() {
@Override
public TanMethod askUserForTanMethod(List<? extends TanMethod> supportedTanMethods, TanMethod suggestedTanMethod) {
// E.g. show a dialog to ask for user's TAN method.
// In most cases it's senseful to simply return suggestedTanMethod and to let
// user select TAN method when entering TAN is required (see enterTan() below)
return suggestedTanMethod;
}
@Override
public EnterTanResult enterTan(CustomerData customer, TanChallenge tanChallenge) {
// e.g. show
// - Android: net.dankito.banking.ui.android.dialogs.EnterTanDialog
// - JavaFX: net.dankito.banking.ui.javafx.dialogs.tan.EnterTanDialog
return EnterTanResult.Companion.userDidNotEnterTan(); // user did not enter TAN. aborts operation
}
@Override
public EnterTanGeneratorAtcResult enterTanGeneratorAtc(CustomerData customer, TanGeneratorTanMedium tanMedium) {
// needed only in rare cases to synchronize TAN generator for chipTAN methods. E.g. show
// - Android: net.dankito.banking.ui.android.dialogs.EnterAtcDialog
return EnterTanGeneratorAtcResult.Companion.userDidNotEnterAtc(); // user did not enter TAN and ATC. aborts operation
}
};
// may also check if FinTsClientForCustomer fits your needs, avoids passing bank and customer to each method
FinTsClient finTsClient = new FinTsClient(callback, new Java8Base64Service());
AddAccountResponse addAccountResponse = finTsClient.addAccount(bank, customer);
if (addAccountResponse.isSuccessful() == false) {
System.out.println("Could not add account for " + bank.getBankCode() + " " + customer.getCustomerId() + ":");
showResponseError(addAccountResponse);
return;
}
System.out.println("Successfully added account for " + bank.getBankCode() + " " + customer.getCustomerId());
for (AccountData account : customer.getAccounts()) { // accounts are now retrieved
if (account.supportsFeature(AccountFeature.RetrieveAccountTransactions)) {
// Most banks support retrieving account transactions of last 90 without TAN, may also your bank.
// Alternatively call getTransactions() to retrieve all account transactions. But then entering a TAN is required.
GetTransactionsResponse response = finTsClient.tryGetTransactionsOfLast90DaysWithoutTan(bank, customer, account);
showGetTransactionsResponse(response);
}
if (account.supportsFeature(AccountFeature.TransferMoney)) {
// Transfer 0.01 to yourself. Transferring money to one self doesn't require a TAN.
BankTransferData data = new BankTransferData(customer.getName(), account.getIban(), bank.getBic(),
new BigDecimal("0.01"), "Give me some money", false);
FinTsClientResponse transferMoneyResponse = finTsClient.doBankTransfer(data, bank, customer, account);
if (transferMoneyResponse.isSuccessful()) {
System.out.println("Successfully transferred " + data.getAmount() + " to " + data.getCreditorIban());
}
else {
showResponseError(transferMoneyResponse);
}
}
}
}
}
private static void showGetTransactionsResponse(GetTransactionsResponse response) {
if (response.isSuccessful()) {
System.out.println("Balance (Saldo) = " + response.getBalance());
System.out.println("Account transactions (Umsätze):");
for (AccountTransaction transaction : response.getBookedTransactions()) {
System.out.println(transaction.toString());
}
}
else {
if (response.isStrongAuthenticationRequired()) {
System.out.println("Sorry, your bank doesn't support retrieving account " +
"transactions of last 90 days without TAN");
}
else {
System.out.println("An error occurred:");
showResponseError(response);
}
}
}
private static void showResponseError(FinTsClientResponse response) {
if (response.getException() != null) { // something severe occurred
System.out.println(response.getException().getMessage());
}
// error messages retrieved from bank (e.g. PIN is wrong, message contains errors, ...)
for (String retrievedErrorMessage : response.getErrorsToShowToUser()) {
System.out.println(retrievedErrorMessage);
}
}
}

View File

@ -1,269 +0,0 @@
package net.dankito.banking.fints
import ch.tutteli.atrium.api.fluent.en_GB.*
import ch.tutteli.atrium.api.verbs.expect
import net.dankito.banking.bankfinder.InMemoryBankFinder
import net.dankito.banking.fints.callback.FinTsClientCallback
import net.dankito.banking.fints.extensions.isTrue
import net.dankito.banking.fints.extensions.isFalse
import net.dankito.banking.fints.messages.datenelemente.implementierte.Dialogsprache
import net.dankito.banking.fints.messages.datenelemente.implementierte.KundensystemStatus
import net.dankito.banking.fints.messages.datenelemente.implementierte.KundensystemStatusWerte
import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.TanEinsatzOption
import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.TanGeneratorTanMedium
import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.TanMedienArtVersion
import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.TanMediumKlasse
import net.dankito.banking.fints.messages.segmente.id.CustomerSegmentId
import net.dankito.banking.fints.model.*
import net.dankito.banking.fints.response.client.*
import net.dankito.utils.multiplatform.Date
import net.dankito.utils.multiplatform.DateFormatter
import net.dankito.utils.multiplatform.UUID
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicReference
import kotlin.test.DefaultAsserter.fail
import kotlin.test.Ignore
import kotlin.test.Test
@Ignore // not an automatic test, supply your settings below
open class FinTsClientTestBase {
companion object {
// TODO: add your settings here:
val BankCode = "<your bank code (BLZ) here>"
val CustomerId = "<your customer id (Online-Banking Login Name) here>"
val Password = "<your PIN (Online-Banking Passwort) here>"
val DateTimeFormatForUniqueBankTransferReference = DateFormatter("yyyy-MM-dd'T'HH:mm:ss.SSS")
}
private var didAskUserForTanMethod = false
private var didAskUserToEnterTan = false
private val callback = object : FinTsClientCallback {
override fun askUserForTanMethod(supportedTanMethods: List<TanMethod>, suggestedTanMethod: TanMethod?, callback: (TanMethod?) -> Unit) {
didAskUserForTanMethod = true
callback(suggestedTanMethod) // simply return suggestedTanMethod as in most cases it's the best fitting one
}
override fun enterTan(bank: BankData, tanChallenge: TanChallenge, callback: (EnterTanResult) -> Unit) {
didAskUserToEnterTan = true
callback(EnterTanResult.userDidNotEnterTan())
}
override fun enterTanGeneratorAtc(bank: BankData, tanMedium: TanGeneratorTanMedium, callback: (EnterTanGeneratorAtcResult) -> Unit) {
fail("Bank asks you to synchronize your TAN generator for card ${tanMedium.cardNumber} " +
"(card sequence number ${tanMedium.cardSequenceNumber}). Please do this via your online banking portal or Banking UI.")
}
}
private val underTest = FinTsClient(callback)
private val BankDataAnonymous = BankData.anonymous("10070000", "https://fints.deutsche-bank.de/", "DEUTDEBBXXX")
private val bankInfo = InMemoryBankFinder().findBankByBankCode(BankCode).first()
private val Bank = BankData(bankInfo.bankCode, CustomerId, Password, bankInfo.pinTanAddress ?: "", bankInfo.bic, bankInfo.name)
@Test
fun getAnonymousBankInfo() {
// when
underTest.getAnonymousBankInfo(BankDataAnonymous) { result ->
// then
expect(result.successful).isTrue()
expect(BankDataAnonymous.supportedHbciVersions).isNotEmpty()
expect(BankDataAnonymous.tanMethodsSupportedByBank).isNotEmpty()
expect(BankDataAnonymous.supportedJobs).isNotEmpty()
expect(BankDataAnonymous.supportedLanguages).isNotEmpty()
expect(BankDataAnonymous.bankName).isNotEmpty()
}
}
@Test
fun addAccount() {
// given
val response = AtomicReference<AddAccountResponse>()
val countDownLatch = CountDownLatch(1)
// when
underTest.addAccountAsync(Bank.toAddAccountParameter()) {
response.set(it)
countDownLatch.countDown()
}
// then
countDownLatch.await(30, TimeUnit.SECONDS)
val result = response.get()
expect(result.successful).isTrue()
expect(didAskUserForTanMethod).isFalse()
expect(Bank.bankName).isNotEmpty()
expect(Bank.supportedJobs).isNotEmpty() // supported jobs are now known
expect(Bank.tanMethodsSupportedByBank).isNotEmpty() // supported tan methods are now known
expect(Bank.supportedHbciVersions).isNotEmpty() // supported HBIC versions are now known
expect(Bank.supportedLanguages).isNotEmpty() // supported languages are now known
expect(Bank.customerName).isNotEmpty()
expect(Bank.tanMethodsAvailableForUser).isNotEmpty()
expect(Bank.selectedLanguage).notToBe(Dialogsprache.Default) // language is set now
expect(Bank.customerSystemId).notToBe(KundensystemStatus.SynchronizingCustomerSystemId.code) // customer system id is now set
expect(Bank.customerSystemStatus).toBe(KundensystemStatusWerte.Benoetigt) // customerSystemStatus is set now
expect(Bank.accounts).isNotEmpty() // accounts are now known
expect(Bank.accounts.first().allowedJobs).isNotEmpty() // allowed jobs are now known
}
@ExperimentalWithOptions
@Test
fun getTransactions() {
// given
val response = AtomicReference<GetAccountTransactionsResponse>()
val countDownLatch = CountDownLatch(1)
underTest.addAccountAsync(Bank.toAddAccountParameter(false)) { // retrieve basic data, e.g. accounts
val account = Bank.accounts.firstOrNull { it.supportsFeature(AccountFeature.RetrieveAccountTransactions) }
expect(account).withRepresentation("We need at least one account that supports retrieving account transactions (${CustomerSegmentId.AccountTransactionsMt940.id})").notToBeNull()
// when
// some banks support retrieving account transactions of last 90 days without TAN
underTest.tryGetAccountTransactionsOfLast90DaysWithoutTan(Bank, account!!) {
response.set(it)
countDownLatch.countDown()
}
}
// then
countDownLatch.await(30, TimeUnit.SECONDS)
val result = response.get()
expect(result.successful).isTrue()
expect(result.retrievedData.map { it.bookedTransactions }).isNotEmpty()
}
@Test
fun getTanMediaList() {
// this test is only senseful for accounts using chipTAN / TAN generator as TAN method
// given
val response = AtomicReference<GetTanMediaListResponse>()
val countDownLatch = CountDownLatch(1)
val anonymousBankInfoCountDownLatch = CountDownLatch(1)
underTest.getAnonymousBankInfo(Bank) {
anonymousBankInfoCountDownLatch.countDown()
}
anonymousBankInfoCountDownLatch.await(30, TimeUnit.SECONDS)
val supportsRetrievingTanMedia = Bank.supportedJobs.firstOrNull { it.jobName == "HKTAB" } != null
if (supportsRetrievingTanMedia == false) { // accounts with appTAN, pushTAN, smsTAN, ... would fail here -> simply return
println("Bank ${Bank.bankName} does not support retrieving TAN media. Therefore cannot execute test getTanMediaList()")
return
}
expect(Bank.tanMedia).isEmpty()
// when
underTest.getTanMediaList(Bank, TanMedienArtVersion.Alle, TanMediumKlasse.AlleMedien) { result ->
response.set(result)
countDownLatch.countDown()
}
// then
countDownLatch.await(30, TimeUnit.SECONDS)
val result = response.get()
expect(result.successful).isTrue()
expect(result.tanMediaList).notToBeNull()
expect(result.tanMediaList!!.usageOption).toBe(TanEinsatzOption.KundeKannGenauEinMediumZuEinerZeitNutzen) // TODO: may adjust to your value
expect(result.tanMediaList!!.tanMedia).isNotEmpty()
expect(Bank.tanMedia).isNotEmpty()
}
@Ignore // only works with banks that don't support HKTAB version 5
@Test
fun getTanMediaList_UnsupportedTanMediumClass() {
// when
expect {
underTest.getTanMediaList(Bank, TanMedienArtVersion.Alle, TanMediumKlasse.BilateralVereinbart) { }
}.toThrow<UnsupportedOperationException>()
// then
// exception gets thrown
}
@ExperimentalWithOptions
@Test
fun testBankTransfer() {
// given
val response = AtomicReference<FinTsClientResponse>()
val countDownLatch = CountDownLatch(1)
underTest.addAccountAsync(Bank.toAddAccountParameter(false)) { // retrieve basic data, e.g. accounts
// we need at least one account that supports cash transfer
val account = Bank.accounts.firstOrNull { it.supportsFeature(AccountFeature.TransferMoney) }
expect(account).withRepresentation("We need at least one account that supports cash transfer (${CustomerSegmentId.SepaBankTransfer.id})").notToBeNull()
// IBAN should be set
expect(account?.iban).withRepresentation("Account IBAN must be set").notToBeNull()
// transfer 1 cent to yourself. Transferring money to oneself also doesn't require to enter a TAN according to PSD2
val BankTransferData = BankTransferData(Bank.customerName, account?.iban!!, Bank.bic, Money(Amount("0,01"), "EUR"),
"${DateTimeFormatForUniqueBankTransferReference.format(Date())} Test transaction ${UUID.random()}")
// when
underTest.doBankTransferAsync(BankTransferData, Bank, account) { result ->
response.set(result)
countDownLatch.countDown()
}
}
// then
countDownLatch.await(30, TimeUnit.SECONDS)
val result = response.get()
expect(result.successful).isTrue()
}
}

View File

@ -1,317 +0,0 @@
package net.dankito.banking.fints.bankdetails
import kotlinx.coroutines.runBlocking
import net.dankito.banking.bankfinder.InMemoryBankFinder
import net.dankito.banking.fints.callback.NoOpFinTsClientCallback
import net.dankito.banking.fints.messages.MessageBuilder
import net.dankito.banking.fints.messages.MessageBuilderResult
import net.dankito.banking.fints.messages.Separators
import net.dankito.banking.fints.messages.datenelemente.implementierte.Dialogsprache
import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.AuftraggeberkontoErforderlich
import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.BezeichnungDesTanMediumsErforderlich
import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.SmsAbbuchungskontoErforderlich
import net.dankito.banking.fints.model.*
import net.dankito.banking.bankfinder.BankInfo
import net.dankito.banking.fints.FinTsJobExecutor
import net.dankito.banking.fints.callback.SimpleFinTsClientCallback
import net.dankito.banking.fints.model.mapper.ModelMapper
import net.dankito.banking.fints.response.BankResponse
import net.dankito.banking.fints.response.segments.SepaAccountInfoParameters
import net.dankito.banking.fints.response.segments.TanInfo
import net.dankito.banking.fints.response.segments.TanMethodParameters
import net.dankito.banking.fints.util.*
import org.apache.commons.csv.CSVFormat
import org.apache.commons.csv.CSVPrinter
import org.junit.Ignore
import kotlin.test.Test
import org.slf4j.LoggerFactory
import java.io.File
import java.io.FileWriter
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicReference
@Ignore // not a real test, run manually to retrieve FinTS information from all banks
class BanksFinTsDetailsRetriever {
companion object {
private val OutputFolderDateFormat = SimpleDateFormat("yyyy_MM_dd_HH_mm_ss")
private val log = LoggerFactory.getLogger(BanksFinTsDetailsRetriever::class.java)
}
private val bankFinder = InMemoryBankFinder()
private val product: ProductData = ProductData("15E53C26816138699C7B6A3E8", "1.0.0") // TODO: get version dynamically
private val messageBuilder = MessageBuilder()
private val modelMapper = object : ModelMapper(messageBuilder) {
fun mapToTanMethodTypePublic(parameters: TanMethodParameters): TanMethodType? {
return super.mapToTanMethodType(parameters)
}
}
private val jobExecutor = object : FinTsJobExecutor(modelMapper = modelMapper) {
fun getAndHandleResponseForMessagePublic(context: JobContext, message: MessageBuilderResult, callback: (BankResponse) -> Unit) {
getAndHandleResponseForMessage(context, message, callback)
}
}
private val requestNotSuccessful = mutableListOf<BankInfo>()
private val tanMethodParameter = mutableMapOf<String, MutableSet<TanMethodParameters>>()
private val tanMethodTypes = mutableMapOf<TanMethodType?, MutableSet<TanMethodParameters>>()
private val tanMethodParameterTechnicalIdentification = mutableSetOf<String>()
private val tanMethodParameterVersionDkTanMethod = mutableSetOf<String?>()
private val requiresSmsAbbuchungskonto = mutableListOf<BankInfo>()
private val requiresAuftraggeberkonto = mutableListOf<BankInfo>()
private val requiresChallengeClass = mutableListOf<BankInfo>()
private val signatureStructured = mutableListOf<BankInfo>()
private val requiresNameOfTanMedia = mutableListOf<BankInfo>()
private val requiresHhdUcResponse = mutableListOf<BankInfo>()
private val doesNotSupportHKTAN6 = mutableListOf<BankInfo>()
private val doesNotSupportHKSAL5or7 = mutableListOf<BankInfo>()
private val doesNotSupportHKKAZ5to7 = mutableListOf<BankInfo>()
private val doesNotSupportHKCCS1 = mutableListOf<BankInfo>()
private val supportsHhd13 = mutableListOf<BankInfo>()
private val supportsHhd14 = mutableListOf<BankInfo>()
private val doesNotSupportPain_001_001_or_003_03 = mutableListOf<BankInfo>()
@Test
fun retrieveAllBanksFinTsDetails() {
val allBanks = bankFinder.getBankList()
val banksSupportingFinTs = allBanks.filter { it.supportsFinTs3_0 }
val outputFolder = File("bankData", OutputFolderDateFormat.format(Date()))
val responsesFolder = File(outputFolder, "responses")
responsesFolder.mkdirs()
val csvFile = FileWriter(File(outputFolder, "bank_details.csv"))
val csvPrinter = CSVPrinter(csvFile, CSVFormat.DEFAULT.withHeader(
"BLZ", "Name", "Ort", "BPD", "Tanverfahren", "Technische Tanverfahrennamen", "HHD 1.3?", "HHD 1.4?",
"HKTAN 6?", "HKTAN", "HKSAL 5?", "HKSAL", "HKKAZ 5-7?", "HKKAZ", "HKCAZ", "HKCCS 1?", "HKCCS",
"pain.001.001.03?", "SEPA Formate", "Sprachen", "Untersstützte Geschäftsvorfälle"
))
val uniqueBanks = banksSupportingFinTs.associateBy { "${it.bankCode}_${it.name}" }
var bankIndex = 0
uniqueBanks.forEach { bankName, bankInfo ->
log.info("[${++bankIndex}] Getting details for $bankName ...")
getAndSaveBankDetails(bankInfo, responsesFolder, csvPrinter)
}
printStatistics()
csvPrinter.close()
csvFile.close()
}
private fun getAnonymousBankInfo(bank: BankData): BankResponse {
val context = JobContext(JobContextType.AnonymousBankInfo, SimpleFinTsClientCallback(), product, bank)
context.startNewDialog()
val requestBody = messageBuilder.createAnonymousDialogInitMessage(context)
val anonymousBankInfoResponse = AtomicReference<BankResponse>()
val countDownLatch = CountDownLatch(1)
jobExecutor.getAndHandleResponseForMessagePublic(context, requestBody) {
anonymousBankInfoResponse.set(it)
countDownLatch.countDown()
}
countDownLatch.await(30, TimeUnit.SECONDS)
modelMapper.updateBankData(bank, anonymousBankInfoResponse.get())
return anonymousBankInfoResponse.get()
}
private fun getAndSaveBankDetails(bankInfo: BankInfo, responsesFolder: File, csvPrinter: CSVPrinter) = runBlocking {
val bank = BankData.anonymous(bankInfo.bankCode, bankInfo.pinTanAddress ?: "", bankInfo.bic)
bank.bankName = bankInfo.name
val anonymousBankInfoResponse = getAnonymousBankInfo(bank)
File(responsesFolder, "${bankInfo.bankCode}_${bankInfo.name.replace('/', '-')}").writeText(
anonymousBankInfoResponse.receivedSegments.joinToString(System.lineSeparator()) { it.segmentString + Separators.SegmentSeparator })
if (anonymousBankInfoResponse.successful == false) {
requestNotSuccessful.add(bankInfo)
log.warn("Did not receive response from bank $bankInfo: ${anonymousBankInfoResponse.receivedSegments.joinToString(System.lineSeparator()) { it.segmentString + Separators.SegmentSeparator }}")
return@runBlocking
}
val supportsHKTAN6 = supportsJobInVersion(bank, "HKTAN", 6)
val supportsHKSAL5or7 = supportsJobInVersion(bank, "HKSAL", listOf(5, 7))
val supportsHKKAZ5to7 = supportsJobInVersion(bank, "HKKAZ", listOf(5, 6, 7))
val supportsHKCCS1 = supportsJobInVersion(bank, "HKCCS", 1)
val tanInfo = anonymousBankInfoResponse.receivedSegments.filterIsInstance(TanInfo::class.java)
val tanMethodParameters = tanInfo.flatMap { it.tanProcedureParameters.methodParameters }
val supportedTanMethods = tanMethodParameters.map { it.technicalTanMethodIdentification }
val hhd13Supported = supportedTanMethods.firstOrNull { it.startsWith("hhd1.3", true) } != null
val hhd14Supported = supportedTanMethods.firstOrNull { it.startsWith("hhd1.4", true) } != null
val supportedHKTANVersions = tanInfo.map { it.segmentVersion }
val supportedHKSALVersions = getSupportedVersions(bank, "HKSAL")
val supportedHKKAZVersions = getSupportedVersions(bank, "HKKAZ")
val supportedHKCAZVersions = getSupportedVersions(bank, "HKCAZ")
val supportedHKCCSVersions = getSupportedVersions(bank, "HKCCS")
val sepaAccountInfoParameters = anonymousBankInfoResponse.receivedSegments.filterIsInstance<SepaAccountInfoParameters>()
val supportedSepaFormats = sepaAccountInfoParameters.flatMap { it.supportedSepaFormats }.map { it.substring(it.indexOf(":xsd:") + ":xsd:".length) }
val supportsPain_001_001_or_003_03 = supportedSepaFormats.firstOrNull { it.contains("pain.001.001.03") or it.contains("pain.001.003.03") } != null
csvPrinter.printRecord(bankInfo.bankCode, bankInfo.name, bankInfo.city,
bank.bpdVersion,
bank.tanMethodsSupportedByBank.joinToString(", ") { it.securityFunction.code + ": " + it.displayName + " (" + it.type + ")" },
supportedTanMethods.joinToString(", "),
hhd13Supported,
hhd14Supported,
supportsHKTAN6,
supportedHKTANVersions.joinToString(", "),
supportsHKSAL5or7,
supportedHKSALVersions.joinToString(", "),
supportsHKKAZ5to7,
supportedHKKAZVersions.joinToString(", "),
supportedHKCAZVersions.joinToString(", "),
supportsHKCCS1,
supportedHKCCSVersions.joinToString(", "),
supportsPain_001_001_or_003_03,
supportedSepaFormats.joinToString(", "),
bank.supportedLanguages.filter { it != Dialogsprache.German }.joinToString(", ") { it.name },
bank.supportedJobs.joinToString(", ") { it.jobName + " " + it.segmentVersion }
)
tanMethodParameters.forEach { methodParameter ->
if (tanMethodParameter.containsKey(methodParameter.methodName) == false) {
tanMethodParameter.put(methodParameter.methodName, mutableSetOf(methodParameter))
}
else {
tanMethodParameter[methodParameter.methodName]?.add(methodParameter)
}
val tanMethodType = modelMapper.mapToTanMethodTypePublic(methodParameter)
if (tanMethodTypes.containsKey(tanMethodType) == false) {
tanMethodTypes.put(tanMethodType, mutableSetOf(methodParameter))
}
else {
tanMethodTypes[tanMethodType]?.add(methodParameter)
}
tanMethodParameterTechnicalIdentification.add(methodParameter.technicalTanMethodIdentification)
tanMethodParameterVersionDkTanMethod.add(methodParameter.versionDkTanMethod)
if (methodParameter.smsDebitAccountRequired == SmsAbbuchungskontoErforderlich.SmsAbbuchungskontoMussAngegebenWerden) {
requiresSmsAbbuchungskonto.add(bankInfo)
}
if (methodParameter.initiatorAccountRequired == AuftraggeberkontoErforderlich.AuftraggeberkontoMussAngegebenWerdenWennImGeschaeftsvorfallEnthalten) {
requiresAuftraggeberkonto.add(bankInfo)
}
if (methodParameter.challengeClassRequired) {
requiresChallengeClass.add(bankInfo)
}
if (methodParameter.signatureStructured) {
signatureStructured.add(bankInfo)
}
if (methodParameter.nameOfTanMediumRequired == BezeichnungDesTanMediumsErforderlich.BezeichnungDesTanMediumsMussAngegebenWerden) {
requiresNameOfTanMedia.add(bankInfo)
}
if (methodParameter.hhdUcResponseRequired) {
requiresHhdUcResponse.add(bankInfo)
}
}
if (supportsHKTAN6 == false) {
doesNotSupportHKTAN6.add(bankInfo)
}
if (supportsHKSAL5or7 == false) {
doesNotSupportHKSAL5or7.add(bankInfo)
}
if (supportsHKKAZ5to7 == false) {
doesNotSupportHKKAZ5to7.add(bankInfo)
}
if (supportsHKCCS1 == false) {
doesNotSupportHKCCS1.add(bankInfo)
}
if (hhd13Supported) {
supportsHhd13.add(bankInfo)
}
if (hhd14Supported) {
supportsHhd14.add(bankInfo)
}
if (supportsPain_001_001_or_003_03 == false) {
doesNotSupportPain_001_001_or_003_03.add(bankInfo)
}
}
private fun getSupportedVersions(bank: BankData, jobName: String): List<Int> {
return bank.supportedJobs.filter { it.jobName == jobName }.map { it.segmentVersion }
}
private fun supportsJobInVersion(bank: BankData, jobName: String, version: Int): Boolean {
return supportsJobInVersion(bank, jobName, listOf(version))
}
private fun supportsJobInVersion(bank: BankData, jobName: String, versions: List<Int>): Boolean {
return bank.supportedJobs.firstOrNull { it.jobName == jobName && versions.contains(it.segmentVersion) } != null
}
private fun printStatistics() {
log.info("Did not receive response from Banks ${printBanks(requestNotSuccessful)}")
log.info("Mapped tanMethodTypes: ${tanMethodTypes.map { System.lineSeparator() + it.key + ": " + it.value.map { it.methodName + " " + it.dkTanMethod + " " + it.technicalTanMethodIdentification + " (" + it.descriptionToShowToUser + ")" }.toSet().joinToString(", ") }}\n\n")
log.info("TanMethodParameters:${tanMethodParameter.map { System.lineSeparator() + it.key + ": " + it.value.map { it.securityFunction.code + " " + it.dkTanMethod + " " + it.technicalTanMethodIdentification + " (" + it.descriptionToShowToUser + ")" }.toSet().joinToString(", ") } }\n\n")
log.info("TanMethodParameters TechnicalIdentification:${tanMethodParameterTechnicalIdentification.joinToString(", ") } \n\n")
log.info("TanMethodParameters VersionDkTanMethod:${tanMethodParameterVersionDkTanMethod.joinToString(", ") } \n\n")
log.info("Requires SmsAbbuchungskonto ${printBanks(requiresSmsAbbuchungskonto)}") // no (only 2)
log.info("Requires Auftraggeberkonto ${printBanks(requiresAuftraggeberkonto)}") // yes, a lot of (12631)
log.info("Requires ChallengeClass ${printBanks(requiresChallengeClass)}") // no
log.info("Has structured signature ${printBanks(signatureStructured)}") // yes, a lot of (12651)
log.info("Requires NameOfTanMedia ${printBanks(requiresNameOfTanMedia)}") // yes, a lot of (912)
log.info("Requires HhdUcResponse ${printBanks(requiresHhdUcResponse)}") // no (only 2)
log.info("Banks supporting HHD 1.3 (${supportsHhd13.size}):${printBanks(supportsHhd13)}")
log.info("Banks supporting HHD 1.4 (${supportsHhd14.size}):${printBanks(supportsHhd14)}")
log.info("Banks not supporting HKTAN 6 ${printBanks(doesNotSupportHKTAN6)}")
log.info("Banks not supporting HKSAL 5 or 7 ${printBanks(doesNotSupportHKSAL5or7)}")
log.info("Banks not supporting HKKAZ 5-7 ${printBanks(doesNotSupportHKKAZ5to7)}")
log.info("Banks not supporting HKCCS 1 ${printBanks(doesNotSupportHKCCS1)}")
log.info("Banks not supporting pain.001.001.03 or pain.001.003.03 ${printBanks(doesNotSupportPain_001_001_or_003_03)}")
}
private fun printBanks(banks: List<BankInfo>): String {
return "(${banks.size}):${ banks.joinToString { System.lineSeparator() + it } }\n\n\n"
}
}

View File

@ -0,0 +1,270 @@
//package net.dankito.banking.fints
//
//import ch.tutteli.atrium.api.fluent.en_GB.*
//import ch.tutteli.atrium.api.verbs.expect
//import jdk.nashorn.internal.ir.annotations.Ignore
//import net.dankito.banking.bankfinder.InMemoryBankFinder
//import net.dankito.banking.fints.callback.FinTsClientCallback
//import net.dankito.banking.fints.extensions.isTrue
//import net.dankito.banking.fints.extensions.isFalse
//import net.dankito.banking.fints.messages.datenelemente.implementierte.Dialogsprache
//import net.dankito.banking.fints.messages.datenelemente.implementierte.KundensystemStatus
//import net.dankito.banking.fints.messages.datenelemente.implementierte.KundensystemStatusWerte
//import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.TanEinsatzOption
//import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.TanGeneratorTanMedium
//import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.TanMedienArtVersion
//import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.TanMediumKlasse
//import net.dankito.banking.fints.messages.segmente.id.CustomerSegmentId
//import net.dankito.banking.fints.model.*
//import net.dankito.banking.fints.response.client.*
//import net.dankito.utils.multiplatform.Date
//import net.dankito.utils.multiplatform.DateFormatter
//import net.dankito.utils.multiplatform.UUID
//import org.assertj.core.api.Assertions.assertThat
//import org.junit.jupiter.api.Test
//import org.junit.jupiter.api.fail
//import java.util.concurrent.CountDownLatch
//import java.util.concurrent.TimeUnit
//import java.util.concurrent.atomic.AtomicReference
//
//
//@Ignore // not an automatic test, supply your settings below
//open class FinTsClientTestBase {
//
// companion object {
//
// // TODO: add your settings here:
// val BankCode = "<your bank code (BLZ) here>"
//
// val CustomerId = "<your customer id (Online-Banking Login Name) here>"
//
// val Password = "<your PIN (Online-Banking Passwort) here>"
//
//
// val DateTimeFormatForUniqueBankTransferReference = DateFormatter("yyyy-MM-dd'T'HH:mm:ss.SSS")
// }
//
//
// private var didAskUserForTanMethod = false
//
// private var didAskUserToEnterTan = false
//
//
// private val callback = object : FinTsClientCallback {
//
// override fun askUserForTanMethod(supportedTanMethods: List<TanMethod>, suggestedTanMethod: TanMethod?, callback: (TanMethod?) -> Unit) {
// didAskUserForTanMethod = true
// callback(suggestedTanMethod) // simply return suggestedTanMethod as in most cases it's the best fitting one
// }
//
// override fun enterTan(bank: BankData, tanChallenge: TanChallenge, callback: (EnterTanResult) -> Unit) {
// didAskUserToEnterTan = true
//
// callback(EnterTanResult.userDidNotEnterTan())
// }
//
// override fun enterTanGeneratorAtc(bank: BankData, tanMedium: TanGeneratorTanMedium, callback: (EnterTanGeneratorAtcResult) -> Unit) {
// fail("Bank asks you to synchronize your TAN generator for card ${tanMedium.cardNumber} " +
// "(card sequence number ${tanMedium.cardSequenceNumber}). Please do this via your online banking portal or Banking UI.")
// }
//
// }
//
//
// private val underTest = FinTsClient(callback)
//
//
// private val BankDataAnonymous = BankData.anonymous("10070000", "https://fints.deutsche-bank.de/", "DEUTDEBBXXX")
//
// private val bankInfo = InMemoryBankFinder().findBankByBankCode(BankCode).first()
// private val Bank = BankData(bankInfo.bankCode, CustomerId, Password, bankInfo.pinTanAddress ?: "", bankInfo.bic, bankInfo.name)
//
//
//
// @Test
// fun getAnonymousBankInfo() {
//
// // when
// underTest.getAnonymousBankInfo(BankDataAnonymous) { result ->
//
// // then
// expect(result.successful).isTrue()
// expect(BankDataAnonymous.supportedHbciVersions).isNotEmpty()
// expect(BankDataAnonymous.tanMethodsSupportedByBank).isNotEmpty()
// expect(BankDataAnonymous.supportedJobs).isNotEmpty()
// expect(BankDataAnonymous.supportedLanguages).isNotEmpty()
// expect(BankDataAnonymous.bankName).isNotEmpty()
// }
// }
//
//
// @Test
// fun addAccount() {
//
// // given
// val response = AtomicReference<AddAccountResponse>()
// val countDownLatch = CountDownLatch(1)
//
//
// // when
// underTest.addAccountAsync(Bank.toAddAccountParameter()) {
// response.set(it)
// countDownLatch.countDown()
// }
//
//
// // then
// countDownLatch.await(30, TimeUnit.SECONDS)
// val result = response.get()
//
// expect(result.successful).isTrue()
//
// expect(didAskUserForTanMethod).isFalse()
//
// expect(Bank.bankName).isNotEmpty()
// expect(Bank.supportedJobs).isNotEmpty() // supported jobs are now known
// expect(Bank.tanMethodsSupportedByBank).isNotEmpty() // supported tan methods are now known
// expect(Bank.supportedHbciVersions).isNotEmpty() // supported HBIC versions are now known
// expect(Bank.supportedLanguages).isNotEmpty() // supported languages are now known
//
// expect(Bank.customerName).isNotEmpty()
// expect(Bank.tanMethodsAvailableForUser).isNotEmpty()
// expect(Bank.selectedLanguage).notToBe(Dialogsprache.Default) // language is set now
// expect(Bank.customerSystemId).notToBe(KundensystemStatus.SynchronizingCustomerSystemId.code) // customer system id is now set
// expect(Bank.customerSystemStatus).toBe(KundensystemStatusWerte.Benoetigt) // customerSystemStatus is set now
// expect(Bank.accounts).isNotEmpty() // accounts are now known
// expect(Bank.accounts.first().allowedJobs).isNotEmpty() // allowed jobs are now known
// }
//
//
// @ExperimentalWithOptions
// @Test
// fun getTransactions() {
//
// // given
// val response = AtomicReference<GetAccountTransactionsResponse>()
// val countDownLatch = CountDownLatch(1)
//
// underTest.addAccountAsync(Bank.toAddAccountParameter(false)) { // retrieve basic data, e.g. accounts
// val account = Bank.accounts.firstOrNull { it.supportsFeature(AccountFeature.RetrieveAccountTransactions) }
// expect(account).withRepresentation("We need at least one account that supports retrieving account transactions (${CustomerSegmentId.AccountTransactionsMt940.id})").notToBeNull()
//
// // when
//
// // some banks support retrieving account transactions of last 90 days without TAN
// underTest.tryGetAccountTransactionsOfLast90DaysWithoutTan(Bank, account!!) {
// response.set(it)
// countDownLatch.countDown()
// }
// }
//
//
// // then
// countDownLatch.await(30, TimeUnit.SECONDS)
// val result = response.get()
//
// assertThat(result.successful).isTrue()
// assertThat(result.retrievedData).isNotNull()
// }
//
//
// @Test
// fun getTanMediaList() {
//
// // this test is only senseful for accounts using chipTAN / TAN generator as TAN method
//
// // given
// val response = AtomicReference<GetTanMediaListResponse>()
// val countDownLatch = CountDownLatch(1)
// val anonymousBankInfoCountDownLatch = CountDownLatch(1)
//
//
// underTest.getAnonymousBankInfo(Bank) {
// anonymousBankInfoCountDownLatch.countDown()
// }
// anonymousBankInfoCountDownLatch.await(30, TimeUnit.SECONDS)
//
//
// val supportsRetrievingTanMedia = Bank.supportedJobs.firstOrNull { it.jobName == "HKTAB" } != null
//
// if (supportsRetrievingTanMedia == false) { // accounts with appTAN, pushTAN, smsTAN, ... would fail here -> simply return
// println("Bank ${Bank.bankName} does not support retrieving TAN media. Therefore cannot execute test getTanMediaList()")
// return
// }
//
// expect(Bank.tanMedia).isEmpty()
//
//
// // when
// underTest.getTanMediaList(Bank, TanMedienArtVersion.Alle, TanMediumKlasse.AlleMedien) { result ->
// response.set(result)
// countDownLatch.countDown()
// }
//
// // then
// countDownLatch.await(30, TimeUnit.SECONDS)
// val result = response.get()
//
// expect(result.successful).isTrue()
//
// expect(result.tanMediaList).notToBeNull()
// expect(result.tanMediaList!!.usageOption).toBe(TanEinsatzOption.KundeKannGenauEinMediumZuEinerZeitNutzen) // TODO: may adjust to your value
// expect(result.tanMediaList!!.tanMedia).isNotEmpty()
//
// expect(Bank.tanMedia).isNotEmpty()
// }
//
// @Ignore // only works with banks that don't support HKTAB version 5
// @Test
// fun getTanMediaList_UnsupportedTanMediumClass() {
//
// // when
// expect {
// underTest.getTanMediaList(Bank, TanMedienArtVersion.Alle, TanMediumKlasse.BilateralVereinbart) { }
// }.toThrow<UnsupportedOperationException>()
//
//
// // then
// // exception gets thrown
// }
//
//
// @ExperimentalWithOptions
// @Test
// fun testBankTransfer() {
//
// // given
// val response = AtomicReference<FinTsClientResponse>()
// val countDownLatch = CountDownLatch(1)
//
// underTest.addAccountAsync(Bank.toAddAccountParameter(false)) { // retrieve basic data, e.g. accounts
// // we need at least one account that supports cash transfer
// val account = Bank.accounts.firstOrNull { it.supportsFeature(AccountFeature.TransferMoney) }
// expect(account).withRepresentation("We need at least one account that supports cash transfer (${CustomerSegmentId.SepaBankTransfer.id})").notToBeNull()
//
// // IBAN should be set
// expect(account?.iban).withRepresentation("Account IBAN must be set").notToBeNull()
//
// // transfer 1 cent to yourself. Transferring money to oneself also doesn't require to enter a TAN according to PSD2
// val BankTransferData = BankTransferData(Bank.customerName, account?.iban!!, Bank.bic, Money(Amount("0,01"), "EUR"),
// "${DateTimeFormatForUniqueBankTransferReference.format(Date())} Test transaction ${UUID.random()}")
//
//
// // when
// underTest.doBankTransferAsync(BankTransferData, Bank, account) { result ->
// response.set(result)
// countDownLatch.countDown()
// }
//
// }
//
//
// // then
// countDownLatch.await(30, TimeUnit.SECONDS)
// val result = response.get()
//
// expect(result.successful).isTrue()
//
// }
//
//}

View File

@ -0,0 +1,317 @@
//package net.dankito.banking.fints.bankdetails
//
//import kotlinx.coroutines.runBlocking
//import net.dankito.banking.bankfinder.InMemoryBankFinder
//import net.dankito.banking.fints.callback.NoOpFinTsClientCallback
//import net.dankito.banking.fints.messages.MessageBuilder
//import net.dankito.banking.fints.messages.MessageBuilderResult
//import net.dankito.banking.fints.messages.Separators
//import net.dankito.banking.fints.messages.datenelemente.implementierte.Dialogsprache
//import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.AuftraggeberkontoErforderlich
//import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.BezeichnungDesTanMediumsErforderlich
//import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.SmsAbbuchungskontoErforderlich
//import net.dankito.banking.fints.model.*
//import net.dankito.banking.bankfinder.BankInfo
//import net.dankito.banking.fints.FinTsJobExecutor
//import net.dankito.banking.fints.callback.SimpleFinTsClientCallback
//import net.dankito.banking.fints.model.mapper.ModelMapper
//import net.dankito.banking.fints.response.BankResponse
//import net.dankito.banking.fints.response.segments.SepaAccountInfoParameters
//import net.dankito.banking.fints.response.segments.TanInfo
//import net.dankito.banking.fints.response.segments.TanMethodParameters
//import net.dankito.banking.fints.util.*
//import org.apache.commons.csv.CSVFormat
//import org.apache.commons.csv.CSVPrinter
//import org.junit.Ignore
//import kotlin.test.Test
//import org.slf4j.LoggerFactory
//import java.io.File
//import java.io.FileWriter
//import java.text.SimpleDateFormat
//import java.util.*
//import java.util.concurrent.CountDownLatch
//import java.util.concurrent.TimeUnit
//import java.util.concurrent.atomic.AtomicReference
//
//
//@Ignore // not a real test, run manually to retrieve FinTS information from all banks
//class BanksFinTsDetailsRetriever {
//
// companion object {
// private val OutputFolderDateFormat = SimpleDateFormat("yyyy_MM_dd_HH_mm_ss")
//
// private val log = LoggerFactory.getLogger(BanksFinTsDetailsRetriever::class.java)
// }
//
//
// private val bankFinder = InMemoryBankFinder()
//
// private val product: ProductData = ProductData("15E53C26816138699C7B6A3E8", "1.0.0") // TODO: get version dynamically
//
// private val messageBuilder = MessageBuilder()
//
// private val modelMapper = object : ModelMapper(messageBuilder) {
//
// fun mapToTanMethodTypePublic(parameters: TanMethodParameters): TanMethodType? {
// return super.mapToTanMethodType(parameters)
// }
//
// }
//
// private val jobExecutor = object : FinTsJobExecutor(modelMapper = modelMapper) {
//
// fun getAndHandleResponseForMessagePublic(context: JobContext, message: MessageBuilderResult, callback: (BankResponse) -> Unit) {
// getAndHandleResponseForMessage(context, message, callback)
// }
// }
//
//
// private val requestNotSuccessful = mutableListOf<BankInfo>()
//
// private val tanMethodParameter = mutableMapOf<String, MutableSet<TanMethodParameters>>()
// private val tanMethodTypes = mutableMapOf<TanMethodType?, MutableSet<TanMethodParameters>>()
//
// private val tanMethodParameterTechnicalIdentification = mutableSetOf<String>()
// private val tanMethodParameterVersionDkTanMethod = mutableSetOf<String?>()
//
// private val requiresSmsAbbuchungskonto = mutableListOf<BankInfo>()
// private val requiresAuftraggeberkonto = mutableListOf<BankInfo>()
// private val requiresChallengeClass = mutableListOf<BankInfo>()
// private val signatureStructured = mutableListOf<BankInfo>()
// private val requiresNameOfTanMedia = mutableListOf<BankInfo>()
// private val requiresHhdUcResponse = mutableListOf<BankInfo>()
//
// private val doesNotSupportHKTAN6 = mutableListOf<BankInfo>()
// private val doesNotSupportHKSAL5or7 = mutableListOf<BankInfo>()
// private val doesNotSupportHKKAZ5to7 = mutableListOf<BankInfo>()
// private val doesNotSupportHKCCS1 = mutableListOf<BankInfo>()
//
// private val supportsHhd13 = mutableListOf<BankInfo>()
// private val supportsHhd14 = mutableListOf<BankInfo>()
//
// private val doesNotSupportPain_001_001_or_003_03 = mutableListOf<BankInfo>()
//
//
// @Test
// fun retrieveAllBanksFinTsDetails() {
//
// val allBanks = bankFinder.getBankList()
// val banksSupportingFinTs = allBanks.filter { it.supportsFinTs3_0 }
//
// val outputFolder = File("bankData", OutputFolderDateFormat.format(Date()))
// val responsesFolder = File(outputFolder, "responses")
// responsesFolder.mkdirs()
//
// val csvFile = FileWriter(File(outputFolder, "bank_details.csv"))
// val csvPrinter = CSVPrinter(csvFile, CSVFormat.DEFAULT.withHeader(
// "BLZ", "Name", "Ort", "BPD", "Tanverfahren", "Technische Tanverfahrennamen", "HHD 1.3?", "HHD 1.4?",
// "HKTAN 6?", "HKTAN", "HKSAL 5?", "HKSAL", "HKKAZ 5-7?", "HKKAZ", "HKCAZ", "HKCCS 1?", "HKCCS",
// "pain.001.001.03?", "SEPA Formate", "Sprachen", "Untersstützte Geschäftsvorfälle"
// ))
//
//
// val uniqueBanks = banksSupportingFinTs.associateBy { "${it.bankCode}_${it.name}" }
// var bankIndex = 0
//
// uniqueBanks.forEach { bankName, bankInfo ->
// log.info("[${++bankIndex}] Getting details for $bankName ...")
//
// getAndSaveBankDetails(bankInfo, responsesFolder, csvPrinter)
// }
//
// printStatistics()
//
// csvPrinter.close()
// csvFile.close()
// }
//
//
// private fun getAnonymousBankInfo(bank: BankData): BankResponse {
// val context = JobContext(JobContextType.AnonymousBankInfo, SimpleFinTsClientCallback(), product, bank)
// context.startNewDialog()
//
// val requestBody = messageBuilder.createAnonymousDialogInitMessage(context)
//
// val anonymousBankInfoResponse = AtomicReference<BankResponse>()
// val countDownLatch = CountDownLatch(1)
//
// jobExecutor.getAndHandleResponseForMessagePublic(context, requestBody) {
// anonymousBankInfoResponse.set(it)
// countDownLatch.countDown()
// }
//
// countDownLatch.await(30, TimeUnit.SECONDS)
//
// modelMapper.updateBankData(bank, anonymousBankInfoResponse.get())
//
// return anonymousBankInfoResponse.get()
// }
//
// private fun getAndSaveBankDetails(bankInfo: BankInfo, responsesFolder: File, csvPrinter: CSVPrinter) = runBlocking {
// val bank = BankData.anonymous(bankInfo.bankCode, bankInfo.pinTanAddress ?: "", bankInfo.bic)
// bank.bankName = bankInfo.name
//
// val anonymousBankInfoResponse = getAnonymousBankInfo(bank)
//
// File(responsesFolder, "${bankInfo.bankCode}_${bankInfo.name.replace('/', '-')}").writeText(
// anonymousBankInfoResponse.receivedSegments.joinToString(System.lineSeparator()) { it.segmentString + Separators.SegmentSeparator })
//
// if (anonymousBankInfoResponse.successful == false) {
// requestNotSuccessful.add(bankInfo)
// log.warn("Did not receive response from bank $bankInfo: ${anonymousBankInfoResponse.receivedSegments.joinToString(System.lineSeparator()) { it.segmentString + Separators.SegmentSeparator }}")
//
// return@runBlocking
// }
//
//
// val supportsHKTAN6 = supportsJobInVersion(bank, "HKTAN", 6)
// val supportsHKSAL5or7 = supportsJobInVersion(bank, "HKSAL", listOf(5, 7))
// val supportsHKKAZ5to7 = supportsJobInVersion(bank, "HKKAZ", listOf(5, 6, 7))
// val supportsHKCCS1 = supportsJobInVersion(bank, "HKCCS", 1)
//
// val tanInfo = anonymousBankInfoResponse.receivedSegments.filterIsInstance(TanInfo::class.java)
// val tanMethodParameters = tanInfo.flatMap { it.tanProcedureParameters.methodParameters }
// val supportedTanMethods = tanMethodParameters.map { it.technicalTanMethodIdentification }
// val hhd13Supported = supportedTanMethods.firstOrNull { it.startsWith("hhd1.3", true) } != null
// val hhd14Supported = supportedTanMethods.firstOrNull { it.startsWith("hhd1.4", true) } != null
//
// val supportedHKTANVersions = tanInfo.map { it.segmentVersion }
// val supportedHKSALVersions = getSupportedVersions(bank, "HKSAL")
// val supportedHKKAZVersions = getSupportedVersions(bank, "HKKAZ")
// val supportedHKCAZVersions = getSupportedVersions(bank, "HKCAZ")
// val supportedHKCCSVersions = getSupportedVersions(bank, "HKCCS")
//
// val sepaAccountInfoParameters = anonymousBankInfoResponse.receivedSegments.filterIsInstance<SepaAccountInfoParameters>()
// val supportedSepaFormats = sepaAccountInfoParameters.flatMap { it.supportedSepaFormats }.map { it.substring(it.indexOf(":xsd:") + ":xsd:".length) }
// val supportsPain_001_001_or_003_03 = supportedSepaFormats.firstOrNull { it.contains("pain.001.001.03") or it.contains("pain.001.003.03") } != null
//
// csvPrinter.printRecord(bankInfo.bankCode, bankInfo.name, bankInfo.city,
// bank.bpdVersion,
// bank.tanMethodsSupportedByBank.joinToString(", ") { it.securityFunction.code + ": " + it.displayName + " (" + it.type + ")" },
// supportedTanMethods.joinToString(", "),
// hhd13Supported,
// hhd14Supported,
// supportsHKTAN6,
// supportedHKTANVersions.joinToString(", "),
// supportsHKSAL5or7,
// supportedHKSALVersions.joinToString(", "),
// supportsHKKAZ5to7,
// supportedHKKAZVersions.joinToString(", "),
// supportedHKCAZVersions.joinToString(", "),
// supportsHKCCS1,
// supportedHKCCSVersions.joinToString(", "),
// supportsPain_001_001_or_003_03,
// supportedSepaFormats.joinToString(", "),
// bank.supportedLanguages.filter { it != Dialogsprache.German }.joinToString(", ") { it.name },
// bank.supportedJobs.joinToString(", ") { it.jobName + " " + it.segmentVersion }
// )
//
// tanMethodParameters.forEach { methodParameter ->
// if (tanMethodParameter.containsKey(methodParameter.methodName) == false) {
// tanMethodParameter.put(methodParameter.methodName, mutableSetOf(methodParameter))
// }
// else {
// tanMethodParameter[methodParameter.methodName]?.add(methodParameter)
// }
//
// val tanMethodType = modelMapper.mapToTanMethodTypePublic(methodParameter)
// if (tanMethodTypes.containsKey(tanMethodType) == false) {
// tanMethodTypes.put(tanMethodType, mutableSetOf(methodParameter))
// }
// else {
// tanMethodTypes[tanMethodType]?.add(methodParameter)
// }
//
// tanMethodParameterTechnicalIdentification.add(methodParameter.technicalTanMethodIdentification)
// tanMethodParameterVersionDkTanMethod.add(methodParameter.versionDkTanMethod)
//
// if (methodParameter.smsDebitAccountRequired == SmsAbbuchungskontoErforderlich.SmsAbbuchungskontoMussAngegebenWerden) {
// requiresSmsAbbuchungskonto.add(bankInfo)
// }
// if (methodParameter.initiatorAccountRequired == AuftraggeberkontoErforderlich.AuftraggeberkontoMussAngegebenWerdenWennImGeschaeftsvorfallEnthalten) {
// requiresAuftraggeberkonto.add(bankInfo)
// }
// if (methodParameter.challengeClassRequired) {
// requiresChallengeClass.add(bankInfo)
// }
// if (methodParameter.signatureStructured) {
// signatureStructured.add(bankInfo)
// }
// if (methodParameter.nameOfTanMediumRequired == BezeichnungDesTanMediumsErforderlich.BezeichnungDesTanMediumsMussAngegebenWerden) {
// requiresNameOfTanMedia.add(bankInfo)
// }
// if (methodParameter.hhdUcResponseRequired) {
// requiresHhdUcResponse.add(bankInfo)
// }
// }
//
// if (supportsHKTAN6 == false) {
// doesNotSupportHKTAN6.add(bankInfo)
// }
// if (supportsHKSAL5or7 == false) {
// doesNotSupportHKSAL5or7.add(bankInfo)
// }
// if (supportsHKKAZ5to7 == false) {
// doesNotSupportHKKAZ5to7.add(bankInfo)
// }
// if (supportsHKCCS1 == false) {
// doesNotSupportHKCCS1.add(bankInfo)
// }
//
// if (hhd13Supported) {
// supportsHhd13.add(bankInfo)
// }
// if (hhd14Supported) {
// supportsHhd14.add(bankInfo)
// }
//
// if (supportsPain_001_001_or_003_03 == false) {
// doesNotSupportPain_001_001_or_003_03.add(bankInfo)
// }
// }
//
// private fun getSupportedVersions(bank: BankData, jobName: String): List<Int> {
// return bank.supportedJobs.filter { it.jobName == jobName }.map { it.segmentVersion }
// }
//
// private fun supportsJobInVersion(bank: BankData, jobName: String, version: Int): Boolean {
// return supportsJobInVersion(bank, jobName, listOf(version))
// }
//
// private fun supportsJobInVersion(bank: BankData, jobName: String, versions: List<Int>): Boolean {
// return bank.supportedJobs.firstOrNull { it.jobName == jobName && versions.contains(it.segmentVersion) } != null
// }
//
//
// private fun printStatistics() {
// log.info("Did not receive response from Banks ${printBanks(requestNotSuccessful)}")
//
// log.info("Mapped tanMethodTypes: ${tanMethodTypes.map { System.lineSeparator() + it.key + ": " + it.value.map { it.methodName + " " + it.dkTanMethod + " " + it.technicalTanMethodIdentification + " (" + it.descriptionToShowToUser + ")" }.toSet().joinToString(", ") }}\n\n")
// log.info("TanMethodParameters:${tanMethodParameter.map { System.lineSeparator() + it.key + ": " + it.value.map { it.securityFunction.code + " " + it.dkTanMethod + " " + it.technicalTanMethodIdentification + " (" + it.descriptionToShowToUser + ")" }.toSet().joinToString(", ") } }\n\n")
//
// log.info("TanMethodParameters TechnicalIdentification:${tanMethodParameterTechnicalIdentification.joinToString(", ") } \n\n")
// log.info("TanMethodParameters VersionDkTanMethod:${tanMethodParameterVersionDkTanMethod.joinToString(", ") } \n\n")
//
// log.info("Requires SmsAbbuchungskonto ${printBanks(requiresSmsAbbuchungskonto)}") // no (only 2)
// log.info("Requires Auftraggeberkonto ${printBanks(requiresAuftraggeberkonto)}") // yes, a lot of (12631)
// log.info("Requires ChallengeClass ${printBanks(requiresChallengeClass)}") // no
// log.info("Has structured signature ${printBanks(signatureStructured)}") // yes, a lot of (12651)
// log.info("Requires NameOfTanMedia ${printBanks(requiresNameOfTanMedia)}") // yes, a lot of (912)
// log.info("Requires HhdUcResponse ${printBanks(requiresHhdUcResponse)}") // no (only 2)
//
// log.info("Banks supporting HHD 1.3 (${supportsHhd13.size}):${printBanks(supportsHhd13)}")
// log.info("Banks supporting HHD 1.4 (${supportsHhd14.size}):${printBanks(supportsHhd14)}")
//
// log.info("Banks not supporting HKTAN 6 ${printBanks(doesNotSupportHKTAN6)}")
// log.info("Banks not supporting HKSAL 5 or 7 ${printBanks(doesNotSupportHKSAL5or7)}")
// log.info("Banks not supporting HKKAZ 5-7 ${printBanks(doesNotSupportHKKAZ5to7)}")
// log.info("Banks not supporting HKCCS 1 ${printBanks(doesNotSupportHKCCS1)}")
//
// log.info("Banks not supporting pain.001.001.03 or pain.001.003.03 ${printBanks(doesNotSupportPain_001_001_or_003_03)}")
// }
//
// private fun printBanks(banks: List<BankInfo>): String {
// return "(${banks.size}):${ banks.joinToString { System.lineSeparator() + it } }\n\n\n"
// }
//
//}

View File

@ -4,7 +4,7 @@ import net.dankito.banking.fints.FinTsTestBaseJvm
import net.dankito.banking.fints.messages.HbciCharset
import net.dankito.banking.fints.tan.TanImageDecoder
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.jupiter.api.Test
import java.nio.charset.Charset

View File

@ -4,7 +4,7 @@ import net.dankito.banking.fints.FinTsTestBaseJvm
import net.dankito.banking.fints.model.AccountData
import net.dankito.banking.fints.model.BankData
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.jupiter.api.Test
class Mt940AccountTransactionsParserTest : FinTsTestBaseJvm() {

View File

@ -3,7 +3,7 @@ package net.dankito.banking.fints.transactions
import net.dankito.banking.fints.FinTsTestBaseJvm
import net.dankito.banking.fints.transactions.mt940.Mt940Parser
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.jupiter.api.Test
class Mt940ParserTestJvm : FinTsTestBaseJvm() {

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="net.dankito.banking.fints4k.android">
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

View File

@ -1,5 +1,3 @@
android.enableJetifier=true
android.useAndroidX=true
kotlin.code.style=official
# Specifies the JVM arguments used for the daemon process.
@ -7,6 +5,7 @@ kotlin.code.style=official
org.gradle.jvmargs=-Xmx3072m
quarkusVersion=1.11.0.Final
kotlinVersion=1.6.0
#kotlin.js.compiler=ir
coroutinesVersion=1.6.0
ktorVersion=2.0.0-beta-1

View File

@ -1,30 +0,0 @@
apply plugin: 'java-library'
apply plugin: 'kotlin'
sourceCompatibility = "1.7"
targetCompatibility = "1.7"
compileKotlin {
kotlinOptions.jvmTarget = "1.6"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "1.6"
}
dependencies {
implementation project(":BankingUiCommon")
api project(":BankingPersistenceJson")
implementation "net.dankito.search:lucene-4-utils:$luceneUtilsVersion"
testImplementation "junit:junit:$junitVersion"
testImplementation "org.assertj:assertj-core:$assertJVersion"
testImplementation "org.mockito:mockito-core:$mockitoVersion"
testImplementation "org.slf4j:slf4j-simple:$slf4jVersion"
}

View File

@ -1,39 +0,0 @@
package net.dankito.banking
import java.io.File
class LuceneConfig {
companion object {
const val BankAccountIdFieldName = "bank_account_id"
const val IdFieldName = "id"
const val OtherPartyNameFieldName = "other_party_name"
const val OtherPartyBankCodeFieldName = "other_party_bank_code"
const val OtherPartyAccountIdFieldName = "other_party_account_id"
const val BookingDateFieldName = "booking_date"
const val DateSortFieldName = "value_date_sort"
const val ReferenceFieldName = "reference"
const val BookingTextFieldName = "booking_text"
const val AmountFieldName = "amount"
const val CurrencyFieldName = "currency"
const val BalanceFieldName = "balance"
fun getAccountTransactionsIndexFolder(indexFolder: File): File {
return File(indexFolder, "account_transactions")
}
}
}

View File

@ -1,109 +0,0 @@
package net.dankito.banking.persistence
import net.dankito.utils.multiplatform.File
import net.dankito.banking.LuceneConfig
import net.dankito.banking.LuceneConfig.Companion.AmountFieldName
import net.dankito.banking.LuceneConfig.Companion.BalanceFieldName
import net.dankito.banking.LuceneConfig.Companion.BankAccountIdFieldName
import net.dankito.banking.LuceneConfig.Companion.BookingDateFieldName
import net.dankito.banking.LuceneConfig.Companion.DateSortFieldName
import net.dankito.banking.LuceneConfig.Companion.BookingTextFieldName
import net.dankito.banking.LuceneConfig.Companion.CurrencyFieldName
import net.dankito.banking.LuceneConfig.Companion.IdFieldName
import net.dankito.banking.LuceneConfig.Companion.OtherPartyAccountIdFieldName
import net.dankito.banking.LuceneConfig.Companion.OtherPartyBankCodeFieldName
import net.dankito.banking.LuceneConfig.Companion.OtherPartyNameFieldName
import net.dankito.banking.LuceneConfig.Companion.ReferenceFieldName
import net.dankito.banking.ui.model.*
import net.dankito.banking.util.ISerializer
import net.dankito.banking.util.JacksonJsonSerializer
import net.dankito.utils.lucene.index.DocumentsWriter
import net.dankito.utils.lucene.index.FieldBuilder
import org.apache.lucene.index.IndexableField
import org.slf4j.LoggerFactory
open class LuceneBankingPersistence(
protected val indexFolder: File,
databaseFolder: File,
serializer: ISerializer = JacksonJsonSerializer()
) : BankingPersistenceJson(databaseFolder, serializer), IBankingPersistence {
companion object {
// i really hate this solution, but could find no other way to avoid app crashes when
// Android app gets restored as previous IndexWriter is not not destroyed yet and holds
// write lock and a new IndexWriter instance in DocumentsWriter gets instantiated
protected var documentsWriter: DocumentsWriter? = null
private val log = LoggerFactory.getLogger(LuceneBankingPersistence::class.java)
}
protected val fields = FieldBuilder()
override fun saveOrUpdateAccountTransactions(account: TypedBankAccount, transactions: List<IAccountTransaction>) {
val writer = getWriter()
transactions.forEach { transaction ->
writer.updateDocumentForNonNullFields(
IdFieldName, transaction.technicalId,
*createFieldsForAccountTransaction(account, transaction).toTypedArray()
)
}
writer.flushChangesToDisk()
}
protected open fun createFieldsForAccountTransaction(account: TypedBankAccount, transaction: IAccountTransaction): List<IndexableField?> {
return listOf(
fields.keywordField(BankAccountIdFieldName, account.technicalId),
fields.nullableFullTextSearchField(OtherPartyNameFieldName, transaction.otherPartyName, true),
fields.fullTextSearchField(ReferenceFieldName, transaction.reference, true),
fields.nullableFullTextSearchField(BookingTextFieldName, transaction.bookingText, true),
fields.nullableStoredField(OtherPartyBankCodeFieldName, transaction.otherPartyBankCode),
fields.nullableStoredField(OtherPartyAccountIdFieldName, transaction.otherPartyAccountId),
fields.storedField(BookingDateFieldName, transaction.bookingDate),
fields.storedField(AmountFieldName, transaction.amount),
fields.storedField(CurrencyFieldName, transaction.currency),
fields.nullableStoredField(BalanceFieldName, transaction.closingBalance), // TODO: remove
fields.sortField(DateSortFieldName, transaction.valueDate)
)
}
override fun deleteBank(bank: TypedBankData, allBanks: List<TypedBankData>) {
try {
deleteAccountTransactions(bank.accounts)
} catch (e: Exception) {
log.error("Could not delete account transactions of account $bank", e)
}
super.deleteBank(bank, allBanks)
}
protected open fun deleteAccountTransactions(accounts: List<TypedBankAccount>) {
val writer = getWriter()
val accountIds = accounts.map { it.technicalId }
writer.deleteDocumentsAndFlushChangesToDisk(BankAccountIdFieldName, *accountIds.toTypedArray())
}
@Synchronized
protected open fun getWriter(): DocumentsWriter {
documentsWriter?.let { return it }
val writer = DocumentsWriter(LuceneConfig.getAccountTransactionsIndexFolder(indexFolder))
documentsWriter = writer
return writer
}
}

View File

@ -1,45 +0,0 @@
package net.dankito.banking.search
import net.dankito.banking.LuceneConfig
import net.dankito.banking.LuceneConfig.Companion.OtherPartyAccountIdFieldName
import net.dankito.banking.LuceneConfig.Companion.OtherPartyBankCodeFieldName
import net.dankito.banking.LuceneConfig.Companion.OtherPartyNameFieldName
import net.dankito.utils.lucene.mapper.PropertyDescription
import net.dankito.utils.lucene.mapper.PropertyType
import net.dankito.utils.lucene.search.MappedSearchConfig
import net.dankito.utils.lucene.search.QueryBuilder
import net.dankito.utils.lucene.search.Searcher
import java.io.File
open class LuceneTransactionPartySearcher(indexFolder: File) : ITransactionPartySearcher {
companion object {
private val properties = listOf(
PropertyDescription(PropertyType.NullableString, OtherPartyNameFieldName, TransactionParty::name),
PropertyDescription(PropertyType.NullableString, OtherPartyBankCodeFieldName, TransactionParty::bic),
PropertyDescription(PropertyType.NullableString, OtherPartyAccountIdFieldName, TransactionParty::iban)
)
}
protected val queries = QueryBuilder()
protected val searcher = Searcher(LuceneConfig.getAccountTransactionsIndexFolder(indexFolder))
override fun findTransactionParty(query: String): List<TransactionParty> {
val luceneQuery = queries.createQueriesForSingleTerms(query.toLowerCase()) { singleTerm ->
listOf(
queries.fulltextQuery(OtherPartyNameFieldName, singleTerm)
)
}
return searcher.searchAndMap(MappedSearchConfig(luceneQuery, TransactionParty::class.java, properties))
.toSet() // don't display same transaction party multiple times
.filterNot { it.iban.isNullOrBlank() || it.bic.isNullOrBlank() } // e.g. comdirect doesn't supply other party's IBAN and BIC -> filter these as they have no value for auto-entering a transaction party's IBAN and BIC
}
}

View File

@ -1,235 +0,0 @@
package net.dankito.banking.search
import net.dankito.banking.persistence.LuceneBankingPersistence
import net.dankito.banking.ui.model.BankData
import net.dankito.banking.ui.model.AccountTransaction
import net.dankito.banking.ui.model.BankAccount
import net.dankito.utils.io.FileUtils
import net.dankito.utils.multiplatform.File
import net.dankito.utils.multiplatform.toBigDecimal
import net.dankito.utils.multiplatform.toDate
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito.mock
import java.math.BigDecimal
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ThreadLocalRandom
class LuceneTransactionPartySearcherTest {
companion object {
private val dataFolder = File("testData")
private val databaseFolder = File(dataFolder, "db")
private val indexFolder = File(dataFolder, "index")
private val BookingDate = "27.03.2020"
private val OtherPartyName = "Mahatma Gandhi"
private val OtherPartyBankCode = "12345678"
private val OtherPartyAccountId = "0987654321"
private val Amount = BigDecimal.valueOf(123.45)
private val bankAccountMock = BankAccount(mock(BankData::class.java), "", "", null, null, "")
private val dateFormat = SimpleDateFormat("dd.MM.yyyy")
}
private val fileUtils = FileUtils()
private val bankingPersistence = LuceneBankingPersistence(indexFolder, databaseFolder)
private val underTest = LuceneTransactionPartySearcher(indexFolder)
@Before
fun setUp() {
clearDataFolder()
}
@After
fun tearDown() {
clearDataFolder()
}
private fun clearDataFolder() {
fileUtils.deleteFolderRecursively(dataFolder)
}
@Test
fun findTransactionParty_ByFullName() {
// given
val query = OtherPartyName
val before = underTest.findTransactionParty(query)
assertThat(before).isEmpty()
bankingPersistence.saveOrUpdateAccountTransactions(bankAccountMock, listOf(
createTransaction(bankAccountMock, BookingDate, Amount, OtherPartyName, OtherPartyBankCode, OtherPartyAccountId),
createTransaction(),
createTransaction()
))
// when
val result = underTest.findTransactionParty(query)
// then
assertThat(result).hasSize(1)
assertThat(result.first().name).isEqualTo(OtherPartyName)
assertThat(result.first().bic).isEqualTo(OtherPartyBankCode)
assertThat(result.first().iban).isEqualTo(OtherPartyAccountId)
}
@Test
fun findTransactionParty_ByPartialName() {
// given
val query = "gand"
val before = underTest.findTransactionParty(query)
assertThat(before).isEmpty()
bankingPersistence.saveOrUpdateAccountTransactions(bankAccountMock, listOf(
createTransaction(bankAccountMock, BookingDate, Amount, OtherPartyName, OtherPartyBankCode, OtherPartyAccountId),
createTransaction(),
createTransaction()
))
// when
val result = underTest.findTransactionParty(query)
// then
assertThat(result).hasSize(1)
assertThat(result.first().name).isEqualTo(OtherPartyName)
assertThat(result.first().bic).isEqualTo(OtherPartyBankCode)
assertThat(result.first().iban).isEqualTo(OtherPartyAccountId)
}
@Test
fun findTransactionParty_SimilarNames() {
// given
val query = "gand"
val secondOtherPartyName = "Gandalf"
val before = underTest.findTransactionParty(query)
assertThat(before).isEmpty()
bankingPersistence.saveOrUpdateAccountTransactions(bankAccountMock, listOf(
createTransaction(bankAccountMock, BookingDate, Amount, OtherPartyName, OtherPartyBankCode, OtherPartyAccountId),
createTransaction(otherPartyName = secondOtherPartyName),
createTransaction()
))
// when
val result = underTest.findTransactionParty(query)
// then
assertThat(result).hasSize(2)
assertThat(result.map { it.name }).containsExactlyInAnyOrder(OtherPartyName, secondOtherPartyName)
}
@Test
fun findTransactionParty_DuplicateEntries() {
// given
val query = OtherPartyName
val before = underTest.findTransactionParty(query)
assertThat(before).isEmpty()
bankingPersistence.saveOrUpdateAccountTransactions(bankAccountMock, listOf(
createTransaction(bankAccountMock, BookingDate, Amount, OtherPartyName, OtherPartyBankCode, OtherPartyAccountId),
createTransaction(bankAccountMock, "01.02.2020", Amount, OtherPartyName, OtherPartyBankCode, OtherPartyAccountId),
createTransaction(bankAccountMock, "03.04.2020", Amount, OtherPartyName, OtherPartyBankCode, OtherPartyAccountId),
createTransaction(),
createTransaction()
))
// when
val result = underTest.findTransactionParty(query)
// then
assertThat(result).hasSize(1)
assertThat(result.first().name).isEqualTo(OtherPartyName)
assertThat(result.first().bic).isEqualTo(OtherPartyBankCode)
assertThat(result.first().iban).isEqualTo(OtherPartyAccountId)
}
@Test
fun findTransactionParty_OtherName() {
// given
val query = "Mandela"
val before = underTest.findTransactionParty(query)
assertThat(before).isEmpty()
bankingPersistence.saveOrUpdateAccountTransactions(bankAccountMock, listOf(
createTransaction(bankAccountMock, BookingDate, Amount, OtherPartyName, OtherPartyBankCode, OtherPartyAccountId),
createTransaction(),
createTransaction()
))
// when
val result = underTest.findTransactionParty(query)
// then
assertThat(result).isEmpty()
}
private fun createTransaction(bankAccount: BankAccount = bankAccountMock, bookingDate: String, amount: BigDecimal = randomBigDecimal(),
otherPartyName: String = randomString(), otherPartyBankCode: String = randomString(),
otherPartyAccountId: String = randomString(), reference: String = randomString()): AccountTransaction {
return createTransaction(bankAccount, dateFormat.parse(bookingDate), amount, otherPartyName,
otherPartyBankCode, otherPartyAccountId, reference)
}
private fun createTransaction(bankAccount: BankAccount = bankAccountMock, bookingDate: Date = randomDate(), amount: BigDecimal = randomBigDecimal(),
otherPartyName: String = randomString(), otherPartyBankCode: String = randomString(),
otherPartyAccountId: String = randomString(), reference: String = randomString()): AccountTransaction {
return AccountTransaction(bankAccount, amount.toBigDecimal(), "EUR", reference, bookingDate.toDate(), otherPartyName, otherPartyBankCode, otherPartyAccountId, null, bookingDate.toDate())
}
private fun randomString(): String {
return UUID.randomUUID().toString()
}
private fun randomDate(): Date {
val pseudoRandomLong = ThreadLocalRandom.current().nextLong(0, Date().time)
return Date(pseudoRandomLong)
}
private fun randomBigDecimal(): BigDecimal {
val pseudoRandomDouble = ThreadLocalRandom.current().nextDouble(-5-000.0, 12_000.0)
return BigDecimal.valueOf(pseudoRandomDouble)
}
}

View File

@ -1,39 +0,0 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion androidCompileSdkVersion
buildToolsVersion androidBuildToolsVersion
defaultConfig {
minSdkVersion androidMinSdkVersion
targetSdkVersion androidTargetSdkVersion
versionName version
versionCode appVersionCode
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation project(':BankingUiCommon')
implementation "androidx.room:room-runtime:$roomVersion"
kapt "androidx.room:room-compiler:$roomVersion"
implementation "androidx.room:room-ktx:$roomVersion"
implementation "net.zetetic:android-database-sqlcipher:$sqlCipherVersion"
}

View File

@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -1,5 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="net.dankito.banking.persistence">
/
</manifest>

View File

@ -1,33 +0,0 @@
package net.dankito.banking.persistence
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import net.dankito.banking.persistence.dao.*
import net.dankito.banking.persistence.model.*
@Database(entities = [
Bank::class, BankAccount::class, AccountTransaction::class,
TanMethod::class, TanMedium::class,
AppSettings::class, TanMethodSettings::class
], version = 1, exportSchema = false)
@TypeConverters(net.dankito.banking.persistence.TypeConverters::class)
abstract class BankingDatabase : RoomDatabase() {
abstract fun bankDao(): BankDao
abstract fun bankAccountDao(): BankAccountDao
abstract fun accountTransactionDao(): AccountTransactionDao
abstract fun tanMethodDao(): TanMethodDao
abstract fun tanMediumDao(): TanMediumDao
abstract fun appSettingsDao(): AppSettingsDao
abstract fun tanMethodSettingsDao(): TanMethodSettingsDao
}

View File

@ -1,286 +0,0 @@
package net.dankito.banking.persistence
import android.content.Context
import androidx.room.Room
import net.dankito.banking.persistence.dao.BaseDao
import net.dankito.banking.persistence.dao.saveOrUpdate
import net.dankito.banking.persistence.model.*
import net.dankito.banking.search.ITransactionPartySearcher
import net.dankito.banking.search.TransactionParty
import net.dankito.banking.ui.model.IAccountTransaction
import net.dankito.banking.ui.model.TypedBankAccount
import net.dankito.banking.ui.model.TypedBankData
import net.dankito.banking.ui.model.settings.AppSettings
import net.dankito.banking.ui.model.tan.MobilePhoneTanMedium
import net.dankito.banking.ui.model.tan.TanGeneratorTanMedium
import net.dankito.banking.util.persistence.downloadIcon
import net.dankito.utils.multiplatform.asString
import net.sqlcipher.database.SQLiteDatabase
import net.sqlcipher.database.SupportFactory
import org.slf4j.LoggerFactory
import java.util.concurrent.CopyOnWriteArraySet
open class RoomBankingPersistence(protected open val applicationContext: Context) : IBankingPersistence, ITransactionPartySearcher {
companion object {
const val DatabaseName = "banking-database"
const val AppSettingsId = 1
const val FlickerCodeTanMethodSettingsId = 1
const val QrCodeTanMethodSettingsId = 2
const val PhotoTanTanMethodSettingsId = 3
private val log = LoggerFactory.getLogger(RoomBankingPersistence::class.java)
}
protected lateinit var database: BankingDatabase
protected open var isInitialized = false
protected open val initializedListeners = CopyOnWriteArraySet<() -> Unit>()
override fun decryptData(password: CharArray): Boolean {
val result = openDatabase(password)
if (result) {
callInitializedListeners()
}
return result
}
override fun changePassword(newPassword: CharArray): Boolean {
if (this::database.isInitialized) {
val cursor = database.query("PRAGMA rekey = '${newPassword.asString()}';", emptyArray())
return cursor.count == 1 // TODO: also check if first column content is 'ok' ?
}
else { // database hasn't been opened yet, that means we're on the first app run
return openDatabase(newPassword)
}
}
protected open fun openDatabase(password: CharArray): Boolean {
try {
val passphrase = SQLiteDatabase.getBytes(password)
val factory = SupportFactory(passphrase)
database = Room.databaseBuilder(applicationContext, BankingDatabase::class.java, DatabaseName)
.openHelperFactory(factory)
.build()
return true
} catch (e: Exception) {
log.error("Could not open database", e)
}
return false
}
override fun saveOrUpdateBank(bank: TypedBankData, allBanks: List<TypedBankData>) {
(bank as? Bank)?.let { bank ->
bank.selectedTanMethodId = bank.selectedTanMethod?.technicalId
database.bankDao().saveOrUpdate(bank)
// TODO: in this way removed accounts won't be deleted from DB and therefore still be visible to user
val accounts = bank.accounts.filterIsInstance<BankAccount>()
accounts.forEach { it.bankId = bank.id }
database.bankAccountDao().saveOrUpdate(accounts)
// TODO: in this way removed TAN methods won't be deleted from DB and therefore still be visible to user
val tanMethods = bank.supportedTanMethods.filterIsInstance<TanMethod>()
tanMethods.forEach { tantanMethod ->
if (tantanMethod.bankId == BaseDao.ObjectNotInsertedId) {
tantanMethod.bankId = bank.id
database.tanMethodDao().insert(tantanMethod)
}
else {
database.tanMethodDao().update(tantanMethod)
}
}
// TODO: in this way removed TAN media won't be deleted from DB and therefore still be visible to user
val tanMedia = bank.tanMedia.map { tanMedium ->
bank.tanMediumEntities.firstOrNull { it.id == tanMedium.technicalId } ?: map(bank, tanMedium)
}
database.tanMediumDao().saveOrUpdate(tanMedia)
bank.tanMediumEntities = tanMedia
}
}
override fun deleteBank(bank: TypedBankData, allBanks: List<TypedBankData>) {
(bank as? Bank)?.let { bank ->
database.accountTransactionDao().delete(bank.accounts.flatMap { it.bookedTransactions }.filterIsInstance<AccountTransaction>())
database.bankAccountDao().delete(bank.accounts.filterIsInstance<BankAccount>())
database.tanMethodDao().delete(bank.supportedTanMethods.filterIsInstance<TanMethod>())
database.tanMediumDao().delete(bank.tanMedia.filterIsInstance<TanMedium>())
database.bankDao().delete(bank)
}
}
override fun readPersistedBanks(): List<TypedBankData> {
val banks = database.bankDao().getAll()
val accounts = database.bankAccountDao().getAll()
val transactions = database.accountTransactionDao().getAll()
val tanMethods = database.tanMethodDao().getAll()
val tanMedia = database.tanMediumDao().getAll()
banks.forEach { bank ->
bank.accounts = accounts.filter { it.bankId == bank.id }
bank.accounts.filterIsInstance<BankAccount>().forEach { account ->
account.bank = bank
account.bookedTransactions = transactions.filter { it.accountId == account.id }
account.bookedTransactions.filterIsInstance<AccountTransaction>().forEach { transaction ->
transaction.account = account
}
}
bank.supportedTanMethods = tanMethods.filter { it.bankId == bank.id }
bank.selectedTanMethod = bank.supportedTanMethods.firstOrNull { it.technicalId == bank.selectedTanMethodId }
bank.tanMediumEntities = tanMedia.filter { it.bankId == bank.id }
bank.tanMedia = bank.tanMediumEntities.map { map(it) }
}
return banks
}
override fun saveOrUpdateAccountTransactions(account: TypedBankAccount, transactions: List<IAccountTransaction>) {
val accountId = (account as? BankAccount)?.id ?: account.technicalId.toLong()
val mappedTransactions = transactions.filterIsInstance<AccountTransaction>()
mappedTransactions.forEach { it.accountId = accountId }
database.accountTransactionDao().saveOrUpdate(mappedTransactions)
}
protected open fun map(bank: Bank, tanMedium: net.dankito.banking.ui.model.tan.TanMedium): TanMedium {
val type = when (tanMedium) {
is TanGeneratorTanMedium -> TanMediumType.TanGeneratorTanMedium
is MobilePhoneTanMedium -> TanMediumType.MobilePhoneTanMedium
else -> TanMediumType.OtherTanMedium
}
return TanMedium(tanMedium.technicalId, bank.id, type, tanMedium.displayName, tanMedium.status,
(tanMedium as? TanGeneratorTanMedium)?.cardNumber, (tanMedium as? MobilePhoneTanMedium)?.phoneNumber)
}
protected open fun map(tanMedium: TanMedium): net.dankito.banking.ui.model.tan.TanMedium {
val displayName = tanMedium.displayName
val status = tanMedium.status
val mapped = when (tanMedium.type) {
TanMediumType.TanGeneratorTanMedium -> TanGeneratorTanMedium(displayName, status, tanMedium.cardNumber ?: "")
TanMediumType.MobilePhoneTanMedium -> MobilePhoneTanMedium(displayName, status, tanMedium.phoneNumber)
else -> net.dankito.banking.ui.model.tan.TanMedium(displayName, status)
}
mapped.technicalId = tanMedium.id
return mapped
}
override fun saveOrUpdateAppSettings(appSettings: AppSettings) {
val mapped = net.dankito.banking.persistence.model.AppSettings(appSettings.automaticallyUpdateAccountsAfterMinutes,
appSettings.lockAppAfterMinutes, appSettings.screenshotsAllowed, appSettings.lastSelectedOpenPdfFolder, appSettings.lastSelectedImportFolder, appSettings.lastSelectedExportFolder)
database.appSettingsDao().saveOrUpdate(mapped)
saveOrUpdateTanMethodSettings(appSettings.flickerCodeSettings, FlickerCodeTanMethodSettingsId)
saveOrUpdateTanMethodSettings(appSettings.qrCodeSettings, QrCodeTanMethodSettingsId)
saveOrUpdateTanMethodSettings(appSettings.photoTanSettings, PhotoTanTanMethodSettingsId)
}
protected open fun saveOrUpdateTanMethodSettings(settings: net.dankito.banking.ui.model.settings.TanMethodSettings?, id: Int) {
settings?.let {
val settingsEntity = TanMethodSettings(id, it.width, it.height, it.space, it.frequency)
database.tanMethodSettingsDao().saveOrUpdate(settingsEntity)
}
}
override fun readPersistedAppSettings(): AppSettings? {
val tanMethodSettings = database.tanMethodSettingsDao().getAll()
val settings = AppSettings()
database.appSettingsDao().getAll().firstOrNull { it.id == AppSettingsId }?.let { persistedSettings ->
settings.automaticallyUpdateAccountsAfterMinutes = persistedSettings.automaticallyUpdateAccountsAfterMinutes
settings.lockAppAfterMinutes = persistedSettings.lockAppAfterMinutes
settings.screenshotsAllowed = persistedSettings.screenshotsAllowed
settings.lastSelectedOpenPdfFolder = persistedSettings.lastSelectedOpenPdfFolder
settings.lastSelectedImportFolder = persistedSettings.lastSelectedImportFolder
settings.lastSelectedExportFolder = persistedSettings.lastSelectedExportFolder
}
settings.flickerCodeSettings = findTanMethodSettings(FlickerCodeTanMethodSettingsId, tanMethodSettings)
settings.qrCodeSettings = findTanMethodSettings(QrCodeTanMethodSettingsId, tanMethodSettings)
settings.photoTanSettings = findTanMethodSettings(PhotoTanTanMethodSettingsId, tanMethodSettings)
return settings
}
protected open fun findTanMethodSettings(id: Int, settings: List<TanMethodSettings>): TanMethodSettings? {
return settings.firstOrNull { it.id == id }
}
override fun saveBankIcon(bank: TypedBankData, iconUrl: String, fileExtension: String?) {
val iconData = downloadIcon(iconUrl)
bank.iconData = iconData
(bank as? Bank)?.let {
database.bankDao().saveOrUpdate(it)
}
}
override fun findTransactionParty(query: String): List<TransactionParty> {
return database.accountTransactionDao().findTransactionParty(query)
.toSet() // don't display same transaction party multiple times
.filterNot { it.bankCode.isNullOrBlank() || it.accountId.isNullOrBlank() }
.map { TransactionParty(it.name, it.accountId, it.bankCode) }
}
override fun addInitializedListener(listener: () -> Unit) {
if (isInitialized) {
listener()
} else {
initializedListeners.add(listener)
}
}
protected open fun callInitializedListeners() {
isInitialized = true
val copy = ArrayList(initializedListeners)
initializedListeners.clear()
copy.forEach { listener -> {
try {
listener()
} catch (e: Exception) {
log.error("Could not call listener $listener", e)
}
} }
}
}

View File

@ -1,91 +0,0 @@
package net.dankito.banking.persistence
import androidx.room.TypeConverter
import net.dankito.banking.persistence.model.TanMediumType
import net.dankito.banking.ui.model.BankAccountType
import net.dankito.banking.ui.model.tan.AllowedTanFormat
import net.dankito.banking.ui.model.tan.TanMediumStatus
import net.dankito.banking.ui.model.tan.TanMethodType
import net.dankito.utils.multiplatform.BigDecimal
import net.dankito.utils.multiplatform.Date
open class TypeConverters {
@TypeConverter
fun fromMultiplatformBigDecimal(value: BigDecimal?): String? {
return value?.toPlainString()
}
@TypeConverter
fun toMultiplatformBigDecimal(value: String?): BigDecimal? {
return value?.let { BigDecimal(value) }
}
@TypeConverter
fun fromMultiplatformDate(value: Date?): Long? {
return value?.millisSinceEpoch
}
@TypeConverter
fun toMultiplatformDate(value: Long?): Date? {
return value?.let { Date(value) }
}
@TypeConverter
fun fromBankAccountType(value: BankAccountType): Int {
return value.ordinal
}
@TypeConverter
fun toBankAccountType(value: Int): BankAccountType {
return BankAccountType.values().first { it.ordinal == value }
}
@TypeConverter
fun fromTanMethodType(value: TanMethodType): Int {
return value.ordinal
}
@TypeConverter
fun toTanMethodType(value: Int): TanMethodType {
return TanMethodType.values().first { it.ordinal == value }
}
@TypeConverter
fun fromAllowedTanFormat(value: AllowedTanFormat): Int {
return value.ordinal
}
@TypeConverter
fun toAllowedTanFormat(value: Int): AllowedTanFormat {
return AllowedTanFormat.values().first { it.ordinal == value }
}
@TypeConverter
fun fromTanMediumStatus(value: TanMediumStatus): Int {
return value.ordinal
}
@TypeConverter
fun toTanMediumStatus(value: Int): TanMediumStatus {
return TanMediumStatus.values().first { it.ordinal == value }
}
@TypeConverter
fun fromTanMediumTypes(value: TanMediumType): Int {
return value.ordinal
}
@TypeConverter
fun toTanMediumType(value: Int): TanMediumType {
return TanMediumType.values().first { it.ordinal == value }
}
}

View File

@ -1,18 +0,0 @@
package net.dankito.banking.persistence.dao
import androidx.room.Dao
import androidx.room.Query
import net.dankito.banking.persistence.model.AccountTransaction
import net.dankito.banking.persistence.model.TransactionParty
@Dao
interface AccountTransactionDao : BaseDao<AccountTransaction> {
@Query("SELECT * FROM AccountTransaction")
fun getAll(): List<AccountTransaction>
@Query("SELECT otherPartyName, otherPartyBankCode, otherPartyAccountId FROM AccountTransaction WHERE otherPartyName LIKE '%' || :query || '%'")
fun findTransactionParty(query: String): List<TransactionParty>
}

View File

@ -1,13 +0,0 @@
package net.dankito.banking.persistence.dao
import androidx.room.*
import net.dankito.banking.persistence.model.AppSettings
@Dao
interface AppSettingsDao : BaseDao<AppSettings> {
@Query("SELECT * FROM AppSettings")
fun getAll(): List<AppSettings>
}

View File

@ -1,14 +0,0 @@
package net.dankito.banking.persistence.dao
import androidx.room.Dao
import androidx.room.Query
import net.dankito.banking.persistence.model.BankAccount
@Dao
interface BankAccountDao : BaseDao<BankAccount> {
@Query("SELECT * FROM BankAccount")
fun getAll(): List<BankAccount>
}

View File

@ -1,13 +0,0 @@
package net.dankito.banking.persistence.dao
import androidx.room.*
import net.dankito.banking.persistence.model.Bank
@Dao
interface BankDao : BaseDao<Bank> {
@Query("SELECT * FROM Bank")
fun getAll(): List<Bank>
}

View File

@ -1,35 +0,0 @@
package net.dankito.banking.persistence.dao
import androidx.room.*
interface BaseDao<T> {
companion object {
const val ObjectNotInsertedId = -1L
const val IdNotSet = 0L
}
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(obj: T): Long
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(obj: List<T>): List<Long>
@Update(onConflict = OnConflictStrategy.IGNORE)
fun update(obj: T)
@Update(onConflict = OnConflictStrategy.IGNORE)
fun update(obj: List<T>)
@Delete
fun delete(obj: T)
@Delete
fun delete(obj: List<T>)
}

View File

@ -1,58 +0,0 @@
package net.dankito.banking.persistence.dao
import androidx.room.Transaction
import net.dankito.banking.persistence.model.*
/* Room didn't allow me to add these methods to BaseDao directly (Kapt error), so i defined them as extension methods */
@Transaction
fun <T> BaseDao<T>.saveOrUpdate(obj: T) {
val id = insert(obj)
if (wasNotInserted(id)) {
update(obj)
}
else {
setId(obj, id)
}
}
@Transaction
fun <T> BaseDao<T>.saveOrUpdate(objList: List<T>) {
val ids = insert(objList)
// i was not allowed to use mapIndexedNotNull()
val notInsertedObjects = mutableListOf<T>()
ids.forEachIndexed { index, id ->
val obj = objList[index]
if (wasNotInserted(id)) {
notInsertedObjects.add(obj)
}
else {
setId(obj, id)
}
}
update(notInsertedObjects)
}
private fun wasNotInserted(id: Long): Boolean {
return id == BaseDao.ObjectNotInsertedId
}
private fun <T> setId(obj: T, id: Long) {
if (obj is Bank) {
obj.id = id // why doesn't Room set this on itself?
obj.technicalId = obj.id.toString()
}
else if (obj is BankAccount) {
obj.id = id // why doesn't Room set this on itself?
obj.technicalId = obj.id.toString()
}
else if (obj is AccountTransaction) {
obj.id = id // why doesn't Room set this on itself?
obj.technicalId = obj.id.toString()
}
}

View File

@ -1,14 +0,0 @@
package net.dankito.banking.persistence.dao
import androidx.room.Dao
import androidx.room.Query
import net.dankito.banking.persistence.model.TanMedium
@Dao
interface TanMediumDao : BaseDao<TanMedium> {
@Query("SELECT * FROM TanMedium")
fun getAll(): List<TanMedium>
}

View File

@ -1,14 +0,0 @@
package net.dankito.banking.persistence.dao
import androidx.room.Dao
import androidx.room.Query
import net.dankito.banking.persistence.model.TanMethod
@Dao
interface TanMethodDao : BaseDao<TanMethod> {
@Query("SELECT * FROM TanMethod")
fun getAll(): List<TanMethod>
}

View File

@ -1,13 +0,0 @@
package net.dankito.banking.persistence.dao
import androidx.room.*
import net.dankito.banking.persistence.model.TanMethodSettings
@Dao
interface TanMethodSettingsDao : BaseDao<TanMethodSettings> {
@Query("SELECT * FROM TanMethodSettings")
fun getAll(): List<TanMethodSettings>
}

View File

@ -1,94 +0,0 @@
package net.dankito.banking.persistence.model
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import net.dankito.banking.persistence.dao.BaseDao
import net.dankito.banking.ui.model.IAccountTransaction
import net.dankito.utils.multiplatform.*
@Entity
open class AccountTransaction(
@Ignore
override var account: BankAccount,
override var amount: BigDecimal,
override var currency: String,
override var unparsedReference: String,
override var bookingDate: Date,
override var otherPartyName: String?,
override var otherPartyBankCode: String?,
override var otherPartyAccountId: String?,
override var bookingText: String?,
override var valueDate: Date,
override var statementNumber: Int,
override var sequenceNumber: Int?,
override var openingBalance: BigDecimal?,
override var closingBalance: BigDecimal?,
override var endToEndReference: String?,
override var customerReference: String?,
override var mandateReference: String?,
override var creditorIdentifier: String?,
override var originatorsIdentificationCode: String?,
override var compensationAmount: String?,
override var originalAmount: String?,
override var sepaReference: String?,
override var deviantOriginator: String?,
override var deviantRecipient: String?,
override var referenceWithNoSpecialType: String?,
override var primaNotaNumber: String?,
override var textKeySupplement: String?,
override var currencyType: String?,
override var bookingKey: String,
override var referenceForTheAccountOwner: String,
override var referenceOfTheAccountServicingInstitution: String?,
override var supplementaryDetails: String?,
override var transactionReferenceNumber: String,
override var relatedReferenceNumber: String?
) : IAccountTransaction {
// for object deserializers
internal constructor() : this(BankAccount(), null, "", BigDecimal.Zero, Date(), null)
/* convenience constructors for languages not supporting default values */
constructor(account: BankAccount, otherPartyName: String?, unparsedReference: String, amount: BigDecimal, valueDate: Date, bookingText: String?)
: this(account, amount, "EUR", unparsedReference, valueDate,
otherPartyName, null, null, bookingText, valueDate)
constructor(account: BankAccount, amount: BigDecimal, currency: String, unparsedReference: String, bookingDate: Date,
otherPartyName: String?, otherPartyBankCode: String?, otherPartyAccountId: String?,
bookingText: String?, valueDate: Date)
: this(account, amount, currency, unparsedReference, bookingDate,
otherPartyName, otherPartyBankCode, otherPartyAccountId, bookingText, valueDate,
0, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, "", "", null, null, "", null)
@PrimaryKey(autoGenerate = true)
open var id: Long = BaseDao.IdNotSet
override var technicalId: String = buildTransactionIdentifier()
// Room doesn't allow me to add getters and setters -> have to map it manually
open var accountId: Long = BaseDao.ObjectNotInsertedId
override fun equals(other: Any?): Boolean {
return doesEqual(other)
}
override fun hashCode(): Int {
return calculateHashCode()
}
override fun toString(): String {
return stringRepresentation
}
}

View File

@ -1,24 +0,0 @@
package net.dankito.banking.persistence.model
import androidx.room.Entity
import androidx.room.PrimaryKey
import net.dankito.banking.persistence.RoomBankingPersistence
import net.dankito.banking.ui.model.settings.AppSettings
@Entity
open class AppSettings(
open var automaticallyUpdateAccountsAfterMinutes: Int? = AppSettings.DefaultAutomaticallyUpdateAccountsAfterMinutes,
open var lockAppAfterMinutes: Int? = null,
open var screenshotsAllowed: Boolean = false,
open var lastSelectedOpenPdfFolder: String? = null,
open var lastSelectedImportFolder: String? = null,
open var lastSelectedExportFolder: String? = null
) {
internal constructor() : this(AppSettings.DefaultAutomaticallyUpdateAccountsAfterMinutes, null, false)
@PrimaryKey
open var id: Int = RoomBankingPersistence.AppSettingsId
}

View File

@ -1,63 +0,0 @@
package net.dankito.banking.persistence.model
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import net.dankito.banking.persistence.dao.BaseDao
import net.dankito.banking.ui.model.TypedBankAccount
import net.dankito.banking.ui.model.TypedBankData
import net.dankito.banking.ui.model.tan.TanMedium
import net.dankito.banking.ui.model.tan.TanMethod
@Entity
open class Bank(
override var bankCode: String,
override var userName: String,
override var password: String,
override var finTsServerAddress: String,
override var bankName: String,
override var bic: String,
override var customerName: String,
override var userId: String = userName,
override var iconData: ByteArray? = null,
@Ignore
override var accounts: List<TypedBankAccount> = listOf(),
@Ignore
override var supportedTanMethods: List<TanMethod> = listOf(),
@Ignore
override var selectedTanMethod: TanMethod? = null,
@Ignore
override var tanMedia: List<TanMedium> = listOf(),
@PrimaryKey(autoGenerate = true)
open var id: Long = BaseDao.IdNotSet,
override var technicalId: String = id.toString(),
override var wrongCredentialsEntered: Boolean = false,
override var savePassword: Boolean = true,
override var userSetDisplayName: String? = null,
override var displayIndex: Int = 0
) : TypedBankData {
internal constructor() : this("", "", "", "", "", "", "") // for object deserializers
open var selectedTanMethodId: String? = null
@Ignore
open var tanMediumEntities = listOf<net.dankito.banking.persistence.model.TanMedium>()
override fun toString(): String {
return stringRepresentation
}
}

View File

@ -1,82 +0,0 @@
package net.dankito.banking.persistence.model
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import net.dankito.banking.persistence.dao.BaseDao
import net.dankito.banking.ui.model.*
import net.dankito.utils.multiplatform.BigDecimal
import net.dankito.utils.multiplatform.Date
import net.dankito.utils.multiplatform.UUID
@Entity
open class BankAccount(
@Ignore
override var bank: TypedBankData,
override var identifier: String,
override var accountHolderName: String,
override var iban: String?,
override var subAccountNumber: String?,
override var balance: BigDecimal = BigDecimal.Zero,
override var currency: String = "EUR",
override var type: BankAccountType = BankAccountType.CheckingAccount,
override var productName: String? = null,
override var accountLimit: String? = null,
override var retrievedTransactionsFromOn: Date? = null,
override var retrievedTransactionsUpTo: Date? = null,
override var supportsRetrievingAccountTransactions: Boolean = false,
override var supportsRetrievingBalance: Boolean = false,
override var supportsTransferringMoney: Boolean = false,
override var supportsRealTimeTransfer: Boolean = false,
@Ignore
override var bookedTransactions: List<IAccountTransaction> = listOf(),
@Ignore
override var unbookedTransactions: List<Any> = listOf()
) : TypedBankAccount {
internal constructor() : this(Bank(), null, "") // for object deserializers
/* convenience constructors for languages not supporting default values */
constructor(bank: TypedBankData, productName: String?, identifier: String) : this(bank, productName, identifier, BankAccountType.CheckingAccount)
constructor(bank: TypedBankData, productName: String?, identifier: String, type: BankAccountType = BankAccountType.CheckingAccount, balance: BigDecimal = BigDecimal.Zero)
: this(bank, identifier, "", null, null, balance, "EUR", type, productName)
@PrimaryKey(autoGenerate = true)
open var id: Long = BaseDao.IdNotSet
override var technicalId: String = UUID.random()
// Room doesn't allow me to add getters and setters -> have to map it manually
open var bankId: Long = BaseDao.ObjectNotInsertedId
override var haveAllTransactionsBeenRetrieved: Boolean = false
override var isAccountTypeSupportedByApplication: Boolean = true
override var countDaysForWhichTransactionsAreKept: Int? = null
override var userSetDisplayName: String? = null
override var displayIndex: Int = 0
override var hideAccount = false
override var includeInAutomaticAccountsUpdate = true
override var doNotShowStrikingFetchAllTransactionsView = false
override fun toString(): String {
return stringRepresentation
}
}

View File

@ -1,75 +0,0 @@
package net.dankito.banking.persistence.model
import net.dankito.banking.ui.model.IAccountTransaction
import net.dankito.banking.ui.model.TypedBankAccount
import net.dankito.banking.ui.model.TypedBankData
import net.dankito.banking.ui.model.mapper.IModelCreator
import net.dankito.banking.ui.model.tan.AllowedTanFormat
import net.dankito.banking.ui.model.tan.TanMethodType
import net.dankito.utils.multiplatform.BigDecimal
import net.dankito.utils.multiplatform.Date
open class RoomModelCreator : IModelCreator {
override fun createBank(
bankCode: String, userName: String, password: String, finTsServerAddress: String, bankName: String,
bic: String, customerName: String, userId: String, iconData: ByteArray?
): TypedBankData {
return Bank(bankCode, userName, password, finTsServerAddress, bankName, bic, customerName, userId, iconData)
}
override fun createAccount(bank: TypedBankData, productName: String?, identifier: String): TypedBankAccount {
return BankAccount(bank, productName, identifier)
}
override fun createTransaction(
account: TypedBankAccount,
amount: BigDecimal,
currency: String,
unparsedReference: String,
bookingDate: Date,
otherPartyName: String?,
otherPartyBankCode: String?,
otherPartyAccountId: String?,
bookingText: String?,
valueDate: Date,
statementNumber: Int,
sequenceNumber: Int?,
openingBalance: BigDecimal?,
closingBalance: BigDecimal?,
endToEndReference: String?,
customerReference: String?,
mandateReference: String?,
creditorIdentifier: String?,
originatorsIdentificationCode: String?,
compensationAmount: String?,
originalAmount: String?,
sepaReference: String?,
deviantOriginator: String?,
deviantRecipient: String?,
referenceWithNoSpecialType: String?,
primaNotaNumber: String?,
textKeySupplement: String?,
currencyType: String?,
bookingKey: String,
referenceForTheAccountOwner: String,
referenceOfTheAccountServicingInstitution: String?,
supplementaryDetails: String?,
transactionReferenceNumber: String,
relatedReferenceNumber: String?
): IAccountTransaction {
return AccountTransaction(account as BankAccount, amount, currency, unparsedReference, bookingDate, otherPartyName, otherPartyBankCode, otherPartyAccountId,
bookingText, valueDate, statementNumber, sequenceNumber, openingBalance, closingBalance, endToEndReference, customerReference, mandateReference,
creditorIdentifier, originatorsIdentificationCode, compensationAmount, originalAmount, sepaReference, deviantOriginator, deviantRecipient,
referenceWithNoSpecialType, primaNotaNumber, textKeySupplement, currencyType, bookingKey, referenceForTheAccountOwner,
referenceOfTheAccountServicingInstitution, supplementaryDetails, transactionReferenceNumber, relatedReferenceNumber)
}
override fun createTanMethod(displayName: String, type: TanMethodType, bankInternalMethodCode: String, maxTanInputLength: Int?, allowedTanFormat: AllowedTanFormat): net.dankito.banking.ui.model.tan.TanMethod {
return TanMethod(displayName, type, bankInternalMethodCode, maxTanInputLength, allowedTanFormat)
}
}

View File

@ -1,24 +0,0 @@
package net.dankito.banking.persistence.model
import androidx.room.Entity
import androidx.room.PrimaryKey
import net.dankito.banking.persistence.dao.BaseDao
import net.dankito.banking.ui.model.tan.TanMediumStatus
@Entity
open class TanMedium(
@PrimaryKey
open var id: String,
open var bankId: Long,
open var type: TanMediumType,
open var displayName: String,
open var status: TanMediumStatus,
open var cardNumber: String? = null,
open var phoneNumber: String? = null
) {
internal constructor() : this("", BaseDao.ObjectNotInsertedId, TanMediumType.OtherTanMedium, "", TanMediumStatus.Available) // for object deserializers
}

View File

@ -1,12 +0,0 @@
package net.dankito.banking.persistence.model
enum class TanMediumType {
TanGeneratorTanMedium,
MobilePhoneTanMedium,
OtherTanMedium
}

View File

@ -1,29 +0,0 @@
package net.dankito.banking.persistence.model
import androidx.room.Entity
import androidx.room.PrimaryKey
import net.dankito.banking.persistence.dao.BaseDao
import net.dankito.banking.ui.model.tan.AllowedTanFormat
import net.dankito.banking.ui.model.tan.TanMethod
import net.dankito.banking.ui.model.tan.TanMethodType
@Entity
open class TanMethod(
override var displayName: String,
override var type: TanMethodType,
override var bankInternalMethodCode: String,
override var maxTanInputLength: Int? = null,
override var allowedTanFormat: AllowedTanFormat = AllowedTanFormat.Alphanumeric
) : TanMethod(displayName, type, bankInternalMethodCode, maxTanInputLength, allowedTanFormat) {
internal constructor() : this("", TanMethodType.EnterTan, "") // for object deserializers
@PrimaryKey
open var id: String = technicalId
// Room doesn't allow me to add getters and setters -> have to map it manually
open var bankId: Long = BaseDao.ObjectNotInsertedId
}

View File

@ -1,21 +0,0 @@
package net.dankito.banking.persistence.model
import androidx.room.Entity
import androidx.room.PrimaryKey
import net.dankito.banking.persistence.dao.BaseDao
import net.dankito.banking.ui.model.settings.TanMethodSettings
@Entity
open class TanMethodSettings(
@PrimaryKey
open var id: Int,
width: Int,
height: Int,
space: Int = -1,
frequency: Int = -1
) : TanMethodSettings(width, height, space, frequency) {
internal constructor() : this(BaseDao.IdNotSet.toInt(), 0, 0) // for object deserializers
}

View File

@ -1,12 +0,0 @@
package net.dankito.banking.persistence.model
import androidx.room.ColumnInfo
data class TransactionParty(
@ColumnInfo(name = "otherPartyName") val name: String,
@ColumnInfo(name = "otherPartyBankCode") val bankCode: String?,
@ColumnInfo(name = "otherPartyAccountId") val accountId: String?
)

View File

@ -1,7 +0,0 @@
# BankingPersistenceJson
A simple IBankingPersistence implementation based on Json.
Not meant to be a real or useful implementation. Just there to get you up and running fast.
Preferably use another IBankingPersistence implementation, e.g. a JPA based one.

View File

@ -1,27 +0,0 @@
apply plugin: 'java-library'
apply plugin: 'kotlin'
apply plugin: 'kotlin-kapt'
sourceCompatibility = "1.7"
targetCompatibility = "1.7"
compileKotlin {
kotlinOptions.jvmTarget = "1.6"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "1.6"
}
dependencies {
implementation project(':BankingUiCommon')
testImplementation "junit:junit:$junitVersion"
testImplementation "org.assertj:assertj-core:$assertJVersion"
testImplementation "org.mockito:mockito-core:$mockitoVersion"
testImplementation "org.slf4j:slf4j-simple:$slf4jVersion"
}

View File

@ -1,101 +0,0 @@
package net.dankito.banking.persistence
import net.dankito.banking.persistence.model.BankDataEntity
import net.dankito.banking.ui.model.*
import net.dankito.banking.ui.model.settings.AppSettings
import net.dankito.utils.multiplatform.File
import net.dankito.banking.util.ISerializer
import net.dankito.banking.util.persistence.downloadIcon
open class BankingPersistenceJson(
protected val databaseFolder: File,
protected val serializer: ISerializer
) : IBankingPersistence {
companion object {
const val BanksJsonFileName = "accounts.json"
const val AppSettingsJsonFileName = "app_settings.json"
}
protected val banksJsonFile: File
protected val appSettingsJsonFile: File
protected var allBanks: List<TypedBankData>? = null
init {
databaseFolder.mkdirs()
banksJsonFile = File(databaseFolder, BanksJsonFileName)
appSettingsJsonFile = File(databaseFolder, AppSettingsJsonFileName)
}
override fun decryptData(password: CharArray): Boolean {
// TODO: may implement data decryption. But then we have to store password to be able to encrypt data
return true
}
override fun changePassword(newPassword: CharArray): Boolean {
// TODO: may implement data decryption. But then we have to store newPassword to be able to encrypt data
return true
}
override fun saveOrUpdateBank(bank: TypedBankData, allBanks: List<TypedBankData>) {
saveAllBanks(allBanks)
}
override fun deleteBank(bank: TypedBankData, allBanks: List<TypedBankData>) {
saveAllBanks(allBanks)
}
override fun readPersistedBanks(): List<TypedBankData> {
val banks = serializer.deserializeListOr(banksJsonFile, BankDataEntity::class).map { it as TypedBankData }
this.allBanks = banks
return banks
}
override fun saveOrUpdateAccountTransactions(account: TypedBankAccount, transactions: List<IAccountTransaction>) {
// done when called saveOrUpdateAccount()
// TODO: or also call saveAllBanks()?
}
protected open fun saveAllBanks(allBanks: List<TypedBankData>) {
this.allBanks = allBanks
serializer.serializeObject(allBanks, banksJsonFile)
}
override fun saveOrUpdateAppSettings(appSettings: AppSettings) {
serializer.serializeObject(appSettings, appSettingsJsonFile)
}
override fun readPersistedAppSettings(): AppSettings? {
return serializer.deserializeObject(appSettingsJsonFile, AppSettings::class)
}
override fun saveBankIcon(bank: TypedBankData, iconUrl: String, fileExtension: String?) {
bank.iconData = downloadIcon(iconUrl)
allBanks?.let {
saveOrUpdateBank(bank, it)
}
}
override fun addInitializedListener(listener: () -> Unit) {
listener()
}
}

View File

@ -1,74 +0,0 @@
package net.dankito.banking.persistence.mapper
import net.dankito.banking.persistence.model.AccountTransactionEntity
import net.dankito.banking.persistence.model.BankAccountEntity
import net.dankito.banking.persistence.model.BankDataEntity
import net.dankito.banking.ui.model.IAccountTransaction
import net.dankito.banking.ui.model.TypedBankAccount
import net.dankito.banking.ui.model.TypedBankData
import net.dankito.banking.ui.model.mapper.IModelCreator
import net.dankito.utils.multiplatform.BigDecimal
import net.dankito.utils.multiplatform.Date
open class EntitiesModelCreator : IModelCreator {
override fun createBank(
bankCode: String, userName: String, password: String, finTsServerAddress: String, bankName: String, bic: String,
customerName: String, userId: String, iconData: ByteArray?
): TypedBankData {
return BankDataEntity(bankCode, userName, password, finTsServerAddress, bankName, bic, customerName, userId, iconData) as TypedBankData
}
override fun createAccount(bank: TypedBankData, productName: String?, identifier: String): TypedBankAccount {
return BankAccountEntity(bank as BankDataEntity, identifier, "", null, null, productName = productName) as TypedBankAccount
}
override fun createTransaction(
account: TypedBankAccount,
amount: BigDecimal,
currency: String,
unparsedReference: String,
bookingDate: Date,
otherPartyName: String?,
otherPartyBankCode: String?,
otherPartyAccountId: String?,
bookingText: String?,
valueDate: Date,
statementNumber: Int,
sequenceNumber: Int?,
openingBalance: BigDecimal?,
closingBalance: BigDecimal?,
endToEndReference: String?,
customerReference: String?,
mandateReference: String?,
creditorIdentifier: String?,
originatorsIdentificationCode: String?,
compensationAmount: String?,
originalAmount: String?,
sepaReference: String?,
deviantOriginator: String?,
deviantRecipient: String?,
referenceWithNoSpecialType: String?,
primaNotaNumber: String?,
textKeySupplement: String?,
currencyType: String?,
bookingKey: String,
referenceForTheAccountOwner: String,
referenceOfTheAccountServicingInstitution: String?,
supplementaryDetails: String?,
transactionReferenceNumber: String,
relatedReferenceNumber: String?
) : IAccountTransaction {
return AccountTransactionEntity(account as BankAccountEntity, amount, currency, unparsedReference, bookingDate,
otherPartyName, otherPartyBankCode, otherPartyAccountId, bookingText, valueDate, statementNumber, sequenceNumber,
openingBalance, closingBalance, endToEndReference, customerReference, mandateReference, creditorIdentifier,
originatorsIdentificationCode, compensationAmount, originalAmount, sepaReference, deviantOriginator, deviantRecipient,
referenceWithNoSpecialType, primaNotaNumber, textKeySupplement, currencyType, bookingKey, referenceForTheAccountOwner,
referenceOfTheAccountServicingInstitution, supplementaryDetails, transactionReferenceNumber, relatedReferenceNumber)
}
}

View File

@ -1,77 +0,0 @@
package net.dankito.banking.persistence.model
import com.fasterxml.jackson.annotation.JsonIdentityInfo
import com.fasterxml.jackson.annotation.ObjectIdGenerators
import net.dankito.banking.ui.model.IAccountTransaction
import net.dankito.utils.multiplatform.BigDecimal
import net.dankito.utils.multiplatform.Date
import net.dankito.utils.multiplatform.UUID
@JsonIdentityInfo(property = "technicalId", generator = ObjectIdGenerators.PropertyGenerator::class) // to avoid stack overflow due to circular references
// had to define all properties as 'var' 'cause MapStruct cannot handle vals
open class AccountTransactionEntity(
override var account: BankAccountEntity,
override var amount: BigDecimal,
override var currency: String,
override var unparsedReference: String,
override var bookingDate: Date,
override var otherPartyName: String?,
override var otherPartyBankCode: String?,
override var otherPartyAccountId: String?,
override var bookingText: String?,
override var valueDate: Date,
override var statementNumber: Int,
override var sequenceNumber: Int?,
override var openingBalance: BigDecimal?,
override var closingBalance: BigDecimal?,
override var endToEndReference: String?,
override var customerReference: String?,
override var mandateReference: String?,
override var creditorIdentifier: String?,
override var originatorsIdentificationCode: String?,
override var compensationAmount: String?,
override var originalAmount: String?,
override var sepaReference: String?,
override var deviantOriginator: String?,
override var deviantRecipient: String?,
override var referenceWithNoSpecialType: String?,
override var primaNotaNumber: String?,
override var textKeySupplement: String?,
override var currencyType: String?,
override var bookingKey: String,
override var referenceForTheAccountOwner: String,
override var referenceOfTheAccountServicingInstitution: String?,
override var supplementaryDetails: String?,
override var transactionReferenceNumber: String,
override var relatedReferenceNumber: String?,
override var technicalId: String = UUID.random()
) : IAccountTransaction {
// for object deserializers
internal constructor() : this(BankAccountEntity(), BigDecimal.Zero, "", "", Date(), null, null, null, null, Date(),
-1, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, "", "", null,
null, "", null)
constructor(account: BankAccountEntity, otherPartyName: String?, unparsedReference: String, amount: BigDecimal, valueDate: Date, bookingText: String?)
: this(account, amount, "EUR", unparsedReference, valueDate, otherPartyName, null, null, bookingText, valueDate, 0, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null, null,
null, "", "", null, null, "", null)
override fun equals(other: Any?): Boolean {
return doesEqual(other)
}
override fun hashCode(): Int {
return calculateHashCode()
}
override fun toString(): String {
return stringRepresentation
}
}

View File

@ -1,51 +0,0 @@
package net.dankito.banking.persistence.model
import com.fasterxml.jackson.annotation.JsonIdentityInfo
import com.fasterxml.jackson.annotation.ObjectIdGenerators
import net.dankito.banking.ui.model.*
import net.dankito.utils.multiplatform.BigDecimal
import net.dankito.utils.multiplatform.Date
import net.dankito.utils.multiplatform.UUID
@JsonIdentityInfo(property = "technicalId", generator = ObjectIdGenerators.PropertyGenerator::class) // to avoid stack overflow due to circular references
// had to define all properties as 'var' 'cause MapStruct cannot handle vals (and cannot use Pozo's mapstruct-kotlin as SerializableBankAccountBuilder would fail with @Context)
open class BankAccountEntity(
override var bank: BankDataEntity,
override var identifier: String,
override var accountHolderName: String,
override var iban: String?,
override var subAccountNumber: String?,
override var balance: BigDecimal = BigDecimal.Zero,
override var currency: String = "EUR",
override var type: BankAccountType = BankAccountType.CheckingAccount,
override var productName: String? = null,
override var accountLimit: String? = null,
override var retrievedTransactionsFromOn: Date? = null,
override var retrievedTransactionsUpTo: Date? = null,
override var supportsRetrievingAccountTransactions: Boolean = false,
override var supportsRetrievingBalance: Boolean = false,
override var supportsTransferringMoney: Boolean = false,
override var supportsRealTimeTransfer: Boolean = false,
override var bookedTransactions: List<AccountTransactionEntity> = listOf(),
override var unbookedTransactions: List<Any> = listOf(),
override var technicalId: String = UUID.random(),
override var userSetDisplayName: String? = null,
override var haveAllTransactionsBeenRetrieved: Boolean = false,
override var isAccountTypeSupportedByApplication: Boolean = true,
override var countDaysForWhichTransactionsAreKept: Int? = null,
override var displayIndex: Int = 0,
override var hideAccount: Boolean = false,
override var includeInAutomaticAccountsUpdate: Boolean = true,
override var doNotShowStrikingFetchAllTransactionsView: Boolean = false
) : IBankAccount<AccountTransactionEntity> {
internal constructor() : this(BankDataEntity(), "", "", null, null) // for object deserializers
override fun toString(): String {
return stringRepresentation
}
}

View File

@ -1,40 +0,0 @@
package net.dankito.banking.persistence.model
import com.fasterxml.jackson.annotation.*
import net.dankito.banking.ui.model.IBankData
import net.dankito.banking.ui.model.tan.TanMedium
import net.dankito.banking.ui.model.tan.TanMethod
import java.util.*
@JsonIdentityInfo(property = "technicalId", generator = ObjectIdGenerators.PropertyGenerator::class) // to avoid stack overflow due to circular references
// had to define all properties as 'var' 'cause MapStruct cannot handle vals (and cannot use Pozo's mapstruct-kotlin as SerializableCustomerBuilder would fail with @Context)
open class BankDataEntity(
override var bankCode: String,
override var userName: String,
override var password: String,
override var finTsServerAddress: String,
override var bankName: String,
override var bic: String,
override var customerName: String,
override var userId: String = userName,
override var iconData: ByteArray? = null,
override var accounts: List<BankAccountEntity> = listOf(),
override var supportedTanMethods: List<TanMethod> = listOf(),
override var selectedTanMethod: TanMethod? = null,
override var tanMedia: List<TanMedium> = listOf(),
override var technicalId: String = UUID.randomUUID().toString(),
override var wrongCredentialsEntered: Boolean = false,
override var savePassword: Boolean = true,
override var userSetDisplayName: String? = null,
override var displayIndex: Int = 0
) : IBankData<BankAccountEntity, AccountTransactionEntity> {
internal constructor() : this("", "", "", "", "", "", "") // for object deserializers
override fun toString(): String {
return stringRepresentation
}
}

View File

@ -1,238 +0,0 @@
package net.dankito.banking.persistence
import net.dankito.banking.persistence.model.AccountTransactionEntity
import net.dankito.banking.persistence.model.BankAccountEntity
import net.dankito.banking.persistence.model.BankDataEntity
import net.dankito.banking.ui.model.*
import net.dankito.banking.util.JacksonJsonSerializer
import net.dankito.utils.multiplatform.BigDecimal
import net.dankito.utils.multiplatform.Date
import net.dankito.utils.multiplatform.File
import org.assertj.core.api.Assertions.assertThat
import org.junit.Assert
import org.junit.Test
import kotlin.random.Random
class BankingPersistenceJsonTest {
companion object {
const val BankCode = "12345678"
const val CustomerId = "0987654321"
const val Password = "12345"
const val FinTsServerAddress = "http://i-do-not-exist.fail/givemeyourmoney"
const val BankName = "Abzock GmbH"
const val Bic = "ABCDDEBB123"
const val CustomerName = "Hans Dampf"
const val UserId = CustomerId
val NowMillis = System.currentTimeMillis()
val TwoYearsAgoMillis = NowMillis - (2 * 365 * 24 * 60 * 60 * 1000L)
val TestDataFolder = File("testData")
init {
TestDataFolder.mkdirs()
}
}
private val file = File(TestDataFolder, BankingPersistenceJson.BanksJsonFileName)
private val serializer = JacksonJsonSerializer()
private val underTest = BankingPersistenceJson(TestDataFolder, serializer)
@Test
fun saveOrUpdateBank() {
// given
val banks = listOf(
createBank(2),
createBank(3)
)
// when
underTest.saveOrUpdateBank(banks.first() as TypedBankData, banks.map { it as TypedBankData })
// then
val result = serializer.deserializeListOr(file, BankDataEntity::class)
assertBanksEqual(result, banks)
}
@Test
fun saveOrUpdateBankWithAccountsAndTransactions() {
// given
val bank = createBank(2)
// when
underTest.saveOrUpdateBank(bank as TypedBankData, listOf(bank).map { it as TypedBankData })
// then
val result = serializer.deserializeListOr(file, BankDataEntity::class)
assertBanksEqual(result, listOf(bank) as List<BankDataEntity>)
}
@Test
fun readPersistedBanks() {
// given
val banks = listOf(
createBank(2),
createBank(3)
)
serializer.serializeObject(banks, file)
// when
val result = underTest.readPersistedBanks()
// then
assertBanksEqual(banks, result as List<BankDataEntity>)
}
private fun createBank(countAccounts: Int = 0, customerId: String = CustomerId): BankDataEntity {
val result = BankDataEntity(BankCode, customerId, Password, FinTsServerAddress, BankName, Bic, CustomerName, UserId, null)
result.accounts = createAccounts(countAccounts, result)
return result
}
private fun createAccounts(count: Int, customer: BankDataEntity): List<BankAccountEntity> {
val random = Random(System.nanoTime())
return IntRange(1, count).map { accountIndex ->
createAccount("Account_$accountIndex", customer, random.nextInt(2, 50))
}
}
private fun createAccount(productName: String, customer: BankDataEntity, countTransactions: Int = 0): BankAccountEntity {
val result = BankAccountEntity(customer, customer.userName, "AccountHolder", "DE00" + customer.bankCode + customer.userName, null,
BigDecimal(84.25), productName = productName)
result.bookedTransactions = createTransactions(countTransactions, result)
return result
}
private fun createTransactions(countTransactions: Int, account: BankAccountEntity): List<AccountTransactionEntity> {
return IntRange(1, countTransactions).map { transactionIndex ->
createTransaction(transactionIndex, account)
}
}
private fun createTransaction(transactionIndex: Int, account: BankAccountEntity): AccountTransactionEntity {
return AccountTransactionEntity(account, "OtherParty_$transactionIndex", "Reference_$transactionIndex", BigDecimal(transactionIndex.toDouble()), createDate(), null)
}
private fun createDate(): Date {
return Date(Random(System.nanoTime()).nextLong(TwoYearsAgoMillis, NowMillis))
}
private fun assertBanksEqual(deserializedBanks: List<BankDataEntity>, banks: List<BankDataEntity>) {
assertThat(deserializedBanks.size).isEqualTo(banks.size)
deserializedBanks.forEach { deserializedBanks ->
val bank = banks.firstOrNull { it.technicalId == deserializedBanks.technicalId }
if (bank == null) {
Assert.fail("Could not find matching bank for deserialized bank $deserializedBanks. banks = $banks")
}
else {
assertBanksEqual(deserializedBanks, bank)
}
}
}
private fun assertBanksEqual(deserializedBank: BankDataEntity, bank: BankDataEntity) {
assertThat(deserializedBank.bankCode).isEqualTo(bank.bankCode)
assertThat(deserializedBank.userName).isEqualTo(bank.userName)
assertThat(deserializedBank.password).isEqualTo(bank.password)
assertThat(deserializedBank.finTsServerAddress).isEqualTo(bank.finTsServerAddress)
assertThat(deserializedBank.bankName).isEqualTo(bank.bankName)
assertThat(deserializedBank.bic).isEqualTo(bank.bic)
assertThat(deserializedBank.customerName).isEqualTo(bank.customerName)
assertThat(deserializedBank.userId).isEqualTo(bank.userId)
assertThat(deserializedBank.iconData).isEqualTo(bank.iconData)
assertAccountsEqual(deserializedBank.accounts, bank.accounts)
}
private fun assertAccountsEqual(deserializedAccounts: List<BankAccountEntity>, accounts: List<BankAccountEntity>) {
assertThat(deserializedAccounts.size).isEqualTo(accounts.size)
deserializedAccounts.forEach { deserializedAccount ->
val account = accounts.firstOrNull { it.technicalId == deserializedAccount.technicalId }
if (account == null) {
Assert.fail("Could not find matching account for deserialized account $deserializedAccount. accounts = $accounts")
}
else {
assertAccountsEqual(deserializedAccount, account)
}
}
}
private fun assertAccountsEqual(deserializedAccount: BankAccountEntity, account: BankAccountEntity) {
// to check if MapStruct created reference correctly
assertThat(deserializedAccount.bank.technicalId).isEqualTo(account.bank.technicalId)
assertThat(deserializedAccount.identifier).isEqualTo(account.identifier)
assertThat(deserializedAccount.iban).isEqualTo(account.iban)
assertThat(deserializedAccount.balance).isEqualTo(account.balance)
assertThat(deserializedAccount.productName).isEqualTo(account.productName)
assertTransactionsEqual(deserializedAccount.bookedTransactions, account.bookedTransactions)
}
private fun assertTransactionsEqual(deserializedTransactions: List<AccountTransactionEntity>, transactions: List<AccountTransactionEntity>) {
assertThat(deserializedTransactions.size).isEqualTo(transactions.size)
deserializedTransactions.forEach { deserializedTransaction ->
val transaction = transactions.firstOrNull { it.technicalId == deserializedTransaction.technicalId }
if (transaction == null) {
Assert.fail("Could not find matching transaction for deserialized transaction $deserializedTransaction. transactions = $transactions")
}
else {
assertTransactionsEqual(deserializedTransaction, transaction)
}
}
}
private fun assertTransactionsEqual(deserializedTransaction: AccountTransactionEntity, transaction: AccountTransactionEntity) {
// to check if MapStruct created reference correctly
assertThat(deserializedTransaction.account.technicalId).isEqualTo(transaction.account.technicalId)
assertThat(deserializedTransaction.otherPartyName).isEqualTo(transaction.otherPartyName)
assertThat(deserializedTransaction.unparsedReference).isEqualTo(transaction.unparsedReference)
assertThat(deserializedTransaction.amount).isEqualTo(transaction.amount)
assertThat(deserializedTransaction.valueDate).isEqualTo(transaction.valueDate)
}
}

View File

@ -1,5 +0,0 @@
*
!build/*-runner
!build/*-runner.jar
!build/lib/*
!build/quarkus-app/*

View File

@ -1,66 +0,0 @@
plugins {
id 'org.jetbrains.kotlin.jvm'
id "org.jetbrains.kotlin.plugin.allopen" version "1.3.72"
id 'io.quarkus'
}
apply plugin: 'java-library'
apply plugin: 'kotlin'
dependencies {
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
// TODO: why can't Gradle find BankFinder project? .jars have temporarily to be copied to libs folder - which are not committed to repo of course - till this issue is fixed
// implementation project(path: ":common", configuration: 'jvmDefault')
// implementation project(path: ":BankFinder", configuration: 'jvmDefault')
// implementation "net.dankito.banking:BankFinder-jvm:$version"
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "net.dankito.utils:java-utils:$javaUtilsVersion"
implementation enforcedPlatform("io.quarkus:quarkus-bom:$quarkusVersion")
implementation 'io.quarkus:quarkus-kotlin'
implementation 'io.quarkus:quarkus-resteasy'
implementation 'io.quarkus:quarkus-resteasy-jackson'
testImplementation 'io.quarkus:quarkus-junit5'
testImplementation 'io.rest-assured:kotlin-extensions'
}
quarkus {
setOutputDirectory("$projectDir/build/classes/kotlin/main")
}
quarkusDev {
setSourceDir("$projectDir/src/main/kotlin")
}
allOpen {
annotation("javax.ws.rs.Path")
annotation("javax.enterprise.context.ApplicationScoped")
annotation("io.quarkus.test.junit.QuarkusTest")
}
def javaVersion = JavaVersion.VERSION_11
java {
sourceCompatibility = javaVersion
targetCompatibility = javaVersion
}
compileKotlin {
kotlinOptions.jvmTarget = javaVersion
kotlinOptions.javaParameters = true
}
compileTestKotlin {
kotlinOptions.jvmTarget = javaVersion
}
test {
systemProperty "java.util.logging.manager", "org.jboss.logmanager.LogManager"
}

View File

@ -1,57 +0,0 @@
####
# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode
#
# Before building the container image run:
#
# mvn package -Dquarkus.package.type=fast-jar
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.fast-jar -t codinux/bank-finder-fast-jar .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 codinux/bank-finder-fast-jar
#
# If you want to include the debug port into your docker image
# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5050
#
# Then run the container using :
#
# docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" codinux/bank-finder-fast-jar
#
###
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.1
ARG JAVA_PACKAGE=java-11-openjdk-headless
ARG RUN_JAVA_VERSION=1.3.8
ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en'
# Install java and the run-java script
# Also set up permissions for user `1001`
RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \
&& microdnf update \
&& microdnf clean all \
&& mkdir /deployments \
&& chown 1001 /deployments \
&& chmod "g+rwX" /deployments \
&& chown 1001:root /deployments \
&& curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \
&& chown 1001 /deployments/run-java.sh \
&& chmod 540 /deployments/run-java.sh \
&& echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/lib/security/java.security
# Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size.
ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
# We make four distinct layers so if there are application changes the library layers can be re-used
COPY --chown=1001 build/quarkus-app/lib/ /deployments/lib/
COPY --chown=1001 build/quarkus-app/*.jar /deployments/
COPY --chown=1001 build/quarkus-app/app/ /deployments/app/
COPY --chown=1001 build/quarkus-app/quarkus/ /deployments/quarkus/
EXPOSE 8080
USER 1001
ENTRYPOINT [ "/deployments/run-java.sh" ]

View File

@ -1,54 +0,0 @@
####
# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode
#
# Before building the container image run:
#
# mvn package
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.jvm -t codinux/bank-finder-jvm .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 codinux/bank-finder-jvm
#
# If you want to include the debug port into your docker image
# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5050
#
# Then run the container using :
#
# docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" codinux/bank-finder-jvm
#
###
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.1
ARG JAVA_PACKAGE=java-11-openjdk-headless
ARG RUN_JAVA_VERSION=1.3.8
ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en'
# Install java and the run-java script
# Also set up permissions for user `1001`
RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \
&& microdnf update \
&& microdnf clean all \
&& mkdir /deployments \
&& chown 1001 /deployments \
&& chmod "g+rwX" /deployments \
&& chown 1001:root /deployments \
&& curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \
&& chown 1001 /deployments/run-java.sh \
&& chmod 540 /deployments/run-java.sh \
&& echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/lib/security/java.security
# Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size.
ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
COPY build/lib/* /deployments/lib/
COPY build/*-runner.jar /deployments/app.jar
EXPOSE 8080
USER 1001
ENTRYPOINT [ "/deployments/run-java.sh" ]

View File

@ -1,27 +0,0 @@
####
# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode
#
# Before building the container image run:
#
# mvn package -Pnative -Dquarkus.native.container-build=true
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.native -t codinux/bank-finder .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 codinux/bank-finder
#
###
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.1
WORKDIR /work/
RUN chown 1001 /work \
&& chmod "g+rwX" /work \
&& chown 1001:root /work
COPY --chown=1001:root build/*-runner /work/application
EXPOSE 8080
USER 1001
CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]

View File

@ -1,25 +0,0 @@
package net.dankito.banking.bankfinder.rest
import net.dankito.banking.bankfinder.BankInfo
import net.dankito.banking.bankfinder.InMemoryBankFinder
import org.jboss.resteasy.annotations.jaxrs.PathParam
import javax.ws.rs.GET
import javax.ws.rs.Path
import javax.ws.rs.Produces
import javax.ws.rs.core.MediaType
@Path("/bankfinder")
class BankFinderResource {
protected var bankFinder = InMemoryBankFinder()
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("{query}")
fun findBank(@PathParam query: String): List<BankInfo> {
return bankFinder.findBankByNameBankCodeOrCity(query)
}
}

View File

@ -1 +0,0 @@
quarkus.http.port=5666

View File

@ -1,6 +0,0 @@
package net.dankito.banking.bankfinder.rest
import io.quarkus.test.junit.NativeImageTest
@NativeImageTest
class NativeBankFinderResourceIT : ExampleResourceTest()

View File

@ -1,22 +0,0 @@
package net.dankito.banking.bankfinder.rest
import io.quarkus.test.junit.QuarkusTest
import io.restassured.RestAssured.given
import org.hamcrest.CoreMatchers.containsString
import org.junit.jupiter.api.Test
@QuarkusTest
class BankFinderResourceTest {
@Test
fun testSparkasse() {
given()
.`when`().get("/bankfinder/Sparkasse")
.then()
.statusCode(200)
.body(containsString("Berliner Sparkasse"))
.body(containsString("\"bankCode\":\"10050000\""))
}
}

View File

@ -1,5 +0,0 @@
*
!build/*-runner
!build/*-runner.jar
!build/lib/*
!build/quarkus-app/*

View File

@ -1,65 +0,0 @@
plugins {
id 'org.jetbrains.kotlin.jvm'
id "org.jetbrains.kotlin.plugin.allopen" version "1.3.72"
id 'io.quarkus'
}
dependencies {
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
// TODO: why can't Gradle find fints4k project? .jars have temporarily to be copied to libs folder - which are not committed to repo of course - till this issue is fixed
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "net.dankito.utils:java-utils:$javaUtilsVersion"
implementation "io.ktor:ktor-client-okhttp:$ktorVersion"
implementation "org.slf4j:slf4j-api:$slf4jVersion"
implementation enforcedPlatform("io.quarkus:quarkus-bom:$quarkusVersion")
implementation 'io.quarkus:quarkus-kotlin'
implementation 'io.quarkus:quarkus-resteasy'
implementation 'io.quarkus:quarkus-resteasy-jackson'
implementation "ch.qos.logback:logback-classic:$logbackVersion"
testImplementation 'io.quarkus:quarkus-junit5'
testImplementation 'io.rest-assured:kotlin-extensions'
}
quarkus {
setOutputDirectory("$projectDir/build/classes/kotlin/main")
}
quarkusDev {
setSourceDir("$projectDir/src/main/kotlin")
}
allOpen {
annotation("javax.ws.rs.Path")
annotation("javax.enterprise.context.ApplicationScoped")
annotation("io.quarkus.test.junit.QuarkusTest")
}
def javaVersion = JavaVersion.VERSION_11
java {
sourceCompatibility = javaVersion
targetCompatibility = javaVersion
}
compileKotlin {
kotlinOptions.jvmTarget = javaVersion
kotlinOptions.javaParameters = true
}
compileTestKotlin {
kotlinOptions.jvmTarget = javaVersion
}
test {
systemProperty "java.util.logging.manager", "org.jboss.logmanager.LogManager"
}

View File

@ -1,57 +0,0 @@
####
# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode
#
# Before building the container image run:
#
# mvn package -Dquarkus.package.type=fast-jar
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.fast-jar -t codinux/fints4k-fast-jar .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 codinux/fints4k-fast-jar
#
# If you want to include the debug port into your docker image
# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5050
#
# Then run the container using :
#
# docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" codinux/fints4k-fast-jar
#
###
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.1
ARG JAVA_PACKAGE=java-11-openjdk-headless
ARG RUN_JAVA_VERSION=1.3.8
ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en'
# Install java and the run-java script
# Also set up permissions for user `1001`
RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \
&& microdnf update \
&& microdnf clean all \
&& mkdir /deployments \
&& chown 1001 /deployments \
&& chmod "g+rwX" /deployments \
&& chown 1001:root /deployments \
&& curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \
&& chown 1001 /deployments/run-java.sh \
&& chmod 540 /deployments/run-java.sh \
&& echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/lib/security/java.security
# Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size.
ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
# We make four distinct layers so if there are application changes the library layers can be re-used
COPY --chown=1001 build/quarkus-app/lib/ /deployments/lib/
COPY --chown=1001 build/quarkus-app/*.jar /deployments/
COPY --chown=1001 build/quarkus-app/app/ /deployments/app/
COPY --chown=1001 build/quarkus-app/quarkus/ /deployments/quarkus/
EXPOSE 8080
USER 1001
ENTRYPOINT [ "/deployments/run-java.sh" ]

View File

@ -1,54 +0,0 @@
####
# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode
#
# Before building the container image run:
#
# mvn package
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.jvm -t codinux/fints4k-jvm .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 codinux/fints4k-jvm
#
# If you want to include the debug port into your docker image
# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5050
#
# Then run the container using :
#
# docker run -i --rm -p 8080:8080 -p 5005:5005 -e JAVA_ENABLE_DEBUG="true" codinux/fints4k-jvm
#
###
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.1
ARG JAVA_PACKAGE=java-11-openjdk-headless
ARG RUN_JAVA_VERSION=1.3.8
ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en'
# Install java and the run-java script
# Also set up permissions for user `1001`
RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \
&& microdnf update \
&& microdnf clean all \
&& mkdir /deployments \
&& chown 1001 /deployments \
&& chmod "g+rwX" /deployments \
&& chown 1001:root /deployments \
&& curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \
&& chown 1001 /deployments/run-java.sh \
&& chmod 540 /deployments/run-java.sh \
&& echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/lib/security/java.security
# Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size.
ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
COPY build/lib/* /deployments/lib/
COPY build/*-runner.jar /deployments/app.jar
EXPOSE 8080
USER 1001
ENTRYPOINT [ "/deployments/run-java.sh" ]

View File

@ -1,27 +0,0 @@
####
# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode
#
# Before building the container image run:
#
# mvn package -Pnative -Dquarkus.native.container-build=true
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.native -t codinux/fints4k .
#
# Then run the container using:
#
# docker run -i --rm -p 8080:8080 codinux/fints4k
#
###
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.1
WORKDIR /work/
RUN chown 1001 /work \
&& chmod "g+rwX" /work \
&& chown 1001:root /work
COPY --chown=1001:root build/*-runner /work/application
EXPOSE 8080
USER 1001
CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]

View File

@ -1,26 +0,0 @@
package net.dankito.banking.fints.rest
import javax.annotation.Priority
import javax.ws.rs.Priorities
import javax.ws.rs.container.ContainerRequestContext
import javax.ws.rs.container.ContainerResponseContext
import javax.ws.rs.container.ContainerResponseFilter
import javax.ws.rs.core.MultivaluedMap
import javax.ws.rs.ext.Provider
@Provider
@Priority(Priorities.HEADER_DECORATOR)
open class AccessControlResponseFilter : ContainerResponseFilter {
override fun filter(requestContext: ContainerRequestContext, responseContext: ContainerResponseContext) {
val headers: MultivaluedMap<String, Any> = responseContext.headers
headers.add("Access-Control-Allow-Origin", "*")
headers.add("Access-Control-Allow-Headers", "Authorization, Origin, X-Requested-With, Content-Type")
headers.add("Access-Control-Expose-Headers", "Location, Content-Disposition")
headers.add("Access-Control-Allow-Methods", "POST, PUT, GET, DELETE, HEAD, OPTIONS")
}
}

View File

@ -1,58 +0,0 @@
package net.dankito.banking.fints.rest
import com.fasterxml.jackson.databind.ObjectMapper
import org.slf4j.LoggerFactory
import javax.inject.Inject
import javax.ws.rs.container.ContainerRequestContext
import javax.ws.rs.container.ContainerResponseContext
import javax.ws.rs.container.ContainerResponseFilter
import javax.ws.rs.core.Response
import javax.ws.rs.ext.Provider
@Provider
class LoggingFilter : ContainerResponseFilter {
companion object {
private val log = LoggerFactory.getLogger(LoggingFilter::class.java)
}
@Inject
internal lateinit var mapper: ObjectMapper
override fun filter(requestContext: ContainerRequestContext, responseContext: ContainerResponseContext) {
if (responseContext.statusInfo.family != Response.Status.Family.SUCCESSFUL) {
log.warn("Request ${geRequestUrl(requestContext)} failed: ${getResponseStatus(responseContext)}"
+ System.lineSeparator() + getHeadersAsString(responseContext)
+ System.lineSeparator() + getBodyAsString(responseContext))
}
else if (log.isInfoEnabled) {
log.info("Result of request ${geRequestUrl(requestContext)}: ${getResponseStatus(responseContext)}"
+ System.lineSeparator() + getHeadersAsString(responseContext)
+ System.lineSeparator() + getBodyAsString(responseContext))
}
}
private fun geRequestUrl(requestContext: ContainerRequestContext): String {
return "${requestContext.request.method} ${requestContext.uriInfo.requestUri}"
}
private fun getResponseStatus(responseContext: ContainerResponseContext): String {
return "${responseContext.status} ${responseContext.statusInfo.reasonPhrase}"
}
private fun getHeadersAsString(responseContext: ContainerResponseContext): String {
return responseContext.stringHeaders.map { header -> "${header.key}: ${header.value}" }.joinToString("\n", "Headers:\n")
}
private fun getBodyAsString(responseContext: ContainerResponseContext): String {
if (responseContext.hasEntity()) {
return "Body ${responseContext.entityClass.name}:\n" + mapper.writeValueAsString(responseContext.entity)
}
return "<No response body>"
}
}

View File

@ -1,72 +0,0 @@
package net.dankito.banking.fints.rest
import net.dankito.banking.fints.response.client.AddAccountResponse
import net.dankito.banking.fints.response.client.GetTransactionsResponse
import net.dankito.banking.fints.rest.model.dto.request.AddAccountRequestDto
import net.dankito.banking.fints.rest.mapper.DtoMapper
import net.dankito.banking.fints.rest.model.dto.request.GetAccountsTransactionsRequestDto
import net.dankito.banking.fints.rest.model.dto.request.TanResponseDto
import net.dankito.banking.fints.rest.model.dto.response.AddAccountResponseDto
import net.dankito.banking.fints.rest.model.dto.response.GetAccountsTransactionsResponseDto
import net.dankito.banking.fints.rest.model.dto.response.RestResponse
import net.dankito.banking.fints.rest.service.fints4kService
import net.dankito.banking.fints.rest.service.model.GetAccountsTransactionsResponse
import org.slf4j.LoggerFactory
import javax.inject.Inject
import javax.ws.rs.*
import javax.ws.rs.core.MediaType
@Path("/fints/v1")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
class fints4kResource {
@Inject
protected val service = fints4kService()
protected val mapper = DtoMapper()
@POST
@Path("addaccount")
fun addAccount(request: AddAccountRequestDto): RestResponse<AddAccountResponseDto> {
val response = service.getAddAccountResponse(request)
return mapper.createRestResponse(response) { successResponse -> mapper.map(successResponse) }
}
@POST
@Path("transactions")
fun getAccountTransactions(request: GetAccountsTransactionsRequestDto): GetAccountsTransactionsResponseDto {
val response = service.getAccountTransactions(request)
return mapper.map(response)
}
@POST
@Path("tanresponse")
fun tanResponse(dto: TanResponseDto): RestResponse<Any> {
val response = service.handleTanResponse(dto)
// couldn't make it that compiler access ResponseHolder<*> for mapper.createRestResponse(), resulted in very cryptic "{"arity":0}" response -> handle it manually
response.response?.let { successResponse ->
return RestResponse.success(mapSuccessResponse(successResponse))
}
// all other cases map here, the responseMapper callback has no function
return mapper.createRestResponse(response) { it!! }
}
private fun mapSuccessResponse(successResponse: Any): Any {
return when (successResponse) {
is AddAccountResponse -> mapper.map(successResponse)
is GetAccountsTransactionsResponse -> mapper.map(successResponse)
is GetTransactionsResponse -> mapper.mapTransactions(successResponse)
else -> successResponse // add others / new ones here
}
}
}

View File

@ -1,166 +0,0 @@
package net.dankito.banking.fints.rest.mapper
import net.dankito.banking.fints.model.*
import net.dankito.banking.fints.response.client.AddAccountResponse
import net.dankito.banking.fints.response.client.FinTsClientResponse
import net.dankito.banking.fints.response.client.GetTransactionsResponse
import net.dankito.banking.fints.rest.model.ResponseHolder
import net.dankito.banking.fints.rest.model.dto.response.*
import net.dankito.banking.fints.rest.service.model.GetAccountsTransactionsResponse
import java.math.BigDecimal
import javax.ws.rs.InternalServerErrorException
open class DtoMapper {
fun <DomainType, DtoType> createRestResponse(responseHolder: ResponseHolder<DomainType>, responseMapper: (DomainType) -> DtoType): RestResponse<DtoType> {
responseHolder.response?.let { response ->
return RestResponse.success(responseMapper(response))
}
responseHolder.enterTanRequest?.let { enterTanRequest ->
return RestResponse.requiresTan(enterTanRequest)
}
return RestResponse.error(responseHolder.error ?: "Unknown error")
}
open fun map(response: AddAccountResponse): AddAccountResponseDto {
return AddAccountResponseDto(
response.successful,
mapErrorMessage(response),
map(response.bank),
map(response.retrievedData)
)
}
protected open fun map(bank: BankData): BankResponseDto {
return BankResponseDto(
bank.bankCode,
bank.customerId,
bank.finTs3ServerAddress,
bank.bic,
bank.bankName,
bank.userId,
bank.customerName,
mapTanMethods(bank.tanMethodsAvailableForUser),
if (bank.isTanMethodSelected) map(bank.selectedTanMethod) else null,
bank.tanMedia,
bank.supportedHbciVersions.map { it.name.replace("Hbci_", "HBCI ").replace("FinTs_", "FinTS ").replace('_', '.') }
)
}
open fun map(response: GetAccountsTransactionsResponse?): GetAccountsTransactionsResponseDto {
// TODO: is this still the case?
// TODO: if a TAN is required then accountsTransactions contains null value(s) (but why?) -> application crashes
if (response == null) {
throw InternalServerErrorException("Could not fetch account transactions. Either TAN hasn't been entered or developers made a mistake.")
}
return GetAccountsTransactionsResponseDto(
// TODO: is this correct removing accounts from result for which no transactions have been retrieved?
response.transactionsPerAccount.filter { it.response?.retrievedData?.isNotEmpty() != false }
.map { createRestResponse(it) { transactionsResponse -> mapTransactions(transactionsResponse) } }
)
}
open fun mapTransactions(accountTransactions: GetTransactionsResponse): GetAccountTransactionsResponseDto {
val retrievedData = accountTransactions.retrievedData.first()
val balance = mapNullable(retrievedData.balance)
val bookedTransactions = map(retrievedData.bookedTransactions)
return GetAccountTransactionsResponseDto(
retrievedData.account.accountIdentifier,
retrievedData.account.productName,
accountTransactions.successful,
mapErrorMessage(accountTransactions),
balance,
bookedTransactions,
listOf()
)
}
protected open fun map(accountData: List<RetrievedAccountData>): List<BankAccountResponseDto> {
return accountData.map { map(it) }
}
protected open fun map(accountData: RetrievedAccountData): BankAccountResponseDto {
val account = accountData.account
return BankAccountResponseDto(
account.accountIdentifier,
account.subAccountAttribute,
account.iban,
account.accountType,
account.currency,
account.accountHolderName,
account.productName,
account.supportsRetrievingBalance,
account.supportsRetrievingAccountTransactions,
account.supportsTransferringMoney,
account.supportsRealTimeTransfer,
accountData.successfullyRetrievedData,
mapNullable(accountData.balance),
accountData.retrievedTransactionsFrom,
accountData.retrievedTransactionsTo,
map(accountData.bookedTransactions),
listOf()
)
}
protected open fun map(transactions: Collection<AccountTransaction>): Collection<AccountTransactionResponseDto> {
return transactions.map { map(it) }
}
protected open fun map(transaction: AccountTransaction): AccountTransactionResponseDto {
return AccountTransactionResponseDto(
map(transaction.amount),
transaction.amount.currency.code,
transaction.reference,
transaction.bookingDate,
transaction.otherPartyName,
transaction.otherPartyBankCode,
transaction.otherPartyAccountId,
transaction.bookingText,
transaction.valueDate
)
}
protected open fun mapTanMethods(tanMethods: List<TanMethod>): List<TanMethodResponseDto> {
return tanMethods.map { map(it) }
}
protected open fun map(tanMethod: TanMethod): TanMethodResponseDto {
return TanMethodResponseDto(
tanMethod.displayName,
tanMethod.securityFunction.code,
tanMethod.type,
tanMethod.hhdVersion?.name?.replace("HHD_", "")?.replace('_', '.'),
tanMethod.maxTanInputLength,
tanMethod.allowedTanFormat
)
}
protected open fun map(money: Money): BigDecimal {
return money.bigDecimal
}
protected open fun mapNullable(money: Money?): BigDecimal? {
return money?.let { map(it) }
}
protected open fun mapErrorMessage(response: FinTsClientResponse): String? {
// TODO: evaluate fields like isJobAllowed or tanRequiredButWeWereToldToAbortIfSo and set error message accordingly
return response.errorMessage
?: if (response.errorsToShowToUser.isNotEmpty()) response.errorsToShowToUser.joinToString("\n") else null
}
}

View File

@ -1,13 +0,0 @@
package net.dankito.banking.fints.rest.model
open class BankAccessData(
open val bankCode: String,
open val loginName: String,
open val password: String,
open val finTsServerAddress: String? = null
) {
internal constructor() : this("", "", "") // for object deserializers
}

View File

@ -1,14 +0,0 @@
package net.dankito.banking.fints.rest.model
import net.dankito.banking.fints.model.EnterTanResult
import java.util.*
import java.util.concurrent.CountDownLatch
import java.util.concurrent.atomic.AtomicReference
class EnterTanContext(
val enterTanResult: AtomicReference<EnterTanResult>,
val responseHolder: ResponseHolder<*>,
val countDownLatch: CountDownLatch,
val tanRequestedTimeStamp: Date = Date()
)

View File

@ -1,9 +0,0 @@
package net.dankito.banking.fints.rest.model
import net.dankito.banking.fints.model.TanChallenge
class EnteringTanRequested(
val tanRequestId: String,
val tanChallenge: TanChallenge
)

View File

@ -1,63 +0,0 @@
package net.dankito.banking.fints.rest.model
import java.util.concurrent.CountDownLatch
class ResponseHolder<T>() {
private var responseReceivedLatch = CountDownLatch(1)
constructor(error: String) : this() {
setError(error)
}
var response: T? = null
private set
var error: String? = null
private set
var enterTanRequest: EnteringTanRequested? = null
private set
fun setResponse(response: T) {
this.response = response
signalResponseReceived()
}
fun setError(error: String) {
this.error = error
signalResponseReceived()
}
fun setEnterTanRequest(enterTanRequest: EnteringTanRequested) {
this.enterTanRequest = enterTanRequest
signalResponseReceived()
}
fun waitForResponse() {
responseReceivedLatch.await()
}
fun resetAfterEnteringTan() {
this.enterTanRequest = null
responseReceivedLatch = CountDownLatch(1)
}
private fun signalResponseReceived() {
responseReceivedLatch.countDown()
}
override fun toString(): String {
return "Error: $error, TAN requested: $enterTanRequest, success: $response"
}
}

View File

@ -1,6 +0,0 @@
package net.dankito.banking.fints.rest.model.dto.request
open class AccountRequestDto(
open val identifier: String
)

View File

@ -1,4 +0,0 @@
package net.dankito.banking.fints.rest.model.dto.request
open class AddAccountRequestDto : BankAccessDataRequestDto()

View File

@ -1,6 +0,0 @@
package net.dankito.banking.fints.rest.model.dto.request
import net.dankito.banking.fints.rest.model.BankAccessData
open class BankAccessDataRequestDto : BankAccessData()

View File

@ -1,14 +0,0 @@
package net.dankito.banking.fints.rest.model.dto.request
import net.dankito.banking.fints.rest.model.BankAccessData
import net.dankito.utils.multiplatform.Date
open class GetAccountsTransactionsRequestDto(
open val credentials: BankAccessData,
open val accounts: List<AccountRequestDto>,
open val alsoRetrieveBalance: Boolean = true,
open val fromDate: Date? = null,
open val toDate: Date? = null,
open val abortIfTanIsRequired: Boolean = false
)

View File

@ -1,9 +0,0 @@
package net.dankito.banking.fints.rest.model.dto.request
import net.dankito.banking.fints.model.EnterTanResult
class TanResponseDto(
val tanRequestId: String,
val enterTanResult: EnterTanResult
)

View File

@ -1,17 +0,0 @@
package net.dankito.banking.fints.rest.model.dto.response
import net.dankito.utils.multiplatform.Date
import java.math.BigDecimal
open class AccountTransactionResponseDto(
open val amount: BigDecimal,
open val currency: String,
open val reference: String,
open val bookingDate: Date,
open val otherPartyName: String?,
open val otherPartyBankCode: String?,
open val otherPartyAccountId: String?,
open val bookingText: String?,
open val valueDate: Date
)

View File

@ -1,9 +0,0 @@
package net.dankito.banking.fints.rest.model.dto.response
open class AddAccountResponseDto(
successful: Boolean,
errorMessage: String?,
open val bank: BankResponseDto,
open val accounts: List<BankAccountResponseDto>
) : ResponseDtoBase(successful, errorMessage)

View File

@ -1,26 +0,0 @@
package net.dankito.banking.fints.rest.model.dto.response
import net.dankito.banking.fints.response.segments.AccountType
import net.dankito.utils.multiplatform.Date
import java.math.BigDecimal
open class BankAccountResponseDto(
open val accountIdentifier: String,
open val subAccountAttribute: String?,
open val iban: String?,
open val accountType: AccountType?,
open val currency: String?,
open val accountHolderName: String,
open val productName: String?,
open val supportsRetrievingBalance: Boolean,
open val supportsRetrievingAccountTransactions: Boolean,
open val supportsTransferringMoney: Boolean,
open val supportsInstantPaymentMoneyTransfer: Boolean,
open val successfullyRetrievedData: Boolean,
open val balance: BigDecimal?,
open val retrievedTransactionsFrom: Date?,
open val retrievedTransactionsTo: Date?,
open var bookedTransactions: Collection<AccountTransactionResponseDto>,
open var unbookedTransactions: Collection<Any>
)

View File

@ -1,22 +0,0 @@
package net.dankito.banking.fints.rest.model.dto.response
import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.TanMedium
open class BankResponseDto(
open val bankCode: String,
open val userName: String,
open val finTs3ServerAddress: String,
open val bic: String,
open val bankName: String,
open val userId: String,
open val customerName: String,
open val usersTanMethods: List<TanMethodResponseDto>,
open val selectedTanMethod: TanMethodResponseDto?,
open val tanMedia: List<TanMedium>,
open val supportedHbciVersions: List<String>
)

View File

@ -1,14 +0,0 @@
package net.dankito.banking.fints.rest.model.dto.response
import java.math.BigDecimal
open class GetAccountTransactionsResponseDto(
open val identifier: String,
open val productName: String?,
successful: Boolean,
errorMessage: String?,
open val balance: BigDecimal?,
open var bookedTransactions: Collection<AccountTransactionResponseDto>,
open var unbookedTransactions: Collection<Any>
) : ResponseDtoBase(successful, errorMessage)

View File

@ -1,6 +0,0 @@
package net.dankito.banking.fints.rest.model.dto.response
open class GetAccountsTransactionsResponseDto(
open val transactionsPerAccount: List<RestResponse<GetAccountTransactionsResponseDto>>
)

View File

@ -1,7 +0,0 @@
package net.dankito.banking.fints.rest.model.dto.response
open class ResponseDtoBase(
open val successful: Boolean,
open val errorMessage: String?
)

View File

@ -1,12 +0,0 @@
package net.dankito.banking.fints.rest.model.dto.response
enum class ResponseType {
Success,
Error,
TanRequired
}

View File

@ -1,29 +0,0 @@
package net.dankito.banking.fints.rest.model.dto.response
import net.dankito.banking.fints.rest.model.EnteringTanRequested
class RestResponse<T>(
val status: ResponseType,
val errorMessage: String?,
val successResponse: T?,
val enteringTanRequested: EnteringTanRequested? = null
) {
companion object {
fun <T> success(result: T): RestResponse<T> {
return RestResponse(ResponseType.Success, null, result, null)
}
fun <T> error(errorMessage: String): RestResponse<T> {
return RestResponse(ResponseType.Error, errorMessage, null, null)
}
fun <T> requiresTan(enteringTanRequested: EnteringTanRequested): RestResponse<T> {
return RestResponse(ResponseType.TanRequired, null, null, enteringTanRequested)
}
}
}

Some files were not shown because too many files have changed in this diff Show More