Started fints4k REST API
This commit is contained in:
parent
5195f2d0c3
commit
5569594a65
|
@ -0,0 +1,5 @@
|
||||||
|
*
|
||||||
|
!build/*-runner
|
||||||
|
!build/*-runner.jar
|
||||||
|
!build/lib/*
|
||||||
|
!build/quarkus-app/*
|
|
@ -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"
|
||||||
|
}
|
|
@ -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" ]
|
|
@ -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" ]
|
|
@ -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"]
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<GetTransactionsResponse>): 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<RetrievedAccountData>): List<BankAccountResponseDto> {
|
||||||
|
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<AccountTransaction>): Collection<AccountTransactionResponseDto> {
|
||||||
|
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<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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
||||||
|
}
|
|
@ -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<EnterTanResult>,
|
||||||
|
open val countDownLatch: CountDownLatch,
|
||||||
|
open val tanRequestedTimeStamp: Date = Date()
|
||||||
|
)
|
|
@ -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
|
||||||
|
)
|
|
@ -0,0 +1,6 @@
|
||||||
|
package net.dankito.banking.fints.rest.model.dto.request
|
||||||
|
|
||||||
|
|
||||||
|
open class AccountRequestDto(
|
||||||
|
open val identifier: String
|
||||||
|
)
|
|
@ -0,0 +1,4 @@
|
||||||
|
package net.dankito.banking.fints.rest.model.dto.request
|
||||||
|
|
||||||
|
|
||||||
|
open class AddAccountRequestDto : BankAccessDataRequestDto()
|
|
@ -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()
|
|
@ -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<AccountRequestDto>,
|
||||||
|
open val alsoRetrieveBalance: Boolean = true,
|
||||||
|
open val fromDate: Date? = null,
|
||||||
|
open val toDate: Date? = null,
|
||||||
|
open val abortIfTanIsRequired: Boolean = false
|
||||||
|
)
|
|
@ -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
|
||||||
|
)
|
|
@ -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<BankAccountResponseDto>
|
||||||
|
) : ResponseDtoBase(successful, errorMessage)
|
|
@ -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<AccountTransactionResponseDto>,
|
||||||
|
open var unbookedTransactions: Collection<Any>
|
||||||
|
)
|
|
@ -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<TanMethodResponseDto>,
|
||||||
|
open val selectedTanMethod: TanMethodResponseDto?,
|
||||||
|
open val tanMedia: List<TanMedium>,
|
||||||
|
|
||||||
|
open val supportedHbciVersions: List<String>
|
||||||
|
)
|
|
@ -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<AccountTransactionResponseDto>,
|
||||||
|
open var unbookedTransactions: Collection<Any>
|
||||||
|
) : ResponseDtoBase(successful, errorMessage)
|
|
@ -0,0 +1,6 @@
|
||||||
|
package net.dankito.banking.fints.rest.model.dto.response
|
||||||
|
|
||||||
|
|
||||||
|
open class GetAccountsTransactionsResponseDto(
|
||||||
|
open val accounts: List<GetAccountTransactionsResponseDto>
|
||||||
|
)
|
|
@ -0,0 +1,7 @@
|
||||||
|
package net.dankito.banking.fints.rest.model.dto.response
|
||||||
|
|
||||||
|
|
||||||
|
open class ResponseDtoBase(
|
||||||
|
open val successful: Boolean,
|
||||||
|
open val errorMessage: String?
|
||||||
|
)
|
|
@ -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
|
||||||
|
)
|
|
@ -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<String, EnterTanContext>()
|
||||||
|
|
||||||
|
|
||||||
|
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<GetTransactionsResponse> {
|
||||||
|
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 <T> getAsyncResponse(executeRequest: (FinTsClient, ((T) -> Unit)) -> Unit): T {
|
||||||
|
val result = AtomicReference<T>()
|
||||||
|
val countDownLatch = CountDownLatch(1)
|
||||||
|
|
||||||
|
// val client = FinTsClient(SimpleFinTsClientCallback { supportedTanMethods: List<TanMethod>, 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 <T> handleEnterTan(bank: BankData, tanChallenge: TanChallenge, originatingRequestLatch: CountDownLatch, originatingRequestResult: AtomicReference<T>): EnterTanResult {
|
||||||
|
val enterTanResult = AtomicReference<EnterTanResult>()
|
||||||
|
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<BankData, String?> {
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
||||||
|
package net.dankito.banking.fints.rest
|
||||||
|
|
||||||
|
import io.quarkus.test.junit.NativeImageTest
|
||||||
|
|
||||||
|
@NativeImageTest
|
||||||
|
class NativeFints4kIT : ExampleResourceTest()
|
|
@ -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"))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -59,8 +59,10 @@ project(':RoomBankingPersistence').projectDir = "$rootDir/persistence/database/R
|
||||||
/* REST APIs */
|
/* REST APIs */
|
||||||
|
|
||||||
include ':BankFinderRest'
|
include ':BankFinderRest'
|
||||||
|
include ':fints4kRest'
|
||||||
|
|
||||||
project(':BankFinderRest').projectDir = "$rootDir/rest/BankFinderRest/" as File
|
project(':BankFinderRest').projectDir = "$rootDir/rest/BankFinderRest/" as File
|
||||||
|
project(':fints4kRest').projectDir = "$rootDir/rest/fints4kRest/" as File
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue