Skip to main content
Skip table of contents

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

TL:TR

Used Systems

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

/api/v1/dataSources/bankConnections/synchronization

finAPI API Documentation

Get status of the Data Source
(for polling if no callback was used)

GET

/api/v1/dataSources/{dataSourceId}/status

finAPI API Documentation

Create a DAC4Loan

POST

/api/v1/dacCases/loan

finAPI API Documentation

Get all reports

GET

/api/v1/cases/{caseId}/reports

finAPI API Documentation

Get DAC4Loan PDF

POST

/api/v1/reporting/dacLoan/{caseId}

finAPI API Documentation

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:

BASH
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:

JSON
{
  "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:

BASH
curl --location 'https://di-sandbox.finapi.io/api/v1/dataSources/{dataSourceId}/status' \
--header 'Authorization: Bearer <access_token>'

The response looks like this:

JSON
{
  "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:

BASH
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:

JSON
{
  "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:

BASH
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):

JSON
{
  "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:

BASH
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 datasourcespackage: 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.

KOTLIN
@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).

KOTLIN
@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:

KOTLIN
// 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:

KOTLIN
// 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.

KOTLIN
@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.

KOTLIN
@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.

KOTLIN
@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.

KOTLIN
@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.

KOTLIN
@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.

KOTLIN
@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"
    }
}

JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.