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 */
|
||||
|
||||
include ':BankFinderRest'
|
||||
include ':fints4kRest'
|
||||
|
||||
project(':BankFinderRest').projectDir = "$rootDir/rest/BankFinderRest/" as File
|
||||
project(':fints4kRest').projectDir = "$rootDir/rest/fints4kRest/" as File
|
||||
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue