Getting Started - Digital Account Checks
Introduction
Digital Account Checks (DAC) in finAPI Data Intelligence are a combination of reports, their extracts from transactions with aggregated data, for specific business cases.
They are designed to give a special view of the transactions of interest without having to find them out yourself.
The aggregations contained therein allow quick access to the relevant data.
To generate a Digital Account Check, a data source is always required.
A case is not needed in this case because the endpoint creates the case itself.
Thus, creating these reports is greatly simplified and can be completed with just a few requests.
If you want to compose the reports yourself, you can read about it in the Getting Started - Reports.
Prerequisites
The major prerequisite is to have a valid set of client credentials:
client_id
andclient_secret
.A user has already been created
With self-managed user: Getting Started - User Management
With Process Controller: Obtain Authorization via Process Controller (recommended)
At least one account has already been imported via finAPI WebForm 2.0 (see WebForm Documentation)
TL:TR
Used Systems
finAPI Data Intelligence (finAPI API Documentation )
See Environments page for detailed information.
Used Endpoints
To create a DAC, at least the following endpoints are required:
Description | HTTP Method | Process Controller Endpoint | Link to API Doc |
---|---|---|---|
Synchronize Data Source | POST |
| |
Get status of the Data Source | GET |
| |
Create a DAC4Loan | POST |
| |
Get all reports | GET |
| |
Get DAC4Loan PDF | POST |
|
Process Overview
cURL Example
Translated into cURL it looks like the following:
Step 1 - Synchronize Data Source (cURL)
This step is an extract from the Guide for Cases. More information can be found at Getting Started - Synchronize a Data Source.
Before you can synchronize data in Data Intelligence, it must already have been imported via finAPI WebForm 2.0 or finAPI OpenBanking Access.
For synchronization, it is recommended that the callback is used to avoid unnecessary poll requests that can stress client and server systems.
Synchronization request example for full synchronization of the user:
curl --location 'https://di-sandbox.finapi.io/api/v1/dataSources/bankConnections/synchronization' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <access_token>' \
--data '{
"callbackUrl": "https://domain.tld",
"callbackHandle": "38793e87-499d-4860-b947-2c2c8ab10322"
}'
The response looks like this:
{
"bankConnections": [
{
"dataSourceId": "4e760145-2e65-4242-ac33-488943528c93",
"creationDate": "2020-01-01 00:00:00.000",
"lastUpdate": "2020-01-01 00:00:00.000",
"externalId": 123456,
"bic": "COBADEFFXXX",
"bankName": "Commerzbank",
"bankStatus": "IN_PROGRESS",
"updateRequired": false,
"accounts": [
{
"accountId": "5f660145-2e65-4242-ac33-488943528c93",
"creationDate": "2020-01-01 00:00:00.000",
"lastUpdate": "2020-01-01 00:00:00.000",
"externalId": 123456,
"iban": "DE13700800000061110500",
"accountType": "CHECKING",
"status": "UPDATED"
}
]
}
]
}
Step 1.5 - Polling the status of the Data Source (cURL)
It is not recommended to poll the status. If you still want to do it, please make sure that there is at least a 200ms
pause between requests.
Request to the Data Source status:
curl --location 'https://di-sandbox.finapi.io/api/v1/dataSources/{dataSourceId}/status' \
--header 'Authorization: Bearer <access_token>'
The response looks like this:
{
"status": "IN_PROGRESS",
"code": "SYNC_IN_PROGRESS",
}
As long as the status is not SUCCESSFUL
, you cannot proceed with report creation.
Step 2 - Create a Digital Account Check (cURL)
A digital account check, including a case and all the reports that are technically linked to it, is just a call to an endpoint.
Request to the “Create DAC” endpoint:
curl --location 'https://di-sandbox.finapi.io/api/v1/dacCases/loan' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer <access_token>' \
--data '{
"dataSourceIds": [
"4e760145-2e65-4242-ac33-488943528c93"
],
"maxDaysForCase": 89,
"withTransactions": true
}'
The response looks like this:
{
"id": "d5325a2e-9fa5-4ed4-9ba8-3e829a9b69af",
"maxDaysForCase": 89,
"creationDate": "2020-01-01 00:00:00.000",
"dataSources": [
{
"id": "4e760145-2e65-4242-ac33-488943528c93",
"creationDate": "2020-01-01 00:00:00.000"
}
]
}
Instead of maxDaysForCase
, a period can also be set via the body in the datePeriodForCase
field.
The id
in the response corresponds to the caseId
.
Step 3 - Get all reports (cURL)
Request to get all reports:
curl --location 'https://di-sandbox.finapi.io/api/v1/cases/d5325a2e-9fa5-4ed4-9ba8-3e829a9b69af/reports' \
--header 'Authorization: Bearer <access_token>'
d5325a2e-9fa5-4ed4-9ba8-3e829a9b69af
in the path is the id
of the case.
The result looks like this (shortened):
{
"caseId": "d5325a2e-9fa5-4ed4-9ba8-3e829a9b69af",
"reports": {
"bankAndCredit": {
"id": "f8e82ccc-38d2-417f-adcc-d5569463f8eb",
"creationDate": "2023-01-01 00:00:00.000",
"caseId": "d5325a2e-9fa5-4ed4-9ba8-3e829a9b69af",
"type": "BANKANDCREDIT",
"startDate": "2023-01-01 00:00:00.000",
"endDate": "2023-01-01 00:00:00.000",
"daysOfReport": 20,
"transactionsStartDate": "2023-01-01 00:00:00.000",
"totalTransactionsCount": 20,
"countIncomeTransactions": 17,
"countSpendingTransactions": 0,
"totalIncome": 150.75,
"totalSpending": 0,
"totalBalance": 150.75,
"accountData": [
{
"bankName": "Commerzbank",
"bankId": "4e760145-2e65-4242-ac33-488943528c93",
"accountIban": "DE13700800000061110500",
"accountId": "5f660145-2e65-4242-ac33-488943528c93",
"transactions": [
{
"valueDate": "2023-01-01 00:00:00.000",
"bankBookingDate": "2023-01-01 00:00:00.000",
"amount": -99.99,
"purpose": "Restaurantbesuch",
"counterpartName": "Bar Centrale",
"counterpartAccountNumber": "0061110500",
"counterpartIban": "DE13700800000061110500",
"counterpartBlz": "70080000",
"counterpartBic": "DRESDEFF700",
"counterpartBankName": "Commerzbank vormals Dresdner Bank",
"labels": [
"ENUMLABEL"
],
"labelDetails": {
"labelsWithLevelOfDetails": [
{
"lod1": "BANKANDCREDIT"
},
{
"lod1": "RENTANDLIVING",
"lod2": "UTILITIES"
},
{
"lod1": "INSURANCE ",
"lod2": "LIFEINSURANCE"
},
{
"lod1": "INSURANCE"
}
],
"labelsExpandedLowerLod": [
"BANKANDCREDIT",
"UTILITIES",
"LIFEINSURANCE",
"INSURANCE",
"RENTANDLIVING"
],
"mostSignificantLabels": [
{
"lod1": "RENTANDLIVING",
"lod2": "UTILITIES"
},
{
"lod1": "INSURANCE ",
"lod2": "LIFEINSURANCE"
}
],
"mostSignificantLod1": "RENTANDLIVING",
"mostSignificantIncomeLod2": "RENTANDLIVING"
},
"overdraftInformation": {
"overdraftInterestAmount": 99.9,
"startDate": "1.1.2023",
"endDate": "28.3.2023"
},
"chargebackInformation": {
"chargebackAmount": 99.9,
"valueDate": "1.1.2023"
}
}
]
}
]
}
[...]
}
}
Step 3.5 - Get DAC PDF (cURL)
To get a PDF with the results and some edits, the following endpoint can be accessed:
curl --location 'https://di-sandbox.finapi.io/api/v1/reporting/dacLoan/d5325a2e-9fa5-4ed4-9ba8-3e829a9b69af' \
--header 'Content-Type: application/json' \
--header 'Accept: application/pdf' \
--header 'Authorization: Bearer <access_token>' \
--data '{
"locale": "en"
}'
The locale is optional and can be en
or de
.
Implementation Guide
See a full working project here: finAPI Data Intelligence Product Platform Examples (Bitbucket)
Code from this guide can be found here: finAPI Data Sources(Bitbucket), finAPI Reports (Bitbucket)
Environment overview can be found here: Environments
The guidelines and the example project are written in Kotlin with Spring Boot. But it is easily adoptable for other languages and frameworks.
For the HTTP connection, we are using here plain Java HttpClient
, to be not restrictive for the client selection and that everything is transparent.
Only the actual functionality is discussed in this guideline. Used helper classes or the models can be looked up in the source code.
However, we always recommend using a generated client from our API, which reduces the effort of creating models and functions to access the endpoints and eliminates potential sources of error.
To learn how to generate and apply the API client using the Process Controller "Create user and exchange with access_token" as an example, please see the Getting Started - Code Generator (SDK) Integration.
Please also note that the code presented here and also the repository is only an illustration of a possible implementation. Therefore, the code is kept very simple and should not be used 1:1 in a production environment.
Example Code Flow Description
The flow of this example is, that the application synchronizes the data source as soon as the user is done with his connection of the accounts in finAPI WebForm or finAPI OpenBanking Access.
Further processing is done asynchronously by the callback.
As soon as the callback arrives again, it is checked whether the result is positive.
If the callback was positive, the Case and Report are created in the background and the Report is retrieved.
We are working with status updates in the local database. This database should be polled or streamed by the (frontend) application, to know if an error happened in the background or if the user can continue, because everything was successful.
Step 1 - Synchronize Data Source
This step is an extract from the Guide for Cases. More information can be found at Getting Started - Synchronize a Data Source.
Data sources are a central component and not just bound to reports. Therefore we create them in a neutral datasources
package: finAPI Data Sources(Bitbucket) (Data Source)
In this step, we create a simple service that starts the synchronization. In addition, we use a callback so that we don't have to poll for synchronization status.
First, we create a new class named DataSourceService
. This gets the URL of finAPI Data Intelligence (incl. the base path /api/v1
) and the domain of the own service for the callback.
As further classes we get the DataSourcesRepository
and DiProcessesRepository
passed. These classes are example database repositories. In the example, these classes are not discussed further because they only perform the required tasks in the example.
Last but not least we have an ObjectMapper
which is used to map JSON into objects and vice versa.
The first function we create is createPostRequest()
. This is passed the user access_token
, the URI of finAPI Data Intelligence, and the body.
From the URI and the diURL
, which we get via the constructor and the configuration, the method builds the final URL.
@Service
class DataSourceService(
@Value("\${finapi.instances.dataintelligence.url}") private val diUrl: String,
@Value("\${exampleapp.instances.callback.url}") private val callbackUrl: String,
private val dataSourcesRepository: DataSourcesRepository,
private val diProcessesRepository: ReportProcessesRepository,
private val objectMapper: ObjectMapper
) {
/**
* Create a POST HttpRequest.
*/
private fun createPostRequest(
accessToken: String,
endpointUri: String,
body: String
): HttpRequest {
return HttpRequest.newBuilder()
// build URL for https://<data_intelligence><endpointUri>
.uri(URI.create("${diUrl}${endpointUri}"))
// set Content-Type header to application/json
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.header(HttpHeaders.AUTHORIZATION, "Bearer $accessToken")
// add the body
.POST(
// use empty body to sync all accounts
HttpRequest.BodyPublishers.ofString(body)
).build()
}
companion object {
private val log = KotlinLogging.logger { }
}
}
Next, we add to the class the method that will start the synchronization.
Since synchronizing a Data Source is an asynchronous process, we must first create an internal processId
which we will use as a callbackHandle
.
We can use this later to identify and continue our process again via the callback.
Since some calls require parameters, we should also store these at the processId
before synchronizing the data source, so that the callback knows directly what it has to do.
For this we have the object DiProcessEntity
, which we pass to the syncDataSource()
function.
You can find the code of those classes in the repository.
After that, we send the request and use the previously created function createPostRequest()
to create the request.
The result is checked in this example with a simple static function to see if it matches 2xx
. Otherwise, an exception is thrown and the process is completely aborted.
If this check is also successful, the body of the response is mapped into an object, and the data is saved.
In this example, we save the access_token
as well, because we use it later for the callback.
In a production application, this should be encrypted so that no one can gain access to the client's data.
The syncDataSource()
method gets the callbackPath
from outside.
Reason for that is, that we can create different callback paths for different scenarios (e.g. Reports or Checks).
@Service
class DataSourceService(
@Value("\${finapi.instances.dataintelligence.url}") private val diUrl: String,
@Value("\${exampleapp.instances.callback.url}") private val callbackUrl: String,
private val dataSourcesRepository: DataSourcesRepository,
private val diProcessesRepository: DiProcessesRepository,
private val objectMapper: ObjectMapper
) {
/**
* Synchronize the data sources.
*/
@Throws(RuntimeException::class)
fun syncDataSource(
accessToken: String,
callbackPath: String,
processEntity: DiProcessEntity
): BankConnectionsResultModel {
val client = HttpClient.newBuilder().build()
// create the body for the request
val body = DataSourceSyncRequestModel(
callbackUrl = URI("${callbackUrl}${callbackPath}"),
callbackHandle = processEntity.processId
)
// create a request object and send it to DI
val response = client.send(
// create request URI for https://<data_intelligence>/api/v1/dataSources/bankConnections/synchronization
createPostRequest(
accessToken = accessToken,
endpointUri = CreateAndGetReportService.URI_DATA_SOURCE,
body = objectMapper.writeValueAsString(body)
),
HttpResponse.BodyHandlers.ofString()
)
// check for status code is 2xx or log and throw an exception
StatusCodeCheckUtils.checkStatusCodeAndLogErrorMessage(
response = response,
errorMessage = "Unable to sync data source."
)
// return the object of the mapped result
val bankConnectionsResult = objectMapper.readValue(response.body(), BankConnectionsResultModel::class.java)
// save all data sources and the current token to the database.
// the token is required when the asynchronous request of the callback is received to continue the process.
if (bankConnectionsResult.bankConnections != null) {
processEntity.status = EnumProcessStatus.SYNC_STARTED
val dataSources: ArrayList<DataSourceEntity> = ArrayList()
bankConnectionsResult.bankConnections.forEach {
dataSources.add(
DataSourceEntity(
userId = accessToken,
dataSourceId = it.dataSourceId
)
)
}
val dataSourceEntities = dataSourcesRepository.saveAll(dataSources)
processEntity.dataSources.addAll(dataSourceEntities)
// save the accessToken, so that the callback can continue.
// please encrypt this in reality!
processEntity.accessToken = accessToken
diProcessesRepository.save(processEntity)
}
log.info("[${processEntity.processId}] Synchronization with Data Intelligence started")
return bankConnectionsResult
}
[...]
}
With this, we would now be able to create the Data Sources.
Example to sync data sources for Reports:
// create a process entity, which will be stored, if the sync call was successful
val processEntity = DiProcessEntity(
processId = UUID.randomUUID().toString(),
status = EnumProcessStatus.SYNC_CREATED
)
// start synchronizing data sources
dataSourceService.syncDataSource(
accessToken = accessToken,
callbackPath = "${ReportCallbackApi.URI_BASEPATH}${ReportCallbackApi.URI_CB_DATASOURCE_REPORTS}",
processEntity = processEntity
)
Example to sync data sources for Checks with parameters:
// create a process entity, which will be stored, if the sync call was successful
val processEntity = DiProcessEntity(
processId = UUID.randomUUID().toString(),
status = EnumProcessStatus.SYNC_CREATED,
amount = BigDecimal.ONE
)
// start synchronizing data sources
dataSourceService.syncDataSource(
accessToken = accessToken,
callbackPath = "${CheckCallbackApi.URI_BASEPATH}${CheckCallbackApi.URI_CB_DATASOURCE_CHECKS}",
processEntity = processEntity
)
It is important to note that we are referring to an implementation with callback here. Without callback, the state of the data source must be polled. This should always happen with a pause of at least 200ms. Only when the status is SUCCESSFUL
may the creation of a case be continued.
The next step is to create the function for creating a Digital Account Check.
Step 2 - Create a Digital Account Check
For Digital Account Checks, the endpoint /api/v1/dacCases/loan
handles the creation of the case and the business-related reports.
To achieve this we first create a class DacService
.
The URL of Data Intelligence including the base path /api/v1
is passed to the service from the configuration.
To be able to control the process, it gets the DiProcessRepository
where we can store the state. In addition, we have the ObjectMapper
, with which we map JSON into objects.
The first function of this class is a helper method to create the POST
requests and it is called createPostRequest()
.
Since we also want to fetch the PDF later, we define the acceptHeader
argument in the function as optional with the default application/json
.
@Service
@Suppress("TooGenericExceptionCaught")
class DacService(
@Value("\${finapi.instances.dataintelligence.url}") private val diUrl: String,
private val diProcessesRepository: DiProcessesRepository,
private val objectMapper: ObjectMapper
) {
/**
* Create a POST HttpRequest.
*/
private fun createPostRequest(
accessToken: String,
endpointUri: String,
body: String,
acceptHeader: String = MediaType.APPLICATION_JSON_VALUE
): HttpRequest {
return HttpRequest.newBuilder()
// build URL for https://<data_intelligence><endpointUri>
.uri(URI.create("${diUrl}${endpointUri}"))
// set Content-Type header to application/json
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.header(HttpHeaders.AUTHORIZATION, "Bearer $accessToken")
.header(HttpHeaders.ACCEPT, acceptHeader)
// add the body
.POST(
// use empty body to sync all accounts
HttpRequest.BodyPublishers.ofString(body)
).build()
}
companion object {
private val log = KotlinLogging.logger { }
const val URI_DAC4LOAN_CREATE = "/dacCases/loan"
const val URI_DAC4LOAN_PDF = "/reporting/dacLoan/"
const val URI_CASES = "/cases"
const val URI_REPORTS = "/reports"
}
}
After the base is in place, we can add the function that creates the Digital Account Check.
The function receives as parameters the access_token
, the processId
which we created when synchronizing the Data Source and a list of dataSourceIds
.
First, we load the entity of the processId
. If it is not found, there is an error. Therefore we throw an exception here.
Then we create the request body and send the request using the previously created createPostRequest()
function.
We check the result with the help of StatusCodeCheckUtils
that the status corresponds to 2xx
. In production, this class should check much better.
To be able to store the error as a state and make it traceable later we embed the check in a try/catch
block. If something goes wrong, we store this at the process entity.
If everything went successfully, we also save this on the status and return the mapped result.
@Service
@Suppress("TooGenericExceptionCaught")
class DacService(
@Value("\${finapi.instances.dataintelligence.url}") private val diUrl: String,
private val diProcessesRepository: DiProcessesRepository,
private val objectMapper: ObjectMapper
) {
/**
* Create a DAC4Loan.
*/
@Throws(RuntimeException::class)
@Suppress("TooGenericExceptionThrown")
fun createDac4Loan(
accessToken: String,
processId: String,
dataSourceIds: List<String>
): Dac4LoanCreateResponseModel {
log.info("[${processId}] Starting create a DAC...")
// get the process entity or throw exception if not found
val processEntity = diProcessesRepository.findFirstByProcessId(processId = processId)
?: throw RuntimeException("Unable to create DAC. No Process Entity found.")
// create body for case
val body = Dac4LoanCreateRequestModel(
dataSourceIds = dataSourceIds,
)
// send request
val client = HttpClient.newBuilder().build()
val response = client.send(
// create request URI for https://<data_intelligence>/api/v1/dacCases/loan
createPostRequest(
accessToken = accessToken,
endpointUri = URI_DAC4LOAN_CREATE,
body = objectMapper.writeValueAsString(body)
),
HttpResponse.BodyHandlers.ofString()
)
// check for status code is 2xx or log and throw an exception
try {
StatusCodeCheckUtils.checkStatusCodeAndLogErrorMessage(
response = response,
errorMessage = "Unable to create a DAC."
)
} catch (ex: RuntimeException) {
processEntity.status = EnumProcessStatus.DAC_4_LOAN_FAILED
diProcessesRepository.save(processEntity)
throw ex
}
// map the result
val dacCreateReport = objectMapper.readValue(response.body(), Dac4LoanCreateResponseModel::class.java)
// save successful state for check creation
processEntity.status = EnumProcessStatus.DAC_4_LOAN_CREATED
diProcessesRepository.save(processEntity)
log.info("[${processId}] Finished create a DAC...")
return dacCreateReport
}
[...]
}
Step 3 - Get all reports
The next step is to collect the reports.
For this, we create 2 functions:
fetchAllReports()
createGetRequest()
createGetRequest()
is similar to createPostRequest()
, only for the GET
case.
With fetchAllReports()
we first load the reports.
Since we can assume here that the process exists, since we already checked it during creation and exited there with an exception, this does not need to be checked again.
The further procedure is the same as from the previous function, where we check the status code, store the status in the entity, and return the result.
@Service
@Suppress("TooGenericExceptionCaught")
class DacService(
@Value("\${finapi.instances.dataintelligence.url}") private val diUrl: String,
private val diProcessesRepository: DiProcessesRepository,
private val objectMapper: ObjectMapper
) {
/**
* Fetch all reports.
*/
@Throws(RuntimeException::class)
fun fetchAllReports(
accessToken: String,
processId: String,
caseId: String
): ReportsCaseModel {
log.info("[${processId}] Starting fetch all DAC4Loan reports for case ${caseId}...")
val client = HttpClient.newBuilder().build()
val response = client.send(
// create request URI for https://<data_intelligence>/api/v1/cases/{caseId}/reports
createGetRequest(
accessToken = accessToken,
endpointUri = "${URI_CASES}/${caseId}${URI_REPORTS}?withTransactions=true"
),
HttpResponse.BodyHandlers.ofString()
)
// get the process entity
val processEntity = diProcessesRepository.findFirstByProcessId(processId = processId)
// check for status code is 2xx or log and throw an exception
try {
StatusCodeCheckUtils.checkStatusCodeAndLogErrorMessage(
response = response,
errorMessage = "Unable to fetch reports."
)
} catch (ex: RuntimeException) {
processEntity!!.status = EnumProcessStatus.DAC_4_LOAN_FETCH_FAILED
diProcessesRepository.save(processEntity)
throw ex
}
// map the result
val reports = objectMapper.readValue(response.body(), ReportsCaseModel::class.java)
// save successful state for fetching the report
processEntity!!.status = EnumProcessStatus.DAC_4_LOAN_FETCHED
diProcessesRepository.save(processEntity)
log.info("[${processId}] Finished fetch all DAC4Loan reports for case ${caseId}...")
return reports
}
/**
* Create a GET HttpRequest.
*/
private fun createGetRequest(
accessToken: String,
endpointUri: String
): HttpRequest {
return HttpRequest.newBuilder()
// build URL for https://<data_intelligence><endpointUri>
.uri(URI.create("${diUrl}${endpointUri}"))
// set Content-Type header to application/json
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.header(HttpHeaders.AUTHORIZATION, "Bearer $accessToken")
// add the body
.GET()
.build()
}
[...]
}
Step 3.5 - Get DAC PDF
Theoretically, we already have all the functions we need.
However, we still want to retrieve the PDF so that we can store it for documentation purposes or for the business department.
Basically, the function works like the previous ones.
However, now we have a binary file as a response.
Therefore, when calling the createPostRequest()
we need to set the acceptHeader
parameter to application/pdf
.
Also, the body handler is set to BodyHandlers.ofByteArray()
.
In this example, we simply store the file in the file system.
For a productive application, the PDF should be stored in the database or better in a Document Management System (DMS), so that clerks have access to the PDF.
@Service
@Suppress("TooGenericExceptionCaught")
class DacService(
@Value("\${finapi.instances.dataintelligence.url}") private val diUrl: String,
private val diProcessesRepository: DiProcessesRepository,
private val objectMapper: ObjectMapper
) {
/**
* Fetch DAC4Loan PDF.
*/
@Throws(RuntimeException::class)
fun createDac4LoanPDF(
accessToken: String,
processId: String,
caseId: String
) {
log.info("[${processId}] Starting fetch DAC4Loan PDF for case ${caseId}...")
val client = HttpClient.newBuilder().build()
val response = client.send(
// create request URI for https://<data_intelligence>/api/v1/reporting/dacLoan/{caseId}
createPostRequest(
accessToken = accessToken,
endpointUri = "${URI_DAC4LOAN_PDF}${caseId}",
body = "{ \"locale\": \"en\" }",
acceptHeader = MediaType.APPLICATION_PDF_VALUE
),
HttpResponse.BodyHandlers.ofByteArray()
)
// get the process entity
val processEntity = diProcessesRepository.findFirstByProcessId(processId = processId)
// check for status code is 2xx or log and throw an exception
try {
StatusCodeCheckUtils.checkStatusCodeAndLogErrorMessage(
response = response,
errorMessage = "Unable to fetch reports."
)
} catch (ex: RuntimeException) {
processEntity!!.status = EnumProcessStatus.DAC_4_LOAN_PDF_FAILED
diProcessesRepository.save(processEntity)
throw ex
}
// save the PDF (please store in database for real applications)
val pdfAsString = response.body()
File("dacPdf.pdf").writeBytes(pdfAsString)
// Saving the success status for the retrieved DAC
processEntity!!.status = EnumProcessStatus.DAC_4_LOAN_PDF_FETCHED
diProcessesRepository.save(processEntity)
log.info("[${processId}] Finished fetch DAC4Loan PDF for case ${caseId}...")
}
[...]
}
Step 4 - Callback Service
First, we add a function to the DacService
that combines the creation and retrieval of the Digital Account Check.
This way we can have everything done in the callback by a simple call and have no business logic in the callback service.
@Service
@Suppress("TooGenericExceptionCaught")
class DacService(
@Value("\${finapi.instances.dataintelligence.url}") private val diUrl: String,
private val diProcessesRepository: DiProcessesRepository,
private val objectMapper: ObjectMapper
) {
/**
* create case and reports process flow.
*/
@Throws(RuntimeException::class)
fun createAndFetchDac4LoanB2C(
accessToken: String,
processId: String,
dataSourceIds: List<String>
) {
// first we create a case file
val caseFileResult = createDac4Loan(
accessToken = accessToken,
processId = processId,
dataSourceIds = dataSourceIds
)
val dac4LoanReports = fetchAllReports(
accessToken = accessToken,
processId = processId,
caseId = caseFileResult.id
)
val dac4LoanPdf = createDac4LoanPDF(
accessToken = accessToken,
processId = processId,
caseId = caseFileResult.id
)
// save the result to the database and continue with own business logic
}
[...]
}
Now we create the class DacCallbackApi
.
This is a RestController
and has a mapping for an endpoint, which we can provide when creating the callback so that Data Intelligence can call the endpoint after synchronization and thus start further processing.
The callback first checks the incoming data and its own process. If this is not available, the function terminates with an exception.
If the incoming parameters are ok, the data sources statuses are updated in the own database.
If all data sources have been called successfully via the callback, we call the dacService.createAndFetchDac4LoanB2C()
function.
This then executes the further steps.
The check whether all data sources are in order is kept quite simple in this example. In production, this should be worked out in more detail.
@RestController
class DacCallbackApi(
private val dacService: DacService,
private val dataSourcesRepository: DataSourcesRepository,
private val diProcessesRepository: DiProcessesRepository
) {
@Suppress("TooGenericExceptionThrown")
@PostMapping("${URI_BASEPATH}${URI_CB_DATASOURCE_DAC}")
fun dataSourceCallbackForDac(@RequestBody body: CallbackRequestModel) {
// if the body does not contain a callback handle we throw an exception,
// because we cannot assign it to another request
if (body.callbackHandle == null) {
throw RuntimeException("No callback handle provided.")
}
// if we could not find the handle we do the same
val reportProcessEntity = diProcessesRepository.findFirstByProcessId(
processId = body.callbackHandle
) ?: throw RuntimeException("Unable to find the callback handle.")
// validate if the request contains a successful state or something else
if (validateDataSourceRequest(body)) {
// that we've received a data source
val dataSources = reportProcessEntity.dataSources
dataSources.forEach {
if (it.dataSourceId == body.datasourceId) {
it.callbackReceived = true
dataSourcesRepository.save(it)
}
}
// if everything was updated, we can create the case
if (isAllCallbacksReceived(dataSources = dataSources)) {
// figure out all data source ids
val dataSourceIds = ArrayList<String>()
reportProcessEntity.dataSources.forEach {
dataSourceIds.add(it.dataSourceId)
}
// call the "create dac4loan and fetch results" function
dacService.createAndFetchDac4LoanB2C(
accessToken = reportProcessEntity.accessToken!!,
processId = body.callbackHandle,
dataSourceIds = dataSourceIds
)
}
} else {
log.error("""
[${body.callbackHandle}] has received the status [${body.datasource?.status}]
with the code [${body.datasource?.code}]
and message [${body.datasource?.message}]
""".trimIndent()
)
// if the callback was not successful, we store it as error status
reportProcessEntity.status = EnumProcessStatus.CALLBACK_FAILED
diProcessesRepository.save(reportProcessEntity)
}
}
private fun isAllCallbacksReceived(dataSources: List<DataSourceEntity>): Boolean {
var result = true
dataSources.forEach {
if (!it.callbackReceived) {
result = false
}
}
return result
}
private fun validateDataSourceRequest(request: CallbackRequestModel): Boolean {
return (request.callbackHandle != null &&
request.datasourceId != null &&
request.datasource != null &&
EnumStatus.SUCCESSFUL == request.datasource.status)
}
companion object {
private val log = KotlinLogging.logger { }
const val URI_BASEPATH = "/callbacks"
const val URI_CB_DATASOURCE_DAC = "/dataSource/dac4loan"
}
}