diff --git a/.github/dependabot.yml b/.github/dependabot.yml index fa0077fd..0d90c652 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,5 +1,6 @@ version: 2 updates: + # Java dependencies - package-ecosystem: gradle directory: "/" schedule: @@ -8,4 +9,17 @@ updates: prefix: "[Dependabot]" labels: - "skip-release" - - "dependencies" \ No newline at end of file + - "dependencies" + - "java" + + # GitHub Actions dependencies + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + open-pull-requests-limit: 5 + labels: + - "skip-release" + - "dependencies" + - "ci" \ No newline at end of file diff --git a/.github/workflows/acceptance-tests.yml b/.github/workflows/acceptance-tests.yml index 234b6d83..88233e20 100644 --- a/.github/workflows/acceptance-tests.yml +++ b/.github/workflows/acceptance-tests.yml @@ -29,17 +29,17 @@ jobs: acceptance-tests: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ inputs.checkout_ref }} - name: Setup JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: - java-version: '21' + java-version: '25' distribution: 'temurin' cache: 'gradle' - name: Validate Gradle wrapper - uses: gradle/actions/wrapper-validation@v4 + uses: gradle/actions/wrapper-validation@v5 - name: Acceptance tests in sandbox env: TL_CLIENT_ID: ${{ secrets.tl_client_id }} diff --git a/.github/workflows/build-test-coverage.yml b/.github/workflows/build-test-coverage.yml index 6126207d..a9b34b78 100644 --- a/.github/workflows/build-test-coverage.yml +++ b/.github/workflows/build-test-coverage.yml @@ -24,22 +24,22 @@ jobs: matrix: os: [ ubuntu-latest ] java-distribution: [ temurin ] - java-version: [ 11, 17, 20, 21, 22, 23 ] + java-version: [ 17, 20, 23, 25 ] runs-on: ${{ matrix.os }} outputs: project_version: ${{ steps.get_project_version.outputs.project_version }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ inputs.checkout_ref }} - name: Setup JDK ${{ matrix.java }} - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: ${{ matrix.java-version }} distribution: ${{ matrix.java-distribution }} cache: 'gradle' - name: Validate Gradle wrapper - uses: gradle/actions/wrapper-validation@v4 + uses: gradle/actions/wrapper-validation@v5 - name: Get project version id: get_project_version run: | @@ -64,17 +64,17 @@ jobs: name: Test coverage analysis runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ inputs.checkout_ref }} - name: Setup JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: - java-version: '21' + java-version: '25' distribution: 'temurin' cache: 'gradle' - name: Validate Gradle wrapper - uses: gradle/actions/wrapper-validation@v4 + uses: gradle/actions/wrapper-validation@v5 - name: Test coverage run: ./gradlew unit-tests jacocoTestReport coveralls env: diff --git a/.github/workflows/release-snapshot.yml b/.github/workflows/release-snapshot.yml index b5ed93a8..139de5ee 100644 --- a/.github/workflows/release-snapshot.yml +++ b/.github/workflows/release-snapshot.yml @@ -29,17 +29,17 @@ jobs: name: Release to Maven Central runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ inputs.checkout_ref }} - name: Setup JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: - java-version: '21' + java-version: '25' distribution: 'temurin' cache: 'gradle' - name: Validate Gradle wrapper - uses: gradle/actions/wrapper-validation@v4 + uses: gradle/actions/wrapper-validation@v5 - name: Create Snapshot version run: | CHECKOUT_REF=${{inputs.checkout_ref}} diff --git a/.github/workflows/workflow-examples.yml b/.github/workflows/workflow-examples.yml index 2a244fe0..70464d54 100644 --- a/.github/workflows/workflow-examples.yml +++ b/.github/workflows/workflow-examples.yml @@ -19,15 +19,15 @@ jobs: run: working-directory: ./examples/${{ matrix.project-working-dir }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: - java-version: '21' + java-version: '25' distribution: 'temurin' cache: 'gradle' - name: Validate Gradle wrapper - uses: gradle/actions/wrapper-validation@v4 + uses: gradle/actions/wrapper-validation@v5 - name: Lint run: ./gradlew spotlessJavaCheck - name: Build diff --git a/.github/workflows/workflow-main.yml b/.github/workflows/workflow-main.yml index b295b507..9e3476ca 100644 --- a/.github/workflows/workflow-main.yml +++ b/.github/workflows/workflow-main.yml @@ -32,17 +32,17 @@ jobs: runs-on: ubuntu-latest needs: [build-test-coverage, acceptance-tests] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ github.ref }} - name: Setup JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: - java-version: '21' + java-version: '25' distribution: 'temurin' cache: 'gradle' - name: Validate Gradle wrapper - uses: gradle/actions/wrapper-validation@v4 + uses: gradle/actions/wrapper-validation@v5 - name: Create tag id: create_tag uses: mathieudutour/github-tag-action@v6.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index a4689ac7..f3ca2d7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/). +## [17.6.0] - 2025-x-x +### Added +* New ProviderSelection builder method on the StartAuthorizationFlowRequest builder +* New Icon object to ProviderSelection in authorization flow requests +* New Consent type properties within the authorization flow request + +### Changed +* ⚠️ Deprecated `withProviderSelection()` flag on authorization flow request builder +* Various dependency updates + ## [17.5.1] - 2025-10-31 ### Fixed * Update Sonatype Central badge url in order to show latest version diff --git a/build.gradle b/build.gradle index f4d3f6f6..d2ee3d79 100644 --- a/build.gradle +++ b/build.gradle @@ -4,11 +4,11 @@ import com.vanniktech.maven.publish.JavadocJar plugins { id 'java-library' // to unleash the lombok magic - id "io.freefair.lombok" version "8.13.1" + id "io.freefair.lombok" version "9.1.0" // to make our tests output more fancy id 'com.adarshr.test-logger' version '4.0.0' // code linting - id "com.diffplug.spotless" version "7.0.3" + id "com.diffplug.spotless" version "8.1.0" // test coverage id 'jacoco' id 'com.github.kt3k.coveralls' version '2.12.2' @@ -58,7 +58,17 @@ testlogger { theme 'mocha' } +tasks.withType(Test).configureEach { + testlogger { + theme 'mocha' + } +} + tasks.register('unit-tests', Test) { + testClassesDirs = sourceSets.test.output.classesDirs + classpath = sourceSets.test.runtimeClasspath + + dependsOn testClasses outputs.upToDateWhen { false } useJUnitPlatform{ excludeTags "integration" @@ -67,6 +77,10 @@ tasks.register('unit-tests', Test) { } tasks.register('integration-tests', Test) { + testClassesDirs = sourceSets.test.output.classesDirs + classpath = sourceSets.test.runtimeClasspath + + dependsOn testClasses outputs.upToDateWhen { false } useJUnitPlatform{ includeTags "integration" @@ -74,6 +88,10 @@ tasks.register('integration-tests', Test) { } tasks.register('acceptance-tests', Test) { + testClassesDirs = sourceSets.test.output.classesDirs + classpath = sourceSets.test.runtimeClasspath + + dependsOn testClasses outputs.upToDateWhen { false } useJUnitPlatform{ includeTags "acceptance" @@ -84,10 +102,10 @@ tasks.register('acceptance-tests', Test) { dependencies { // Utilities - implementation group: 'org.apache.commons', name: 'commons-configuration2', version: '2.11.0' + implementation group: 'org.apache.commons', name: 'commons-configuration2', version: '2.13.0' // HTTP client - def retrofitVersion = '2.9.0' + def retrofitVersion = '3.0.0' implementation group: 'com.squareup.retrofit2', name: 'retrofit', version: retrofitVersion implementation group: 'com.squareup.retrofit2', name: 'converter-jackson', version: retrofitVersion @@ -95,35 +113,29 @@ dependencies { implementation group: 'com.truelayer', name: 'truelayer-signing', version: '0.2.6' // Serialization - def jacksonVersion = '2.14.1' - implementation group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: jacksonVersion - implementation group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: jacksonVersion - implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jdk8', version: jacksonVersion - implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: jacksonVersion - implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: jacksonVersion + def jacksonVersion = '3.0.3' + implementation group: 'tools.jackson.core', name: 'jackson-core', version: jacksonVersion + implementation group: 'tools.jackson.core', name: 'jackson-databind', version: jacksonVersion + def jacksonDatatypeVersion = '2.20.1' + implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jdk8', version: jacksonDatatypeVersion + implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: jacksonDatatypeVersion + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.20' // Logging - def tinyLogVersion = '2.5.0' + def tinyLogVersion = '2.7.0' implementation group: 'org.tinylog', name: 'tinylog-api', version: tinyLogVersion implementation group: 'org.tinylog', name: 'tinylog-impl', version: tinyLogVersion // JUnit test framework. - testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter', version: '5.12.2' + testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter', version: '6.0.1' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' // Mocking libraries - testImplementation group: 'org.mockito', name: 'mockito-core', version: '5.17.0' - testImplementation group: 'org.wiremock', name: 'wiremock', version: '3.12.1' + testImplementation group: 'org.mockito', name: 'mockito-core', version: '5.21.0' + testImplementation group: 'org.wiremock', name: 'wiremock', version: '3.13.2' // Wait test utility testImplementation group: 'org.awaitility', name: 'awaitility', version: '4.3.0' - - // Transitive dependencies constraints - constraints { - implementation('com.squareup.okhttp3:okhttp:4.12.0') { - because 'version 3.14.9 used by com.squareup.retrofit2:retrofit:2.9.0 has known vulnerabilities' - } - } } jacocoTestReport { @@ -132,7 +144,7 @@ jacocoTestReport { xml.required = true html.required = true } - getExecutionData().setFrom(fileTree(buildDir).include("/jacoco/unit-tests.exec")) + getExecutionData().setFrom(fileTree(rootProject.layout.buildDirectory).include("/jacoco/unit-tests.exec")) afterEvaluate { classDirectories.setFrom(files(classDirectories.files.collect { fileTree(dir: it, exclude: [ diff --git a/gradle.properties b/gradle.properties index ab4e27c5..989d9ea8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ # Main properties group=com.truelayer archivesBaseName=truelayer-java -version=17.5.1 +version=18.0.0 # Artifacts properties project_name=TrueLayer Java diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37f853b1..23449a2b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/src/main/java/com/truelayer/java/entities/ResourceType.java b/src/main/java/com/truelayer/java/entities/ResourceType.java index 7117272b..c51bc379 100644 --- a/src/main/java/com/truelayer/java/entities/ResourceType.java +++ b/src/main/java/com/truelayer/java/entities/ResourceType.java @@ -9,10 +9,12 @@ @RequiredArgsConstructor @Getter public enum ResourceType { + // TODO: can this be deprecated? We should leverage the HPP URI generated by the payments-gateway instead. + // note that deprecating would probably mean creating a new type for our test utils PAYMENT("payments", "payment_id", HPP), + MANDATE("mandates", "mandate_id", HPP), - PAYOUT("payouts", "payout_id", HP2), - ; + PAYOUT("payouts", "payout_id", HP2); private final String hppLinkPath; private final String hppLinkQueryParameter; diff --git a/src/main/java/com/truelayer/java/payments/entities/CreatePaymentRequest.java b/src/main/java/com/truelayer/java/payments/entities/CreatePaymentRequest.java index 84c1b8b8..f6ebe8c6 100644 --- a/src/main/java/com/truelayer/java/payments/entities/CreatePaymentRequest.java +++ b/src/main/java/com/truelayer/java/payments/entities/CreatePaymentRequest.java @@ -40,4 +40,13 @@ public class CreatePaymentRequest { * Optional field for sub-merchant details */ private SubMerchants subMerchants; + + /** + * Optional field for configuring the authorization flow + */ + private StartAuthorizationFlowRequest authorizationFlow; + + // TODO: hosted page + + // TODO: user consent } diff --git a/src/main/java/com/truelayer/java/payments/entities/StartAuthorizationFlowRequest.java b/src/main/java/com/truelayer/java/payments/entities/StartAuthorizationFlowRequest.java index e7072733..468061c9 100644 --- a/src/main/java/com/truelayer/java/payments/entities/StartAuthorizationFlowRequest.java +++ b/src/main/java/com/truelayer/java/payments/entities/StartAuthorizationFlowRequest.java @@ -1,8 +1,10 @@ package com.truelayer.java.payments.entities; +import com.fasterxml.jackson.annotation.JsonValue; import com.truelayer.java.payments.entities.paymentdetail.forminput.Input; import java.net.URI; import java.util.List; +import java.util.Map; import lombok.*; @Getter @@ -13,57 +15,57 @@ public class StartAuthorizationFlowRequest { private final ProviderSelection providerSelection; + private final Map schemeSelection; + private final Redirect redirect; private final Consent consent; private final Form form; - @ToString - @EqualsAndHashCode - public static class ProviderSelection {} - - @Builder - @Getter - @ToString - @EqualsAndHashCode - public static class Redirect { - URI returnUri; - - URI directReturnUri; - } - - @Builder - @ToString - @EqualsAndHashCode - public static class Consent {} - - @Builder - @Getter - @ToString - @EqualsAndHashCode - public static class Form { - List inputTypes; - } + private final Map userAccountSelection; public static StartAuthorizationFlowRequestBuilder builder() { return new StartAuthorizationFlowRequestBuilder(); } + // TODO: remove in future major version, as we will no longer support empty provider selection public static class StartAuthorizationFlowRequestBuilder { private boolean withProviderSelection; + private ProviderSelection providerSelection; + + private Map schemeSelection; + private Redirect redirect; private Consent consent; private Form form; + private Map userAccountSelection; + + /** + * Include an empty provider selection object in the request + * @deprecated use providerSelection(ProviderSelection) instead + * @return the builder object + */ + @Deprecated public StartAuthorizationFlowRequestBuilder withProviderSelection() { this.withProviderSelection = true; return this; } + public StartAuthorizationFlowRequestBuilder providerSelection(ProviderSelection providerSelection) { + this.providerSelection = providerSelection; + return this; + } + + public StartAuthorizationFlowRequestBuilder schemeSelection(Map schemeSelection) { + this.schemeSelection = schemeSelection; + return this; + } + public StartAuthorizationFlowRequestBuilder redirect(Redirect redirect) { this.redirect = redirect; return this; @@ -79,9 +81,118 @@ public StartAuthorizationFlowRequestBuilder form(Form form) { return this; } + public StartAuthorizationFlowRequestBuilder userAccountSelection(Map userAccountSelection) { + this.userAccountSelection = userAccountSelection; + return this; + } + public StartAuthorizationFlowRequest build() { + if (withProviderSelection && providerSelection == null) { + providerSelection = new ProviderSelection(); + } + return new StartAuthorizationFlowRequest( - withProviderSelection ? new ProviderSelection() : null, redirect, consent, form); + providerSelection, schemeSelection, redirect, consent, form, userAccountSelection); + } + } + + @Builder + @ToString + @EqualsAndHashCode + @AllArgsConstructor + @Getter + public static class ProviderSelection { + private Icon icon; + + public ProviderSelection() {} + + @Getter + @NoArgsConstructor + @AllArgsConstructor + @EqualsAndHashCode + @ToString + public static class Icon { + private IconType type; + + @RequiredArgsConstructor + @Getter + public enum IconType { + DEFAULT("default"), + EXTENDED("extended"), + EXTENDED_SMALL("extended_small"), + EXTENDED_MEDIUM("extended_medium"), + EXTENDED_LARGE("extended_large"); + + @JsonValue + private final String type; + } + } + } + + @Builder + @Getter + @ToString + @EqualsAndHashCode + public static class Redirect { + private final URI returnUri; + + private final URI directReturnUri; + } + + @Builder + @ToString + @EqualsAndHashCode + @Getter + public static class Consent { + private final ActionType actionType; + private final Requirements requirements; + + @RequiredArgsConstructor + @Getter + public enum ActionType { + EXPLICIT("explicit"), + ADJACENT("adjacent"); + + @JsonValue + private final String type; + } + + @ToString + @EqualsAndHashCode + @Builder + @Getter + public static class Requirements { + private final Map pis; + + private final AisRequirements ais; + + @Builder + @ToString + @EqualsAndHashCode + @Getter + public static class AisRequirements { + + private final List scopes; + + @RequiredArgsConstructor + @Getter + public enum Scope { + ACCOUNTS("accounts"), + BALANCE("balance"), + ; + + @JsonValue + private final String value; + } + } } } + + @Builder + @Getter + @ToString + @EqualsAndHashCode + public static class Form { + List inputTypes; + } } diff --git a/src/test/java/com/truelayer/java/acceptance/MandatesAcceptanceTests.java b/src/test/java/com/truelayer/java/acceptance/MandatesAcceptanceTests.java index c16d04a9..40565294 100644 --- a/src/test/java/com/truelayer/java/acceptance/MandatesAcceptanceTests.java +++ b/src/test/java/com/truelayer/java/acceptance/MandatesAcceptanceTests.java @@ -146,7 +146,8 @@ public void itShouldGetFunds(String mandatesScope, Mandate.Type mandateType) { // start auth flow StartAuthorizationFlowRequest startAuthorizationFlowRequest = StartAuthorizationFlowRequest.builder() - .withProviderSelection() + .providerSelection(StartAuthorizationFlowRequest.ProviderSelection.builder() + .build()) .redirect(StartAuthorizationFlowRequest.Redirect.builder() .returnUri(URI.create(RETURN_URI)) .build()) @@ -209,7 +210,8 @@ public void itShouldGetConstraints(String mandatesScope, Mandate.Type mandateTyp // start auth flow StartAuthorizationFlowRequest startAuthorizationFlowRequest = StartAuthorizationFlowRequest.builder() - .withProviderSelection() + .providerSelection(StartAuthorizationFlowRequest.ProviderSelection.builder() + .build()) .redirect(StartAuthorizationFlowRequest.Redirect.builder() .returnUri(URI.create(RETURN_URI)) .build()) @@ -323,7 +325,8 @@ public void itShouldCreateAPaymentOnMandate(String mandatesScope, Mandate.Type m // start auth flow StartAuthorizationFlowRequest startAuthorizationFlowRequest = StartAuthorizationFlowRequest.builder() - .withProviderSelection() + .providerSelection(StartAuthorizationFlowRequest.ProviderSelection.builder() + .build()) .redirect(StartAuthorizationFlowRequest.Redirect.builder() .returnUri(URI.create(RETURN_URI)) .build()) @@ -449,7 +452,8 @@ private CreateMandateRequest createMandateRequest( private ApiResponse startAuthFlowForMandate(String mandateId) { // start auth flow StartAuthorizationFlowRequest startAuthorizationFlowRequest = StartAuthorizationFlowRequest.builder() - .withProviderSelection() + .providerSelection(StartAuthorizationFlowRequest.ProviderSelection.builder() + .build()) .redirect(StartAuthorizationFlowRequest.Redirect.builder() .returnUri(URI.create(RETURN_URI)) .build()) diff --git a/src/test/java/com/truelayer/java/acceptance/PaymentsAcceptanceTests.java b/src/test/java/com/truelayer/java/acceptance/PaymentsAcceptanceTests.java index 6ae14c40..bd878edb 100644 --- a/src/test/java/com/truelayer/java/acceptance/PaymentsAcceptanceTests.java +++ b/src/test/java/com/truelayer/java/acceptance/PaymentsAcceptanceTests.java @@ -50,6 +50,7 @@ import java.util.stream.Stream; import lombok.*; import okhttp3.*; +import okhttp3.MediaType; import org.apache.commons.lang3.RandomStringUtils; import org.junit.jupiter.api.*; import org.junit.jupiter.params.ParameterizedTest; @@ -288,7 +289,8 @@ public void shouldCompleteARedirectAuthorizationFlowForAPayment() { // start the auth flow StartAuthorizationFlowRequest startAuthorizationFlowRequest = StartAuthorizationFlowRequest.builder() .redirect(Redirect.builder().returnUri(URI.create(RETURN_URI)).build()) - .withProviderSelection() + .providerSelection(StartAuthorizationFlowRequest.ProviderSelection.builder() + .build()) .consent(StartAuthorizationFlowRequest.Consent.builder().build()) .build(); ApiResponse startAuthorizationFlowResponse = tlClient.payments() @@ -332,7 +334,8 @@ public void shouldCompleteAnEmbeddedAuthorizationFlowForAPayment() { // start the auth flow StartAuthorizationFlowRequest startAuthorizationFlowRequest = StartAuthorizationFlowRequest.builder() .redirect(Redirect.builder().returnUri(URI.create(RETURN_URI)).build()) - .withProviderSelection() + .providerSelection(StartAuthorizationFlowRequest.ProviderSelection.builder() + .build()) .consent(StartAuthorizationFlowRequest.Consent.builder().build()) .form(StartAuthorizationFlowRequest.Form.builder() .inputTypes(Arrays.asList(Input.Type.TEXT, Input.Type.TEXT_WITH_IMAGE, Input.Type.SELECT)) @@ -421,6 +424,46 @@ public void shouldCompleteAnAuthorizationFlowForAPaymentWithPreselectedProvider( assertNotError(createPaymentResponse); assertTrue(createPaymentResponse.getData().isAuthorizationRequired()); + // start the auth flow + StartAuthorizationFlowRequest startAuthorizationFlowRequest = StartAuthorizationFlowRequest.builder() + .redirect(Redirect.builder().returnUri(URI.create(RETURN_URI)).build()) + .providerSelection(StartAuthorizationFlowRequest.ProviderSelection.builder() + .build()) + .build(); + ApiResponse startAuthorizationFlowResponse = tlClient.payments() + .startAuthorizationFlow(createPaymentResponse.getData().getId(), startAuthorizationFlowRequest) + .get(); + + assertNotError(startAuthorizationFlowResponse); + + // assert that the link returned is good to be browsed + URI bankPage = startAuthorizationFlowResponse + .getData() + .asAuthorizing() + .getAuthorizationFlow() + .getActions() + .getNext() + .asRedirect() + .getUri(); + assertCanBrowseLink(bankPage); + } + + @SneakyThrows + @Test + @Deprecated + @DisplayName( + "It should complete an authorization flow for a payment with a preselected provider with deprecated provider selection method") + public void shouldCompleteAnAuthorizationFlowForAPaymentWithPreselectedProviderDeprecatedProviderSelection() { + // create payment + CreatePaymentRequest paymentRequest = + buildPaymentRequestWithProviderSelection(buildPreselectedProviderSelection(), CurrencyCode.GBP); + + ApiResponse createPaymentResponse = + tlClient.payments().createPayment(paymentRequest).get(); + + assertNotError(createPaymentResponse); + assertTrue(createPaymentResponse.getData().isAuthorizationRequired()); + // start the auth flow StartAuthorizationFlowRequest startAuthorizationFlowRequest = StartAuthorizationFlowRequest.builder() .redirect(Redirect.builder().returnUri(URI.create(RETURN_URI)).build()) @@ -464,7 +507,8 @@ public void shouldCompleteAnAuthorizationFlowForAPaymentWithProviderReturn() { .returnUri(URI.create(RETURN_URI)) .directReturnUri(URI.create(RETURN_URI)) .build()) - .withProviderSelection() + .providerSelection(StartAuthorizationFlowRequest.ProviderSelection.builder() + .build()) .build(); ApiResponse startAuthorizationFlowResponse = tlClient.payments() .startAuthorizationFlow(createPaymentResponse.getData().getId(), startAuthorizationFlowRequest) @@ -556,7 +600,8 @@ public void shouldCreateAPaymentRefundAndGetRefundDetails() { .returnUri(URI.create(RETURN_URI)) .directReturnUri(URI.create(RETURN_URI)) .build()) - .withProviderSelection() + .providerSelection(StartAuthorizationFlowRequest.ProviderSelection.builder() + .build()) .build(); ApiResponse startAuthorizationFlowResponse = tlClient.payments() .startAuthorizationFlow(paymentId, startAuthorizationFlowRequest) @@ -818,7 +863,8 @@ private static AuthorizationFlowResponse startAuthorizationFlowWithRetry(String .returnUri(URI.create(RETURN_URI)) .directReturnUri(URI.create(RETURN_URI)) .build()) - .withProviderSelection() + .providerSelection(StartAuthorizationFlowRequest.ProviderSelection.builder() + .build()) .build(); // serialize the base object to json and append retry object diff --git a/src/test/java/com/truelayer/java/acceptance/SignupPlusAcceptanceTests.java b/src/test/java/com/truelayer/java/acceptance/SignupPlusAcceptanceTests.java index 74b26e87..b69f8580 100644 --- a/src/test/java/com/truelayer/java/acceptance/SignupPlusAcceptanceTests.java +++ b/src/test/java/com/truelayer/java/acceptance/SignupPlusAcceptanceTests.java @@ -125,7 +125,8 @@ private String createAndAuthorizePayment(String providerId, CurrencyCode currenc .returnUri(returnUri) .directReturnUri(returnUri) .build()) - .withProviderSelection() + .providerSelection(StartAuthorizationFlowRequest.ProviderSelection.builder() + .build()) .build(); ApiResponse startAuthorizationFlowResponse = tlClient.payments() .startAuthorizationFlow(paymentId, startAuthorizationFlowRequest) diff --git a/src/test/java/com/truelayer/java/integration/PaymentsIntegrationTests.java b/src/test/java/com/truelayer/java/integration/PaymentsIntegrationTests.java index 405b900c..6053e3a7 100644 --- a/src/test/java/com/truelayer/java/integration/PaymentsIntegrationTests.java +++ b/src/test/java/com/truelayer/java/integration/PaymentsIntegrationTests.java @@ -19,6 +19,8 @@ import com.truelayer.java.http.entities.Headers; import com.truelayer.java.http.entities.ProblemDetails; import com.truelayer.java.payments.entities.*; +import com.truelayer.java.payments.entities.StartAuthorizationFlowRequest.ProviderSelection.Icon; +import com.truelayer.java.payments.entities.StartAuthorizationFlowRequest.ProviderSelection.Icon.IconType; import com.truelayer.java.payments.entities.beneficiary.MerchantAccount; import com.truelayer.java.payments.entities.paymentdetail.PaymentDetail; import com.truelayer.java.payments.entities.paymentdetail.Status; @@ -28,8 +30,8 @@ import com.truelayer.java.payments.entities.providerselection.UserSelectedProviderSelection; import com.truelayer.java.payments.entities.schemeselection.userselected.SchemeSelection; import com.truelayer.java.payments.entities.verification.AutomatedVerification; -import java.util.Collections; -import java.util.UUID; +import java.net.URI; +import java.util.*; import java.util.stream.Stream; import lombok.SneakyThrows; import org.junit.jupiter.api.*; @@ -319,7 +321,14 @@ public void shouldThrowARequestInvalidError() { .status(400) .bodyFile(jsonResponseFile) .build(); - CreatePaymentRequest paymentRequest = CreatePaymentRequest.builder().build(); + CreatePaymentRequest paymentRequest = CreatePaymentRequest.builder() + .authorizationFlow(StartAuthorizationFlowRequest.builder() + .schemeSelection(Collections.singletonMap("foo", "bar")) + .providerSelection(StartAuthorizationFlowRequest.ProviderSelection.builder() + .icon(new Icon(IconType.DEFAULT)) + .build()) + .build()) + .build(); ApiResponse paymentResponse = tlClient.payments().createPayment(paymentRequest).get(); @@ -586,4 +595,106 @@ private static Stream provideAutomatedVerifications() { .withRemitterDateOfBirth() .build())); } + + @Deprecated + @Test + @DisplayName("It should create a payment with authorization_flow with deprecated provider selection") + @SneakyThrows + public void shouldCreatePaymentWithAuthorizationFlowDeprecated() { + RequestStub.New() + .method("post") + .path(urlPathEqualTo("/connect/token")) + .status(200) + .bodyFile("auth/200.access_token.json") + .build(); + RequestStub.New() + .method("post") + .path(urlPathEqualTo("/payments")) + .withAuthorization() + .withSignature() + .withIdempotencyKey() + .status(201) + .bodyFile("payments/201.create_payment.AUTHORIZATION_REQUIRED.json") + .build(); + + CreatePaymentRequest paymentRequest = CreatePaymentRequest.builder() + .amountInMinor(100) + .currency(CurrencyCode.GBP) + .paymentMethod(PaymentMethod.bankTransfer().build()) + .authorizationFlow(StartAuthorizationFlowRequest.builder() + .withProviderSelection() + .build()) + .build(); + + tlClient.payments().createPayment(paymentRequest).get(); + + verifyGeneratedToken(Collections.singletonList(PAYMENTS)); + verify(postRequestedFor(urlPathEqualTo("/payments")) + .withRequestBody(matchingJsonPath("$.amount_in_minor", equalTo("100"))) + .withRequestBody(matchingJsonPath("$.currency", equalTo("GBP"))) + .withRequestBody(matchingJsonPath("$.payment_method.type", equalTo("bank_transfer"))) + .withRequestBody(matchingJsonPath("$.authorization_flow.provider_selection", equalToJson("{}")))); + } + + @Deprecated + @Test + @DisplayName("It should create a payment with authorization_flow") + @SneakyThrows + public void shouldCreatePaymentWithAuthorizationFlow() { + RequestStub.New() + .method("post") + .path(urlPathEqualTo("/connect/token")) + .status(200) + .bodyFile("auth/200.access_token.json") + .build(); + RequestStub.New() + .method("post") + .path(urlPathEqualTo("/payments")) + .withAuthorization() + .withSignature() + .withIdempotencyKey() + .status(201) + .bodyFile("payments/201.create_payment.AUTHORIZATION_REQUIRED.json") + .build(); + + CreatePaymentRequest paymentRequest = CreatePaymentRequest.builder() + .amountInMinor(100) + .currency(CurrencyCode.GBP) + .paymentMethod(PaymentMethod.bankTransfer().build()) + .authorizationFlow(StartAuthorizationFlowRequest.builder() + .redirect(StartAuthorizationFlowRequest.Redirect.builder() + .returnUri(URI.create("https://example.com/return")) + .directReturnUri(URI.create("http://example.com/direct-return")) + .build()) + .schemeSelection(Collections.singletonMap("foo", "bar")) + .providerSelection(StartAuthorizationFlowRequest.ProviderSelection.builder() + .icon(new Icon(IconType.EXTENDED)) + .build()) + .form(StartAuthorizationFlowRequest.Form.builder().build()) + .consent(StartAuthorizationFlowRequest.Consent.builder() + .actionType(StartAuthorizationFlowRequest.Consent.ActionType.ADJACENT) + .requirements(StartAuthorizationFlowRequest.Consent.Requirements.builder() + .pis(Collections.singletonMap("req1", "foo")) + .ais( + StartAuthorizationFlowRequest.Consent.Requirements.AisRequirements + .builder() + // scopes + .build()) + .build()) + .build()) + .build()) + .build(); + + tlClient.payments().createPayment(paymentRequest).get(); + + verifyGeneratedToken(Collections.singletonList(PAYMENTS)); + verify(postRequestedFor(urlPathEqualTo("/payments")) + .withRequestBody(matchingJsonPath("$.amount_in_minor", equalTo("100"))) + .withRequestBody(matchingJsonPath("$.currency", equalTo("GBP"))) + .withRequestBody(matchingJsonPath("$.payment_method.type", equalTo("bank_transfer"))) + .withRequestBody(matchingJsonPath( + "$.authorization_flow.redirect.return_uri", equalTo("https://example.com/return"))) + .withRequestBody( + matchingJsonPath("$.authorization_flow.provider_selection.icon.type", equalTo("extended")))); + } } diff --git a/src/test/java/com/truelayer/java/payments/PaymentsHandlerTests.java b/src/test/java/com/truelayer/java/payments/PaymentsHandlerTests.java index f1fad7b6..f03b9d1b 100644 --- a/src/test/java/com/truelayer/java/payments/PaymentsHandlerTests.java +++ b/src/test/java/com/truelayer/java/payments/PaymentsHandlerTests.java @@ -83,8 +83,10 @@ public void shouldCallGetPaymentById() { @Test @DisplayName("It should call the start authorization flow endpoint") public void shouldCallStartAuthorizationFlow() { - StartAuthorizationFlowRequest request = - StartAuthorizationFlowRequest.builder().withProviderSelection().build(); + StartAuthorizationFlowRequest request = StartAuthorizationFlowRequest.builder() + .providerSelection(StartAuthorizationFlowRequest.ProviderSelection.builder() + .build()) + .build(); sut.startAuthorizationFlow(A_PAYMENT_ID, request); @@ -95,8 +97,10 @@ public void shouldCallStartAuthorizationFlow() { @DisplayName("It should call the start authorization flow endpoint with additional headers") public void shouldCallStartAuthorizationFlowWithCustomHeaders() { Headers customHeaders = buildTestHeaders(); - StartAuthorizationFlowRequest request = - StartAuthorizationFlowRequest.builder().withProviderSelection().build(); + StartAuthorizationFlowRequest request = StartAuthorizationFlowRequest.builder() + .providerSelection(StartAuthorizationFlowRequest.ProviderSelection.builder() + .build()) + .build(); sut.startAuthorizationFlow(customHeaders, A_PAYMENT_ID, request);