Implemented handling enter TAN requests
This commit is contained in:
parent
356b0f7823
commit
1216267fec
|
@ -1,16 +1,25 @@
|
|||
package net.dankito.banking.fints.rest
|
||||
|
||||
import net.dankito.banking.fints.response.client.AddAccountResponse
|
||||
import net.dankito.banking.fints.response.client.GetTransactionsResponse
|
||||
import net.dankito.banking.fints.rest.model.dto.request.AddAccountRequestDto
|
||||
import net.dankito.banking.fints.rest.mapper.DtoMapper
|
||||
import net.dankito.banking.fints.rest.model.dto.request.GetAccountsTransactionsRequestDto
|
||||
import net.dankito.banking.fints.rest.model.dto.request.TanResponseDto
|
||||
import net.dankito.banking.fints.rest.model.dto.response.AddAccountResponseDto
|
||||
import net.dankito.banking.fints.rest.model.dto.response.GetAccountsTransactionsResponseDto
|
||||
import net.dankito.banking.fints.rest.model.dto.response.RestResponse
|
||||
import net.dankito.banking.fints.rest.service.fints4kService
|
||||
import net.dankito.banking.fints.rest.service.model.GetAccountsTransactionsResponse
|
||||
import org.slf4j.LoggerFactory
|
||||
import javax.inject.Inject
|
||||
import javax.ws.rs.*
|
||||
import javax.ws.rs.core.MediaType
|
||||
|
||||
|
||||
@Path("/fints/v1")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
class fints4kResource {
|
||||
|
||||
@Inject
|
||||
|
@ -20,31 +29,44 @@ class fints4kResource {
|
|||
|
||||
|
||||
@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)
|
||||
fun addAccount(request: AddAccountRequestDto): RestResponse<AddAccountResponseDto> {
|
||||
val response = service.getAddAccountResponse(request)
|
||||
|
||||
if (showRawResponse) {
|
||||
return clientResponse
|
||||
}
|
||||
|
||||
return mapper.map(clientResponse)
|
||||
return mapper.createRestResponse(response) { successResponse -> mapper.map(successResponse) }
|
||||
}
|
||||
|
||||
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("transactions")
|
||||
fun getAccountTransactions(request: GetAccountsTransactionsRequestDto): GetAccountsTransactionsResponseDto {
|
||||
val accountsTransactions = service.getAccountTransactions(request)
|
||||
val response = service.getAccountTransactions(request)
|
||||
|
||||
return mapper.mapTransactions(accountsTransactions)
|
||||
return mapper.map(response)
|
||||
}
|
||||
|
||||
|
||||
@POST
|
||||
@Path("tanresponse")
|
||||
fun tanResponse(dto: TanResponseDto): RestResponse<Any> {
|
||||
val response = service.handleTanResponse(dto)
|
||||
|
||||
// couldn't make it that compiler access ResponseHolder<*> for mapper.createRestResponse(), resulted in very cryptic "{"arity":0}" response -> handle it manually
|
||||
response.response?.let { successResponse ->
|
||||
return RestResponse.success(mapSuccessResponse(successResponse))
|
||||
}
|
||||
|
||||
// all other cases map here, the responseMapper callback has no function
|
||||
return mapper.createRestResponse(response) { it!! }
|
||||
}
|
||||
|
||||
private fun mapSuccessResponse(successResponse: Any): Any {
|
||||
return when (successResponse) {
|
||||
is AddAccountResponse -> mapper.map(successResponse)
|
||||
is GetAccountsTransactionsResponse -> mapper.map(successResponse)
|
||||
is GetTransactionsResponse -> mapper.mapTransactions(successResponse)
|
||||
else -> successResponse // add others / new ones here
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -4,13 +4,28 @@ import net.dankito.banking.fints.model.*
|
|||
import net.dankito.banking.fints.response.client.AddAccountResponse
|
||||
import net.dankito.banking.fints.response.client.FinTsClientResponse
|
||||
import net.dankito.banking.fints.response.client.GetTransactionsResponse
|
||||
import net.dankito.banking.fints.rest.model.ResponseHolder
|
||||
import net.dankito.banking.fints.rest.model.dto.response.*
|
||||
import net.dankito.banking.fints.rest.service.model.GetAccountsTransactionsResponse
|
||||
import java.math.BigDecimal
|
||||
import javax.ws.rs.InternalServerErrorException
|
||||
|
||||
|
||||
open class DtoMapper {
|
||||
|
||||
fun <DomainType, DtoType> createRestResponse(responseHolder: ResponseHolder<DomainType>, responseMapper: (DomainType) -> DtoType): RestResponse<DtoType> {
|
||||
responseHolder.response?.let { response ->
|
||||
return RestResponse.success(responseMapper(response))
|
||||
}
|
||||
|
||||
responseHolder.enterTanRequest?.let { enterTanRequest ->
|
||||
return RestResponse.requiresTan(enterTanRequest)
|
||||
}
|
||||
|
||||
return RestResponse.error(responseHolder.error ?: "Unknown error")
|
||||
}
|
||||
|
||||
|
||||
open fun map(response: AddAccountResponse): AddAccountResponseDto {
|
||||
return AddAccountResponseDto(
|
||||
response.successful,
|
||||
|
@ -38,20 +53,21 @@ open class DtoMapper {
|
|||
}
|
||||
|
||||
|
||||
open fun mapTransactions(accountsTransactions: List<GetTransactionsResponse>?): GetAccountsTransactionsResponseDto {
|
||||
open fun map(response: GetAccountsTransactionsResponse?): GetAccountsTransactionsResponseDto {
|
||||
// TODO: is this still the case?
|
||||
// TODO: if a TAN is required then accountsTransactions contains null value(s) (but why?) -> application crashes
|
||||
if (accountsTransactions == null) {
|
||||
if (response == null) {
|
||||
throw InternalServerErrorException("Could not fetch account transactions. Either TAN hasn't been entered or developers made a mistake.")
|
||||
}
|
||||
|
||||
return GetAccountsTransactionsResponseDto(accountsTransactions.mapNotNull { map(it) }) // TODO: is this correct removing accounts from result for which no transactions have been retrieved?
|
||||
}
|
||||
|
||||
open fun map(accountTransactions: GetTransactionsResponse): GetAccountTransactionsResponseDto? {
|
||||
if (accountTransactions.retrievedData.isEmpty()) {
|
||||
return null
|
||||
return GetAccountsTransactionsResponseDto(
|
||||
// TODO: is this correct removing accounts from result for which no transactions have been retrieved?
|
||||
response.transactionsPerAccount.filter { it.response?.retrievedData?.isNotEmpty() != false }
|
||||
.map { createRestResponse(it) { transactionsResponse -> mapTransactions(transactionsResponse) } }
|
||||
)
|
||||
}
|
||||
|
||||
open fun mapTransactions(accountTransactions: GetTransactionsResponse): GetAccountTransactionsResponseDto {
|
||||
val retrievedData = accountTransactions.retrievedData.first()
|
||||
val balance = mapNullable(retrievedData.balance)
|
||||
val bookedTransactions = map(retrievedData.bookedTransactions)
|
||||
|
|
|
@ -6,8 +6,9 @@ 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()
|
||||
class EnterTanContext(
|
||||
val enterTanResult: AtomicReference<EnterTanResult>,
|
||||
val responseHolder: ResponseHolder<*>,
|
||||
val countDownLatch: CountDownLatch,
|
||||
val tanRequestedTimeStamp: Date = Date()
|
||||
)
|
|
@ -1,11 +1,9 @@
|
|||
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
|
||||
class EnteringTanRequested(
|
||||
val tanRequestId: String,
|
||||
val tanChallenge: TanChallenge
|
||||
)
|
|
@ -0,0 +1,63 @@
|
|||
package net.dankito.banking.fints.rest.model
|
||||
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
|
||||
class ResponseHolder<T>() {
|
||||
|
||||
private var responseReceivedLatch = CountDownLatch(1)
|
||||
|
||||
constructor(error: String) : this() {
|
||||
setError(error)
|
||||
}
|
||||
|
||||
|
||||
var response: T? = null
|
||||
private set
|
||||
|
||||
var error: String? = null
|
||||
private set
|
||||
|
||||
var enterTanRequest: EnteringTanRequested? = null
|
||||
private set
|
||||
|
||||
|
||||
fun setResponse(response: T) {
|
||||
this.response = response
|
||||
|
||||
signalResponseReceived()
|
||||
}
|
||||
|
||||
fun setError(error: String) {
|
||||
this.error = error
|
||||
|
||||
signalResponseReceived()
|
||||
}
|
||||
|
||||
fun setEnterTanRequest(enterTanRequest: EnteringTanRequested) {
|
||||
this.enterTanRequest = enterTanRequest
|
||||
|
||||
signalResponseReceived()
|
||||
}
|
||||
|
||||
|
||||
fun waitForResponse() {
|
||||
responseReceivedLatch.await()
|
||||
}
|
||||
|
||||
fun resetAfterEnteringTan() {
|
||||
this.enterTanRequest = null
|
||||
|
||||
responseReceivedLatch = CountDownLatch(1)
|
||||
}
|
||||
|
||||
private fun signalResponseReceived() {
|
||||
responseReceivedLatch.countDown()
|
||||
}
|
||||
|
||||
|
||||
override fun toString(): String {
|
||||
return "Error: $error, TAN requested: $enterTanRequest, success: $response"
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package net.dankito.banking.fints.rest.model.dto.request
|
||||
|
||||
import net.dankito.banking.fints.model.EnterTanResult
|
||||
|
||||
|
||||
class TanResponseDto(
|
||||
val tanRequestId: String,
|
||||
val enterTanResult: EnterTanResult
|
||||
)
|
|
@ -2,5 +2,5 @@ package net.dankito.banking.fints.rest.model.dto.response
|
|||
|
||||
|
||||
open class GetAccountsTransactionsResponseDto(
|
||||
open val accounts: List<GetAccountTransactionsResponseDto>
|
||||
open val transactionsPerAccount: List<RestResponse<GetAccountTransactionsResponseDto>>
|
||||
)
|
|
@ -0,0 +1,12 @@
|
|||
package net.dankito.banking.fints.rest.model.dto.response
|
||||
|
||||
|
||||
enum class ResponseType {
|
||||
|
||||
Success,
|
||||
|
||||
Error,
|
||||
|
||||
TanRequired
|
||||
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package net.dankito.banking.fints.rest.model.dto.response
|
||||
|
||||
import net.dankito.banking.fints.rest.model.EnteringTanRequested
|
||||
|
||||
|
||||
class RestResponse<T>(
|
||||
val status: ResponseType,
|
||||
val errorMessage: String?,
|
||||
val successResponse: T?,
|
||||
val enteringTanRequested: EnteringTanRequested? = null
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
fun <T> success(result: T): RestResponse<T> {
|
||||
return RestResponse(ResponseType.Success, null, result, null)
|
||||
}
|
||||
|
||||
fun <T> error(errorMessage: String): RestResponse<T> {
|
||||
return RestResponse(ResponseType.Error, errorMessage, null, null)
|
||||
}
|
||||
|
||||
fun <T> requiresTan(enteringTanRequested: EnteringTanRequested): RestResponse<T> {
|
||||
return RestResponse(ResponseType.TanRequired, null, null, enteringTanRequested)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -2,15 +2,18 @@ package net.dankito.banking.fints.rest.service
|
|||
|
||||
import net.dankito.banking.bankfinder.InMemoryBankFinder
|
||||
import net.dankito.banking.fints.FinTsClientForCustomer
|
||||
import net.dankito.banking.fints.callback.FinTsClientCallback
|
||||
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.dto.request.AccountRequestDto
|
||||
import net.dankito.banking.fints.rest.model.EnteringTanRequested
|
||||
import net.dankito.banking.fints.rest.model.ResponseHolder
|
||||
import net.dankito.banking.fints.rest.model.dto.request.GetAccountsTransactionsRequestDto
|
||||
import net.dankito.banking.fints.rest.model.dto.request.TanResponseDto
|
||||
import net.dankito.banking.fints.rest.service.model.GetAccountsTransactionsResponse
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
@ -25,21 +28,21 @@ class fints4kService {
|
|||
|
||||
protected val clientCache = ConcurrentHashMap<String, FinTsClientForCustomer>()
|
||||
|
||||
protected val tanRequests = mutableMapOf<String, EnterTanContext>()
|
||||
// TODO: create clean up job for timed out TAN requests
|
||||
protected val tanRequests = ConcurrentHashMap<String, EnterTanContext>()
|
||||
|
||||
|
||||
fun getAddAccountResponse(accessData: BankAccessData): AddAccountResponse {
|
||||
fun getAddAccountResponse(accessData: BankAccessData): ResponseHolder<AddAccountResponse> {
|
||||
val (bank, errorMessage) = mapToBankData(accessData)
|
||||
|
||||
if (errorMessage != null) {
|
||||
return AddAccountResponse(BankResponse(false, errorMessage = errorMessage), bank)
|
||||
return ResponseHolder(errorMessage)
|
||||
}
|
||||
|
||||
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 {
|
||||
protected fun getAccountData(bank: BankData): ResponseHolder<AddAccountResponse> {
|
||||
return getAsyncResponse(bank) { client, responseRetrieved ->
|
||||
client.addAccountAsync(AddAccountParameter(bank)) { response ->
|
||||
responseRetrieved(response)
|
||||
|
@ -48,28 +51,32 @@ class fints4kService {
|
|||
}
|
||||
|
||||
|
||||
fun getAccountTransactions(dto: GetAccountsTransactionsRequestDto): List<GetTransactionsResponse> {
|
||||
fun getAccountTransactions(dto: GetAccountsTransactionsRequestDto): GetAccountsTransactionsResponse {
|
||||
val (bank, errorMessage) = mapToBankData(dto.credentials)
|
||||
|
||||
if (errorMessage != null) {
|
||||
return listOf(GetTransactionsResponse(BankResponse(false, errorMessage = errorMessage)))
|
||||
return GetAccountsTransactionsResponse(listOf(ResponseHolder(errorMessage)))
|
||||
}
|
||||
|
||||
return dto.accounts.map { accountDto ->
|
||||
val account = findAccount(dto.credentials, accountDto)
|
||||
val retrievedAccounts = getAccounts(bank)
|
||||
|
||||
val transactionsPerAccount = dto.accounts.map { accountDto ->
|
||||
val account = retrievedAccounts?.firstOrNull { it.accountIdentifier == accountDto.identifier }
|
||||
|
||||
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: " +
|
||||
"${getCachedClient(dto.credentials)?.bank?.accounts?.map { it.accountIdentifier }?.joinToString(", ")}"))
|
||||
}
|
||||
ResponseHolder("Account with identifier '${accountDto.identifier}' not found. Available accounts: " +
|
||||
"${retrievedAccounts?.joinToString(", ") { it.accountIdentifier }}")
|
||||
}
|
||||
}
|
||||
|
||||
fun getAccountTransactions(bank: BankData, parameter: GetTransactionsParameter): GetTransactionsResponse {
|
||||
return GetAccountsTransactionsResponse(transactionsPerAccount)
|
||||
}
|
||||
|
||||
fun getAccountTransactions(bank: BankData, parameter: GetTransactionsParameter): ResponseHolder<GetTransactionsResponse> {
|
||||
return getAsyncResponse(bank) { client, responseRetrieved ->
|
||||
client.getTransactionsAsync(parameter) { response ->
|
||||
responseRetrieved(response)
|
||||
|
@ -78,50 +85,68 @@ class fints4kService {
|
|||
}
|
||||
|
||||
|
||||
protected fun <T> getAsyncResponse(bank: BankData, executeRequest: (FinTsClientForCustomer, ((T) -> Unit)) -> Unit): T {
|
||||
val result = AtomicReference<T>()
|
||||
val countDownLatch = CountDownLatch(1)
|
||||
fun handleTanResponse(dto: TanResponseDto): ResponseHolder<*> {
|
||||
tanRequests.remove(dto.tanRequestId)?.let { enterTanContext ->
|
||||
val responseHolder = enterTanContext.responseHolder
|
||||
responseHolder.resetAfterEnteringTan()
|
||||
|
||||
val client = getClient(bank, result, countDownLatch)
|
||||
enterTanContext.enterTanResult.set(dto.enterTanResult)
|
||||
enterTanContext.countDownLatch.countDown()
|
||||
|
||||
responseHolder.waitForResponse()
|
||||
|
||||
return responseHolder
|
||||
}
|
||||
|
||||
return ResponseHolder<Any>("No TAN request found for TAN Request ID '${dto.tanRequestId}'")
|
||||
}
|
||||
|
||||
|
||||
protected fun <T> getAsyncResponse(bank: BankData, executeRequest: (FinTsClientForCustomer, ((T) -> Unit)) -> Unit): ResponseHolder<T> {
|
||||
val responseHolder = ResponseHolder<T>()
|
||||
|
||||
val client = getClient(bank, responseHolder)
|
||||
|
||||
executeRequest(client) { response ->
|
||||
result.set(response)
|
||||
countDownLatch.countDown()
|
||||
responseHolder.setResponse(response)
|
||||
}
|
||||
|
||||
countDownLatch.await()
|
||||
responseHolder.waitForResponse()
|
||||
|
||||
return result.get()
|
||||
return responseHolder
|
||||
}
|
||||
|
||||
private fun <T> getClient(bank: BankData, result: AtomicReference<T>, countDownLatch: CountDownLatch): FinTsClientForCustomer {
|
||||
private fun <T> getClient(bank: BankData, responseHolder: ResponseHolder<T>): FinTsClientForCustomer {
|
||||
val cacheKey = getCacheKey(bank.bankCode, bank.customerId)
|
||||
|
||||
clientCache[cacheKey]?.let {
|
||||
// TODO: this will not work for two parallel calls for the same account if both calls require entering a TAN as second one overwrites callback and ResponseHolder of first one -> first one blocks forever
|
||||
it.setCallback(createFinTsClientCallback(responseHolder)) // we have to newly create callback otherwise ResponseHolder instance of when client was created is used -> its CountDownLatch would never signal
|
||||
return it
|
||||
}
|
||||
|
||||
// val client = FinTsClient(SimpleFinTsClientCallback { supportedTanMethods: List<TanMethod>, suggestedTanMethod: TanMethod? ->
|
||||
val client = FinTsClientForCustomer(bank, SimpleFinTsClientCallback({ bank, tanChallenge -> handleEnterTan(bank, tanChallenge, countDownLatch, result) }) { supportedTanMethods, suggestedTanMethod ->
|
||||
suggestedTanMethod
|
||||
})
|
||||
val client = FinTsClientForCustomer(bank, createFinTsClientCallback(responseHolder))
|
||||
|
||||
clientCache[cacheKey] = client
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
protected fun <T> handleEnterTan(bank: BankData, tanChallenge: TanChallenge, originatingRequestLatch: CountDownLatch, originatingRequestResult: AtomicReference<T>): EnterTanResult {
|
||||
private fun <T> createFinTsClientCallback(responseHolder: ResponseHolder<T>): FinTsClientCallback {
|
||||
return SimpleFinTsClientCallback({ bank, tanChallenge -> handleEnterTan(bank, tanChallenge, responseHolder) }) { supportedTanMethods, suggestedTanMethod ->
|
||||
suggestedTanMethod
|
||||
}
|
||||
}
|
||||
|
||||
protected fun <T> handleEnterTan(bank: BankData, tanChallenge: TanChallenge, responseHolder: ResponseHolder<T>): EnterTanResult {
|
||||
val enterTanResult = AtomicReference<EnterTanResult>()
|
||||
val enterTanLatch = CountDownLatch(1)
|
||||
|
||||
val tanRequestId = UUID.randomUUID().toString()
|
||||
|
||||
// TODO: find a solution for returning TAN challenge to caller
|
||||
//originatingRequestResult.set(EnteringTanRequested(tanRequestId, bank, tanChallenge))
|
||||
originatingRequestLatch.countDown()
|
||||
tanRequests.put(tanRequestId, EnterTanContext(enterTanResult, responseHolder, enterTanLatch))
|
||||
|
||||
tanRequests.put(tanRequestId, EnterTanContext(enterTanResult, enterTanLatch))
|
||||
responseHolder.setEnterTanRequest(EnteringTanRequested(tanRequestId, tanChallenge))
|
||||
|
||||
enterTanLatch.await()
|
||||
|
||||
|
@ -146,10 +171,23 @@ class fints4kService {
|
|||
return Pair(bank, null)
|
||||
}
|
||||
|
||||
protected fun findAccount(credentials: BankAccessData, accountDto: AccountRequestDto): AccountData? {
|
||||
return getCachedClient(credentials)?.bank?.accounts?.firstOrNull { it.accountIdentifier == accountDto.identifier }
|
||||
|
||||
protected fun getAccounts(bank: BankData): List<AccountData>? {
|
||||
getCachedClient(bank)?.bank?.accounts?.let {
|
||||
return it
|
||||
}
|
||||
|
||||
val addAccountResponse = getAccountData(bank)
|
||||
|
||||
return addAccountResponse.response?.bank?.accounts
|
||||
}
|
||||
|
||||
|
||||
private fun getCachedClient(bank: BankData): FinTsClientForCustomer? {
|
||||
val cacheKey = getCacheKey(bank.bankCode, bank.customerId)
|
||||
|
||||
return clientCache[cacheKey]
|
||||
}
|
||||
|
||||
private fun getCachedClient(credentials: BankAccessData): FinTsClientForCustomer? {
|
||||
val cacheKey = getCacheKey(credentials.bankCode, credentials.loginName)
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package net.dankito.banking.fints.rest.service.model
|
||||
|
||||
import net.dankito.banking.fints.response.client.GetTransactionsResponse
|
||||
import net.dankito.banking.fints.rest.model.ResponseHolder
|
||||
|
||||
|
||||
class GetAccountsTransactionsResponse(
|
||||
val transactionsPerAccount: List<ResponseHolder<GetTransactionsResponse>>
|
||||
)
|
Loading…
Reference in New Issue