diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d5ab8f8c..64c0693c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -26,5 +26,9 @@ updates: interval: "daily" - package-ecosystem: "gradle" directory: "/in-person-payments-example" + schedule: + interval: "daily" + - package-ecosystem: "gradle" + directory: "/authorisation-adjustment-example" schedule: interval: "daily" \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0ebe121f..be9d6331 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -107,6 +107,7 @@ jobs: run: chmod +x paybylink-example/gradlew - name: Build with Gradle run: cd paybylink-example; ./gradlew build + build-giving: runs-on: ubuntu-latest strategy: @@ -125,3 +126,20 @@ jobs: - name: Build with Gradle run: cd giving-example; ./gradlew build + build-authorisation-adjustment: + runs-on: ubuntu-latest + strategy: + matrix: + java: [ '17' ] + name: Java ${{ matrix.Java }} sample + steps: + - uses: actions/checkout@v3 + - name: Setup java + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: ${{ matrix.java }} + - name: Grant execute permission for gradlew + run: chmod +x authorisation-adjustment-example/gradlew + - name: Build with Gradle + run: cd authorisation-adjustment-example; ./gradlew build diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 6008b0ce..7863e730 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -131,3 +131,26 @@ jobs: run: docker run --rm --name adyen-testing-suite -e PLAYWRIGHT_FOLDERNAME=giving -e ADYEN_HMAC_KEY=${{ secrets.ADYEN_HMAC_KEY }} --network host ghcr.io/adyen-examples/adyen-testing-suite:main + authorisation-adjustment: + + runs-on: ubuntu-latest + steps: + - name: Authorisation Adjustment project + uses: actions/checkout@v3 + - name: Setup java + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: 17 + - name: Grant execute permission for gradlew + run: chmod +x authorisation-adjustment-example/gradlew + - name: Build authorisation-adjustment-example with Gradle + run: cd authorisation-adjustment-example; ./gradlew build + - name: Build authorisation-adjustment-example image + run: docker build -t authorisation-adjustment-example:latest authorisation-adjustment-example + - name: Start authorisation-adjustment container + run: docker run --rm -d --name authorisation-adjustment-example -p 8080:8080 -e ADYEN_API_KEY="${{ secrets.ADYEN_API_KEY }}" -e ADYEN_MERCHANT_ACCOUNT=${{ secrets.ADYEN_MERCHANT_ACCOUNT }} -e ADYEN_CLIENT_KEY=${{ secrets.ADYEN_CLIENT_KEY }} -e ADYEN_HMAC_KEY=${{ secrets.ADYEN_HMAC_KEY }} authorisation-adjustment-example:latest + - name: Run testing suite + run: docker run --rm --name adyen-testing-suite -e PLAYWRIGHT_FOLDERNAME=authorisation-adjustment -e ADYEN_HMAC_KEY=${{ secrets.ADYEN_HMAC_KEY }} --network host ghcr.io/adyen-examples/adyen-testing-suite:main + + diff --git a/.gitpod.yml b/.gitpod.yml index 6522fa62..ce7bae17 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -43,6 +43,10 @@ tasks: echo "Build paybylink-example application" cd paybylink-example && ./gradlew bootJar ;; + "authorisation-adjustment-example") + echo "Build authorisation-adjustment-example application" + cd authorisation-adjustment-example && ./gradlew bootJar + ;; *) echo "Build default checkout-example application instead because '$path' is not defined ..." cd checkout-example && ./gradlew bootJar diff --git a/README.md b/README.md index 04e09efa..b072e594 100644 --- a/README.md +++ b/README.md @@ -8,15 +8,16 @@ The demos below leverages Adyen's API Library for Java using Spring ([GitHub](ht Get started by navigating to one of the supported demos below. -| Demos | Description | Details | -|---------------------------------------------------------:|:---------------------------------------------------------------------------------|:----------------------------------------| -| [`Checkout Example`](checkout-example) | E-commerce checkout flow with different payment methods. | [See below](#checkout-example) | -| [`Advanced Checkout Example`](checkout-example-advanced) | E-commerce checkout flow with different payment methods, using the 3 steps flow. | [See below](#advanced-checkout-example) | -| [`In-person Payments Example`](in-person-payments-example) | In-person payments using a POS terminal and the terminal-api/sync endpoint. | [See below](#in-person-payments-example) | -| [`Gift Card Example`](giftcard-example) | Gift Cards checkout flow using partial orders. | [See below](#gift-card-example) | -| [`Pay By Link Example`](paybylink-example) | Create payment links in seconds. | [See below](#paybylink-example) | -| [`Subscription Example`](subscription-example) | Subscription flow using Adyen tokenization. | [See below](#subscription-example) | -| [`Giving Example`](giving-example) | Donation flow using Adyen Giving. | [See below](#giving-example) | +| Demos | Description | Details | +|------------------------------------------------------------------------:|:-----------------------------------------------------------------------------------------|:-----------------------------------------------| +| [`Checkout Example`](checkout-example) | E-commerce checkout flow with different payment methods. | [See below](#checkout-example) | +| [`Advanced Checkout Example`](checkout-example-advanced) | E-commerce checkout flow with different payment methods, using the 3 steps flow. | [See below](#advanced-checkout-example) | +| [`Authorisation Adjustment Example`](authorisation-adjustment-example) | Pre-authorise a payment, adjust the authorised amount, capture or reverse the payment. | [See below](#authorisation-adjustment-example) | +| [`In-person Payments Example`](in-person-payments-example) | In-person payments using a POS terminal and the terminal-api/sync endpoint. | [See below](#in-person-payments-example) | +| [`Gift Card Example`](giftcard-example) | Gift Cards checkout flow using partial orders. | [See below](#gift-card-example) | +| [`Pay By Link Example`](paybylink-example) | Create payment links in seconds. | [See below](#paybylink-example) | +| [`Subscription Example`](subscription-example) | Subscription flow using Adyen tokenization. | [See below](#subscription-example) | +| [`Giving Example`](giving-example) | Donation flow using Adyen Giving. | [See below](#giving-example) | ## [Checkout Example](checkout-example) @@ -41,6 +42,16 @@ See the [advanced integration flow](https://docs.adyen.com/online-payments/web-d ![Card Checkout Demo](checkout-example/src/main/resources/static/images/cardcheckout.gif) +## [Authorisation Adjustment Example](authorisation-adjustment-example) +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/adyen-examples/adyen-java-spring-online-payments/tree/main/authorisation-adjustment-example) + +[First time with Gitpod?](https://github.com/adyen-examples/.github/blob/main/pages/gitpod-get-started.md) + +The [`authorisation adjustment example`](authorisation-adjustment-example) repository includes adjust authorisation example for the following three use cases after a pre-authorised payment: incremental, decremental adjustments. Within this demo app, you'll find a simplified version of a hotel booking, where the shopper perform a booking and administrators can **[1] adjust** (increase/decrease) the payment amount, **[2] extend** the authorisation expiry date, **[3] capture** the final amount and/or **[4] reverse** (cancel or refund) an authorised payment + +![Authorisation Adjustment Card Demo](authorisation-adjustment-example/src/main/resources/static/images/cardauthorisationadjustment.gif) + + ## [In-person Payments Example](in-person-payments-example) [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/adyen-examples/adyen-dotnet-online-payments/tree/main/in-person-payments-example) diff --git a/authorisation-adjustment-example/Dockerfile b/authorisation-adjustment-example/Dockerfile new file mode 100644 index 00000000..3bf5f000 --- /dev/null +++ b/authorisation-adjustment-example/Dockerfile @@ -0,0 +1,3 @@ +FROM amazoncorretto:17-alpine-jdk +COPY build/libs/adyen-java-spring-online-payments-authorisation-adjustment-0.0.1-SNAPSHOT.jar adyen-java-spring-online-payments-authorisation-adjustment-0.0.1-SNAPSHOT.jar +ENTRYPOINT ["java","-jar","/adyen-java-spring-online-payments-authorisation-adjustment-0.0.1-SNAPSHOT.jar"] diff --git a/authorisation-adjustment-example/README.md b/authorisation-adjustment-example/README.md new file mode 100644 index 00000000..fffa9205 --- /dev/null +++ b/authorisation-adjustment-example/README.md @@ -0,0 +1,296 @@ +# Adyen [Authorisation Adjustment](https://docs.adyen.com/online-payments/classic-integrations/modify-payments/adjust-authorisation) Integration Demo + +## Run demo in one-click +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/adyen-examples/adyen-java-spring-online-payments/tree/main/authorisation-adjustment-example) + +[First time with Gitpod?](https://github.com/adyen-examples/.github/blob/main/pages/gitpod-get-started.md) + + +## Description + +This repository includes an adjust authorisation example for the following three use cases after a pre-authorised payment: incremental, decremental adjustments. Within this demo app, you'll find a simplified version of a hotel booking, where the shopper perform a booking and administrators can **[1] adjust** (increase/decrease) the payment amount, **[2] extend** the authorisation expiry date, **[3] capture** the final amount and **[4] reverse** (cancel or refund) an authorised payment. + +> **Note:** We've included a technical [blog post](https://www.adyen.com/knowledge-hub/pre-authorizations-and-authorization-adjustments-for-developers) that explains every step of this demo. + +![Authorisation Adjustment Card Demo](src/main/resources/static/images/cardauthorisationadjustment.gif) + + +This demo leverages Adyen's API Library for Java ([GitHub](https://github.com/Adyen/adyen-java-api-library) | [Docs](https://docs.adyen.com/development-resources/libraries#java)). + +## Requirements +- [Adyen API Credentials](https://docs.adyen.com/development-resources/api-credentials/) +- Java 17 + +## 1. Installation + +Clone this repository: + +``` +git clone https://github.com/adyen-examples/adyen-java-spring-online-payments.git +``` + + +## 2. Set the environment variables +Create a `./.env` file with all required configuration + - [Adyen API key](https://docs.adyen.com/user-management/how-to-get-the-api-key) + - [Adyen Client Key](https://docs.adyen.com/user-management/client-side-authentication) + - [Adyen Merchant Account](https://docs.adyen.com/account/account-structure) + - [Adyen HMAC Key](https://docs.adyen.com/development-resources/webhooks/verify-hmac-signatures) + +In your Customer Area, remember to include `http://localhost:8080` in the list of `Allowed Origins` to allow the Adyen.Component to load. + +``` +PORT=8080 +ADYEN_API_KEY="your_API_key_here" +ADYEN_MERCHANT_ACCOUNT="your_merchant_account_here" +ADYEN_CLIENT_KEY="your_client_key_here" +ADYEN_HMAC_KEY="your_hmac_key_here" +``` + +3. Run the application + +``` +./gradlew bootRun +``` + +4. Usage + +To try out this application with test card numbers, visit [Test card numbers](https://docs.adyen.com/development-resources/test-cards/test-card-numbers). We recommend saving multiple test cards in your browser so you can test your integration faster in the future. + +1. Make a booking in the `Booking View` +2. Visit the `Admin Panel` to see the incoming webhooks and perform operations on the initial preauthorisation. + +A success scenario for a payment followed by two adjustments, a capture and a reversal looks like: + +`AUTHORISATION` (preauthorisation) → `AUTHORISATION_ADJUSTMENT` (adjust) → `AUTHORISATION_ADJUSTMENT` (adjust) → `CAPTURE` (capture) → `CANCEL_OR_REFUND` (reversal) + +Adyen expires an authorisation request automatically after XX days depending on the card brand. +The `EXTEND` operation in this sample is used to extend the expiry date manually, for the exact days, refer to the [documentation](https://docs.adyen.com/online-payments/adjust-authorisation/#validity) (section: validity). + +When CAPTURE is executed, it will perform the operation on the latest amount. You'll have to wait for the `AUTHORISATION_ADJUSTMENT` response, before making the capture once it's final. + +# Webhooks + +Webhooks deliver asynchronous notifications about the payment status and other events that are important to receive and process. +You can find more information about webhooks in [this blog post](https://www.adyen.com/knowledge-hub/consuming-webhooks). + +### Webhook setup + +In the Customer Area under the `Developers → Webhooks` section, [create](https://docs.adyen.com/development-resources/webhooks/#set-up-webhooks-in-your-customer-area) a new `Standard webhook`. + +A good practice is to set up basic authentication, copy the generated HMAC Key and set it as an environment variable. The application will use this to verify the [HMAC signatures](https://docs.adyen.com/development-resources/webhooks/verify-hmac-signatures/). + +Make sure the webhook is **enabled**, so it can receive notifications. + +### Expose an endpoint + +This demo provides a simple webhook implementation exposed at `/api/webhooks/notifications` that shows you how to receive, validate and consume the webhook payload. + +### Test your webhook + +The following webhooks `events` should be enabled: +* **AUTHORISATION** +* **AUTHORISATION_ADJUSTMENT** +* **CAPTURE** +* **CANCEL_OR_REFUND** +* **REFUND_FAILED** +* **REFUNDED_REVERSED** + +To make sure that the Adyen platform can reach your application, we have written a [Webhooks Testing Guide](https://github.com/adyen-examples/.github/blob/main/pages/webhooks-testing.md) +that explores several options on how you can easily achieve this (e.g. running on localhost or cloud). + + +## Contributing + +We commit all our new features directly into our GitHub repository. Feel free to request or suggest new features or code changes yourself as well! + +Find out more in our [Contributing](https://github.com/adyen-examples/.github/blob/main/CONTRIBUTING.md) guidelines. + +## License + +MIT license. For more information, see the **LICENSE** file in the root directory. + + + + + + + + + + + + + + + + + +_____ +# Adyen [Tokenization](https://docs.adyen.com/online-payments-tokenization) Integration Demo + +This repository includes a tokenization example for subscriptions. Within this demo app, you'll find a simplified version of a website that offers a music subscription service. +The shopper can purchase a subscription and administrators can manage the saved (tokenized) payment methods on a separate admin panel. +The panel allows admins to make payments on behalf of the shopper using this token. We refer to this token as `recurringDetailReference` in this application. + +## Workflow + +The sample app implements the following workflow: + +* send a zero-auth transaction to request the Recurring Payment +* receive the webhook with the token (`recurringDetailReference`) +* perform a payment using the token +* receive the webhook with the payment authorisation + +> **Note:** Checkout the technical [blog post](https://www.adyen.com/blog/use-adyen-tokenization-to-implement-recurring-in-dotnet) that explains every step of this demo. + +![Subscription Demo](public/images/cardsubscription.gif) + + +## Run integration on [Gitpod](https://gitpod.io/) +1. Open your [Adyen Test Account](https://ca-test.adyen.com/ca/ca/overview/default.shtml) and create a set of [API keys](https://docs.adyen.com/user-management/how-to-get-the-api-key). + - [`ADYEN_API_KEY`](https://docs.adyen.com/user-management/how-to-get-the-api-key) + - [`ADYEN_CLIENT_KEY`](https://docs.adyen.com/user-management/client-side-authentication) + - [`ADYEN_MERCHANT_ACCOUNT`](https://docs.adyen.com/account/account-structure) + + +2. Go to [Gitpod Environmental Variables](https://gitpod.io/variables) and set the following variables: [`ADYEN_API_KEY`](https://docs.adyen.com/user-management/how-to-get-the-api-key), [`ADYEN_CLIENT_KEY`](https://docs.adyen.com/user-management/client-side-authentication) and [`ADYEN_MERCHANT_ACCOUNT`](https://docs.adyen.com/account/account-structure) with a scope of `*/*` + + +3. To allow the Adyen Drop-In and Components to load, add `https://*.gitpod.io` as allowed origin by going to your `ADYEN_MERCHANT_ACCOUNT` in the Customer Area: `Developers` → `API credentials` → Find your `ws_user` → `Client settings` → `Add Allowed origins`. +> **Warning** You should only allow wild card (*) domains in the **test** environment. In a **live** environment, you should specify the exact URL of the application. + +This demo provides a simple webhook integration at `/api/webhooks/notifications`. For it to work, you need to provide a way for Adyen's servers to reach your running application on Gitpod and add a standard webhook in the Customer Area. + + +4. To receive notifications asynchronously, add a webhook: + - In the Customer Area go to `Developers` → `Webhooks` and add a new `Standard notification webhook` + - Define username and password (Basic Authentication) to [protect your endpoint](https://docs.adyen.com/development-resources/webhooks/best-practices#security) - Basic authentication only guarantees that the notification was sent by Adyen, not that it wasn't modified during transmission + - Generate the [HMAC Key](https://docs.adyen.com/development-resources/webhooks/verify-hmac-signatures) and set the `ADYEN_HMAC_KEY` in your [Gitpod Environment Variables](https://gitpod.io/variables) with a scope of `*/*` - This key is used to [verify](https://docs.adyen.com/development-resources/webhooks/best-practices#security) whether the HMAC signature that is included in the notification, was sent by Adyen and not modified during transmission + - For the URL, enter `https://gitpod.io` for now, we will need to update this webhook URL in step 7 + - Make sure that the `Recurring contract` setting is **enabled** on `Merchant` account-level - In the `Customer Area`, under `Developers` -> `Webhooks` -> `Settings` -> Enable `Recurring contract` on `Merchant`-level and hit "Save". + - Make sure that your webhook sends the `RECURRING_CONTRACT` event when you've created the webhook + - Make sure the webhook is **enabled** to send notifications + + +5. In the Customer Area, go to `Developers` → `Additional Settings` → Under `Payment` enable `Recurring Details` for subscriptions. + + +6. Click the button below to launch the application in Gitpod. + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/adyen-examples/adyen-java-spring-online-payments/tree/main/authorisation-adjustment-example) + +7. Update your webhook in the Customer Area with the public url that is generated by Gitpod + - In the Customer Area, go to `Developers` → `Webhooks` → Select your `Webhook` that is created in step 4 → `Server Configuration` + - Update the URL of your application/endpoint (e.g. `https://8080-myorg-myrepo-y8ad7pso0w5.ws-eu75.gitpod.io/api/webhooks/notifications/` + - Hit `Apply` → `Save changes` and Gitpod should be able to receive notifications + +> **Note** When exiting Gitpod a new URL is generated, make sure to **update the Webhook URL** in the Customer Area as described in the final step. +> You can find more information about webhooks in [this detailed blog post](https://www.adyen.com/blog/Integrating-webhooks-notifications-with-Adyen-Checkout). + + +## Run integration on localhost using a proxy +You will need Java 17 to run this application locally. + +1. Clone this repository. + +``` +git clone https://github.com/adyen-examples/adyen-java-spring-online-payments.git +``` + +2. Open your [Adyen Test Account](https://ca-test.adyen.com/ca/ca/overview/default.shtml) and create a set of [API keys](https://docs.adyen.com/user-management/how-to-get-the-api-key). + - [`ADYEN_API_KEY`](https://docs.adyen.com/user-management/how-to-get-the-api-key) + - [`ADYEN_CLIENT_KEY`](https://docs.adyen.com/user-management/client-side-authentication) + - [`ADYEN_MERCHANT_ACCOUNT`](https://docs.adyen.com/account/account-structure) + + +3. To allow the Adyen Drop-In and Components to load, add `https://localhost:8080` as allowed origin by going to your MerchantAccount in the Customer Area: `Developers` → `API credentials` → Find your `ws_user` → `Client settings` → `Add Allowed origins`. +> **Warning** You should only allow wild card (*) domains in the **test** environment. In a **live** environment, you should specify the exact URL of the application. + +This demo provides a simple webhook integration at `/api/webhooks/notifications`. For it to work, you need to provide a way for Adyen's servers to reach your running application and add a standard webhook in the Customer Area. +To expose this endpoint locally you can use a tunneling software (see point 4) + +4. Expose your localhost with tunneling software (i.e. ngrok). + - Add `https://*.ngrok.io` to your allowed origins + +If you use a tunneling service like ngrok, the webhook URL will be the generated URL (i.e. `https://c991-80-113-16-28.ngrok.io/api/webhooks/notifications/`). + +```bash + $ ngrok http 8080 + + Session Status online + Account ############ + Version ######### + Region United States (us) + Forwarding http://c991-80-113-16-28.ngrok.io -> http://localhost:8080 + Forwarding https://c991-80-113-16-28.ngrok.io -> http://localhost:8080 +``` + +6. To receive notifications asynchronously, add a webhook: + - In the Customer Area go to `Developers` → `Webhooks` and add a new `Standard notification webhook` + - Define username and password (Basic Authentication) to [protect your endpoint](https://docs.adyen.com/development-resources/webhooks/best-practices#security) - Basic authentication only guarantees that the notification was sent by Adyen, not that it wasn't modified during transmission + - Generate the [HMAC Key](https://docs.adyen.com/development-resources/webhooks/verify-hmac-signatures) - This key is used to [verify](https://docs.adyen.com/development-resources/webhooks/best-practices#security) whether the HMAC signature that is included in the notification, was sent by Adyen and not modified during transmission + - See script below that allows you to easily set your environmental variables + - For the URL, enter `https://ngrok.io` for now - We will need to update this webhook URL in step 10 + - Make sure that the `Recurring contract` setting is **enabled** on `Merchant` account-level - In the `Customer Area`, under `Developers` -> `Webhooks` -> `Settings` -> Enable `Recurring contract` on `Merchant`-level and hit "Save". + - Make sure that your webhook sends the `RECURRING_CONTRACT` event when you've created the webhook + - Make sure the webhook is **enabled** to send notifications + + +7. Set the following environment variables in your terminal environment: `ADYEN_API_KEY`, `ADYEN_CLIENT_KEY`, `ADYEN_MERCHANT_ACCOUNT` and `ADYEN_HMAC_KEY`. Note that some IDEs will have to be restarted for environmental variables to be injected properly. + +```shell +export ADYEN_API_KEY=yourAdyenApiKey +export ADYEN_MERCHANT_ACCOUNT=yourAdyenMerchantAccount +export ADYEN_CLIENT_KEY=yourAdyenClientKey +export ADYEN_HMAC_KEY=yourAdyenHmacKey +``` + +On Windows CMD you can use this command instead. + +```shell +set ADYEN_API_KEY=yourAdyenApiKey +set ADYEN_MERCHANT_ACCOUNT=yourAdyenMerchantAccount +set ADYEN_CLIENT_KEY=yourAdyenClientKey +set ADYEN_HMAC_KEY=yourAdyenHmacKey +``` + +Alternatively it is possible to define the settings in the `application.properties` +```# application.properties +ADYEN_API_KEY=yourAdyenApiKey +ADYEN_MERCHANT_ACCOUNT=yourAdyenMerchantAccount +ADYEN_CLIENT_KEY=yourAdyenClientKey +ADYEN_HMAC_KEY=yourHmacKey +``` +8. In the Customer Area, go to `Developers` → `Additional Settings` → Under `Payment` enable `Recurring Details` for subscriptions. + + +9. Start the application and visit localhost. + +``` +./gradlew bootRun +``` + +10. Update your webhook in your Customer Area with the public url that is generated. + - In the Customer Area go to `Developers` → `Webhooks` → Select your `Webhook` that is created in step 6 → `Server Configuration` + - Update the URL of your application/endpoint (e.g. `https://c991-80-113-16-28.ngrok.io/api/webhooks/notifications/`) + - Hit `Apply` → `Save changes` and Gitpod should be able to receive notifications + +> **Note** When exiting ngrok or Visual Studio a new URL is generated, make sure to **update the Webhook URL** in the Customer Area as described in the final step. +> You can find more information about webhooks in [this detailed blog post](https://www.adyen.com/blog/Integrating-webhooks-notifications-with-Adyen-Checkout). + + + +## Usage +To try out this application with test card numbers, visit [Test card numbers](https://docs.adyen.com/development-resources/test-cards/test-card-numbers). We recommend saving multiple test cards in your browser so you can test your integration faster in the future. + +1. Visit the main page 'Shopper View' to test the application, enter one or multiple card details. Once the payment is authorized, you will receive a webhook notification with the recurringDetailReference. Enter multiple cards to receive multiple different recurringDetailReferences. + +2. Visit 'Admin Panel' to find the saved recurringDetailReferences and choose to make a payment request or disable the recurringDetailReference. + +3. Visit the Customer Area `Developers` → `API logs` to view your logs. + +> **Note** We currently store these values in a local memory cache, if you restart/stop the application these values are lost. However, the tokens will still be persisted on the Adyen Platform. +> You can view the stored payment details by going to a recent payment of the shopper in the Customer Area: `Transactions` → `Payments` → `Shopper Details` → `Recurring: View stored payment details`. + + diff --git a/authorisation-adjustment-example/build.gradle b/authorisation-adjustment-example/build.gradle new file mode 100644 index 00000000..212a21bb --- /dev/null +++ b/authorisation-adjustment-example/build.gradle @@ -0,0 +1,29 @@ +plugins { + alias(libs.plugins.spring.framework) + alias(libs.plugins.spring.dependency) + + id 'java' +} + +group = 'com.adyen' +version = '0.0.1-SNAPSHOT' + +sourceCompatibility = 17 +targetCompatibility = 17 + +repositories { + mavenCentral() +} + +dependencies { + implementation libs.adyen.java + implementation libs.bundles.spring + + testImplementation(libs.bundles.spring.test){ + exclude (group: 'org.junit.vintage', module: 'junit-vintage-engine') + } +} + +test { + useJUnitPlatform() +} diff --git a/authorisation-adjustment-example/gradle/wrapper/gradle-wrapper.jar b/authorisation-adjustment-example/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..7454180f Binary files /dev/null and b/authorisation-adjustment-example/gradle/wrapper/gradle-wrapper.jar differ diff --git a/authorisation-adjustment-example/gradle/wrapper/gradle-wrapper.properties b/authorisation-adjustment-example/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..070cb702 --- /dev/null +++ b/authorisation-adjustment-example/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/authorisation-adjustment-example/gradlew b/authorisation-adjustment-example/gradlew new file mode 100755 index 00000000..1b6c7873 --- /dev/null +++ b/authorisation-adjustment-example/gradlew @@ -0,0 +1,234 @@ +#!/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. +# + +############################################################################## +# +# 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 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 patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# 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 + +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 + which java >/dev/null 2>&1 || 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 + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +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 \ + "$@" + +# 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 +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/authorisation-adjustment-example/gradlew.bat b/authorisation-adjustment-example/gradlew.bat new file mode 100644 index 00000000..107acd32 --- /dev/null +++ b/authorisation-adjustment-example/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/authorisation-adjustment-example/settings.gradle b/authorisation-adjustment-example/settings.gradle new file mode 100644 index 00000000..c796e85c --- /dev/null +++ b/authorisation-adjustment-example/settings.gradle @@ -0,0 +1,9 @@ +rootProject.name = 'adyen-java-spring-online-payments-authorisation-adjustment' + +dependencyResolutionManagement { + versionCatalogs { + libs { + from(files("../gradle/libs.versions.toml")) + } + } +} diff --git a/authorisation-adjustment-example/src/main/java/com/adyen/checkout/ApplicationProperty.java b/authorisation-adjustment-example/src/main/java/com/adyen/checkout/ApplicationProperty.java new file mode 100644 index 00000000..907e54eb --- /dev/null +++ b/authorisation-adjustment-example/src/main/java/com/adyen/checkout/ApplicationProperty.java @@ -0,0 +1,63 @@ +package com.adyen.checkout; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +public class ApplicationProperty { + + @Value("${server.port}") + private int serverPort; + + @Value("${ADYEN_API_KEY:#{null}}") + private String apiKey; + + @Value("${ADYEN_MERCHANT_ACCOUNT:#{null}}") + private String merchantAccount; + + @Value("${ADYEN_CLIENT_KEY:#{null}}") + private String clientKey; + + @Value("${ADYEN_HMAC_KEY:#{null}}") + private String hmacKey; + + public int getServerPort() { + return serverPort; + } + + public void setServerPort(int serverPort) { + this.serverPort = serverPort; + } + + public String getApiKey() { + return apiKey; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + + public String getMerchantAccount() { + return merchantAccount; + } + + public void setMerchantAccount(String merchantAccount) { + this.merchantAccount = merchantAccount; + } + + public String getClientKey() { + return clientKey; + } + + public void setClientKey(String clientKey) { + this.clientKey = clientKey; + } + + public String getHmacKey() { + return hmacKey; + } + + public void setHmacKey(String hmacKey) { + this.hmacKey = hmacKey; + } +} diff --git a/authorisation-adjustment-example/src/main/java/com/adyen/checkout/AuthorisationAdjustmentExampleApplication.java b/authorisation-adjustment-example/src/main/java/com/adyen/checkout/AuthorisationAdjustmentExampleApplication.java new file mode 100644 index 00000000..978c2619 --- /dev/null +++ b/authorisation-adjustment-example/src/main/java/com/adyen/checkout/AuthorisationAdjustmentExampleApplication.java @@ -0,0 +1,30 @@ +package com.adyen.checkout; + +import jakarta.annotation.PostConstruct; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + + +@SpringBootApplication +public class AuthorisationAdjustmentExampleApplication { + + private static final Logger log = LoggerFactory.getLogger(AuthorisationAdjustmentExampleApplication.class); + + @Autowired + private ApplicationProperty applicationProperty; + + public static void main(String[] args) { + SpringApplication.run(AuthorisationAdjustmentExampleApplication.class, args); + } + + @PostConstruct + public void init() { + log.info("\n----------------------------------------------------------\n\t" + + "Application is running on http://localhost:" + applicationProperty.getServerPort() + + "\n----------------------------------------------------------"); + } + +} diff --git a/authorisation-adjustment-example/src/main/java/com/adyen/checkout/Config.java b/authorisation-adjustment-example/src/main/java/com/adyen/checkout/Config.java new file mode 100644 index 00000000..cdb5b03b --- /dev/null +++ b/authorisation-adjustment-example/src/main/java/com/adyen/checkout/Config.java @@ -0,0 +1,12 @@ +package com.adyen.checkout; + +import org.springframework.context.annotation.Bean; + +import nz.net.ultraq.thymeleaf.layoutdialect.LayoutDialect; + +public class Config { + @Bean + public LayoutDialect layoutDialect() { + return new LayoutDialect(); + } +} diff --git a/authorisation-adjustment-example/src/main/java/com/adyen/checkout/api/ApiController.java b/authorisation-adjustment-example/src/main/java/com/adyen/checkout/api/ApiController.java new file mode 100644 index 00000000..9921f60e --- /dev/null +++ b/authorisation-adjustment-example/src/main/java/com/adyen/checkout/api/ApiController.java @@ -0,0 +1,200 @@ +package com.adyen.checkout.api; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.UUID; + +import com.adyen.checkout.ApplicationProperty; +import com.adyen.checkout.model.PaymentModel; +import com.adyen.checkout.util.Storage; +import com.adyen.service.checkout.PaymentsApi; +import jakarta.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.view.RedirectView; +import com.adyen.Client; +import com.adyen.enums.Environment; +import com.adyen.model.checkout.*; +import com.adyen.service.exception.ApiException; + +/** + * REST controller for using Adyen Payments API + */ +@RestController +@RequestMapping("/api") +public class ApiController { + private final Logger log = LoggerFactory.getLogger(ApiController.class); + + private final ApplicationProperty applicationProperty; + + private final PaymentsApi paymentsApi; + + public ApiController(ApplicationProperty applicationProperty) { + this.applicationProperty = applicationProperty; + + if(applicationProperty.getApiKey() == null) { + log.warn("ADYEN_KEY is UNDEFINED"); + throw new RuntimeException("ADYEN_KEY is UNDEFINED"); + } + + var client = new Client(applicationProperty.getApiKey(), Environment.TEST); + this.paymentsApi = new PaymentsApi(client); + } + + /** + * {@code POST /getPaymentMethods} : Get valid payment methods. + * + * @return the {@link ResponseEntity} with status {@code 200 (Ok)} and with body the paymentMethods response. + * @throws IOException from Adyen API. + * @throws ApiException from Adyen API. + */ + @PostMapping("/getPaymentMethods") + public ResponseEntity paymentMethods() throws IOException, ApiException { + var paymentMethodsRequest = new PaymentMethodsRequest(); + paymentMethodsRequest.setMerchantAccount(this.applicationProperty.getMerchantAccount()); + paymentMethodsRequest.setChannel(PaymentMethodsRequest.ChannelEnum.WEB); + + log.info("REST request to get Adyen payment methods {}", paymentMethodsRequest); + var response = paymentsApi.paymentMethods(paymentMethodsRequest); + return ResponseEntity.ok() + .body(response); + } + + /** + * {@code POST /initiatePayment} : Make a payment. + * + * @return the {@link ResponseEntity} with status {@code 200 (Ok)} and with body the paymentMethods response. + * @throws IOException from Adyen API. + * @throws ApiException from Adyen API. + */ + @PostMapping("/pre-authorisation") + public ResponseEntity payments(@RequestHeader String host, @RequestBody PaymentRequest body, HttpServletRequest request) throws IOException, ApiException { + var paymentRequest = new PaymentRequest(); + + var orderRef = UUID.randomUUID().toString(); + var amount = new Amount() + .currency("EUR") + .value(24999L); // value is 249.99€ in minor units + + paymentRequest.setMerchantAccount(this.applicationProperty.getMerchantAccount()); // required + paymentRequest.setReference(orderRef); // required + paymentRequest.setReturnUrl(request.getScheme() + "://" + host + "/api/handleShopperRedirect?orderRef=" + orderRef); + + paymentRequest.setAmount(amount); + var authenticationData = new AuthenticationData(); + authenticationData.setAttemptAuthentication(AuthenticationData.AttemptAuthenticationEnum.ALWAYS); + paymentRequest.setAuthenticationData(authenticationData); + + paymentRequest.setAdditionalData(new HashMap<>() { { + put ("authorisationType", "PreAuth"); + }}); + + // required for 3ds2 redirect flow + paymentRequest.setOrigin(request.getScheme() + "://" + host); + // required for 3ds2 + paymentRequest.setBrowserInfo(body.getBrowserInfo()); + // required by some issuers for 3ds2 + paymentRequest.setShopperIP(request.getRemoteAddr()); + paymentRequest.setPaymentMethod(body.getPaymentMethod()); + paymentRequest.setShopperEmail("test@adyen.com"); + + paymentRequest.setChannel(PaymentRequest.ChannelEnum.WEB); + // we strongly recommend that you the billingAddress in your request + // card schemes require this for channel web, iOS, and Android implementations + BillingAddress billingAddress = new BillingAddress(); + billingAddress.setCountry("NL"); + billingAddress.setCity("Amsterdam"); + billingAddress.setStreet("Street"); + billingAddress.setHouseNumberOrName("1"); + billingAddress.setStateOrProvince("North Holland"); + billingAddress.setPostalCode("1000XX"); + paymentRequest.setBillingAddress(billingAddress); + + log.info("REST request to make Adyen payment {}", paymentRequest); + var response = paymentsApi.payments(paymentRequest); + + if (response.getResultCode() == PaymentResponse.ResultCodeEnum.AUTHORISED) { + var payment = new PaymentModel(response.getMerchantReference(), + response.getPspReference(), + response.getAmount().getValue(), + response.getAmount().getCurrency(), + LocalDateTime.now(), + // for demo purposes, we add 28 days pre-authorisation to the expiry date + // the value of '28' varies per scheme, see: https://docs.adyen.com/online-payments/adjust-authorisation/#validity + LocalDateTime.now().plusDays(28), + response.getPaymentMethod().getBrand(), + new ArrayList<>() + ); + Storage.put(payment); + } + return ResponseEntity.ok() + .body(response); + } + + /** + * {@code POST /submitAdditionalDetails} : Make a payment. + * + * @return the {@link ResponseEntity} with status {@code 200 (Ok)} and with body the paymentMethods response. + * @throws IOException from Adyen API. + * @throws ApiException from Adyen API. + */ + @PostMapping("/submitAdditionalDetails") + public ResponseEntity payments(@RequestBody PaymentDetailsRequest detailsRequest) throws IOException, ApiException { + log.info("REST request to make Adyen payment details {}", detailsRequest); + var response = paymentsApi.paymentsDetails(detailsRequest); + return ResponseEntity.ok() + .body(response); + } + + /** + * {@code GET /handleShopperRedirect} : Handle redirect during payment. + * + * @return the {@link RedirectView} with status {@code 302} + * @throws IOException from Adyen API. + * @throws ApiException from Adyen API. + */ + @GetMapping("/handleShopperRedirect") + public RedirectView redirect(@RequestParam(required = false) String payload, @RequestParam(required = false) String redirectResult, @RequestParam String orderRef) throws IOException, ApiException { + var detailsRequest = new PaymentDetailsRequest(); + + PaymentCompletionDetails details = new PaymentCompletionDetails(); + if (redirectResult != null && !redirectResult.isEmpty()) { + // for redirect, you are redirected to an Adyen domain to complete the 3DS2 challenge + // after completing the 3DS2 challenge, you get the redirect result from Adyen in the returnUrl + // we then pass on the redirectResult + details.redirectResult(redirectResult); + } else if (payload != null && !payload.isEmpty()) { + details.payload(payload); + } + + detailsRequest.setDetails(details); + return getRedirectView(detailsRequest); + } + + private RedirectView getRedirectView(final PaymentDetailsRequest detailsRequest) throws ApiException, IOException { + log.info("REST request to handle payment redirect {}", detailsRequest); + var response = paymentsApi.paymentsDetails(detailsRequest); + var redirectURL = "/result/"; + switch (response.getResultCode()) { + case AUTHORISED: + redirectURL += "success"; + break; + case PENDING: + case RECEIVED: + redirectURL += "pending"; + break; + case REFUSED: + redirectURL += "failed"; + break; + default: + redirectURL += "error"; + break; + } + return new RedirectView(redirectURL + "?reason=" + response.getResultCode()); + } +} diff --git a/authorisation-adjustment-example/src/main/java/com/adyen/checkout/api/WebhookController.java b/authorisation-adjustment-example/src/main/java/com/adyen/checkout/api/WebhookController.java new file mode 100644 index 00000000..89e9025e --- /dev/null +++ b/authorisation-adjustment-example/src/main/java/com/adyen/checkout/api/WebhookController.java @@ -0,0 +1,164 @@ +package com.adyen.checkout.api; + +import com.adyen.checkout.ApplicationProperty; +import com.adyen.checkout.model.PaymentDetailsModel; +import com.adyen.checkout.util.Storage; +import com.adyen.model.notification.NotificationRequest; +import com.adyen.model.notification.NotificationRequestItem; +import com.adyen.util.HMACValidator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.security.SignatureException; +import java.time.LocalDateTime; + +/** + * REST controller for receiving Adyen webhook notifications + */ +@RestController +@RequestMapping("/api") +public class WebhookController { + private final Logger log = LoggerFactory.getLogger(WebhookController.class); + + private final ApplicationProperty applicationProperty; + + @Autowired + public WebhookController(ApplicationProperty applicationProperty) { + this.applicationProperty = applicationProperty; + + if (this.applicationProperty.getHmacKey() == null) { + log.warn("ADYEN_HMAC_KEY is UNDEFINED (HMAC signatures cannot be validated when the app receives webhooks)"); + throw new RuntimeException("ADYEN_HMAC_KEY is UNDEFINED"); + } + } + + /** + * Process incoming Webhook notification: get NotificationRequestItem, validate HMAC signature, + * consume the event asynchronously, send response ["accepted"] + * + * @param json Payload of the webhook event + * @return + */ + @PostMapping("/webhooks/notifications") + public ResponseEntity webhooks(@RequestBody String json) throws Exception { + // from JSON string to object + var notificationRequest = NotificationRequest.fromJson(json); + + // fetch first (and only) NotificationRequestItem + var notificationRequestItem = notificationRequest.getNotificationItems().stream().findFirst(); + + if (notificationRequestItem.isPresent()) { + var item = notificationRequestItem.get(); + + try { + if (getHmacValidator().validateHMAC(item, this.applicationProperty.getHmacKey())) { + log.info(""" + Received webhook with event {} :\s + Merchant Reference: {} + Alias : {} + PSP reference : {}""" + , item.getEventCode(), item.getMerchantReference(), item.getAdditionalData().get("alias"), item.getPspReference()); + + // consume event asynchronously + consumeEvent(item); + + } else { + // invalid HMAC signature: do not send [accepted] response + log.warn("Could not validate HMAC signature for incoming webhook message: {}", item); + throw new RuntimeException("Invalid HMAC signature"); + } + } catch (SignatureException e) { + // Unexpected error during HMAC validation: do not send [accepted] response + log.error("Error while validating HMAC Key", e); + throw new SignatureException(e); + } + + } else { + // Unexpected event with no payload: do not send [accepted] response + log.warn("Empty NotificationItem"); + throw new Exception("empty"); + } + + // Acknowledge event has been consumed + return ResponseEntity.ok().body("[accepted]"); + } + + // process payload asynchronously + private void consumeEvent(NotificationRequestItem notification) { + switch (notification.getEventCode()) { + case "AUTHORISATION": + log.info("Payment authorised - pspReference: {} eventCode: {}", notification.getPspReference(), notification.getEventCode()); + savePayment(notification); + break; + + case "AUTHORISATION_ADJUSTMENT": + log.info("Authorisation adjustment - pspReference: {} eventCode: {}", notification.getPspReference(), notification.getEventCode()); + savePayment(notification); + if (notification.isSuccess()) { + // for demo purposes, we add 28 days pre-authorisation to the expiry date + // the value of '28' varies per scheme, see: https://docs.adyen.com/online-payments/adjust-authorisation/#validity + var expiryDate = LocalDateTime.now().plusDays(28); + Storage.updatePayment(notification.getMerchantReference(), notification.getAmount().getValue(), expiryDate); + } + break; + + case "CAPTURE": + log.info("Payment capture - pspReference: {} eventCode: {}", notification.getPspReference(), notification.getEventCode()); + savePayment(notification); + break; + + case "CAPTURE_FAILED": + log.info("Payment capture failed - pspReference: {} eventCode: {}", notification.getPspReference(), notification.getEventCode()); + savePayment(notification); + break; + + case "CANCEL_OR_REFUND": + log.info("Payment cancel_or_refund - pspReference: {} eventCode: {}", notification.getPspReference(), notification.getEventCode()); + savePayment(notification); + break; + + case "REFUND_FAILED": + log.info("Payment refund failed - pspReference: {} eventCode: {}", notification.getPspReference(), notification.getEventCode()); + savePayment(notification); + break; + + case "REFUNDED_REVERSED": + log.info("Payment refund reversed - pspReference: {} eventCode: {}", notification.getPspReference(), notification.getEventCode()); + savePayment(notification); + break; + + default: + log.warn("Unexpected eventCode: {}", notification.getEventCode()); + break; + } + } + + private void savePayment(NotificationRequestItem notification) { + PaymentDetailsModel paymentDetails = new PaymentDetailsModel( + notification.getMerchantReference(), + notification.getPspReference(), + notification.getOriginalReference(), + notification.getAmount().getValue(), + notification.getAmount().getCurrency(), + LocalDateTime.now(), + notification.getEventCode(), + notification.getReason(), + notification.getPaymentMethod(), + notification.isSuccess() + ); + Storage.addPaymentToHistory(paymentDetails); + } + + @Bean + public HMACValidator getHmacValidator() { + return new HMACValidator(); + } +} diff --git a/authorisation-adjustment-example/src/main/java/com/adyen/checkout/model/PaymentDetailsModel.java b/authorisation-adjustment-example/src/main/java/com/adyen/checkout/model/PaymentDetailsModel.java new file mode 100644 index 00000000..e54a75b0 --- /dev/null +++ b/authorisation-adjustment-example/src/main/java/com/adyen/checkout/model/PaymentDetailsModel.java @@ -0,0 +1,109 @@ +package com.adyen.checkout.model; + +import java.time.LocalDateTime; + +public class PaymentDetailsModel { + private String merchantReference; + private String pspReference; + private String originalReference; + private long amount; + private String currency; + private LocalDateTime dateTime; + private String eventCode; + private String refusalReason; + private String paymentMethodBrand; + private boolean success; + + public PaymentDetailsModel(String merchantReference, String pspReference, String originalReference, long amount, String currency, LocalDateTime dateTime, String eventCode, String refusalReason, String paymentMethodBrand, boolean success) { + this.merchantReference = merchantReference; + this.pspReference = pspReference; + this.originalReference = originalReference; + this.amount = amount; + this.currency = currency; + this.dateTime = dateTime; + this.eventCode = eventCode; + this.refusalReason = refusalReason; + this.paymentMethodBrand = paymentMethodBrand; + this.success = success; + } + + public String getMerchantReference() { + return merchantReference; + } + + public void setMerchantReference(String merchantReference) { + this.merchantReference = merchantReference; + } + + public String getPspReference() { + return pspReference; + } + + public void setPspReference(String pspReference) { + this.pspReference = pspReference; + } + + public String getOriginalReference() { + return originalReference; + } + + public void setOriginalReference(String originalReference) { + this.originalReference = originalReference; + } + + public double getAmount() { + return amount; + } + + public void setAmount(long amount) { + this.amount = amount; + } + + public String getCurrency() { + return currency; + } + + public void setCurrency(String currency) { + this.currency = currency; + } + + public LocalDateTime getDateTime() { + return dateTime; + } + + public void setDateTime(LocalDateTime dateTime) { + this.dateTime = dateTime; + } + + public String getEventCode() { + return eventCode; + } + + public void setEventCode(String eventCode) { + this.eventCode = eventCode; + } + + public String getRefusalReason() { + return refusalReason; + } + + public void setRefusalReason(String refusalReason) { + this.refusalReason = refusalReason; + } + + public String getPaymentMethodBrand() { + return paymentMethodBrand; + } + + public void setPaymentMethodBrand(String paymentMethodBrand) { + this.paymentMethodBrand = paymentMethodBrand; + } + + public boolean getSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } +} diff --git a/authorisation-adjustment-example/src/main/java/com/adyen/checkout/model/PaymentModel.java b/authorisation-adjustment-example/src/main/java/com/adyen/checkout/model/PaymentModel.java new file mode 100644 index 00000000..33adaa47 --- /dev/null +++ b/authorisation-adjustment-example/src/main/java/com/adyen/checkout/model/PaymentModel.java @@ -0,0 +1,102 @@ +package com.adyen.checkout.model; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.List; + +public class PaymentModel { + private String merchantReference; + private String pspReference; + private long amount; + private String currency; + private LocalDateTime bookingDate; + private LocalDateTime expiryDate; + private String paymentMethodBrand; + private List paymentDetailsModelList; + + public PaymentModel(String merchantReference, String pspReference, long amount, String currency, LocalDateTime bookingDate, LocalDateTime expiryDate, String paymentMethodBrand, List paymentDetailsModelList) { + this.merchantReference = merchantReference; + this.pspReference = pspReference; + this.amount = amount; + this.currency = currency; + this.bookingDate = bookingDate; + this.expiryDate = expiryDate; + this.paymentMethodBrand = paymentMethodBrand; + this.paymentDetailsModelList = paymentDetailsModelList; + } + + public long getDaysUntilExpiry() { + return ChronoUnit.DAYS.between(bookingDate, expiryDate); + } + + public String getFormattedExpiryDate() { + return expiryDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + } + + public String getMerchantReference() { + return merchantReference; + } + + public void setMerchantReference(String merchantReference) { + this.merchantReference = merchantReference; + } + + public String getPspReference() { + return pspReference; + } + + public void setPspReference(String pspReference) { + this.pspReference = pspReference; + } + + public long getAmount() { + return amount; + } + + public void setAmount(long amount) { + this.amount = amount; + } + + public String getCurrency() { + return currency; + } + + public void setCurrency(String currency) { + this.currency = currency; + } + + public LocalDateTime getBookingDate() { + return bookingDate; + } + + public void setBookingDate(LocalDateTime bookingDate) { + this.bookingDate = bookingDate; + } + + public LocalDateTime getExpiryDate() { + return expiryDate; + } + + public void setExpiryDate(LocalDateTime expiryDate) { + this.expiryDate = expiryDate; + } + + public String getPaymentMethodBrand() { + return paymentMethodBrand; + } + + public void setPaymentMethodBrand(String paymentMethodBrand) { + this.paymentMethodBrand = paymentMethodBrand; + } + + public List getPaymentDetailsModelList() { + return paymentDetailsModelList; + } + + public void setPaymentDetailsModelList(List paymentDetailsModelList) { + this.paymentDetailsModelList = paymentDetailsModelList; + } +} + diff --git a/authorisation-adjustment-example/src/main/java/com/adyen/checkout/request/CapturePaymentRequest.java b/authorisation-adjustment-example/src/main/java/com/adyen/checkout/request/CapturePaymentRequest.java new file mode 100644 index 00000000..67adde1d --- /dev/null +++ b/authorisation-adjustment-example/src/main/java/com/adyen/checkout/request/CapturePaymentRequest.java @@ -0,0 +1,22 @@ +package com.adyen.checkout.request; + +public class CapturePaymentRequest { + public String reference; + public long amount; + + public long getAmount() { + return amount; + } + + public void setAmount(long amount) { + this.amount = amount; + } + + public String getReference() { + return reference; + } + + public void setReference(String reference) { + this.reference = reference; + } +} diff --git a/authorisation-adjustment-example/src/main/java/com/adyen/checkout/request/ReversalPaymentRequest.java b/authorisation-adjustment-example/src/main/java/com/adyen/checkout/request/ReversalPaymentRequest.java new file mode 100644 index 00000000..6e1f9241 --- /dev/null +++ b/authorisation-adjustment-example/src/main/java/com/adyen/checkout/request/ReversalPaymentRequest.java @@ -0,0 +1,13 @@ +package com.adyen.checkout.request; + +public class ReversalPaymentRequest { + public String reference; + + public String getReference() { + return reference; + } + + public void setReference(String reference) { + this.reference = reference; + } +} diff --git a/authorisation-adjustment-example/src/main/java/com/adyen/checkout/request/UpdatePaymentAmountRequest.java b/authorisation-adjustment-example/src/main/java/com/adyen/checkout/request/UpdatePaymentAmountRequest.java new file mode 100644 index 00000000..1902cde7 --- /dev/null +++ b/authorisation-adjustment-example/src/main/java/com/adyen/checkout/request/UpdatePaymentAmountRequest.java @@ -0,0 +1,22 @@ +package com.adyen.checkout.request; + +public class UpdatePaymentAmountRequest { + public String reference; + public long amount; + + public long getAmount() { + return amount; + } + + public void setAmount(long amount) { + this.amount = amount; + } + + public String getReference() { + return reference; + } + + public void setReference(String reference) { + this.reference = reference; + } +} diff --git a/authorisation-adjustment-example/src/main/java/com/adyen/checkout/util/Storage.java b/authorisation-adjustment-example/src/main/java/com/adyen/checkout/util/Storage.java new file mode 100644 index 00000000..81a840bc --- /dev/null +++ b/authorisation-adjustment-example/src/main/java/com/adyen/checkout/util/Storage.java @@ -0,0 +1,53 @@ +package com.adyen.checkout.util; + +import com.adyen.checkout.model.*; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/* + Local storage for storing pre-authorised payments (pre-authorisations) used in the Admin Panel, payments are saved in-memory. + Each PaymentModel (pre-authorisation) contains a list of PaymentDetailsModels that gets appended in subsequent actions such as: adjust, extend, capture or reversal. + */ +public class Storage { + private static List payments = new ArrayList<>(); + + public static List getAll() { + return payments; + } + + public static PaymentModel findByMerchantReference(String merchantReference) { + for (PaymentModel payment : payments) { + if (payment.getMerchantReference().equals(merchantReference)) { + return payment; + } + } + return null; + } + + public static void addPaymentToHistory(PaymentDetailsModel paymentDetailsModel) { + if (paymentDetailsModel.getMerchantReference() == null) { + throw new IllegalArgumentException("Merchant Reference is undefined"); + } + + PaymentModel paymentModel = findByMerchantReference(paymentDetailsModel.getMerchantReference()); + + if (paymentModel != null) { + paymentModel.getPaymentDetailsModelList().add(paymentDetailsModel); + } + } + + public static void updatePayment(String merchantReference, long amount, LocalDateTime expiryDate) { + PaymentModel payment = findByMerchantReference(merchantReference); + + if (payment != null) { + payment.setAmount(amount); + payment.setExpiryDate(expiryDate); + } + } + + public static void put(PaymentModel paymentModel) { + payments.add(paymentModel); + } +} diff --git a/authorisation-adjustment-example/src/main/java/com/adyen/checkout/web/AdminController.java b/authorisation-adjustment-example/src/main/java/com/adyen/checkout/web/AdminController.java new file mode 100644 index 00000000..c740f5b7 --- /dev/null +++ b/authorisation-adjustment-example/src/main/java/com/adyen/checkout/web/AdminController.java @@ -0,0 +1,149 @@ +package com.adyen.checkout.web; + +import com.adyen.Client; +import com.adyen.checkout.ApplicationProperty; +import com.adyen.checkout.model.PaymentModel; +import com.adyen.checkout.request.CapturePaymentRequest; +import com.adyen.checkout.request.ReversalPaymentRequest; +import com.adyen.checkout.request.UpdatePaymentAmountRequest; +import com.adyen.checkout.util.Storage; +import com.adyen.enums.Environment; +import com.adyen.model.checkout.*; +import com.adyen.service.checkout.ModificationsApi; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.*; + + +@Controller +public class AdminController { + private final Logger log = LoggerFactory.getLogger(AdminController.class); + + private final ModificationsApi modificationsApi; + + @Autowired + private ApplicationProperty applicationProperty; + + @Autowired + public AdminController(ApplicationProperty applicationProperty) { + this.applicationProperty = applicationProperty; + + if(applicationProperty.getApiKey() == null) { + log.warn("ADYEN_KEY is UNDEFINED"); + throw new RuntimeException("ADYEN_KEY is UNDEFINED"); + } + var client = new Client(applicationProperty.getApiKey(), Environment.TEST); + this.modificationsApi = new ModificationsApi(client); + } + + @GetMapping("/admin") + public String index(Model model) { + model.addAttribute("data", Storage.getAll()); + return "admin/index"; + } + + @GetMapping("/admin/result/{status}/{reference}") + public String result(@PathVariable String status, @PathVariable String reference, @RequestParam(required = false) String refusalReason, Model model) { + String result; + + if (status.equals("received")) { + result = "success"; + } else { + result = "error"; + } + + model.addAttribute("type", result); + model.addAttribute("reference", reference); + model.addAttribute("refusalReason", refusalReason); + + return "admin/result"; + } + + @GetMapping("/admin/details/{reference}") + public String details(@PathVariable String reference, Model model) { + PaymentModel data = Storage.findByMerchantReference(reference); + model.addAttribute("data", data); + return "admin/details"; + } + + @PostMapping("/admin/capture-payment") + public ResponseEntity capturePayment(@RequestBody CapturePaymentRequest request) { + try { + PaymentModel payment = Storage.findByMerchantReference(request.getReference()); + + if (payment == null) { + throw new Exception("Payment not found in storage - Reference: " + request.getReference()); + } + + var paymentCaptureRequest = new PaymentCaptureRequest(); + paymentCaptureRequest.setMerchantAccount(applicationProperty.getMerchantAccount()); + paymentCaptureRequest.setReference(payment.getMerchantReference()); + + var amount = new Amount(); + amount.setValue(payment.getAmount()); + amount.setCurrency(payment.getCurrency()); + paymentCaptureRequest.setAmount(amount); + + var response = modificationsApi.captureAuthorisedPayment(payment.getPspReference(), paymentCaptureRequest); + log.info(response.toJson()); + return ResponseEntity.ok(response); + } catch (Exception e) { + log.error(e.getMessage()); + return ResponseEntity.status(500).build(); + } + } + + @PostMapping("/admin/update-payment-amount") + public ResponseEntity updatePaymentAmount(@RequestBody UpdatePaymentAmountRequest request) { + try { + PaymentModel payment = Storage.findByMerchantReference(request.getReference()); + + if (payment == null) { + throw new Exception("Payment not found in storage - Reference: " + request.getReference()); + } + + var paymentAmountUpdateRequest = new PaymentAmountUpdateRequest(); + paymentAmountUpdateRequest.setMerchantAccount(applicationProperty.getMerchantAccount()); + paymentAmountUpdateRequest.setReference(payment.getMerchantReference()); + paymentAmountUpdateRequest.setIndustryUsage(PaymentAmountUpdateRequest.IndustryUsageEnum.DELAYEDCHARGE); + + var a = new Amount(); + a.setValue(request.getAmount()); + a.setCurrency(payment.getCurrency()); + paymentAmountUpdateRequest.setAmount(a); + + var response = modificationsApi.updateAuthorisedAmount(payment.getPspReference(), paymentAmountUpdateRequest); + log.info(response.toJson()); + return ResponseEntity.ok(response); + } catch (Exception e) { + log.error(e.getMessage()); + return ResponseEntity.status(500).build(); + } + } + + @PostMapping("/admin/reversal-payment") + public ResponseEntity reversalPayment(@RequestBody ReversalPaymentRequest request) { + try { + PaymentModel payment = Storage.findByMerchantReference(request.getReference()); + + if (payment == null) { + throw new Exception("Payment not found in storage - Reference: " + request.getReference()); + } + + var paymentReversalRequest = new PaymentReversalRequest(); + paymentReversalRequest.setMerchantAccount(applicationProperty.getMerchantAccount()); + paymentReversalRequest.setReference(payment.getMerchantReference()); + + var response = modificationsApi.refundOrCancelPayment(payment.getPspReference(), paymentReversalRequest); + log.info(response.toJson()); + return ResponseEntity.ok(response); + } catch (Exception e) { + log.error(e.getMessage()); + return ResponseEntity.status(500).build(); + } + } +} diff --git a/authorisation-adjustment-example/src/main/java/com/adyen/checkout/web/WebController.java b/authorisation-adjustment-example/src/main/java/com/adyen/checkout/web/WebController.java new file mode 100644 index 00000000..82d8fbe1 --- /dev/null +++ b/authorisation-adjustment-example/src/main/java/com/adyen/checkout/web/WebController.java @@ -0,0 +1,54 @@ +package com.adyen.checkout.web; + +import com.adyen.checkout.ApplicationProperty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; + +@Controller +public class WebController { + + private final Logger log = LoggerFactory.getLogger(WebController.class); + + @Autowired + private ApplicationProperty applicationProperty; + + @Autowired + public WebController(ApplicationProperty applicationProperty) { + this.applicationProperty = applicationProperty; + + if(this.applicationProperty.getClientKey() == null) { + Logger log = LoggerFactory.getLogger(WebController.class); + log.warn("ADYEN_CLIENT_KEY is undefined "); + } + } + + @GetMapping("/") + public String index() { + return "index"; + } + + @GetMapping("/preview") + public String preview(@RequestParam String type, Model model) { + model.addAttribute("type", type); + return "preview"; + } + + @GetMapping("/booking") + public String checkout(@RequestParam String type, Model model) { + model.addAttribute("type", type); + model.addAttribute("clientKey", this.applicationProperty.getClientKey()); + return "booking"; + } + + @GetMapping("/result/{type}") + public String result(@PathVariable String type, Model model) { + model.addAttribute("type", type); + return "result"; + } +} diff --git a/authorisation-adjustment-example/src/main/resources/application.properties b/authorisation-adjustment-example/src/main/resources/application.properties new file mode 100644 index 00000000..4029dfe5 --- /dev/null +++ b/authorisation-adjustment-example/src/main/resources/application.properties @@ -0,0 +1,5 @@ +spring.jackson.default-property-inclusion=non_null +spring.jackson.serialization.indent_output = true + +server.port=8080 + diff --git a/authorisation-adjustment-example/src/main/resources/static/adminpanel-capturePayment-bindings.js b/authorisation-adjustment-example/src/main/resources/static/adminpanel-capturePayment-bindings.js new file mode 100644 index 00000000..4ee78f47 --- /dev/null +++ b/authorisation-adjustment-example/src/main/resources/static/adminpanel-capturePayment-bindings.js @@ -0,0 +1,48 @@ +// Sends POST request to url +async function sendPostRequest(url, data) { + const res = await fetch(url, { + method: "POST", + body: data ? JSON.stringify(data) : "", + headers: { + "Content-Type": "application/json", + }, + }); + + return await res.json(); +} + +// Captures payment of the given reference +async function sendCapturePaymentRequest(reference) { + try { + const res = await sendPostRequest("/admin/capture-payment", { reference: reference}); + console.log(res); + switch (res.status) { + case "received": + window.location.href = "admin/result/received/" + reference; + break; + default: + window.location.href = "admin/result/error/" + reference; + break; + }; + } catch (error) { + console.error(error); + alert("Error occurred. Look at console for details"); + } +} + +// Binds submit buttons to `capture-payment`-endpoint +function bindCapturePaymentFormButtons() { + var elements = document.getElementsByName('capturePaymentForm'); + for (var i = 0; i < elements.length; i++) { + elements[i].addEventListener('submit', async function(event) { + event.preventDefault(); + + var formData = new FormData(event.target); + var reference = formData.get('reference'); + + await sendCapturePaymentRequest(reference); + }); + } +} + +bindCapturePaymentFormButtons(); \ No newline at end of file diff --git a/authorisation-adjustment-example/src/main/resources/static/adminpanel-reversalPayment-bindings.js b/authorisation-adjustment-example/src/main/resources/static/adminpanel-reversalPayment-bindings.js new file mode 100644 index 00000000..302c06e2 --- /dev/null +++ b/authorisation-adjustment-example/src/main/resources/static/adminpanel-reversalPayment-bindings.js @@ -0,0 +1,48 @@ +// Sends POST request to url +async function sendPostRequest(url, data) { + const res = await fetch(url, { + method: "POST", + body: data ? JSON.stringify(data) : "", + headers: { + "Content-Type": "application/json", + }, + }); + + return await res.json(); +} + +// Captures payment of the given reference +async function sendReversalPaymentRequest(reference) { + try { + const res = await sendPostRequest("/admin/reversal-payment", { reference: reference}); + console.log(res); + switch (res.status) { + case "received": + window.location.href = "admin/result/received/" + reference; + break; + default: + window.location.href = "admin/result/error/" + reference; + break; + }; + } catch (error) { + console.error(error); + alert("Error occurred. Look at console for details"); + } +} + +// Binds submit buttons to `reversal-payment`-endpoint +function bindReversalPaymentFormButtons() { + var elements = document.getElementsByName('reversalPaymentForm'); + for (var i = 0; i < elements.length; i++) { + elements[i].addEventListener('submit', async function(event) { + event.preventDefault(); + + var formData = new FormData(event.target); + var reference = formData.get('reference'); + + await sendReversalPaymentRequest(reference); + }); + } +} + +bindReversalPaymentFormButtons(); \ No newline at end of file diff --git a/authorisation-adjustment-example/src/main/resources/static/adminpanel-scripts.js b/authorisation-adjustment-example/src/main/resources/static/adminpanel-scripts.js new file mode 100644 index 00000000..d4e9e152 --- /dev/null +++ b/authorisation-adjustment-example/src/main/resources/static/adminpanel-scripts.js @@ -0,0 +1,19 @@ +// make the form visible +function showForm(id) { + let element = document.getElementById(id); + element.removeAttribute("hidden"); + + hideOtherForms(id); +} + +// hide forms excepted form with given id +function hideOtherForms(id) { + let elements = document.getElementsByClassName("paymentOperationForm"); + + for (let i = 0; i < elements.length; i++) { + if(elements[i].id != id) { + elements[i].setAttribute("hidden", "hidden"); + } + } +} + diff --git a/authorisation-adjustment-example/src/main/resources/static/adminpanel-updatePaymentAmount-bindings.js b/authorisation-adjustment-example/src/main/resources/static/adminpanel-updatePaymentAmount-bindings.js new file mode 100644 index 00000000..16c173fe --- /dev/null +++ b/authorisation-adjustment-example/src/main/resources/static/adminpanel-updatePaymentAmount-bindings.js @@ -0,0 +1,67 @@ +// Sends POST request to url +async function sendPostRequest(url, data) { + const res = await fetch(url, { + method: "POST", + body: data ? JSON.stringify(data) : "", + headers: { + "Content-Type": "application/json", + }, + }); + + return await res.json(); +} + +// Updates payment amount of the given reference +async function sendUpdatePaymentAmountRequest(reference, amount) { + try { + const res = await sendPostRequest("/admin/update-payment-amount", { reference: reference, amount: amount}); + console.log(res); + switch (res.status) { + case "received": + window.location.href = "admin/result/received/" + reference; + break; + default: + window.location.href = "admin/result/error/" + reference; + break; + }; + } catch (error) { + console.error(error); + alert("Error occurred. Look at console for details"); + } +} + +// Binds submit buttons to `update-payment-amount`-endpoint +function bindUpdatePaymentAmountFormButtons() { + var elements = document.getElementsByName('updatePaymentAmountForm'); + for (var i = 0; i < elements.length; i++) { + elements[i].addEventListener('submit', async function(event) { + event.preventDefault(); + + var formData = new FormData(event.target); + var amount = formData.get('amount') * 100; // Multiple by 100, so that `12.34` EUR becomes `1234` in minor units + var reference = formData.get('reference'); + + await sendUpdatePaymentAmountRequest(reference, amount); + }); + } +} + +// Binds submit buttons to `update-payment-amount`-endpoint +// The amount cannot be specified here and follows the same logic as `bindUpdatePaymentAmountFormButtons()` +function bindExtendPaymentFormButtons() { + var elements = document.getElementsByName('extendPaymentForm'); + for (var i = 0; i < elements.length; i++) { + elements[i].addEventListener('submit', async function(event) { + event.preventDefault(); + + var formData = new FormData(event.target); + var amount = formData.get('amount') * 100; // Multiple by 100, so that `12.34` EUR becomes `1234` in minor units + var reference = formData.get('reference'); + + await sendUpdatePaymentAmountRequest(reference, amount); + }); + } +} + +bindUpdatePaymentAmountFormButtons(); +bindExtendPaymentFormButtons(); \ No newline at end of file diff --git a/authorisation-adjustment-example/src/main/resources/static/adyenAuthorisationAdjustmentImplementation.js b/authorisation-adjustment-example/src/main/resources/static/adyenAuthorisationAdjustmentImplementation.js new file mode 100644 index 00000000..404e2efc --- /dev/null +++ b/authorisation-adjustment-example/src/main/resources/static/adyenAuthorisationAdjustmentImplementation.js @@ -0,0 +1,90 @@ +const clientKey = document.getElementById("clientKey").innerHTML; + +async function initCheckout() { + try { + const paymentMethodsResponse = await sendPostRequest("/api/getPaymentMethods"); + const configuration = { + // Pass paymentMethodsResponse to configuration + paymentMethodsResponse: paymentMethodsResponse, + clientKey, + locale: "en_US", + environment: "test", + showPayButton: true, + paymentMethodsConfiguration: { + card: { + hasHolderName: true, + holderNameRequired: true, + name: "Credit or debit card", + amount: { + value: 24999, + currency: "EUR", + }, + }, + }, + onSubmit: (state, component) => { + if (state.isValid) { + handleSubmission(state, component, "/api/pre-authorisation"); + } + }, + onAdditionalDetails: (state, component) => { + handleSubmission(state, component, "/api/submitAdditionalDetails"); + }, + }; + const checkout = await new AdyenCheckout(configuration); + // pay with "card" + checkout.create("card").mount(document.getElementById("payment")); + } catch (error) { + console.error(error); + alert("Error occurred. Look at console for details"); + } +} + +// Event handlers called when the shopper selects the pay button, +// or when additional information is required to complete the payment +async function handleSubmission(state, component, url) { + try { + const res = await sendPostRequest(url, state.data); + handleServerResponse(res, component); + } catch (error) { + console.error(error); + alert("Error occurred. Look at console for details"); + } +} + +// Sends POST request to url +async function sendPostRequest(url, data) { + const res = await fetch(url, { + method: "POST", + body: data ? JSON.stringify(data) : "", + headers: { + "Content-Type": "application/json", + }, + }); + + return await res.json(); +} + +// Handles responses sent from your server to the client +function handleServerResponse(res, component) { + if (res.action) { + component.handleAction(res.action); + } else { + switch (res.resultCode) { + case "Authorised": + window.location.href = "/result/success"; + break; + case "Pending": + case "Received": + window.location.href = "/result/pending"; + break; + case "Refused": + window.location.href = "/result/failed"; + break; + default: + window.location.href = "/result/error"; + break; + } + } +} + +initCheckout(); \ No newline at end of file diff --git a/authorisation-adjustment-example/src/main/resources/static/css/application.css b/authorisation-adjustment-example/src/main/resources/static/css/application.css new file mode 100644 index 00000000..0d0138bb --- /dev/null +++ b/authorisation-adjustment-example/src/main/resources/static/css/application.css @@ -0,0 +1,561 @@ +/* General page body */ + +html, +body { + width: 100%; + margin: 0; + font-family: "Fakt", sans-serif, Helvetica, Arial; +} + +*, +:after, +:before { + box-sizing: border-box; +} + +a { + text-decoration: none; + color: #0abf53; +} + +h1 { + font-weight: bold; + text-align: center; + margin-top: 32px; +} + +u { + text-decoration: none; +} + +a:hover { + text-decoration: none; +} + +.hidden { + display: none; +} + +#header { + border-bottom: 1px solid #e6e9eb; + text-align: center; + top: 0; + width: 100%; + z-index: 2; + box-sizing: border-box; +} + +/* Buttons */ + +.button { + background: #00112c; + border: 0; + border-radius: 6px; + color: #fff; + cursor: pointer; + display: inline-block; + font-size: 1em; + font-weight: 500; + margin: 0; + padding: 15px; + text-align: center; + transition: background 0.3s ease-out, box-shadow 0.3s ease-out; + width: 100%; +} + +.button:hover { + background: #1c3045; + box-shadow: 0 3px 4px rgba(0, 15, 45, 0.2); +} + +.button:active { + background: #3a4a5c; +} + +.button:disabled { + background: #e6e9eb; + box-shadow: none; + cursor: not-allowed; + -webkit-user-select: all; + -moz-user-select: all; + -ms-user-select: all; + user-select: all; +} + +/* end General page body */ + +/* Index page */ + +.main-container { + margin-top: -7%; + flex-direction: column; +} + +.integration-list { + display: inline-block; + width: 100%; + margin-left: -16px; +} + +.integration-list-item { + background: #fcfcfc; + border-radius: 8px; + position: relative; + display: flex; + justify-content: center; + align-items: center; + border: 3px solid black; + margin: 32px 0px 16px 0px; +} + +.integration-list-item:hover { + box-shadow: 0 16px 24px 0 #e5eaef; + text-decoration: none; + border: 3px solid #0abf53; +} + +.integration-list-item-link { + padding: 20px; + width: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +@media (min-width: 768px) { + .integration-list-item-link { + max-width:100%; + padding-left: 28px; + padding-bottom: 28px; + padding-right: 28px; + padding-top: 28px; + } +} + +.integration-list-item-title { + margin: 0; + text-align: center; + color: #00112c; + font-size: 1em; + font-weight: 700; +} + +@media (min-width: 768px) { + .integration-list-item-title { + font-size: 24px; + margin-left: 0; + margin-bottom: 6px; + margin-right: 0; + } +} + +.title-container { + display: flex; + flex-direction: column; + align-items: center; +} + +.info { + margin-top: 10%; + color: #00112c; +} + +/* end Index page */ + +/* Preview page */ + +.shopping-cart { + float: right; +} +@media (min-width: 768px) { + .shopping-cart { + padding-left: 0; + padding-bottom: 0; + padding-right: 0; + padding-top: 3px; + } +} +.shopping-cart-link { + display: inline-block; + position: relative; +} +.order-summary-list { + border-top: 1px solid #e6e9eb; +} +.order-summary-list-list-item { + border-bottom: 1px solid #e6e9eb; + display: flex; + height: 97px; +} +.order-summary-list-list-item-image { + height: 64px; + margin: 16px; + width: 64px; +} +.order-summary-list-list-item-title { + font-weight: 700; + margin: auto auto auto 0; +} +.order-summary-list-list-item-price { + color: #687282; + margin: auto 16px; + text-align: right; + width: 80px; +} +@media (min-width: 768px) { + .order-summary-list-list-item-price { + margin-left: 24px; + margin-bottom: auto; + margin-right: 24px; + margin-top: auto; + } +} +.order-summary-list-list-item-remove-product { + background: none; + border: 0; + cursor: pointer; + height: 25px; + margin: auto 0; + padding: 0; + width: 25px; +} +.cart { + text-align: center; +} +.cart-footer { + font-weight: 700; + margin-top: 17px; + text-align: right; +} +@media (min-width: 768px) { + .cart-footer { + margin-top: 24px; + } +} +.cart-footer .button { + margin-top: 30px; + width: 100%; +} +@media (min-width: 768px) { + .cart-footer .button { + margin-top: 0; + width: 288px; + } +} +.cart-footer-amount { + margin-left: 16px; + margin-right: 24px; +} +.whole-preview { + margin: auto; + max-width: 1110px; + padding: 0 16px; +} +@media (min-width: 1440px) { + .whole-preview { + padding-left: 0; + padding-bottom: 0; + padding-right: 0; + padding-top: 0; + } +} + +/* end of Cart preview page */ + +/* Payment page */ + +#payment-page { + display: flex; + flex-direction: column; + align-items: center; +} + +#payment-page .container { + margin-top: 100px; + display: flex; + flex-direction: row; + justify-content: space-between; + width: 100%; + max-width: 1110px; + padding-left: 8px; + padding-right: 8px; +} + +.checkout-component { + background: #f7f8f9; + border: 1px solid #e6e9eb; + border-radius: 12px; + margin: 8px 0; +} + +/* Adyen Components */ +.payment { + width: 100%; + padding-top: 0px !important; + padding-left: 20px; + padding-right: 20px; +} + +@media screen and (max-width: 1076px) { + #payment-page .container { + display: flex; + flex-direction: column; + align-items: center; + } + + .payment { + align-self: center; + max-width: 610px; + } +} + +.payment-container { + display: flex; + justify-content: center; + background: #f7f8f9; + border: 1px solid #e6e9eb; + border-radius: 12px; + padding-top: 18px; + padding-bottom: 18px; + width: 100%; + height: 100%; +} + +/* end Payments page */ + +/* Dropin page */ + +#dropin { + width: 100%; +} + +@media screen and (max-width: 1076px) { + #dropin { + width: 100%; + } +} + +/* end Dropin page */ + + +/* Status page */ + +.status-container { + height: 100vh; + display: flex; + align-items: center; + justify-content: center; +} + +.status { + margin: 100px 0 126px; + text-align: center; +} + +.status .status-image { + display: block; + height: 100px; + margin: 16px auto 0; +} + +.status .status-image-thank-you { + height: 66px; +} + +.status .status-message { + margin: 8px 0 24px; +} + +.status .button { + max-width: 236px; +} + +@media (min-width: 768px) { + .status .button { + max-width: 200px; + } +} + +/* end Status page */ + +/* start Navigation bar menu */ + +.navMenu { + position: sticky; + overflow: hidden; + z-index: 9999; + top: 0; +} + +.navMenu a { + padding-right: 16px; + margin-left: 32px; + margin-right: 32px; + text-align: left; + color: #0abf53; + text-decoration: none; + font-size: 1.1em; + text-transform: uppercase; + font-weight: bold; + display: inline-block; + -webkit-transition: all 0.15s ease-in-out; + transition: all 0.15s ease-in-out; +} + +.navMenu a:hover { + color: #00112c; +} + +/* end Navigation bar menu */ + +/* start Booking View */ + +.booking-view-container { + background-color: #f1f1f1; + padding: 16px; +} + +/* end Booking View */ + +/* start Admin panel view */ + +.admin-panel-container { + background-color: #fff3f3; + padding: 16px; +} + +.admin-panel-token-container { + padding: 16px; +} + +.adminList { + display: inline-grid; + background-color: #f6ffd4; + padding: 12px 8px 12px 8px; + list-style-type: decimal; + border: 2px solid #0e0e0e; + border-radius: 8px; + margin-right: 12px; + width: 60%; +} + +.adminList li { + display: flex; + list-style-type: none; + padding: 4px 64px 4px 4px; +} + +.display-flex { + display: flex; +} + +/* `Adjust` submit button */ +.adjustSubmitButton { + background-color: #dbae00; + color: white; + padding: 6px 32px 6px 32px; + margin: 10px; + border: none; + cursor: pointer; + font-weight: bold; + border: 2px solid #dbae00; + border-radius: 8px; +} + +.adjustSubmitButton:hover { + background-color: #dbae00; + border: 2px solid #db7900; + border-radius: 8px; + transition: border-color 0.15s linear; +} + +.adjustSubmitButton:disabled, +adjustSubmitButton[disabled=disabled] { + border: 2px solid #cccccc; + background-color: #cccccc; + color: #999999; + pointer-events: none; +} + +.adjustAmountText { + padding: 4px; + border: 1px solid #ccc; + max-width: 120px; + border-radius: 4px; + outline: none; +} + +.adjustAmountText:disabled, +adjustAmountText[disabled=disabled] { + border: 2px solid #cccccc; + background-color: #cccccc; + color: #999999; + pointer-events: none; +} + +.adjustAmountText:focus { + border: 2px solid #dbae00; +} + + +/* `Extend`/`Capture`/`Reversal` submit button */ +.submitButton { + background-color: #0abf53; + color: white; + padding: 6px 24px 6px 24px; + margin: 10px; + border: none; + cursor: pointer; + font-weight: bold; + border: 2px solid #0abf53; + border-radius: 8px; +} + +.submitButton:hover { + background-color: #0abf53; + border: 2px solid #005623; + border-radius: 8px; + transition: border-color 0.15s linear; +} + +.submitButton:disabled, +submitButton[disabled=disabled]{ + border: 2px solid #cccccc; + background-color: #cccccc; + color: #999999; + pointer-events: none; +} + +/* end Admin panel view */ + +/* start Admin panel details view */ + +.details-panel-payment-container { + background-color: #fafff3; + padding: 4px 4px 16px 4px; +} + +.detailsList { + display: grid; + background-color: #f6ffd4; + padding: 12px 8px 12px 8px; + list-style-type: decimal; + border: 2px solid #0e0e0e; + border-radius: 8px; +} + +.detailsList li { + list-style-type: none; + padding: 4px 64px 4px 4px; +} + +.arrow-down { + display: grid; + justify-content: center; +} + +/* success label values (true, false) */ +.success {color: #04AA6D; font-size: 1.1em;} +.failure {color: #ff9800; font-size: 1.1em;} + +/* end Admin panel details view */ \ No newline at end of file diff --git a/authorisation-adjustment-example/src/main/resources/static/images/.keep b/authorisation-adjustment-example/src/main/resources/static/images/.keep new file mode 100644 index 00000000..e69de29b diff --git a/authorisation-adjustment-example/src/main/resources/static/images/cardauthorisationadjustment.gif b/authorisation-adjustment-example/src/main/resources/static/images/cardauthorisationadjustment.gif new file mode 100644 index 00000000..a73fe44e Binary files /dev/null and b/authorisation-adjustment-example/src/main/resources/static/images/cardauthorisationadjustment.gif differ diff --git a/authorisation-adjustment-example/src/main/resources/static/images/error.svg b/authorisation-adjustment-example/src/main/resources/static/images/error.svg new file mode 100644 index 00000000..4db9773b --- /dev/null +++ b/authorisation-adjustment-example/src/main/resources/static/images/error.svg @@ -0,0 +1,4 @@ + + + + diff --git a/authorisation-adjustment-example/src/main/resources/static/images/failed.svg b/authorisation-adjustment-example/src/main/resources/static/images/failed.svg new file mode 100644 index 00000000..4db9773b --- /dev/null +++ b/authorisation-adjustment-example/src/main/resources/static/images/failed.svg @@ -0,0 +1,4 @@ + + + + diff --git a/authorisation-adjustment-example/src/main/resources/static/images/hotel.svg b/authorisation-adjustment-example/src/main/resources/static/images/hotel.svg new file mode 100644 index 00000000..a05050d9 --- /dev/null +++ b/authorisation-adjustment-example/src/main/resources/static/images/hotel.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/authorisation-adjustment-example/src/main/resources/static/images/myhotel-logo.svg b/authorisation-adjustment-example/src/main/resources/static/images/myhotel-logo.svg new file mode 100644 index 00000000..fbbdba3f --- /dev/null +++ b/authorisation-adjustment-example/src/main/resources/static/images/myhotel-logo.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/authorisation-adjustment-example/src/main/resources/static/images/pending.svg b/authorisation-adjustment-example/src/main/resources/static/images/pending.svg new file mode 100644 index 00000000..465c4b46 --- /dev/null +++ b/authorisation-adjustment-example/src/main/resources/static/images/pending.svg @@ -0,0 +1,24 @@ + + + + Icon + Created with Sketch. + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/authorisation-adjustment-example/src/main/resources/static/images/success.svg b/authorisation-adjustment-example/src/main/resources/static/images/success.svg new file mode 100644 index 00000000..465c4b46 --- /dev/null +++ b/authorisation-adjustment-example/src/main/resources/static/images/success.svg @@ -0,0 +1,24 @@ + + + + Icon + Created with Sketch. + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/authorisation-adjustment-example/src/main/resources/static/images/thank-you.svg b/authorisation-adjustment-example/src/main/resources/static/images/thank-you.svg new file mode 100644 index 00000000..587d3b5b --- /dev/null +++ b/authorisation-adjustment-example/src/main/resources/static/images/thank-you.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/authorisation-adjustment-example/src/main/resources/templates/admin/details.html b/authorisation-adjustment-example/src/main/resources/templates/admin/details.html new file mode 100644 index 00000000..72be7f3e --- /dev/null +++ b/authorisation-adjustment-example/src/main/resources/templates/admin/details.html @@ -0,0 +1,55 @@ + + + Admin Details View + + +
+
+

Payment History

+

This page shows all payment details for this reference, as they are delivered by webhooks.

+ +
+ + +
    +
  • Merchant Reference:   +
  • +
  • Event Code:  
  • +
  • PspReference:  
  • +
  • Original Reference:   +
  • + +
  • Refusal Reason:  
  • +
    +
  • Amount:   +
  • +
  • PaymentMethodBrand:   +
  • +
  • DateTime:  
  • + +
  • Success:   +
  • +
    + +
  • Success:   +
  • +
    +
+
+
+ +

+ No payment history is found for this Merchant Reference. +

+
+ + Return +
+
+
+ + + \ No newline at end of file diff --git a/authorisation-adjustment-example/src/main/resources/templates/admin/index.html b/authorisation-adjustment-example/src/main/resources/templates/admin/index.html new file mode 100644 index 00000000..66111700 --- /dev/null +++ b/authorisation-adjustment-example/src/main/resources/templates/admin/index.html @@ -0,0 +1,115 @@ + + + Adyen Admin Panel + + +
+
+

ADMIN PANEL

+
+

The Admin Panel shows all payments of the hotel bookings. In order to perform actions on the + payments, follow the readme to ensure that you have set up your webhooks correctly to receive + payment updates asynchronously.

+ +
+ +
    +
  • Merchant Reference:  
  • +
  • Pre-authorisation PspReference:  
  • +
  • Amount:   +
  • +
  • PaymentMethodBrand:  
  • +
  • Expiry Date:   +   (   days until expiry) +
  • +
  • + Actions:    + Adjust       + Extend       + Capture       + Reversal       +
  • + + + + + + + + + +
+
+
+
+ +

+ No payments are stored. You can make a card payment in the Booking + View. +

+
+ + +
+ +
+ + + + + + + + + + + + +
+ + + diff --git a/authorisation-adjustment-example/src/main/resources/templates/admin/result.html b/authorisation-adjustment-example/src/main/resources/templates/admin/result.html new file mode 100644 index 00000000..550a1955 --- /dev/null +++ b/authorisation-adjustment-example/src/main/resources/templates/admin/result.html @@ -0,0 +1,27 @@ + + + + + +
+
+ + + +

+ Request received for Merchant Reference: . Wait a bit to receive the + asynchronous webhook response. +

+

+ Error! Refusal reason: . + Please review response in console and refer to refusal reasons. +

+ Return to Admin panel + +
+
+ diff --git a/authorisation-adjustment-example/src/main/resources/templates/booking.html b/authorisation-adjustment-example/src/main/resources/templates/booking.html new file mode 100644 index 00000000..64c26fe4 --- /dev/null +++ b/authorisation-adjustment-example/src/main/resources/templates/booking.html @@ -0,0 +1,23 @@ + + + Booking + + +
+ + + +
+ + +
+
+
+
+ + +
+ diff --git a/authorisation-adjustment-example/src/main/resources/templates/index.html b/authorisation-adjustment-example/src/main/resources/templates/index.html new file mode 100644 index 00000000..6eba891c --- /dev/null +++ b/authorisation-adjustment-example/src/main/resources/templates/index.html @@ -0,0 +1,34 @@ + + + Booking View + + +
+
+

BOOKING VIEW

+
+

This demo shows how pre-authorisation and payment adjustments works. Please book a hotel/room + first.

+

Once you've booked the hotel, you can increase/decrease the amount in the Admin + panel page.

+

To learn more, check out the documentation about authorisation + adjustments

+
+
+ + +
+ + + diff --git a/authorisation-adjustment-example/src/main/resources/templates/layout.html b/authorisation-adjustment-example/src/main/resources/templates/layout.html new file mode 100644 index 00000000..7d493702 --- /dev/null +++ b/authorisation-adjustment-example/src/main/resources/templates/layout.html @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + diff --git a/authorisation-adjustment-example/src/main/resources/templates/preview.html b/authorisation-adjustment-example/src/main/resources/templates/preview.html new file mode 100644 index 00000000..5152b5ae --- /dev/null +++ b/authorisation-adjustment-example/src/main/resources/templates/preview.html @@ -0,0 +1,29 @@ + + + Preview + + +
+
+

Booking Details

+
+
    +
  • + +

    Hotel

    +

    249.99

    +
  • +
+
+ +
+
+ diff --git a/authorisation-adjustment-example/src/main/resources/templates/redirect.html b/authorisation-adjustment-example/src/main/resources/templates/redirect.html new file mode 100644 index 00000000..3c7e758d --- /dev/null +++ b/authorisation-adjustment-example/src/main/resources/templates/redirect.html @@ -0,0 +1,15 @@ + + + + + +
+ + + + +
+ diff --git a/authorisation-adjustment-example/src/main/resources/templates/result.html b/authorisation-adjustment-example/src/main/resources/templates/result.html new file mode 100644 index 00000000..0d765056 --- /dev/null +++ b/authorisation-adjustment-example/src/main/resources/templates/result.html @@ -0,0 +1,24 @@ + + + + + +
+
+ + +

+ Your payment has been successfully authorised. + Your order has been received! Payment completion pending. + The payment was refused. Please try a different payment method or card. + Error! Please review response in console and refer to + Response handling. + +

+ Return to Booking View +
+
+ diff --git a/authorisation-adjustment-example/src/test/java/com/adyen/checkout/AuthorisationAdjustmentExampleApplicationTests.java b/authorisation-adjustment-example/src/test/java/com/adyen/checkout/AuthorisationAdjustmentExampleApplicationTests.java new file mode 100644 index 00000000..74ef93d4 --- /dev/null +++ b/authorisation-adjustment-example/src/test/java/com/adyen/checkout/AuthorisationAdjustmentExampleApplicationTests.java @@ -0,0 +1,26 @@ +package com.adyen.checkout; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.event.annotation.BeforeTestClass; + +@SpringBootTest +class AuthorisationAdjustmentExampleApplicationTests { + + @BeforeAll + public static void onceExecutedBeforeAll() { + System.setProperty("ADYEN_API_KEY", "testKey"); + System.setProperty("ADYEN_MERCHANT_ACCOUNT", "testAccount"); + System.setProperty("ADYEN_CLIENT_KEY", "testKey"); + } + + @AfterAll + public static void onceExecutedAfterAll(){ + System.clearProperty("ADYEN_API_KEY"); + System.clearProperty("ADYEN_MERCHANT_ACCOUNT"); + System.clearProperty("ADYEN_CLIENT_KEY"); + } + +} diff --git a/authorisation-adjustment-example/startDocker.sh b/authorisation-adjustment-example/startDocker.sh new file mode 100755 index 00000000..455b2a9c --- /dev/null +++ b/authorisation-adjustment-example/startDocker.sh @@ -0,0 +1,6 @@ +docker run \ +-e ADYEN_CLIENT_KEY \ +-e ADYEN_MERCHANT_ACCOUNT \ +-e ADYEN_HMAC_KEY \ +-e ADYEN_API_KEY \ +-p8080:8080 adyen-java-spring-authorisation-adjustment-example:latest diff --git a/checkout-example-advanced/src/main/java/com/adyen/checkout/api/WebhookResource.java b/checkout-example-advanced/src/main/java/com/adyen/checkout/api/WebhookResource.java index 52fcee34..07de56bb 100644 --- a/checkout-example-advanced/src/main/java/com/adyen/checkout/api/WebhookResource.java +++ b/checkout-example-advanced/src/main/java/com/adyen/checkout/api/WebhookResource.java @@ -45,7 +45,7 @@ public WebhookResource(ApplicationProperty applicationProperty) { * @return */ @PostMapping("/webhooks/notifications") - public ResponseEntity webhooks(@RequestBody String json) throws IOException { + public ResponseEntity webhooks(@RequestBody String json) throws Exception { // from JSON string to object var notificationRequest = NotificationRequest.fromJson(json); @@ -77,11 +77,13 @@ public ResponseEntity webhooks(@RequestBody String json) throws IOExcept } catch (SignatureException e) { // Unexpected error during HMAC validation: do not send [accepted] response log.error("Error while validating HMAC Key", e); + throw new SignatureException(e); } } else { // Unexpected event with no payload: do not send [accepted] response log.warn("Empty NotificationItem"); + throw new Exception("empty"); } // Acknowledge event has been consumed diff --git a/checkout-example/src/main/java/com/adyen/checkout/api/WebhookResource.java b/checkout-example/src/main/java/com/adyen/checkout/api/WebhookResource.java index b9dec98d..4a51fba5 100644 --- a/checkout-example/src/main/java/com/adyen/checkout/api/WebhookResource.java +++ b/checkout-example/src/main/java/com/adyen/checkout/api/WebhookResource.java @@ -45,7 +45,7 @@ public WebhookResource(ApplicationProperty applicationProperty) { * @return */ @PostMapping("/webhooks/notifications") - public ResponseEntity webhooks(@RequestBody String json) throws IOException { + public ResponseEntity webhooks(@RequestBody String json) throws Exception { // from JSON string to object var notificationRequest = NotificationRequest.fromJson(json); @@ -76,11 +76,13 @@ public ResponseEntity webhooks(@RequestBody String json) throws IOExcept } catch (SignatureException e) { // Unexpected error during HMAC validation: do not send [accepted] response log.error("Error while validating HMAC Key", e); + throw new SignatureException(e); } } else { // Unexpected event with no payload: do not send [accepted] response log.warn("Empty NotificationItem"); + throw new Exception("empty"); } // Acknowledge event has been consumed diff --git a/giftcard-example/src/main/java/com/adyen/giftcard/api/WebhookResource.java b/giftcard-example/src/main/java/com/adyen/giftcard/api/WebhookResource.java index 85c84428..4bcd0759 100644 --- a/giftcard-example/src/main/java/com/adyen/giftcard/api/WebhookResource.java +++ b/giftcard-example/src/main/java/com/adyen/giftcard/api/WebhookResource.java @@ -45,7 +45,7 @@ public WebhookResource(ApplicationProperty applicationProperty) { * @return */ @PostMapping("/webhooks/notifications") - public ResponseEntity webhooks(@RequestBody String json) throws IOException { + public ResponseEntity webhooks(@RequestBody String json) throws Exception { // from JSON string to object var notificationRequest = NotificationRequest.fromJson(json); @@ -107,11 +107,13 @@ public ResponseEntity webhooks(@RequestBody String json) throws IOExcept } catch (SignatureException e) { // Unexpected error during HMAC validation: do not send [accepted] response log.error("Error while validating HMAC Key", e); + throw new SignatureException(e); } } else { // Unexpected event with no payload: do not send [accepted] response log.warn("Empty NotificationItem"); + throw new Exception("empty"); } // Acknowledge event has been consumed diff --git a/giving-example/src/main/java/com/adyen/giving/api/WebhookResource.java b/giving-example/src/main/java/com/adyen/giving/api/WebhookResource.java index 5c039461..1cb5aa27 100644 --- a/giving-example/src/main/java/com/adyen/giving/api/WebhookResource.java +++ b/giving-example/src/main/java/com/adyen/giving/api/WebhookResource.java @@ -45,7 +45,7 @@ public WebhookResource(ApplicationProperty applicationProperty) { * @return */ @PostMapping("/webhooks/notifications") - public ResponseEntity webhooks(@RequestBody String json) throws IOException { + public ResponseEntity webhooks(@RequestBody String json) throws Exception { // from JSON string to object var notificationRequest = NotificationRequest.fromJson(json); @@ -77,11 +77,13 @@ public ResponseEntity webhooks(@RequestBody String json) throws IOExcept } catch (SignatureException e) { // Unexpected error during HMAC validation: do not send [accepted] response log.error("Error while validating HMAC Key", e); + throw new SignatureException(e); } } else { // Unexpected event with no payload: do not send [accepted] response log.warn("Empty NotificationItem"); + throw new Exception("empty"); } // Acknowledge event has been consumed diff --git a/paybylink-example/src/main/java/com/adyen/paybylink/api/WebhookController.java b/paybylink-example/src/main/java/com/adyen/paybylink/api/WebhookController.java index f43e9118..1a82ee54 100644 --- a/paybylink-example/src/main/java/com/adyen/paybylink/api/WebhookController.java +++ b/paybylink-example/src/main/java/com/adyen/paybylink/api/WebhookController.java @@ -46,7 +46,7 @@ public WebhookController(ApplicationProperty applicationProperty) { * @return */ @PostMapping("/webhooks/notifications") - public ResponseEntity webhooks(@RequestBody String json) throws IOException { + public ResponseEntity webhooks(@RequestBody String json) throws Exception { // from JSON string to object var notificationRequest = NotificationRequest.fromJson(json); @@ -55,7 +55,6 @@ public ResponseEntity webhooks(@RequestBody String json) throws IOExcept var notificationRequestItem = notificationRequest.getNotificationItems().stream().findFirst(); if (notificationRequestItem.isPresent()) { - var item = notificationRequestItem.get(); try { @@ -78,11 +77,13 @@ public ResponseEntity webhooks(@RequestBody String json) throws IOExcept } catch (SignatureException e) { // Unexpected error during HMAC validation: do not send [accepted] response log.error("Error while validating HMAC Key", e); + throw new SignatureException(e); } } else { // Unexpected event with no payload: do not send [accepted] response log.warn("Empty NotificationItem"); + throw new Exception("empty"); } // Acknowledge event has been consumed