From 5569594a65703789c7f7073a8fd61b43dd3cc715 Mon Sep 17 00:00:00 2001 From: dankito Date: Sun, 18 Oct 2020 14:56:54 +0200 Subject: [PATCH] Started fints4k REST API --- rest/fints4kRest/.dockerignore | 5 + rest/fints4kRest/build.gradle | 62 ++++++++ .../src/main/docker/Dockerfile.fast-jar | 57 ++++++++ .../src/main/docker/Dockerfile.jvm | 54 +++++++ .../src/main/docker/Dockerfile.native | 27 ++++ .../banking/fints/rest/fints4kResource.kt | 50 +++++++ .../banking/fints/rest/mapper/DtoMapper.kt | 137 ++++++++++++++++++ .../fints/rest/model/BankAccessData.kt | 13 ++ .../fints/rest/model/EnterTanContext.kt | 13 ++ .../fints/rest/model/EnteringTanRequested.kt | 11 ++ .../model/dto/request/AccountRequestDto.kt | 6 + .../model/dto/request/AddAccountRequestDto.kt | 4 + .../dto/request/BankAccessDataRequestDto.kt | 6 + .../GetAccountsTransactionsRequestDto.kt | 14 ++ .../response/AccountTransactionResponseDto.kt | 15 ++ .../dto/response/AddAccountResponseDto.kt | 9 ++ .../dto/response/BankAccountResponseDto.kt | 26 ++++ .../model/dto/response/BankResponseDto.kt | 22 +++ .../GetAccountTransactionsResponseDto.kt | 14 ++ .../GetAccountsTransactionsResponseDto.kt | 6 + .../model/dto/response/ResponseDtoBase.kt | 7 + .../dto/response/TanMethodResponseDto.kt | 15 ++ .../fints/rest/service/fints4kService.kt | 134 +++++++++++++++++ .../src/main/resources/application.properties | 5 + .../banking/fints/rest/NativeFints4kIT.kt | 6 + .../banking/fints/rest/fints4kResourceTest.kt | 20 +++ settings.gradle | 2 + 27 files changed, 740 insertions(+) create mode 100644 rest/fints4kRest/.dockerignore create mode 100644 rest/fints4kRest/build.gradle create mode 100644 rest/fints4kRest/src/main/docker/Dockerfile.fast-jar create mode 100644 rest/fints4kRest/src/main/docker/Dockerfile.jvm create mode 100644 rest/fints4kRest/src/main/docker/Dockerfile.native create mode 100644 rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/fints4kResource.kt create mode 100644 rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/mapper/DtoMapper.kt create mode 100644 rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/BankAccessData.kt create mode 100644 rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/EnterTanContext.kt create mode 100644 rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/EnteringTanRequested.kt create mode 100644 rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/request/AccountRequestDto.kt create mode 100644 rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/request/AddAccountRequestDto.kt create mode 100644 rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/request/BankAccessDataRequestDto.kt create mode 100644 rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/request/GetAccountsTransactionsRequestDto.kt create mode 100644 rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/AccountTransactionResponseDto.kt create mode 100644 rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/AddAccountResponseDto.kt create mode 100644 rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/BankAccountResponseDto.kt create mode 100644 rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/BankResponseDto.kt create mode 100644 rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/GetAccountTransactionsResponseDto.kt create mode 100644 rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/GetAccountsTransactionsResponseDto.kt create mode 100644 rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/ResponseDtoBase.kt create mode 100644 rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/TanMethodResponseDto.kt create mode 100644 rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/service/fints4kService.kt create mode 100644 rest/fints4kRest/src/main/resources/application.properties create mode 100644 rest/fints4kRest/src/native-test/kotlin/net/dankito/banking/fints/rest/NativeFints4kIT.kt create mode 100644 rest/fints4kRest/src/test/kotlin/net/dankito/banking/fints/rest/fints4kResourceTest.kt diff --git a/rest/fints4kRest/.dockerignore b/rest/fints4kRest/.dockerignore new file mode 100644 index 00000000..4361d2fb --- /dev/null +++ b/rest/fints4kRest/.dockerignore @@ -0,0 +1,5 @@ +* +!build/*-runner +!build/*-runner.jar +!build/lib/* +!build/quarkus-app/* \ No newline at end of file diff --git a/rest/fints4kRest/build.gradle b/rest/fints4kRest/build.gradle new file mode 100644 index 00000000..d0fb7b04 --- /dev/null +++ b/rest/fints4kRest/build.gradle @@ -0,0 +1,62 @@ +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-universe-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" +} \ No newline at end of file diff --git a/rest/fints4kRest/src/main/docker/Dockerfile.fast-jar b/rest/fints4kRest/src/main/docker/Dockerfile.fast-jar new file mode 100644 index 00000000..ed4e2dd9 --- /dev/null +++ b/rest/fints4kRest/src/main/docker/Dockerfile.fast-jar @@ -0,0 +1,57 @@ +#### +# 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" ] \ No newline at end of file diff --git a/rest/fints4kRest/src/main/docker/Dockerfile.jvm b/rest/fints4kRest/src/main/docker/Dockerfile.jvm new file mode 100644 index 00000000..0e76ac92 --- /dev/null +++ b/rest/fints4kRest/src/main/docker/Dockerfile.jvm @@ -0,0 +1,54 @@ +#### +# 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" ] \ No newline at end of file diff --git a/rest/fints4kRest/src/main/docker/Dockerfile.native b/rest/fints4kRest/src/main/docker/Dockerfile.native new file mode 100644 index 00000000..c1522ea4 --- /dev/null +++ b/rest/fints4kRest/src/main/docker/Dockerfile.native @@ -0,0 +1,27 @@ +#### +# 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"] \ No newline at end of file diff --git a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/fints4kResource.kt b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/fints4kResource.kt new file mode 100644 index 00000000..9f4b96c3 --- /dev/null +++ b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/fints4kResource.kt @@ -0,0 +1,50 @@ +package net.dankito.banking.fints.rest + +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.response.GetAccountsTransactionsResponseDto +import net.dankito.banking.fints.rest.service.fints4kService +import javax.inject.Inject +import javax.ws.rs.* +import javax.ws.rs.core.MediaType + + +@Path("/") +class fints4kResource { + + @Inject + protected val service = fints4kService() + + protected val mapper = DtoMapper() + + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Path("addaccount") + fun addAccount( + request: AddAccountRequestDto, + @DefaultValue("false") @QueryParam("showRawResponse") showRawResponse: Boolean + ): Any { + val clientResponse = service.getAddAccountResponse(request) + + if (showRawResponse) { + return clientResponse + } + + return mapper.map(clientResponse) + } + + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Path("transactions") + fun getAccountTransactions(request: GetAccountsTransactionsRequestDto): GetAccountsTransactionsResponseDto { + val accountsTransactions = service.getAccountTransactions(request) + + return mapper.mapTransactions(accountsTransactions) + } + +} \ No newline at end of file diff --git a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/mapper/DtoMapper.kt b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/mapper/DtoMapper.kt new file mode 100644 index 00000000..83f35443 --- /dev/null +++ b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/mapper/DtoMapper.kt @@ -0,0 +1,137 @@ +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.dto.response.* +import java.math.BigDecimal + + +open class DtoMapper { + + 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.userName, + 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 mapTransactions(accountsTransactions: List): GetAccountsTransactionsResponseDto { + return GetAccountsTransactionsResponseDto(accountsTransactions.map { map(it) }) + } + + open fun map(accountTransactions: GetTransactionsResponse): GetAccountTransactionsResponseDto { + val retrievedData = accountTransactions.retrievedData.first() + val balance = mapNullable(retrievedData.balance) + val bookedTransactions = map(retrievedData.bookedTransactions) + + return GetAccountTransactionsResponseDto( + retrievedData.accountData.accountIdentifier, + retrievedData.accountData.productName, + accountTransactions.successful, + mapErrorMessage(accountTransactions), + balance, + bookedTransactions, + listOf() + ) + } + + + protected open fun map(accountData: List): List { + return accountData.map { map(it) } + } + + protected open fun map(accountData: RetrievedAccountData): BankAccountResponseDto { + return BankAccountResponseDto( + accountData.accountData.accountIdentifier, + accountData.accountData.subAccountAttribute, + accountData.accountData.iban, + accountData.accountData.userName, + accountData.accountData.accountType, + accountData.accountData.currency, + accountData.accountData.accountHolderName, + accountData.accountData.productName, + accountData.accountData.supportsRetrievingBalance, + accountData.accountData.supportsRetrievingAccountTransactions, + accountData.accountData.supportsTransferringMoney, + accountData.accountData.supportsRealTimeTransfer, + accountData.successfullyRetrievedData, + mapNullable(accountData.balance), + accountData.retrievedTransactionsFrom, + accountData.retrievedTransactionsTo, + map(accountData.bookedTransactions), + listOf() + ) + } + + + protected open fun map(transactions: Collection): Collection { + return transactions.map { map(it) } + } + + protected open fun map(transaction: AccountTransaction): AccountTransactionResponseDto { + return AccountTransactionResponseDto( + map(transaction.amount), + transaction.reference, + transaction.otherPartyName, + transaction.otherPartyBankCode, + transaction.otherPartyAccountId, + transaction.bookingText, + transaction.valueDate + ) + } + + + protected open fun mapTanMethods(tanMethods: List): List { + 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 + } + +} \ No newline at end of file diff --git a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/BankAccessData.kt b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/BankAccessData.kt new file mode 100644 index 00000000..c0506723 --- /dev/null +++ b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/BankAccessData.kt @@ -0,0 +1,13 @@ +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 + +} \ No newline at end of file diff --git a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/EnterTanContext.kt b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/EnterTanContext.kt new file mode 100644 index 00000000..841d874e --- /dev/null +++ b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/EnterTanContext.kt @@ -0,0 +1,13 @@ +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 + + +open class EnterTanContext( + open val enterTanResult: AtomicReference, + open val countDownLatch: CountDownLatch, + open val tanRequestedTimeStamp: Date = Date() +) \ No newline at end of file diff --git a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/EnteringTanRequested.kt b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/EnteringTanRequested.kt new file mode 100644 index 00000000..749a262a --- /dev/null +++ b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/EnteringTanRequested.kt @@ -0,0 +1,11 @@ +package net.dankito.banking.fints.rest.model + +import net.dankito.banking.fints.model.BankData +import net.dankito.banking.fints.model.TanChallenge + + +open class EnteringTanRequested( + open val tanRequestId: String, + open val bank: BankData, + open val tanChallenge: TanChallenge +) \ No newline at end of file diff --git a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/request/AccountRequestDto.kt b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/request/AccountRequestDto.kt new file mode 100644 index 00000000..413695a3 --- /dev/null +++ b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/request/AccountRequestDto.kt @@ -0,0 +1,6 @@ +package net.dankito.banking.fints.rest.model.dto.request + + +open class AccountRequestDto( + open val identifier: String +) \ No newline at end of file diff --git a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/request/AddAccountRequestDto.kt b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/request/AddAccountRequestDto.kt new file mode 100644 index 00000000..0eeb34d7 --- /dev/null +++ b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/request/AddAccountRequestDto.kt @@ -0,0 +1,4 @@ +package net.dankito.banking.fints.rest.model.dto.request + + +open class AddAccountRequestDto : BankAccessDataRequestDto() \ No newline at end of file diff --git a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/request/BankAccessDataRequestDto.kt b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/request/BankAccessDataRequestDto.kt new file mode 100644 index 00000000..1533e2dc --- /dev/null +++ b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/request/BankAccessDataRequestDto.kt @@ -0,0 +1,6 @@ +package net.dankito.banking.fints.rest.model.dto.request + +import net.dankito.banking.fints.rest.model.BankAccessData + + +open class BankAccessDataRequestDto : BankAccessData() \ No newline at end of file diff --git a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/request/GetAccountsTransactionsRequestDto.kt b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/request/GetAccountsTransactionsRequestDto.kt new file mode 100644 index 00000000..fdad36af --- /dev/null +++ b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/request/GetAccountsTransactionsRequestDto.kt @@ -0,0 +1,14 @@ +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, + open val alsoRetrieveBalance: Boolean = true, + open val fromDate: Date? = null, + open val toDate: Date? = null, + open val abortIfTanIsRequired: Boolean = false +) \ No newline at end of file diff --git a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/AccountTransactionResponseDto.kt b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/AccountTransactionResponseDto.kt new file mode 100644 index 00000000..6440df66 --- /dev/null +++ b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/AccountTransactionResponseDto.kt @@ -0,0 +1,15 @@ +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 reference: String, + open val otherPartyName: String?, + open val otherPartyBankCode: String?, + open val otherPartyAccountId: String?, + open val bookingText: String?, + open val valueDate: Date +) \ No newline at end of file diff --git a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/AddAccountResponseDto.kt b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/AddAccountResponseDto.kt new file mode 100644 index 00000000..fab0a71c --- /dev/null +++ b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/AddAccountResponseDto.kt @@ -0,0 +1,9 @@ +package net.dankito.banking.fints.rest.model.dto.response + + +open class AddAccountResponseDto( + successful: Boolean, + errorMessage: String?, + open val bank: BankResponseDto, + open val accounts: List +) : ResponseDtoBase(successful, errorMessage) \ No newline at end of file diff --git a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/BankAccountResponseDto.kt b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/BankAccountResponseDto.kt new file mode 100644 index 00000000..d49b216e --- /dev/null +++ b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/BankAccountResponseDto.kt @@ -0,0 +1,26 @@ +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, + open var unbookedTransactions: Collection +) \ No newline at end of file diff --git a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/BankResponseDto.kt b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/BankResponseDto.kt new file mode 100644 index 00000000..2d1b5492 --- /dev/null +++ b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/BankResponseDto.kt @@ -0,0 +1,22 @@ +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, + open val selectedTanMethod: TanMethodResponseDto?, + open val tanMedia: List, + + open val supportedHbciVersions: List +) \ No newline at end of file diff --git a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/GetAccountTransactionsResponseDto.kt b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/GetAccountTransactionsResponseDto.kt new file mode 100644 index 00000000..fa4005d9 --- /dev/null +++ b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/GetAccountTransactionsResponseDto.kt @@ -0,0 +1,14 @@ +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, + open var unbookedTransactions: Collection +) : ResponseDtoBase(successful, errorMessage) \ No newline at end of file diff --git a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/GetAccountsTransactionsResponseDto.kt b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/GetAccountsTransactionsResponseDto.kt new file mode 100644 index 00000000..b6b7f228 --- /dev/null +++ b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/GetAccountsTransactionsResponseDto.kt @@ -0,0 +1,6 @@ +package net.dankito.banking.fints.rest.model.dto.response + + +open class GetAccountsTransactionsResponseDto( + open val accounts: List +) \ No newline at end of file diff --git a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/ResponseDtoBase.kt b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/ResponseDtoBase.kt new file mode 100644 index 00000000..e0747d1a --- /dev/null +++ b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/ResponseDtoBase.kt @@ -0,0 +1,7 @@ +package net.dankito.banking.fints.rest.model.dto.response + + +open class ResponseDtoBase( + open val successful: Boolean, + open val errorMessage: String? +) \ No newline at end of file diff --git a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/TanMethodResponseDto.kt b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/TanMethodResponseDto.kt new file mode 100644 index 00000000..f8557b7e --- /dev/null +++ b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/model/dto/response/TanMethodResponseDto.kt @@ -0,0 +1,15 @@ +package net.dankito.banking.fints.rest.model.dto.response + +import net.dankito.banking.fints.messages.datenelemente.implementierte.tan.AllowedTanFormat +import net.dankito.banking.fints.model.TanMethodType + + +open class TanMethodResponseDto( + open val displayName: String, + open val bankInternalMethodCode: String, + open val type: TanMethodType, + open val hhdVersion: String? = null, + open val maxTanInputLength: Int? = null, + open val allowedTanFormat: AllowedTanFormat = AllowedTanFormat.Alphanumeric, + open val nameOfTanMediumRequired: Boolean = false +) \ No newline at end of file diff --git a/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/service/fints4kService.kt b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/service/fints4kService.kt new file mode 100644 index 00000000..5f350fca --- /dev/null +++ b/rest/fints4kRest/src/main/kotlin/net/dankito/banking/fints/rest/service/fints4kService.kt @@ -0,0 +1,134 @@ +package net.dankito.banking.fints.rest.service + +import net.dankito.banking.bankfinder.InMemoryBankFinder +import net.dankito.banking.fints.FinTsClient +import net.dankito.banking.fints.callback.SimpleFinTsClientCallback +import net.dankito.banking.fints.model.* +import net.dankito.banking.fints.response.BankResponse +import net.dankito.banking.fints.response.client.AddAccountResponse +import net.dankito.banking.fints.response.client.GetTransactionsResponse +import net.dankito.banking.fints.rest.model.BankAccessData +import net.dankito.banking.fints.rest.model.EnterTanContext +import net.dankito.banking.fints.rest.model.EnteringTanRequested +import net.dankito.banking.fints.rest.model.dto.request.AccountRequestDto +import net.dankito.banking.fints.rest.model.dto.request.GetAccountsTransactionsRequestDto +import java.util.* +import java.util.concurrent.CountDownLatch +import java.util.concurrent.atomic.AtomicReference +import javax.enterprise.context.ApplicationScoped + + +@ApplicationScoped +class fints4kService { + + protected val bankFinder = InMemoryBankFinder() + + protected val tanRequests = mutableMapOf() + + + fun getAddAccountResponse(accessData: BankAccessData): AddAccountResponse { + val (bank, errorMessage) = mapToBankData(accessData) + + if (errorMessage != null) { + return AddAccountResponse(BankResponse(false, errorMessage = errorMessage), bank) + } + + return getAccountData(bank) + } + + // TODO: as in most cases we really just want the account data, so just retrieve these without balances and transactions + protected fun getAccountData(bank: BankData): AddAccountResponse { + return getAsyncResponse { client, responseRetrieved -> + client.addAccountAsync(bank) { response -> + responseRetrieved(response) + } + } + } + + + fun getAccountTransactions(dto: GetAccountsTransactionsRequestDto): List { + val (bank, errorMessage) = mapToBankData(dto.credentials) + + if (errorMessage != null) { + return listOf(GetTransactionsResponse(BankResponse(false, errorMessage = errorMessage))) + } + + val accountData = getAccountData(bank) + + return dto.accounts.map { accountDto -> + val account = findAccount(accountData, accountDto) + + return@map if (account != null) { + val parameter = GetTransactionsParameter(account, dto.alsoRetrieveBalance, dto.fromDate, dto.toDate, abortIfTanIsRequired = dto.abortIfTanIsRequired) + getAccountTransactions(bank, parameter) + } + else { + GetTransactionsResponse(BankResponse(false, errorMessage = "Account with identifier '${accountDto.identifier}' not found. Available accounts: ${accountData.bank.accounts.map { it.accountIdentifier }.joinToString(", ")}")) + } + } + } + + fun getAccountTransactions(bank: BankData, parameter: GetTransactionsParameter): GetTransactionsResponse { + return getAsyncResponse { client, responseRetrieved -> + client.getTransactionsAsync(parameter, bank) { response -> + responseRetrieved(response) + } + } + } + + + protected fun getAsyncResponse(executeRequest: (FinTsClient, ((T) -> Unit)) -> Unit): T { + val result = AtomicReference() + val countDownLatch = CountDownLatch(1) + +// val client = FinTsClient(SimpleFinTsClientCallback { supportedTanMethods: List, suggestedTanMethod: TanMethod? -> + val client = FinTsClient(SimpleFinTsClientCallback({ bank, tanChallenge -> handleEnterTan(bank, tanChallenge, countDownLatch, result) }) { supportedTanMethods, suggestedTanMethod -> + suggestedTanMethod + }) + + executeRequest(client) { response -> + result.set(response) + countDownLatch.countDown() + } + + countDownLatch.await() + + return result.get() + } + + protected fun handleEnterTan(bank: BankData, tanChallenge: TanChallenge, originatingRequestLatch: CountDownLatch, originatingRequestResult: AtomicReference): EnterTanResult { + val enterTanResult = AtomicReference() + val enterTanLatch = CountDownLatch(1) + + val tanRequestId = UUID.randomUUID().toString() + + originatingRequestResult.set(EnteringTanRequested(tanRequestId, bank, tanChallenge)) + originatingRequestLatch.countDown() + + tanRequests.put(tanRequestId, EnterTanContext(enterTanResult, enterTanLatch)) + + enterTanLatch.await() + + return enterTanResult.get() + } + + + protected fun mapToBankData(accessData: BankAccessData): Pair { + val bankSearchResult = bankFinder.findBankByBankCode(accessData.bankCode) + val fintsServerAddress = accessData.finTsServerAddress ?: bankSearchResult.firstOrNull { it.pinTanAddress != null }?.pinTanAddress + val bank = BankData(accessData.bankCode, accessData.loginName, accessData.password, fintsServerAddress ?: "", bankSearchResult.firstOrNull()?.bic ?: "") + + if (fintsServerAddress == null) { + val errorMessage = if (bankSearchResult.isEmpty()) "No bank found for bank code '${accessData.bankCode}'" else "Bank '${bankSearchResult.firstOrNull()?.name} does not support FinTS 3.0" + + return Pair(bank, errorMessage) + } + + return Pair(bank, null) + } + + protected fun findAccount(allAccounts: AddAccountResponse, accountDto: AccountRequestDto): AccountData? { + return allAccounts.bank.accounts.firstOrNull { it.accountIdentifier == accountDto.identifier } + } + +} \ No newline at end of file diff --git a/rest/fints4kRest/src/main/resources/application.properties b/rest/fints4kRest/src/main/resources/application.properties new file mode 100644 index 00000000..df7c19ea --- /dev/null +++ b/rest/fints4kRest/src/main/resources/application.properties @@ -0,0 +1,5 @@ +quarkus.http.port=5555 + +# enable https support in native builds +quarkus.native.enable-https-url-handler=true +quarkus.native.enable-all-security-services=true \ No newline at end of file diff --git a/rest/fints4kRest/src/native-test/kotlin/net/dankito/banking/fints/rest/NativeFints4kIT.kt b/rest/fints4kRest/src/native-test/kotlin/net/dankito/banking/fints/rest/NativeFints4kIT.kt new file mode 100644 index 00000000..8f497ac9 --- /dev/null +++ b/rest/fints4kRest/src/native-test/kotlin/net/dankito/banking/fints/rest/NativeFints4kIT.kt @@ -0,0 +1,6 @@ +package net.dankito.banking.fints.rest + +import io.quarkus.test.junit.NativeImageTest + +@NativeImageTest +class NativeFints4kIT : ExampleResourceTest() \ No newline at end of file diff --git a/rest/fints4kRest/src/test/kotlin/net/dankito/banking/fints/rest/fints4kResourceTest.kt b/rest/fints4kRest/src/test/kotlin/net/dankito/banking/fints/rest/fints4kResourceTest.kt new file mode 100644 index 00000000..338e8f8a --- /dev/null +++ b/rest/fints4kRest/src/test/kotlin/net/dankito/banking/fints/rest/fints4kResourceTest.kt @@ -0,0 +1,20 @@ +package net.dankito.banking.fints.rest + +import io.quarkus.test.junit.QuarkusTest +import io.restassured.RestAssured.given +import org.hamcrest.CoreMatchers.`is` +import org.junit.jupiter.api.Test + +@QuarkusTest +class fints4kResourceTest { + + @Test + fun testHelloEndpoint() { + given() + .`when`().get("/hello") + .then() + .statusCode(200) + .body(`is`("hello")) + } + +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 936a3db3..7b21624c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -59,8 +59,10 @@ project(':RoomBankingPersistence').projectDir = "$rootDir/persistence/database/R /* REST APIs */ include ':BankFinderRest' +include ':fints4kRest' project(':BankFinderRest').projectDir = "$rootDir/rest/BankFinderRest/" as File +project(':fints4kRest').projectDir = "$rootDir/rest/fints4kRest/" as File