diff --git a/.gitignore b/.gitignore index 702f5f7..79c7052 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ *~ *.key *.pem +.idea +.local diff --git a/kotlin_example/.gitignore b/kotlin_example/.gitignore new file mode 100644 index 0000000..151fdd4 --- /dev/null +++ b/kotlin_example/.gitignore @@ -0,0 +1,2 @@ +build +.gradle \ No newline at end of file diff --git a/kotlin_example/README.md b/kotlin_example/README.md new file mode 100644 index 0000000..452ed22 --- /dev/null +++ b/kotlin_example/README.md @@ -0,0 +1,74 @@ +# Enable Banking API — Kotlin Example + +This implementation uses [Spring Boot 4](https://spring.io/projects/spring-boot) with +[Spring's RestClient](https://docs.spring.io/spring-framework/reference/integration/rest-clients.html) +for HTTP calls and [Auth0 java-jwt](https://github.com/auth0/java-jwt) for JWT signing. + +## Prerequisites + +- **JDK 25** — download from [adoptium.net](https://adoptium.net/) or [sdkman.io](https://sdkman.io/) +- No separate Gradle install needed — the included `./gradlew` wrapper downloads Gradle 9 automatically + +## Configuration + +Edit `src/main/resources/application.yml` with your application credentials: + +```yaml +enablebanking: + key-path: "../.pem" # path to your PEM private key, relative to this directory + application-id: "" + redirect-url: "http://localhost:8080/auth_redirect" +``` + +The `key-path` and `application-id` values come from your application registered at +[enablebanking.com/cp/applications](https://enablebanking.com/cp/applications). + +## Running + +### Account information (AIS) + +```bash +./gradlew bootRun --args="--spring.profiles.active=ais" +``` + +### Payment initiation (PIS) + +```bash +./gradlew bootRun --args="--spring.profiles.active=pis" +``` + +## Authorization flow (AIS) + +The account information example includes an interactive consent step: + +1. The app prints a bank authorization URL — **open it in your browser** +2. Log in with your bank credentials and approve the consent + - Sandbox credentials: **customera / 12345678** +3. The bank redirects your browser to the `redirect-url` from `application.yml` + (e.g. `http://localhost:8080/auth_redirect?code=...`) — the page may show an error, that is expected +4. **Copy the full URL** from the browser address bar and paste it into the terminal when prompted +5. The app extracts the `code` parameter automatically and continues to fetch balances and transactions + +## Integrating into an existing Spring Boot 4 service + +`EnableBankingService` is a standard `@Service` bean — inject it anywhere: + +```kotlin +@Service +class YourService(private val enableBanking: EnableBankingService) { + + fun fetchBalances(accountId: String) = enableBanking.getBalances(accountId) +} +``` + +Add the following to your `application.yml`: + +```yaml +enablebanking: + key-path: "/path/to/your-key.pem" + application-id: "your-application-id" + redirect-url: "https://your-redirect-url" +``` + +Copy across: `Config.kt`, `Models.kt`, `JwtUtils.kt`, `EnableBankingService.kt`, and add +`com.auth0:java-jwt:4.4.0` to your dependencies. diff --git a/kotlin_example/build.gradle.kts b/kotlin_example/build.gradle.kts new file mode 100644 index 0000000..f564723 --- /dev/null +++ b/kotlin_example/build.gradle.kts @@ -0,0 +1,42 @@ +plugins { + kotlin("jvm") version "2.1.21" + kotlin("plugin.spring") version "2.1.21" + id("org.springframework.boot") version "4.1.0" + id("io.spring.dependency-management") version "1.1.7" +} + +group = "com.enablebanking" +version = "1.0.0" + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(25) + } +} + +// Kotlin 2.1.x does not yet support JVM target 25; align both compilers to 23 +tasks.withType { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_23 + } +} + +tasks.withType { + sourceCompatibility = "23" + targetCompatibility = "23" +} + +repositories { + mavenCentral() +} + +tasks.named("bootRun") { + standardInput = System.`in` +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter") + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("com.auth0:java-jwt:4.4.0") +} diff --git a/kotlin_example/gradle/wrapper/gradle-wrapper.jar b/kotlin_example/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..1b33c55 Binary files /dev/null and b/kotlin_example/gradle/wrapper/gradle-wrapper.jar differ diff --git a/kotlin_example/gradle/wrapper/gradle-wrapper.properties b/kotlin_example/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..1e922f4 --- /dev/null +++ b/kotlin_example/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.0-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/kotlin_example/gradlew b/kotlin_example/gradlew new file mode 100755 index 0000000..b7635d7 --- /dev/null +++ b/kotlin_example/gradlew @@ -0,0 +1,187 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by "Gradle" +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other POSIX-compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for modifying this script: +# * When modifying this script, please restrict changes to those +# required by the upgrade of Gradle or JDK version being used. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is reliable if the script is not sourced. +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + ;; + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# temporary IFS-entry separators, and then use eval/cat to build up the +# temporary $@ array. +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^a-zA-Z0-9/=]~\\&~g; ' | + tr '\n' ' ' + ) $@" + +exec "$JAVACMD" "$@" diff --git a/kotlin_example/settings.gradle.kts b/kotlin_example/settings.gradle.kts new file mode 100644 index 0000000..8de67f8 --- /dev/null +++ b/kotlin_example/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "enablebanking-kotlin-example" diff --git a/kotlin_example/src/main/kotlin/com/enablebanking/AccountInformationRunner.kt b/kotlin_example/src/main/kotlin/com/enablebanking/AccountInformationRunner.kt new file mode 100644 index 0000000..98e3a4d --- /dev/null +++ b/kotlin_example/src/main/kotlin/com/enablebanking/AccountInformationRunner.kt @@ -0,0 +1,96 @@ +package com.enablebanking + +import org.springframework.boot.CommandLineRunner +import org.springframework.context.annotation.Profile +import org.springframework.stereotype.Component +import java.net.URI +import java.time.ZoneOffset +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.util.Scanner +import java.util.UUID + +@Component +@Profile("ais") +class AccountInformationRunner( + private val service: EnableBankingService, + private val config: EnableBankingConfig, +) : CommandLineRunner { + + private val aspspName = "Nordea" + private val aspspCountry = "FI" + + override fun run(vararg args: String) { + val app = service.getApplication() + println("Application details: $app") + + service.getAspsps() + // ASPSP list omitted — output is too long + + val validUntil = ZonedDateTime.now(ZoneOffset.UTC) + .plusDays(10) + .format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) + + @Suppress("UNCHECKED_CAST") + val redirectUrls = (app["redirect_urls"] as? List) ?: emptyList() + val redirectUrl = redirectUrls.firstOrNull() ?: config.redirectUrl + + val authResponse = service.startAuth( + AuthRequest( + access = AccessRequest(validUntil = validUntil), + aspsp = AspspRequest(name = aspspName, country = aspspCountry), + state = UUID.randomUUID().toString(), + redirectUrl = redirectUrl, + ) + ) + + println() + println("=== Authorization required ===") + println("1. Open the following URL in your browser:") + println(" ${authResponse.url}") + println("2. Log in with your bank credentials and grant consent") + println(" (Sandbox credentials: customera / 12345678)") + println("3. You will be redirected to: $redirectUrl") + println(" The page may not load — that is expected") + println("4. Copy the full URL from the browser address bar and paste it below:") + print("> ") + + val scanner = Scanner(System.`in`) + val redirectedUrl = scanner.nextLine().trim() + val code = URI(redirectedUrl).query + .split("&") + .first { it.startsWith("code=") } + .removePrefix("code=") + + val session = service.createSession(code) + println("Session created: session_id=${session.sessionId}") + + val sessionDetails = service.getSession(session.sessionId) + println("Session details: $sessionDetails") + + val accountUid = session.accounts.first().uid + println("Using account: $accountUid") + + val balances = service.getBalances(accountUid) + println("Balances: $balances") + + val dateFrom = ZonedDateTime.now(ZoneOffset.UTC) + .minusDays(90) + .toLocalDate() + .toString() + + var continuationKey: String? = null + while (true) { + val txResp = service.getTransactions(accountUid, dateFrom, continuationKey) + println("Transactions (${txResp.transactions.size}): ${txResp.transactions}") + continuationKey = txResp.continuationKey + if (continuationKey == null) { + println("All transactions fetched.") + break + } + println("Fetching more transactions with continuation_key=$continuationKey") + } + + println("All done!") + } +} diff --git a/kotlin_example/src/main/kotlin/com/enablebanking/Config.kt b/kotlin_example/src/main/kotlin/com/enablebanking/Config.kt new file mode 100644 index 0000000..8e8869c --- /dev/null +++ b/kotlin_example/src/main/kotlin/com/enablebanking/Config.kt @@ -0,0 +1,10 @@ +package com.enablebanking + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties("enablebanking") +data class EnableBankingConfig( + val keyPath: String, + val applicationId: String, + val redirectUrl: String, +) diff --git a/kotlin_example/src/main/kotlin/com/enablebanking/EnableBankingApplication.kt b/kotlin_example/src/main/kotlin/com/enablebanking/EnableBankingApplication.kt new file mode 100644 index 0000000..eadd07e --- /dev/null +++ b/kotlin_example/src/main/kotlin/com/enablebanking/EnableBankingApplication.kt @@ -0,0 +1,13 @@ +package com.enablebanking + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.boot.runApplication + +@SpringBootApplication +@EnableConfigurationProperties(EnableBankingConfig::class) +class EnableBankingApplication + +fun main(args: Array) { + runApplication(*args) +} diff --git a/kotlin_example/src/main/kotlin/com/enablebanking/EnableBankingService.kt b/kotlin_example/src/main/kotlin/com/enablebanking/EnableBankingService.kt new file mode 100644 index 0000000..06e03a3 --- /dev/null +++ b/kotlin_example/src/main/kotlin/com/enablebanking/EnableBankingService.kt @@ -0,0 +1,99 @@ +package com.enablebanking + +import org.springframework.stereotype.Service +import org.springframework.web.client.RestClient + +@Service +class EnableBankingService(private val config: EnableBankingConfig) { + + private val apiOrigin = "https://api.enablebanking.com" + + private fun client(): RestClient { + val jwt = generateJwt(config.keyPath, config.applicationId) + return RestClient.builder() + .baseUrl(apiOrigin) + .defaultHeader("Authorization", "Bearer $jwt") + .defaultHeader("Content-Type", "application/json") + .build() + } + + fun getApplication(): Map<*, *> = + client().get().uri("/application") + .retrieve() + .onStatus({ it.isError }) { _, response -> + error("GET /application failed: ${response.statusCode}") + } + .body(Map::class.java)!! + + fun getAspsps(): Map<*, *> = + client().get().uri("/aspsps") + .retrieve() + .onStatus({ it.isError }) { _, response -> + error("GET /aspsps failed: ${response.statusCode}") + } + .body(Map::class.java)!! + + fun startAuth(request: AuthRequest): AuthResponse = + client().post().uri("/auth") + .body(request) + .retrieve() + .onStatus({ it.isError }) { _, response -> + error("POST /auth failed: ${response.statusCode}") + } + .body(AuthResponse::class.java)!! + + fun createSession(code: String): SessionResponse = + client().post().uri("/sessions") + .body(SessionCodeRequest(code)) + .retrieve() + .onStatus({ it.isError }) { _, response -> + error("POST /sessions failed: ${response.statusCode}") + } + .body(SessionResponse::class.java)!! + + fun getSession(sessionId: String): Map<*, *> = + client().get().uri("/sessions/$sessionId") + .retrieve() + .onStatus({ it.isError }) { _, response -> + error("GET /sessions/$sessionId failed: ${response.statusCode}") + } + .body(Map::class.java)!! + + fun getBalances(accountId: String): Map<*, *> = + client().get().uri("/accounts/$accountId/balances") + .retrieve() + .onStatus({ it.isError }) { _, response -> + error("GET /accounts/$accountId/balances failed: ${response.statusCode}") + } + .body(Map::class.java)!! + + fun getTransactions(accountId: String, dateFrom: String, continuationKey: String? = null): TransactionsResponse { + val uri = buildString { + append("/accounts/$accountId/transactions?date_from=$dateFrom") + if (continuationKey != null) append("&continuation_key=$continuationKey") + } + return client().get().uri(uri) + .retrieve() + .onStatus({ it.isError }) { _, response -> + error("GET $uri failed: ${response.statusCode}") + } + .body(TransactionsResponse::class.java)!! + } + + fun initiatePayment(request: PaymentRequest): PaymentResponse = + client().post().uri("/payments") + .body(request) + .retrieve() + .onStatus({ it.isError }) { _, response -> + error("POST /payments failed: ${response.statusCode}") + } + .body(PaymentResponse::class.java)!! + + fun getPaymentStatus(paymentId: String): Map<*, *> = + client().get().uri("/payments/$paymentId") + .retrieve() + .onStatus({ it.isError }) { _, response -> + error("GET /payments/$paymentId failed: ${response.statusCode}") + } + .body(Map::class.java)!! +} diff --git a/kotlin_example/src/main/kotlin/com/enablebanking/JwtUtils.kt b/kotlin_example/src/main/kotlin/com/enablebanking/JwtUtils.kt new file mode 100644 index 0000000..9e890ae --- /dev/null +++ b/kotlin_example/src/main/kotlin/com/enablebanking/JwtUtils.kt @@ -0,0 +1,38 @@ +package com.enablebanking + +import com.auth0.jwt.JWT +import com.auth0.jwt.algorithms.Algorithm +import java.io.File +import java.security.KeyFactory +import java.security.interfaces.RSAPrivateKey +import java.security.spec.PKCS8EncodedKeySpec +import java.util.Base64 +import java.util.Date + +fun generateJwt(keyPath: String, applicationId: String): String { + val privateKey = loadPrivateKey(keyPath) + val algorithm = Algorithm.RSA256(null, privateKey) + val now = Date() + val expiry = Date(now.time + 3600_000L) + + return JWT.create() + .withKeyId(applicationId) + .withIssuer("enablebanking.com") + .withAudience("api.enablebanking.com") + .withIssuedAt(now) + .withExpiresAt(expiry) + .sign(algorithm) +} + +private fun loadPrivateKey(path: String): RSAPrivateKey { + val pem = File(path).readText() + val keyBody = pem + .replace("-----BEGIN PRIVATE KEY-----", "") + .replace("-----END PRIVATE KEY-----", "") + .replace("-----BEGIN RSA PRIVATE KEY-----", "") + .replace("-----END RSA PRIVATE KEY-----", "") + .replace("\\s".toRegex(), "") + val decoded = Base64.getDecoder().decode(keyBody) + val keySpec = PKCS8EncodedKeySpec(decoded) + return KeyFactory.getInstance("RSA").generatePrivate(keySpec) as RSAPrivateKey +} diff --git a/kotlin_example/src/main/kotlin/com/enablebanking/Models.kt b/kotlin_example/src/main/kotlin/com/enablebanking/Models.kt new file mode 100644 index 0000000..aa181ec --- /dev/null +++ b/kotlin_example/src/main/kotlin/com/enablebanking/Models.kt @@ -0,0 +1,91 @@ +package com.enablebanking + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty + +data class AuthRequest( + val access: AccessRequest, + val aspsp: AspspRequest, + val state: String, + @JsonProperty("redirect_url") val redirectUrl: String, + @JsonProperty("psu_type") val psuType: String = "personal", +) + +data class AccessRequest( + @JsonProperty("valid_until") val validUntil: String, +) + +data class AspspRequest( + val name: String, + val country: String, +) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class AuthResponse( + val url: String, +) + +data class SessionCodeRequest( + val code: String, +) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class SessionResponse( + @JsonProperty("session_id") val sessionId: String, + val accounts: List, +) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class AccountRef( + val uid: String, +) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class TransactionsResponse( + val transactions: List, + @JsonProperty("continuation_key") val continuationKey: String?, +) + +data class PaymentRequest( + @JsonProperty("payment_type") val paymentType: String, + @JsonProperty("payment_request") val paymentRequest: PaymentDetails, + val aspsp: AspspRequest, + val state: String, + @JsonProperty("redirect_url") val redirectUrl: String, + @JsonProperty("psu_type") val psuType: String = "personal", +) + +data class PaymentDetails( + @JsonProperty("credit_transfer_transaction") val creditTransferTransaction: List, +) + +data class CreditTransferTransaction( + val beneficiary: Beneficiary, + @JsonProperty("instructed_amount") val instructedAmount: InstructedAmount, + @JsonProperty("reference_number") val referenceNumber: String, +) + +data class Beneficiary( + @JsonProperty("creditor_account") val creditorAccount: CreditorAccount, + val creditor: Creditor, +) + +data class CreditorAccount( + @JsonProperty("scheme_name") val schemeName: String, + val identification: String, +) + +data class Creditor( + val name: String, +) + +data class InstructedAmount( + val amount: String, + val currency: String, +) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class PaymentResponse( + val url: String, + @JsonProperty("payment_id") val paymentId: String, +) diff --git a/kotlin_example/src/main/kotlin/com/enablebanking/PaymentInitiationRunner.kt b/kotlin_example/src/main/kotlin/com/enablebanking/PaymentInitiationRunner.kt new file mode 100644 index 0000000..3714192 --- /dev/null +++ b/kotlin_example/src/main/kotlin/com/enablebanking/PaymentInitiationRunner.kt @@ -0,0 +1,58 @@ +package com.enablebanking + +import org.springframework.boot.CommandLineRunner +import org.springframework.context.annotation.Profile +import org.springframework.stereotype.Component +import java.util.UUID + +@Component +@Profile("pis") +class PaymentInitiationRunner( + private val service: EnableBankingService, + private val config: EnableBankingConfig, +) : CommandLineRunner { + + private val aspspName = "S-Pankki" + private val aspspCountry = "FI" + + override fun run(vararg args: String) { + val app = service.getApplication() + println("Application details: $app") + + service.getAspsps() + // ASPSP list omitted — output is too long + + val paymentResponse = service.initiatePayment( + PaymentRequest( + paymentType = "SEPA", + paymentRequest = PaymentDetails( + creditTransferTransaction = listOf( + CreditTransferTransaction( + beneficiary = Beneficiary( + creditorAccount = CreditorAccount( + schemeName = "IBAN", + identification = "FI7473834510057469", + ), + creditor = Creditor(name = "Test"), + ), + instructedAmount = InstructedAmount(amount = "2.00", currency = "EUR"), + referenceNumber = "123", + ) + ) + ), + aspsp = AspspRequest(name = aspspName, country = aspspCountry), + state = UUID.randomUUID().toString(), + redirectUrl = config.redirectUrl, + ) + ) + + println() + println("=== Payment authorization required ===") + println("Sandbox credentials: customera / 12345678") + println("To authenticate open URL:") + println(" ${paymentResponse.url}") + + val paymentStatus = service.getPaymentStatus(paymentResponse.paymentId) + println("Payment status: $paymentStatus") + } +} diff --git a/kotlin_example/src/main/resources/application.yml b/kotlin_example/src/main/resources/application.yml new file mode 100644 index 0000000..24c837b --- /dev/null +++ b/kotlin_example/src/main/resources/application.yml @@ -0,0 +1,9 @@ +enablebanking: + key-path: "../.local/0377783c-3414-4bb1-a7d4-0c3eb54cd4b0.pem" + application-id: "0377783c-3414-4bb1-a7d4-0c3eb54cd4b0" + redirect-url: "http://localhost:8080/auth_redirect" + +spring: + main: + # Prevents Spring from shutting down if no web server is started + web-application-type: none