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:
parent
3c096d655f
commit
f637897dc3
122
build.gradle
122
build.gradle
|
@ -2,99 +2,12 @@
|
||||||
ext {
|
ext {
|
||||||
appVersionName = '1.0.0-Alpha-10'
|
appVersionName = '1.0.0-Alpha-10'
|
||||||
|
|
||||||
appVersionCode = 10
|
|
||||||
|
|
||||||
|
|
||||||
/* MPP / basic dependencies */
|
/* MPP / basic dependencies */
|
||||||
|
|
||||||
kotlinVersion = '1.4.21'
|
|
||||||
kotlinCoroutinesVersion = "1.3.7"
|
|
||||||
|
|
||||||
|
|
||||||
ktorVersion = "1.4.2"
|
|
||||||
|
|
||||||
|
|
||||||
javaUtilsVersion = '1.0.18'
|
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 */
|
/* Test */
|
||||||
|
|
||||||
|
@ -113,17 +26,13 @@ ext {
|
||||||
}
|
}
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.3.72'
|
|
||||||
// ext.kotlin_version = '1.4.10'
|
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
google()
|
google()
|
||||||
jcenter()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:4.0.1'
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
|
||||||
// classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
|
||||||
|
|
||||||
// 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
|
// 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"
|
classpath "io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.10.0"
|
||||||
|
@ -142,10 +51,9 @@ allprojects {
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
google()
|
google()
|
||||||
jcenter()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
group 'net.dankito.banking'
|
group 'net.codinux.banking'
|
||||||
version appVersionName
|
version appVersionName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -154,26 +62,6 @@ allprojects {
|
||||||
task jarAll {
|
task jarAll {
|
||||||
dependsOn = [
|
dependsOn = [
|
||||||
"common:jvmJar",
|
"common:jvmJar",
|
||||||
"fints4k:jvm6Jar",
|
"fints4k:jvmJar",
|
||||||
"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"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -1,41 +1,52 @@
|
||||||
plugins {
|
plugins {
|
||||||
id "org.jetbrains.kotlin.multiplatform"
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
id "com.android.library"
|
|
||||||
id "maven-publish"
|
id "maven-publish"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ext.artifactName = "multiplatform-utils"
|
ext.artifactName = "multiplatform-utils"
|
||||||
|
|
||||||
def frameworkName = "MultiplatformUtils"
|
|
||||||
|
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvm {
|
jvm {
|
||||||
compilations.main.kotlinOptions {
|
compilations.all {
|
||||||
jvmTarget = "1.6"
|
kotlinOptions.jvmTarget = '1.8'
|
||||||
|
}
|
||||||
|
withJava()
|
||||||
|
testRuns["test"].executionTask.configure {
|
||||||
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
targets {
|
// js(BOTH) {
|
||||||
final def iOSTarget = iOSIsRealDevice ? presets.iosArm64 : presets.iosX64
|
// browser {
|
||||||
|
// commonWebpackConfig {
|
||||||
|
// cssSupport.enabled = true
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
fromPreset(iOSTarget, 'ios') {
|
ios {
|
||||||
binaries {
|
binaries {
|
||||||
framework {
|
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 {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation kotlin("stdlib-common")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,7 +62,6 @@ kotlin {
|
||||||
|
|
||||||
jvmMain {
|
jvmMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api kotlin("stdlib-jdk7")
|
|
||||||
|
|
||||||
compileOnly "org.slf4j:slf4j-api:$slf4jVersion"
|
compileOnly "org.slf4j:slf4j-api:$slf4jVersion"
|
||||||
|
|
||||||
|
@ -79,97 +89,9 @@ kotlin {
|
||||||
|
|
||||||
iosMain {
|
iosMain {
|
||||||
dependencies {
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -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>
|
|
|
@ -1,10 +0,0 @@
|
||||||
apply plugin: 'java-library'
|
|
||||||
apply plugin: 'kotlin'
|
|
||||||
|
|
||||||
sourceCompatibility = "1.7"
|
|
||||||
targetCompatibility = "1.7"
|
|
||||||
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
api project(":fints4k")
|
|
||||||
}
|
|
|
@ -1,96 +1,75 @@
|
||||||
|
|
||||||
buildscript {
|
|
||||||
repositories {
|
|
||||||
jcenter()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id "org.jetbrains.kotlin.multiplatform"
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
id "com.android.library"
|
|
||||||
id "maven-publish"
|
id "maven-publish"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvm("jvm6") {
|
jvm {
|
||||||
compilations.main.kotlinOptions {
|
compilations.all {
|
||||||
jvmTarget = "1.6"
|
kotlinOptions.jvmTarget = '1.8'
|
||||||
|
}
|
||||||
|
withJava()
|
||||||
|
testRuns["test"].executionTask.configure {
|
||||||
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
android()
|
// js(BOTH) {
|
||||||
|
// browser {
|
||||||
|
// commonWebpackConfig {
|
||||||
|
// cssSupport.enabled = true
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
targets {
|
ios {
|
||||||
final def iOSTarget = iOSIsRealDevice ? presets.iosArm64 : presets.iosX64
|
binaries {
|
||||||
|
framework {
|
||||||
fromPreset(iOSTarget, 'ios') {
|
baseName = "fints4k"
|
||||||
binaries {
|
|
||||||
framework {
|
|
||||||
baseName = "fints4k"
|
|
||||||
|
|
||||||
embedBitcode(embedBitcodeValue)
|
|
||||||
|
|
||||||
export(project(":common"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
js() {
|
// def hostOs = System.getProperty("os.name")
|
||||||
|
// def isMingwX64 = hostOs.startsWith("Windows")
|
||||||
nodejs {
|
// def nativeTarget
|
||||||
testTask {
|
// if (hostOs == "Mac OS X") nativeTarget = macosX64('native') { binaries.executable() }
|
||||||
enabled = false
|
// 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.")
|
||||||
|
|
||||||
browser {
|
|
||||||
testTask {
|
|
||||||
enabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation kotlin("stdlib-common")
|
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-common:$kotlinCoroutinesVersion"
|
|
||||||
|
|
||||||
api project(":common")
|
api project(":common")
|
||||||
|
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion"
|
||||||
|
|
||||||
implementation "io.ktor:ktor-client-core:$ktorVersion"
|
implementation "io.ktor:ktor-client-core:$ktorVersion"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
commonTest {
|
commonTest {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation kotlin("test-common")
|
implementation kotlin("test")
|
||||||
implementation kotlin("test-annotations-common")
|
|
||||||
|
|
||||||
implementation "ch.tutteli.atrium:atrium-fluent-en_GB-common:$atriumVersion"
|
implementation "ch.tutteli.atrium:atrium-fluent-en_GB-common:$atriumVersion"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
jvm6Main {
|
jvmMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
// implementation "io.ktor:ktor-client-cio:$ktorVersion"
|
|
||||||
implementation "io.ktor:ktor-client-okhttp:$ktorVersion"
|
implementation "io.ktor:ktor-client-okhttp:$ktorVersion"
|
||||||
|
|
||||||
implementation "org.slf4j:slf4j-api:$slf4jVersion"
|
implementation "org.slf4j:slf4j-api:$slf4jVersion"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
jvm6Test {
|
jvmTest {
|
||||||
dependencies {
|
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.assertj:assertj-core:$assertJVersion"
|
||||||
implementation "org.mockito:mockito-core:$mockitoVersion"
|
implementation "org.mockito:mockito-core:$mockitoVersion"
|
||||||
|
@ -98,9 +77,9 @@ kotlin {
|
||||||
implementation "ch.tutteli.atrium:atrium-fluent-en_GB:$atriumVersion"
|
implementation "ch.tutteli.atrium:atrium-fluent-en_GB:$atriumVersion"
|
||||||
|
|
||||||
|
|
||||||
implementation project(":BankingUiCommon")
|
// implementation project(":BankingUiCommon")
|
||||||
implementation project(":BankFinder")
|
// implementation project(":BankFinder")
|
||||||
implementation project(":fints4kBankingClient")
|
// implementation project(":fints4kBankingClient")
|
||||||
|
|
||||||
|
|
||||||
implementation "org.apache.commons:commons-csv:1.8"
|
implementation "org.apache.commons:commons-csv:1.8"
|
||||||
|
@ -111,111 +90,36 @@ kotlin {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
androidMain {
|
// jsMain {
|
||||||
dependsOn jvm6Main
|
// dependencies {
|
||||||
|
// implementation "io.ktor:ktor-client-js:$ktorVersion"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// jsTest {
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation "io.ktor:ktor-client-android:$ktorVersion"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
iosMain {
|
iosMain {
|
||||||
dependencies {
|
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-ios:$ktorVersion"
|
||||||
implementation "io.ktor:ktor-client-core-native:$ktorVersion"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
jsMain {
|
nativeMain {
|
||||||
dependsOn commonMain
|
|
||||||
|
|
||||||
dependencies {
|
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"
|
// requires cURL to be installed on your system
|
||||||
implementation "io.ktor:ktor-client-encoding-js:$ktorVersion"
|
implementation "io.ktor:ktor-client-curl:$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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -24,7 +24,7 @@ open class MessageBuilderResult(
|
||||||
}
|
}
|
||||||
|
|
||||||
open val getHighestAllowedVersion: Int?
|
open val getHighestAllowedVersion: Int?
|
||||||
get() = allowedVersions.max()
|
get() = allowedVersions.maxOrNull()
|
||||||
|
|
||||||
open fun isSendEnteredTanMessage(): Boolean {
|
open fun isSendEnteredTanMessage(): Boolean {
|
||||||
// contains only a ZweiSchrittTanEinreichung segment
|
// contains only a ZweiSchrittTanEinreichung segment
|
||||||
|
|
|
@ -2,10 +2,10 @@ package net.dankito.banking.fints.webclient
|
||||||
|
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.request.post
|
import io.ktor.client.request.post
|
||||||
import io.ktor.client.statement.HttpResponse
|
import io.ktor.client.request.setBody
|
||||||
import io.ktor.client.statement.readText
|
import io.ktor.client.statement.bodyAsText
|
||||||
import io.ktor.content.TextContent
|
|
||||||
import io.ktor.http.ContentType
|
import io.ktor.http.ContentType
|
||||||
|
import io.ktor.http.contentType
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.cancel
|
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) {
|
override fun post(url: String, body: String, contentType: String, userAgent: String, callback: (WebClientResponse) -> Unit) {
|
||||||
GlobalScope.async {
|
GlobalScope.async {
|
||||||
try {
|
try {
|
||||||
val clientResponse = client.post<HttpResponse>(url) {
|
val clientResponse = client.post(url) {
|
||||||
this.body = TextContent(body, contentType = ContentType.Application.OctetStream)
|
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))
|
callback(WebClientResponse(clientResponse.status.value == 200, clientResponse.status.value, body = responseBody))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
|
@ -15,6 +15,7 @@ import net.dankito.banking.fints.util.FinTsUtils
|
||||||
import net.dankito.utils.multiplatform.Date
|
import net.dankito.utils.multiplatform.Date
|
||||||
import net.dankito.utils.multiplatform.Month
|
import net.dankito.utils.multiplatform.Month
|
||||||
import kotlin.test.AfterTest
|
import kotlin.test.AfterTest
|
||||||
|
import kotlin.test.Ignore
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
|
|
||||||
|
|
||||||
|
@ -191,6 +192,7 @@ class MessageBuilderTest : FinTsTestBase() {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
@Test
|
@Test
|
||||||
fun createGetTransactionsMessage_WithContinuationIdSet() {
|
fun createGetTransactionsMessage_WithContinuationIdSet() {
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ import net.dankito.banking.fints.extensions.isFalse
|
||||||
import net.dankito.banking.fints.extensions.isTrue
|
import net.dankito.banking.fints.extensions.isTrue
|
||||||
import net.dankito.banking.fints.model.Amount
|
import net.dankito.banking.fints.model.Amount
|
||||||
import net.dankito.utils.multiplatform.Date
|
import net.dankito.utils.multiplatform.Date
|
||||||
|
import kotlin.test.Ignore
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.fail
|
import kotlin.test.fail
|
||||||
|
|
||||||
|
@ -460,6 +461,7 @@ class ResponseParserTest : FinTsTestBase() {
|
||||||
?: run { fail("No segment of type UserParameters found in ${result.receivedSegments}") }
|
?: run { fail("No segment of type UserParameters found in ${result.receivedSegments}") }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Ignore
|
||||||
@Test
|
@Test
|
||||||
fun parseAccountInfo() {
|
fun parseAccountInfo() {
|
||||||
|
|
||||||
|
@ -1315,9 +1317,9 @@ class ResponseParserTest : FinTsTestBase() {
|
||||||
assertCouldParseSegment(segment, segmentId, segmentNumber, segmentVersion, referenceSegmentNumber)
|
assertCouldParseSegment(segment, segmentId, segmentNumber, segmentVersion, referenceSegmentNumber)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assertCouldParseSegment(segment: ReceivedSegment?, segmentId: ISegmentId, segmentNumber: Int,
|
private fun assertCouldParseSegment(segment: ReceivedSegment?, segmentId: ISegmentId, segmentNumber: Int,
|
||||||
segmentVersion: Int, referenceSegmentNumber: Int?) {
|
segmentVersion: Int, referenceSegmentNumber: Int?) {
|
||||||
|
|
||||||
expect(segment).notToBeNull()
|
expect(segment).notToBeNull()
|
||||||
|
|
||||||
segment?.let {
|
segment?.let {
|
||||||
|
@ -1328,10 +1330,10 @@ class ResponseParserTest : FinTsTestBase() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun assertCouldParseJobParametersSegment(segment: JobParameters?, segmentId: ISegmentId, segmentNumber: Int,
|
private fun assertCouldParseJobParametersSegment(segment: JobParameters?, segmentId: ISegmentId, segmentNumber: Int,
|
||||||
segmentVersion: Int, referenceSegmentNumber: Int?, jobName: String,
|
segmentVersion: Int, referenceSegmentNumber: Int?, jobName: String,
|
||||||
maxCountJobs: Int, minimumCountSignatures: Int, securityClass: Int?) {
|
maxCountJobs: Int, minimumCountSignatures: Int, securityClass: Int?) {
|
||||||
|
|
||||||
assertCouldParseSegment(segment, segmentId, segmentNumber, segmentVersion, referenceSegmentNumber)
|
assertCouldParseSegment(segment, segmentId, segmentNumber, segmentVersion, referenceSegmentNumber)
|
||||||
|
|
||||||
segment?.let {
|
segment?.let {
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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()
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//}
|
|
@ -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"
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//}
|
|
@ -4,7 +4,7 @@ import net.dankito.banking.fints.FinTsTestBaseJvm
|
||||||
import net.dankito.banking.fints.messages.HbciCharset
|
import net.dankito.banking.fints.messages.HbciCharset
|
||||||
import net.dankito.banking.fints.tan.TanImageDecoder
|
import net.dankito.banking.fints.tan.TanImageDecoder
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.Test
|
import org.junit.jupiter.api.Test
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import net.dankito.banking.fints.FinTsTestBaseJvm
|
||||||
import net.dankito.banking.fints.model.AccountData
|
import net.dankito.banking.fints.model.AccountData
|
||||||
import net.dankito.banking.fints.model.BankData
|
import net.dankito.banking.fints.model.BankData
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
|
||||||
class Mt940AccountTransactionsParserTest : FinTsTestBaseJvm() {
|
class Mt940AccountTransactionsParserTest : FinTsTestBaseJvm() {
|
|
@ -3,7 +3,7 @@ package net.dankito.banking.fints.transactions
|
||||||
import net.dankito.banking.fints.FinTsTestBaseJvm
|
import net.dankito.banking.fints.FinTsTestBaseJvm
|
||||||
import net.dankito.banking.fints.transactions.mt940.Mt940Parser
|
import net.dankito.banking.fints.transactions.mt940.Mt940Parser
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
|
||||||
class Mt940ParserTestJvm : FinTsTestBaseJvm() {
|
class Mt940ParserTestJvm : FinTsTestBaseJvm() {
|
|
@ -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>
|
|
|
@ -1,5 +1,3 @@
|
||||||
android.enableJetifier=true
|
|
||||||
android.useAndroidX=true
|
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
|
|
||||||
# Specifies the JVM arguments used for the daemon process.
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
@ -7,6 +5,7 @@ kotlin.code.style=official
|
||||||
org.gradle.jvmargs=-Xmx3072m
|
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
|
|
@ -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"
|
|
||||||
}
|
|
|
@ -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")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
|
|
||||||
}
|
|
|
@ -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
|
|
|
@ -1,5 +0,0 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="net.dankito.banking.persistence">
|
|
||||||
|
|
||||||
/
|
|
||||||
</manifest>
|
|
|
@ -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
|
|
||||||
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
} }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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 }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
|
|
||||||
}
|
|
|
@ -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>)
|
|
||||||
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
package net.dankito.banking.persistence.model
|
|
||||||
|
|
||||||
|
|
||||||
enum class TanMediumType {
|
|
||||||
|
|
||||||
TanGeneratorTanMedium,
|
|
||||||
|
|
||||||
MobilePhoneTanMedium,
|
|
||||||
|
|
||||||
OtherTanMedium
|
|
||||||
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
|
|
||||||
}
|
|
|
@ -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?
|
|
||||||
)
|
|
|
@ -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.
|
|
|
@ -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"
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
*
|
|
||||||
!build/*-runner
|
|
||||||
!build/*-runner.jar
|
|
||||||
!build/lib/*
|
|
||||||
!build/quarkus-app/*
|
|
|
@ -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"
|
|
||||||
}
|
|
|
@ -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" ]
|
|
|
@ -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" ]
|
|
|
@ -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"]
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
quarkus.http.port=5666
|
|
|
@ -1,6 +0,0 @@
|
||||||
package net.dankito.banking.bankfinder.rest
|
|
||||||
|
|
||||||
import io.quarkus.test.junit.NativeImageTest
|
|
||||||
|
|
||||||
@NativeImageTest
|
|
||||||
class NativeBankFinderResourceIT : ExampleResourceTest()
|
|
|
@ -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\""))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
*
|
|
||||||
!build/*-runner
|
|
||||||
!build/*-runner.jar
|
|
||||||
!build/lib/*
|
|
||||||
!build/quarkus-app/*
|
|
|
@ -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"
|
|
||||||
}
|
|
|
@ -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" ]
|
|
|
@ -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" ]
|
|
|
@ -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"]
|
|
|
@ -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")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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>"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
)
|
|
|
@ -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
|
|
||||||
)
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
package net.dankito.banking.fints.rest.model.dto.request
|
|
||||||
|
|
||||||
|
|
||||||
open class AccountRequestDto(
|
|
||||||
open val identifier: String
|
|
||||||
)
|
|
|
@ -1,4 +0,0 @@
|
||||||
package net.dankito.banking.fints.rest.model.dto.request
|
|
||||||
|
|
||||||
|
|
||||||
open class AddAccountRequestDto : BankAccessDataRequestDto()
|
|
|
@ -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()
|
|
|
@ -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
|
|
||||||
)
|
|
|
@ -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
|
|
||||||
)
|
|
|
@ -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
|
|
||||||
)
|
|
|
@ -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)
|
|
|
@ -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>
|
|
||||||
)
|
|
|
@ -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>
|
|
||||||
)
|
|
|
@ -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)
|
|
|
@ -1,6 +0,0 @@
|
||||||
package net.dankito.banking.fints.rest.model.dto.response
|
|
||||||
|
|
||||||
|
|
||||||
open class GetAccountsTransactionsResponseDto(
|
|
||||||
open val transactionsPerAccount: List<RestResponse<GetAccountTransactionsResponseDto>>
|
|
||||||
)
|
|
|
@ -1,7 +0,0 @@
|
||||||
package net.dankito.banking.fints.rest.model.dto.response
|
|
||||||
|
|
||||||
|
|
||||||
open class ResponseDtoBase(
|
|
||||||
open val successful: Boolean,
|
|
||||||
open val errorMessage: String?
|
|
||||||
)
|
|
|
@ -1,12 +0,0 @@
|
||||||
package net.dankito.banking.fints.rest.model.dto.response
|
|
||||||
|
|
||||||
|
|
||||||
enum class ResponseType {
|
|
||||||
|
|
||||||
Success,
|
|
||||||
|
|
||||||
Error,
|
|
||||||
|
|
||||||
TanRequired
|
|
||||||
|
|
||||||
}
|
|
|
@ -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
Loading…
Reference in New Issue