diff --git a/.gitignore b/.gitignore index f17b03d2e..2b6869c34 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ **/target/** .DS_Store .vscode + +# test configuration +src/test/resources/config.properties \ No newline at end of file diff --git a/README.md b/README.md index 82a201592..798d2b8ec 100644 --- a/README.md +++ b/README.md @@ -15,26 +15,26 @@ The Library supports all APIs under the following services: | [BIN lookup API](https://docs.adyen.com/api-explorer/BinLookup/54/overview) | The BIN Lookup API provides endpoints for retrieving information based on a given BIN. | BinLookup | **v54** | | [Checkout API](https://docs.adyen.com/api-explorer/Checkout/71/overview) | Our latest integration for accepting online payments. | Checkout | **v71** | | [Configuration API](https://docs.adyen.com/api-explorer/balanceplatform/2/overview) | The Configuration API enables you to create a platform where you can onboard your users as account holders and create balance accounts, cards, and business accounts. | balanceplatform package subclasses | **v2** | +| [Cloud device API](https://docs.adyen.com/api-explorer/cloud-device-api/1/overview) | Cloud Device point-of-sale integration. | clouddevice package subclasses | **v1** | | [DataProtection API](https://docs.adyen.com/development-resources/data-protection-api) | Adyen Data Protection API provides a way for you to process [Subject Erasure Requests](https://gdpr-info.eu/art-17-gdpr/) as mandated in GDPR. Use our API to submit a request to delete shopper's data, including payment details and other related information (for example, delivery address or shopper email) | DataProtection | **v1** | +| [Disputes API](https://docs.adyen.com/api-explorer/Disputes/30/overview) | You can use the [Disputes API](https://docs.adyen.com/risk-management/disputes-api) to automate the dispute handling process so that you can respond to disputes and chargebacks as soon as they are initiated. The Disputes API lets you retrieve defense reasons, supply and delete defense documents, and accept or defend disputes. | DisputesApi | **v30** | | [Legal Entity Management API](https://docs.adyen.com/api-explorer/legalentity/3/overview) | Manage legal entities that contain information required for verification. | legalentitymanagement package subclasses | **v3** | | [Local/Cloud-based Terminal API](https://docs.adyen.com/point-of-sale/terminal-api-reference) | Our point-of-sale integration. | TerminalLocalAPI or TerminalCloudAPI | **v1** | | [Management API](https://docs.adyen.com/api-explorer/Management/3/overview) | Configure and manage your Adyen company and merchant accounts, stores, and payment terminals. | management package subclasses | **v3** | | [Open Banking API](https://docs.adyen.com/api-explorer/open-banking/1/overview) | The Open Banking API provides secure endpoints to share financial data and services with third parties. | openbanking package subclasses | **v1** | | [Payments API](https://docs.adyen.com/api-explorer/Payment/68/overview) | Our classic integration for online payments. | Payment | **v68** | +| [Payments App API](https://docs.adyen.com/api-explorer/payments-app/1/overview) | The Payments App API is used to Board and manage the Adyen Payments App on your Android mobile devices. | PaymentsAppApi | **v1** | | [Payouts API](https://docs.adyen.com/api-explorer/Payout/68/overview) | Endpoints for sending funds to your customers. | Payout | **v68** | +| [POS Mobile API](https://docs.adyen.com/api-explorer/possdk/68/overview) | The POS Mobile API is used in the mutual authentication flow between an Adyen Android or iOS [POS Mobile SDK](https://docs.adyen.com/point-of-sale/ipp-mobile/) and the Adyen payments platform. The POS Mobile SDK for Android or iOS devices enables businesses to accept in-person payments using a commercial off-the-shelf (COTS) device like a phone. For example, Tap to Pay transactions, or transactions on a mobile device in combination with a card reader | POS Mobile | **v68** | | [POS Terminal Management API](https://docs.adyen.com/api-explorer/postfmapi/1/overview) | ~~Endpoints for managing your point-of-sale payment terminals.~~ ‼️ **Deprecated**: use instead the [Management API](https://docs.adyen.com/api-explorer/Management/latest/overview) for the management of your terminal fleet. | ~~TerminalManagement~~ | ~~**v1**~~ | | [Recurring API](https://docs.adyen.com/api-explorer/Recurring/68/overview) | Endpoints for managing saved payment details. | Recurring | **v68** | | [Session Authentication API](https://docs.adyen.com/api-explorer/sessionauthentication/1/overview) | Create and manage the JSON Web Tokens (JWT) required for integrating [Onboarding](https://docs.adyen.com/platforms/onboard-users/components) and [Platform Experience](https://docs.adyen.com/platforms/build-user-dashboards) components. | SessionAuthentication | **v1** | | [Stored Value API](https://docs.adyen.com/payment-methods/gift-cards/stored-value-api) | Manage both online and point-of-sale gift cards and other stored-value cards. | StoredValue | **v46** | | [Transfers API](https://docs.adyen.com/api-explorer/transfers/4/overview) | The Transfers API provides endpoints that can be used to get information about all your transactions, move funds within your balance platform or send funds from your balance platform to a transfer instrument. | Transfers | **v4** | -| [Webhooks](https://docs.adyen.com/api-explorer/Webhooks/1/overview) | Adyen uses webhooks to send notifications about payment status updates, newly available reports, and other events that can be subscribed to. For more information, refer to our [documentation](https://docs.adyen.com/development-resources/webhooks). | *Models only* | **v1** | | [Classic Platforms Account API](https://docs.adyen.com/api-explorer/Account/6/overview) | This API is used for the classic integration. If you are just starting your implementation, refer to our new integration guide instead. | ClassicPlatformAccountApi | **v6** | | [Classic Platforms Fund API](https://docs.adyen.com/api-explorer/Fund/6/overview) | This API is used for the classic integration. If you are just starting your implementation, refer to our new integration guide instead. | ClassicPlatformFundApi | **v6** | | [Classic Platforms Hosted Onboarding Page API](https://docs.adyen.com/api-explorer/Hop/6/overview) | This API is used for the classic integration. If you are just starting your implementation, refer to our new integration guide instead. | ClassicPlatformHopApi | **v6** | | [Classic Platforms Notification Configuration API](https://docs.adyen.com/api-explorer/NotificationConfiguration/6/overview) | This API is used for the classic integration. If you are just starting your implementation, refer to our new integration guide instead. | ClassicPlatformConfigurationApi | **v6** | -| [Disputes API](https://docs.adyen.com/api-explorer/Disputes/30/overview) | You can use the [Disputes API](https://docs.adyen.com/risk-management/disputes-api) to automate the dispute handling process so that you can respond to disputes and chargebacks as soon as they are initiated. The Disputes API lets you retrieve defense reasons, supply and delete defense documents, and accept or defend disputes. | DisputesApi | **v30** | -| [POS Mobile API](https://docs.adyen.com/api-explorer/possdk/68/overview) | The POS Mobile API is used in the mutual authentication flow between an Adyen Android or iOS [POS Mobile SDK](https://docs.adyen.com/point-of-sale/ipp-mobile/) and the Adyen payments platform. The POS Mobile SDK for Android or iOS devices enables businesses to accept in-person payments using a commercial off-the-shelf (COTS) device like a phone. For example, Tap to Pay transactions, or transactions on a mobile device in combination with a card reader | POS Mobile | **v68** | -| [Payments App API](https://docs.adyen.com/api-explorer/payments-app/1/overview) | The Payments App API is used to Board and manage the Adyen Payments App on your Android mobile devices. | PaymentsAppApi | **v1** | ## Supported Webhook versions The library supports all webhooks under the following model directories: @@ -49,9 +49,9 @@ The library supports all webhooks under the following model directories: | [Negative Balance Warning Webhooks](https://docs.adyen.com/api-explorer/Webhooks/1/overview) | Adyen sends this webhook to inform you about a balance account whose balance has been negative for a given number of days. | [negativebalancewarningwebhooks](src/main/java/com/adyen/model/negativebalancewarningwebhooks) | **v1** | | [Notification Webhooks](https://docs.adyen.com/api-explorer/Webhooks/1/overview) | We use webhooks to send you updates about payment status updates, newly available reports, and other events that you can subscribe to. For more information, refer to our documentation | [notification](src/main/java/com/adyen/model/notification) | **v1** | | [Management Webhooks](https://docs.adyen.com/api-explorer/ManagementNotification/3/overview) | Adyen uses webhooks to inform your system about events that happen with your Adyen company and merchant accounts, stores, payment terminals, and payment methods when using Management API | [managementwebhooks](src/main/java/com/adyen/model/managementwebhooks) | **v3** | -| [Classic Platform Webhooks](https://docs.adyen.com/api-explorer/Notification/6/overview#retry) | The Notification API sends notifications to the endpoints specified in a given subscription. Subscriptions are managed through the Notification Configuration API. The API specifications listed here detail the format of each notification. | [marketpaywebhooks](src/main/java/com/adyen/model/marketpaywebhooks) | **v6** | | [Transaction Webhooks](https://docs.adyen.com/api-explorer/transaction-webhooks/4/overview) | Adyen sends webhooks to inform your system about incoming and outgoing transfers in your platform. You can use these webhooks to build your implementation. For example, you can use this information to update balances in your own dashboards or to keep track of incoming funds. | [transactionwebhooks](src/main/java/com/adyen/model/transactionwebhooks) | **v4** | | [Tokenization Webhooks](https://docs.adyen.com/api-explorer/Tokenization-webhooks/1/overview) | Adyen sends webhooks to inform you about the creation and changes to the recurring tokens. | [tokenizationwebhooks](src/main/java/com/adyen/model/tokenizationwebhooks) | **v1** | +| [Classic Platform Webhooks](https://docs.adyen.com/api-explorer/Notification/6/overview#retry) | The Notification API sends notifications to the endpoints specified in a given subscription. Subscriptions are managed through the Notification Configuration API. The API specifications listed here detail the format of each notification. | [marketpaywebhooks](src/main/java/com/adyen/model/marketpaywebhooks) | **v6** | For more information, refer to our [documentation](https://docs.adyen.com/) or the [API Explorer](https://docs.adyen.com/api-explorer/). @@ -313,260 +313,25 @@ try { System.out.println(error.getInvalidFields()); } ``` +## In-person payments integration -## Using the Cloud Terminal API Integration -In order to submit In-Person requests with [Terminal API over Cloud](https://docs.adyen.com/point-of-sale/design-your-integration/choose-your-architecture/cloud/) you need to initialize the client in a similar way as the steps listed above for Ecommerce transactions, but make sure to include `TerminalCloudAPI`: -``` java -// Step 1: Import the required classes -import com.adyen.Client; -import com.adyen.enums.Environment; -import com.adyen.service.TerminalCloudAPI; -import com.adyen.model.nexo.*; -import com.adyen.model.terminal.*; +Build a feature-rich [in-person payments](https://docs.adyen.com/point-of-sale/) integrations that accept payments around the world, with global and local payment methods and create a unique shopping experience for your customers. -// Step 2: Initialize the client object -Client client = new Client("Your YOUR_API_KEY", Environment.TEST); +### Using the Cloud Device API -// for LIVE environment use -// Config config = new Config(); -// config.setEnvironment(Environment.LIVE); -// config.setTerminalApiRegion(Region.EU); -// Client client = new Client(config); - -// Step 3: Initialize the API object -TerminalCloudAPI terminalCloudApi = new TerminalCloudAPI(client); - -// Step 4: Create the request object -String serviceID = "123456789"; -String saleID = "POS-SystemID12345"; -String POIID = "Your Device Name(eg V400m-123456789)"; - -// Use a unique transaction for every other transaction you perform -String transactionID = "TransactionID"; -TerminalAPIRequest terminalAPIRequest = new TerminalAPIRequest(); -SaleToPOIRequest saleToPOIRequest = new SaleToPOIRequest(); - -MessageHeader messageHeader = new MessageHeader(); -messageHeader.setMessageClass(MessageClassType.SERVICE); -messageHeader.setMessageCategory(MessageCategoryType.PAYMENT); -messageHeader.setMessageType(MessageType.REQUEST); -messageHeader.setProtocolVersion("3.0"); -messageHeader.setServiceID(serviceID); -messageHeader.setSaleID(saleID); -messageHeader.setPOIID(POIID); - -saleToPOIRequest.setMessageHeader(messageHeader); - -com.adyen.model.nexo.PaymentRequest paymentRequest = new com.adyen.model.nexo.PaymentRequest(); -SaleData saleData = new SaleData(); -TransactionIdentification transactionIdentification = new TransactionIdentification(); -transactionIdentification.setTransactionID("001"); -XMLGregorianCalendar timestamp = DatatypeFactory.newInstance().newXMLGregorianCalendar(new GregorianCalendar()); -transactionIdentification.setTimeStamp(timestamp); -saleData.setSaleTransactionID(transactionIdentification); - -SaleToAcquirerData saleToAcquirerData = new SaleToAcquirerData(); -ApplicationInfo applicationInfo = new ApplicationInfo(); -CommonField merchantApplication = new CommonField(); -merchantApplication.setVersion("1"); -merchantApplication.setName("Test"); -applicationInfo.setMerchantApplication(merchantApplication); -saleToAcquirerData.setApplicationInfo(applicationInfo); -saleData.setSaleToAcquirerData(saleToAcquirerData); - -PaymentTransaction paymentTransaction = new PaymentTransaction(); -AmountsReq amountsReq = new AmountsReq(); -amountsReq.setCurrency("EUR"); -amountsReq.setRequestedAmount(BigDecimal.valueOf(1000)); -paymentTransaction.setAmountsReq(amountsReq); - -paymentRequest.setPaymentTransaction(paymentTransaction); -paymentRequest.setSaleData(saleData); - -saleToPOIRequest.setPaymentRequest(paymentRequest); - -terminalAPIRequest.setSaleToPOIRequest(saleToPOIRequest); - -// Step 5: Make the request -TerminalAPIResponse terminalAPIResponse = terminalCloudApi.sync(terminalAPIRequest); -``` +For In-Person Payments integrations, the recommended solution is the [Cloud Device API](https://docs.adyen.com/api-explorer/cloud-device-api/1/overview). -### Optional: perform an abort request - -To perform an [abort request](https://docs.adyen.com/point-of-sale/basic-tapi-integration/cancel-a-transaction/) you can use the following example: -``` java -TerminalAPIRequest terminalAPIRequest = new TerminalAPIRequest(); -SaleToPOIRequest saleToPOIRequest = new SaleToPOIRequest(); - -MessageHeader messageHeader = new MessageHeader(); -messageHeader.setMessageClass(MessageClassType.SERVICE); -messageHeader.setMessageCategory(MessageCategoryType.ABORT); -messageHeader.setMessageType(MessageType.REQUEST); -messageHeader.setProtocolVersion("3.0"); -messageHeader.setServiceID("Different service ID"); -messageHeader.setSaleID(saleID); -messageHeader.setPOIID(POIID); - -AbortRequest abortRequest = new AbortRequest(); -abortRequest.setAbortReason("MerchantAbort"); -MessageReference messageReference = new MessageReference(); -messageReference.setMessageCategory(MessageCategoryType.PAYMENT); -messageReference.setSaleID(saleID); -messageReference.setPOIID(POIID); -// Service ID of the payment you're aborting -messageReference.setServiceID(serviceID); -abortRequest.setMessageReference(messageReference); - -saleToPOIRequest.setAbortRequest(abortRequest); -saleToPOIRequest.setMessageHeader(messageHeader); - -terminalAPIRequest.setSaleToPOIRequest(saleToPOIRequest); - -TerminalAPIResponse terminalAPIResponse = terminalCloudApi.sync(terminalAPIRequest); -``` +Check the [Cloud Device API README](doc/CloudDeviceApi.md). -### Optional: perform a status request +### Using the Terminal API -To perform a [status request](https://docs.adyen.com/point-of-sale/basic-tapi-integration/verify-transaction-status/) you can use the following example: -```java -TerminalAPIRequest terminalAPIRequest = new TerminalAPIRequest(); -SaleToPOIRequest saleToPOIRequest = new SaleToPOIRequest(); - -MessageHeader messageHeader = new MessageHeader(); -messageHeader.setMessageClass(MessageClassType.SERVICE); -messageHeader.setMessageCategory(MessageCategoryType.TRANSACTION_STATUS); -messageHeader.setMessageType(MessageType.REQUEST); -messageHeader.setProtocolVersion("3.0"); -messageHeader.setServiceID("Different service ID"); -messageHeader.setSaleID(saleID); -messageHeader.setPOIID(POIID); - -TransactionStatusRequest transactionStatusRequest = new TransactionStatusRequest(); -transactionStatusRequest.setReceiptReprintFlag(true); -transactionStatusRequest.getDocumentQualifier().add(DocumentQualifierType.CASHIER_RECEIPT); -transactionStatusRequest.getDocumentQualifier().add(DocumentQualifierType.CUSTOMER_RECEIPT); -MessageReference messageReference = new MessageReference(); -messageReference.setMessageCategory(MessageCategoryType.PAYMENT); -messageReference.setSaleID(saleID); -// serviceID of the transaction you want the status update from -messageReference.setServiceID(serviceID); -transactionStatusRequest.setMessageReference(messageReference); - -saleToPOIRequest.setTransactionStatusRequest(transactionStatusRequest); -saleToPOIRequest.setMessageHeader(messageHeader); - -terminalAPIRequest.setSaleToPOIRequest(saleToPOIRequest); - -TerminalAPIResponse terminalAPIResponse = terminalCloudApi.sync(terminalAPIRequest); -``` +With the [Terminal API](https://docs.adyen.com/api-explorer/terminal-api/1/overview) you can send and receive Terminal API messages in the following ways: -### Helper classes +* Local communications: using your local network, your POS system sends the request directly to the IP address of the terminal, and receives the result synchronously. +* Cloud communications: using the internet to access the cloud `/sync` and `/async` endpoints. You should consider adopting the [Cloud Device API](doc/CloudDeviceApi.md) instead. -Use `PredefinedContentHelper` to parse Display notification types which you find in `PredefinedContent->ReferenceID` -```java -PredefinedContentHelper helper = new PredefinedContentHelper(predefinedContent.getReferenceID()); - -// Safely extract and use the event type with Optional -helper.getEvent().ifPresent(event -> { - System.out.println("Received event: " + event); - if (event == PredefinedContentHelper.DisplayNotificationEvent.PIN_ENTERED) { - // Handle PIN entry event - System.out.println("The user has entered their PIN."); - } -}); -``` - -## Using the Local Terminal API Integration -The request and response payloads are identical to the Cloud Terminal API, however, additional encryption details are required to perform the requests. -### Local terminal API Using Keystore -~~~~ java -// Import the required classes -import com.adyen.Client; -import com.adyen.Config; -import com.adyen.enums.Environment; -import com.adyen.httpclient.TerminalLocalAPIHostnameVerifier; -import com.adyen.service.TerminalLocalAPI; -import com.adyen.model.terminal.security.*; -import com.adyen.model.terminal.*; -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManagerFactory; -import java.security.KeyStore; -import java.security.SecureRandom; - -// Create a KeyStore for the terminal certificate -KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); -keyStore.load(null, null); -keyStore.setCertificateEntry("adyenRootCertificate", adyenRootCertificate); - -// Create a TrustManagerFactory that trusts the CAs in our KeyStore -TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); -trustManagerFactory.init(keyStore); - -// Create an SSLContext with the desired protocol that uses our TrustManagers -SSLContext sslContext = SSLContext.getInstance("SSL"); -sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom()); - -// Configure a client for TerminalLocalAPI -Config config = new Config(); -config.setEnvironment(environment); -config.setTerminalApiLocalEndpoint("https://" + terminalIpAddress); -config.setSSLContext(sslContext); -config.setHostnameVerifier(new TerminalLocalAPIHostnameVerifier(environment)); -Client client = new Client(config); - -// Create your SecurityKey object used for encrypting the payload (keyIdentifier/passphrase you set up beforehand in CA) -SecurityKey securityKey = new SecurityKey(); -securityKey.setKeyVersion(1); -securityKey.setAdyenCryptoVersion(1); -securityKey.setKeyIdentifier("keyIdentifier"); -securityKey.setPassphrase("passphrase"); - -// Use TerminalLocalAPI -TerminalLocalAPI terminalLocalAPI = new TerminalLocalAPI(client, securityKey); -TerminalAPIResponse terminalAPIResponse = terminalLocalAPI.request(terminalAPIRequest); -~~~~ - -## Using Attachments in Document API -When using Attachments, ensure content is provided as a byte array. It's important to convert it to a [Base64-encoded string](https://docs.adyen.com/api-explorer/legalentity/3/post/documents#request-attachments) before initiating the request. - -## Using the Local Terminal API Integration without Encryption (Only on TEST) -If you wish to develop the Local Terminal API integration parallel to your encryption implementation, you can opt for the unencrypted version. Be sure to remove any encryption details from the CA terminal config page. Consider this ONLY for development and testing on localhost. -```java -// Step 1: Import the required classes -import com.adyen.service.TerminalLocalAPI; -import com.adyen.model.nexo.*; -import com.adyen.model.terminal.*; -import javax.net.ssl.SSLContext; - -// Step 2: Add your Certificate Path and Local Endpoint to the config path. -Client client = new Client(); -client.getConfig().setTerminalApiLocalEndpoint("The IP of your terminal (eg https://192.168.47.169)"); -client.getConfig().setEnvironment(Environment.TEST); -config.setSSLContext(createTrustSSLContext()); // Trust all certificates for testing only -client.setConfig(config); - -// Step 3: Create an SSL context that accepts all certificates (Use in TEST only). -SSLContext createTrustSSLContext() throws Exception { - TrustManager[] trustAllCerts = new TrustManager[]{ - new X509TrustManager() { - java.security.cert.X509Certificate[] getAcceptedIssuers() { return null; } - checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {} - checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {} - } - }; - SSLContext sc = SSLContext.getInstance("SSL"); - sc.init(null, trustAllCerts, new java.security.SecureRandom()); - return sc; -} - -// Step 4: Construct a TerminalAPIRequest object -Gson gson = new GsonBuilder().create(); -TerminalAPIRequest terminalAPIPaymentRequest = new TerminalAPIRequest(); - -// Step 5: Make the request -TerminalAPIResponse terminalAPIResponse = terminalLocalAPI.request(terminalAPIRequest); -``` +Check the [Terminal API README](doc/TerminalApi.md). ### Example integrations @@ -579,15 +344,12 @@ These include commented code, highlighting key features and concepts, and exampl ## Feedback We value your input! Help us enhance our API Libraries and improve the integration experience by providing your feedback. Please take a moment to fill out [our feedback form](https://forms.gle/A4EERrR6CWgKWe5r9) to share your thoughts, suggestions or ideas. -## Contributing - +## Contributing We encourage you to contribute to this repository, so everyone can benefit from new features, bug fixes, and any other improvements. - Have a look at our [contributing guidelines](CONTRIBUTING.md) to find out how to raise a pull request. - ## Support If you have a feature request, or spotted a bug or a technical problem, [create an issue here](https://github.com/Adyen/adyen-java-api-library/issues/new/choose). @@ -596,8 +358,7 @@ For other questions, [contact our Support Team](https://www.adyen.help/hc/en-us/ ## Licence This repository is available under the [MIT license](https://github.com/Adyen/adyen-java-api-library/blob/main/LICENSE). - - + ## See also * Example integrations: diff --git a/doc/CloudDeviceApi.md b/doc/CloudDeviceApi.md new file mode 100644 index 000000000..adfcf5bdd --- /dev/null +++ b/doc/CloudDeviceApi.md @@ -0,0 +1,156 @@ +# Cloud Device API + +The [Cloud Device API](https://docs.adyen.com/api-explorer/cloud-device-api/1/overview) is our solution to create best-in-class in-person payments integrations. + +With the Cloud device API you can: + +- send Terminal API requests to a cloud endpoint. You can use this communication method when it is not an option to send Terminal API requests over your local network directly to a payment terminal. +- check the cloud connection of a payment terminal or of a device used in a Mobile solution for in-person payments. + +## Benefits of the Cloud Device API + +The Cloud Device API offers the following benefits: +- access to API logs in the Customer Area for troubleshooting errors +- using a version strategy for the API endpoints for controlled and safer rollouts +- improved reliability and security (OAuth support) + +New features and products will be released exclusively on the Cloud Device API + +## Use the Cloud Device API + +### Setup + +First you must initialise the Client **setting the closest** [Region](https://docs.adyen.com/point-of-sale/design-your-integration/terminal-api/#cloud): +``` java +// Import the required classes +import com.adyen.Client; +import com.adyen.enums.Environment; +import com.adyen.service.checkout.PaymentsApi; +import com.adyen.model.checkout.*; + +// Setup Client and Service +Client client = new Client("Your X-API-KEY", Environment.TEST); +CloudDeviceApi cloudDeviceApi = new CloudDeviceApi(client); + +``` + +### Send a payment SYNC request + +```java + +SaleToPOIRequest saleToPOIRequest = new SaleToPOIRequest(); + +MessageHeader messageHeader = new MessageHeader(); + messageHeader.setProtocolVersion("3.0"); + messageHeader.setMessageClass(MessageClassType.SERVICE); + messageHeader.setMessageCategory(MessageCategoryType.PAYMENT); + messageHeader.setMessageType(MessageType.REQUEST); + messageHeader.setSaleID("001"); + messageHeader.setServiceID("001"); + messageHeader.setPOIID("P400Plus-123456789"); + + saleToPOIRequest.setMessageHeader(messageHeader); + +PaymentRequest paymentRequest = new PaymentRequest(); + +SaleData saleData = new SaleData(); +TransactionIdentification transactionIdentification = new TransactionIdentification(); + transactionIdentification.setTransactionID("001"); +OffsetDateTime timestamp = OffsetDateTime.now(ZoneOffset.UTC); + transactionIdentification.setTimeStamp(timestamp); + saleData.setSaleTransactionID(transactionIdentification); + +PaymentTransaction paymentTransaction = new PaymentTransaction(); +AmountsReq amountsReq = new AmountsReq(); + amountsReq.setCurrency("EUR"); + amountsReq.setRequestedAmount(BigDecimal.ONE); + paymentTransaction.setAmountsReq(amountsReq); + + paymentRequest.setSaleData(saleData); + paymentRequest.setPaymentTransaction(paymentTransaction); + + saleToPOIRequest.setPaymentRequest(paymentRequest); + +CloudDeviceApiRequest cloudDeviceApiRequest = new CloudDeviceApiRequest(); + cloudDeviceApiRequest.setSaleToPOIRequest(saleToPOIRequest); + +var response = cloudDeviceApi.sendSync("myMerchant", "P400Plus-123456789", cloudDeviceApiRequest); + +``` + + +### Send a payment ASYNC request + +If you choose to receive the response asynchronously, you only need to use a different method (`sendAsync`). +Don't forget to set up [event notifications](https://docs.adyen.com/point-of-sale/design-your-integration/notifications/event-notifications/) in the CA to be able to receive the Cloud Device API responses. + +```java + +... + +// define the request (same as per sendSync) +CloudDeviceApiRequest cloudDeviceApiRequest = new CloudDeviceApiRequest(); +cloudDeviceApiRequest.setSaleToPOIRequest(saleToPOIRequest); + +CloudDeviceApiAsyncResponse response = cloudDeviceApi.sendAsync("myMerchant", "P400Plus-123456789", cloudDeviceApiRequest); + +// +if("ok".equals(response.getResult())) { + + // success +} else { + // request failed: see details in the EventNotification object + EventNotification eventNotification = response.getSaleToPOIRequest().getEventNotification(); +} +``` + +### Verify the status of the terminals + + +The Cloud Device API allows your integration to check the status of the terminals. + +```java + +// list of payment terminals or SDK mobile installation IDs +ConnectedDevicesResponse response = cloudDeviceApi.getConnectedDevices("myMerchant"); +System.out.println(response.getUniqueDeviceIds()); + +// check the payment terminal or SDK mobile installation ID +DeviceStatusResponse response = cloudDeviceApi.getDeviceStatus("myMerchant", "AMS1-000168242800763"); +System.out.println(response.getStatus()); +``` + +### Protect cloud communication + +The Adyen Java library supports encrypting request and response payloads, allowing you to secure communication between your integration and the cloud. + +```java + +// Encryption credentials from the Terminal configuration on CA +EncryptionCredentialDetails encryptionCredentialDetails = + new EncryptionCredentialDetails() + .adyenCryptoVersion(0) + .keyIdentifier("CryptoKeyIdentifier12345") + .keyVersion(0) + .passphrase("p@ssw0rd123456"); + +var response = + cloudDeviceApi.sendEncryptedSync( + "TestMerchantAccount", + "V400m-123456789", + cloudDeviceApiRequest, + encryptionCredentialDetails); + +System.out.println(response); +``` + +In case of asynchronous integration, you can decrypt the payload of the event notifications using `decryptNotification()` method. + +```java +// JSON with encrypted SaleToPOIResponse (for async responses) or SaleToPOIRequest (for event notifications) +var payload = "..."; + +var response = cloudDeviceApi.decryptNotification(payload, encryptionCredentialDetails); +System.out.println(response); + +``` \ No newline at end of file diff --git a/doc/TerminalApi.md b/doc/TerminalApi.md new file mode 100644 index 000000000..e515166af --- /dev/null +++ b/doc/TerminalApi.md @@ -0,0 +1,249 @@ +### Using the Terminal API +For In-Person Payments integrations with the [Cloud Terminal API](https://docs.adyen.com/point-of-sale/design-your-integration/choose-your-architecture/cloud/), you must initialise the Client **setting the closest** [Region](https://docs.adyen.com/point-of-sale/design-your-integration/terminal-api/#cloud): +``` java +// Step 1: Import the required classes +import com.adyen.Client; +import com.adyen.enums.Environment; +import com.adyen.service.TerminalCloudAPI; +import com.adyen.model.nexo.*; +import com.adyen.model.terminal.*; + +// Step 2: Initialize the client object +Client client = new Client("Your YOUR_API_KEY", Environment.TEST); + +// for LIVE environment use +// Config config = new Config(); +// config.setEnvironment(Environment.LIVE); +// config.setTerminalApiRegion(Region.EU); +// Client client = new Client(config); + +// Step 3: Initialize the API object +TerminalCloudAPI terminalCloudApi = new TerminalCloudAPI(client); + +// Step 4: Create the request object +String serviceID = "123456789"; +String saleID = "POS-SystemID12345"; +String POIID = "Your Device Name(eg V400m-123456789)"; + +// Use a unique transaction for every other transaction you perform +String transactionID = "TransactionID"; +TerminalAPIRequest terminalAPIRequest = new TerminalAPIRequest(); +SaleToPOIRequest saleToPOIRequest = new SaleToPOIRequest(); + +MessageHeader messageHeader = new MessageHeader(); +messageHeader.setMessageClass(MessageClassType.SERVICE); +messageHeader.setMessageCategory(MessageCategoryType.PAYMENT); +messageHeader.setMessageType(MessageType.REQUEST); +messageHeader.setProtocolVersion("3.0"); +messageHeader.setServiceID(serviceID); +messageHeader.setSaleID(saleID); +messageHeader.setPOIID(POIID); + +saleToPOIRequest.setMessageHeader(messageHeader); + +com.adyen.model.nexo.PaymentRequest paymentRequest = new com.adyen.model.nexo.PaymentRequest(); +SaleData saleData = new SaleData(); +TransactionIdentification transactionIdentification = new TransactionIdentification(); +transactionIdentification.setTransactionID("001"); +XMLGregorianCalendar timestamp = DatatypeFactory.newInstance().newXMLGregorianCalendar(new GregorianCalendar()); +transactionIdentification.setTimeStamp(timestamp); +saleData.setSaleTransactionID(transactionIdentification); + +SaleToAcquirerData saleToAcquirerData = new SaleToAcquirerData(); +ApplicationInfo applicationInfo = new ApplicationInfo(); +CommonField merchantApplication = new CommonField(); +merchantApplication.setVersion("1"); +merchantApplication.setName("Test"); +applicationInfo.setMerchantApplication(merchantApplication); +saleToAcquirerData.setApplicationInfo(applicationInfo); +saleData.setSaleToAcquirerData(saleToAcquirerData); + +PaymentTransaction paymentTransaction = new PaymentTransaction(); +AmountsReq amountsReq = new AmountsReq(); +amountsReq.setCurrency("EUR"); +amountsReq.setRequestedAmount(BigDecimal.valueOf(1000)); +paymentTransaction.setAmountsReq(amountsReq); + +paymentRequest.setPaymentTransaction(paymentTransaction); +paymentRequest.setSaleData(saleData); + +saleToPOIRequest.setPaymentRequest(paymentRequest); + +terminalAPIRequest.setSaleToPOIRequest(saleToPOIRequest); + +// Step 5: Make the request +TerminalAPIResponse terminalAPIResponse = terminalCloudApi.sync(terminalAPIRequest); +``` + +#### Optional: perform an abort request + +To perform an [abort request](https://docs.adyen.com/point-of-sale/basic-tapi-integration/cancel-a-transaction/) you can use the following example: +``` java +TerminalAPIRequest terminalAPIRequest = new TerminalAPIRequest(); +SaleToPOIRequest saleToPOIRequest = new SaleToPOIRequest(); + +MessageHeader messageHeader = new MessageHeader(); +messageHeader.setMessageClass(MessageClassType.SERVICE); +messageHeader.setMessageCategory(MessageCategoryType.ABORT); +messageHeader.setMessageType(MessageType.REQUEST); +messageHeader.setProtocolVersion("3.0"); +messageHeader.setServiceID("Different service ID"); +messageHeader.setSaleID(saleID); +messageHeader.setPOIID(POIID); + +AbortRequest abortRequest = new AbortRequest(); +abortRequest.setAbortReason("MerchantAbort"); +MessageReference messageReference = new MessageReference(); +messageReference.setMessageCategory(MessageCategoryType.PAYMENT); +messageReference.setSaleID(saleID); +messageReference.setPOIID(POIID); +// Service ID of the payment you're aborting +messageReference.setServiceID(serviceID); +abortRequest.setMessageReference(messageReference); + +saleToPOIRequest.setAbortRequest(abortRequest); +saleToPOIRequest.setMessageHeader(messageHeader); + +terminalAPIRequest.setSaleToPOIRequest(saleToPOIRequest); + +TerminalAPIResponse terminalAPIResponse = terminalCloudApi.sync(terminalAPIRequest); +``` + +#### Optional: perform a status request + +To perform a [status request](https://docs.adyen.com/point-of-sale/basic-tapi-integration/verify-transaction-status/) you can use the following example: +``` java +TerminalAPIRequest terminalAPIRequest = new TerminalAPIRequest(); +SaleToPOIRequest saleToPOIRequest = new SaleToPOIRequest(); + +MessageHeader messageHeader = new MessageHeader(); +messageHeader.setMessageClass(MessageClassType.SERVICE); +messageHeader.setMessageCategory(MessageCategoryType.TRANSACTION_STATUS); +messageHeader.setMessageType(MessageType.REQUEST); +messageHeader.setProtocolVersion("3.0"); +messageHeader.setServiceID("Different service ID"); +messageHeader.setSaleID(saleID); +messageHeader.setPOIID(POIID); + +TransactionStatusRequest transactionStatusRequest = new TransactionStatusRequest(); +transactionStatusRequest.setReceiptReprintFlag(true); +transactionStatusRequest.getDocumentQualifier().add(DocumentQualifierType.CASHIER_RECEIPT); +transactionStatusRequest.getDocumentQualifier().add(DocumentQualifierType.CUSTOMER_RECEIPT); +MessageReference messageReference = new MessageReference(); +messageReference.setMessageCategory(MessageCategoryType.PAYMENT); +messageReference.setSaleID(saleID); +// serviceID of the transaction you want the status update from +messageReference.setServiceID(serviceID); +transactionStatusRequest.setMessageReference(messageReference); + +saleToPOIRequest.setTransactionStatusRequest(transactionStatusRequest); +saleToPOIRequest.setMessageHeader(messageHeader); + +terminalAPIRequest.setSaleToPOIRequest(saleToPOIRequest); + +TerminalAPIResponse terminalAPIResponse = terminalCloudApi.sync(terminalAPIRequest); +``` +### Helper classes + +Use `PredefinedContentHelper` to parse Display notification types which you find in `PredefinedContent->ReferenceID` +```java +PredefinedContentHelper helper = new PredefinedContentHelper(predefinedContent.getReferenceID()); + +// Safely extract and use the event type with Optional +helper.getEvent().ifPresent(event -> { + System.out.println("Received event: " + event); + if (event == PredefinedContentHelper.DisplayNotificationEvent.PIN_ENTERED) { + // Handle PIN entry event + System.out.println("The user has entered their PIN."); + } +}); +``` + +### Using the Local Terminal API Integration +The procedure to send In-Person requests using [Terminal API over Local Connection](https://docs.adyen.com/point-of-sale/design-your-integration/choose-your-architecture/local/) is similar to the Cloud Terminal API one, however, additional encryption details are required to perform the requests. Make sure to [install the certificate as described here](https://docs.adyen.com/point-of-sale/design-your-integration/choose-your-architecture/local/#protect-communications) +#### Local terminal API Using Keystore +```java +// Import the required classes +import com.adyen.Client; +import com.adyen.Config; +import com.adyen.enums.Environment; +import com.adyen.httpclient.TerminalLocalAPIHostnameVerifier; +import com.adyen.service.TerminalLocalAPI; +import com.adyen.model.terminal.security.*; +import com.adyen.model.terminal.*; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import java.security.KeyStore; +import java.security.SecureRandom; + +// Create a KeyStore for the terminal certificate +KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); +keyStore.load(null, null); +keyStore.setCertificateEntry("adyenRootCertificate", adyenRootCertificate); + +// Create a TrustManagerFactory that trusts the CAs in our KeyStore +TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); +trustManagerFactory.init(keyStore); + +// Create an SSLContext with the desired protocol that uses our TrustManagers +SSLContext sslContext = SSLContext.getInstance("SSL"); +sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom()); + +// Configure a client for TerminalLocalAPI +Config config = new Config(); +config.setEnvironment(environment); +config.setTerminalApiLocalEndpoint("https://" + terminalIpAddress); +config.setSSLContext(sslContext); +config.setHostnameVerifier(new TerminalLocalAPIHostnameVerifier(environment)); +Client client = new Client(config); + +// Create your SecurityKey object used for encrypting the payload (keyIdentifier/passphrase you set up beforehand in CA) +SecurityKey securityKey = new SecurityKey(); +securityKey.setKeyVersion(1); +securityKey.setAdyenCryptoVersion(1); +securityKey.setKeyIdentifier("keyIdentifier"); +securityKey.setPassphrase("passphrase"); + +// Use TerminalLocalAPI +TerminalLocalAPI terminalLocalAPI = new TerminalLocalAPI(client, securityKey); +TerminalAPIResponse terminalAPIResponse = terminalLocalAPI.request(terminalAPIRequest); +``` + +#### Using the Local Terminal API Integration without Encryption (Only on TEST) +If you wish to develop the Local Terminal API integration parallel to your encryption implementation, you can opt for the unencrypted version. Be sure to remove any encryption details from the CA terminal config page. Consider this ONLY for development and testing on localhost. + +```java +// Step 1: Import the required classes +import com.adyen.service.TerminalLocalAPI; +import com.adyen.model.nexo.*; +import com.adyen.model.terminal.*; +import javax.net.ssl.SSLContext; + +// Step 2: Add your Certificate Path and Local Endpoint to the config path. +Client client = new Client(); +client.getConfig().setTerminalApiLocalEndpoint("The IP of your terminal (eg https://192.168.47.169)"); +client.getConfig().setEnvironment(Environment.TEST); +config.setSSLContext(createTrustSSLContext()); // Trust all certificates for testing only +client.setConfig(config); + +// Step 3: Create an SSL context that accepts all certificates (Use in TEST only). +SSLContext createTrustSSLContext() throws Exception { + TrustManager[] trustAllCerts = new TrustManager[]{ + new X509TrustManager() { + java.security.cert.X509Certificate[] getAcceptedIssuers() { return null; } + checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {} + checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {} + } + }; + SSLContext sc = SSLContext.getInstance("SSL"); + sc.init(null, trustAllCerts, new java.security.SecureRandom()); + return sc; +} + +// Step 4: Construct a TerminalAPIRequest object +Gson gson = new GsonBuilder().create(); +TerminalAPIRequest terminalAPIPaymentRequest = new TerminalAPIRequest(); + +// Step 5: Make the request +TerminalAPIResponse terminalAPIResponse = terminalLocalAPI.request(terminalAPIRequest); +``` \ No newline at end of file diff --git a/src/main/java/com/adyen/model/clouddevice/AbortRequest.java b/src/main/java/com/adyen/model/clouddevice/AbortRequest.java new file mode 100644 index 000000000..80bddfa8d --- /dev/null +++ b/src/main/java/com/adyen/model/clouddevice/AbortRequest.java @@ -0,0 +1,101 @@ +package com.adyen.model.clouddevice; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * Definition: Body of the Abort Request messageType. -- Usage: It conveys Information requested for + * identification of the messageType request carrying the transaction to abort. A messageType to + * display on the CustomerError Device could be sent by the Sale System (DisplayOutput). + */ +public class AbortRequest { + + @JsonProperty("MessageReference") + @Schema(description = "Identification of a previous POI transaction.") + protected MessageReference messageReference; + + @JsonProperty("AbortReason") + @Schema(description = "Reason of aborting a transaction") + protected String abortReason; + + @JsonProperty("DisplayOutput") + @Schema( + description = + "Information to display and the way to process the display. --Rule: To display an abort message to the Customer") + protected DisplayOutput displayOutput; + + /** + * Gets the message reference. + * + * @return the message reference + */ + public MessageReference getMessageReference() { + return messageReference; + } + + /** + * Sets the message reference. + * + * @param value the message reference + */ + public void setMessageReference(MessageReference value) { + this.messageReference = value; + } + + /** + * Gets the abort reason. + * + * @return the abort reason + */ + public String getAbortReason() { + return abortReason; + } + + /** + * Sets the abort reason. + * + * @param value the abort reason + */ + public void setAbortReason(String value) { + this.abortReason = value; + } + + /** + * Gets the display output. + * + * @return the display output + */ + public DisplayOutput getDisplayOutput() { + return displayOutput; + } + + /** + * Sets the display output. + * + * @param value the display output + */ + public void setDisplayOutput(DisplayOutput value) { + this.displayOutput = value; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class AbortRequest {\n"); + sb.append(" messageReference: ").append(toIndentedString(messageReference)).append("\n"); + sb.append(" abortReason: ").append(toIndentedString(abortReason)).append("\n"); + sb.append(" displayOutput: ").append(toIndentedString(displayOutput)).append("\n"); + sb.append("}\n"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} diff --git a/src/main/java/com/adyen/model/clouddevice/AccountType.java b/src/main/java/com/adyen/model/clouddevice/AccountType.java new file mode 100644 index 000000000..3c08e01a5 --- /dev/null +++ b/src/main/java/com/adyen/model/clouddevice/AccountType.java @@ -0,0 +1,69 @@ +package com.adyen.model.clouddevice; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Arrays; + +/** Account type. */ +public enum AccountType { + + /** Default account */ + @Schema(description = "Default account") + DEFAULT("Default"), + + /** Savings account */ + @Schema(description = "Savings account") + SAVINGS("Savings"), + + /** Checking account */ + @Schema(description = "Checking account") + CHECKING("Checking"), + + /** Credit card account */ + @Schema(description = "Credit card account") + CREDIT_CARD("CreditCard"), + + /** Universal account */ + @Schema(description = "Universal account") + UNIVERSAL("Universal"), + + /** Investment account */ + @Schema(description = "Investment account") + INVESTMENT("Investment"), + + /** Card totals */ + @Schema(description = "Card totals") + CARD_TOTALS("CardTotals"), + + /** e-Purse card account */ + @Schema(description = "e-Purse card account") + EPURSE_CARD("EpurseCard"); + + private final String value; + + AccountType(String v) { + value = v; + } + + /** + * Returns the string representation of the AccountType. + * + * @return the string value + */ + @JsonValue + public String value() { + return value; + } + + /** + * Creates an AccountType from a string value. + * + * @param v the string value + * @return the corresponding AccountType + */ + @JsonCreator + public static AccountType fromValue(String v) { + return Arrays.stream(values()).filter(s -> s.value.equals(v)).findFirst().orElse(null); + } +} diff --git a/src/main/java/com/adyen/model/clouddevice/AdminRequest.java b/src/main/java/com/adyen/model/clouddevice/AdminRequest.java new file mode 100644 index 000000000..281f50456 --- /dev/null +++ b/src/main/java/com/adyen/model/clouddevice/AdminRequest.java @@ -0,0 +1,49 @@ +package com.adyen.model.clouddevice; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * Definition: Content of the Custom Admin Request messageType. -- Usage: Empty + * + *
Java class for AdminRequest complex type. + * + *
The following schema fragment specifies the expected content contained within this class. + * + *
+ * <complexType name="AdminRequest">
+ * <complexContent>
+ * <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+ * <sequence>
+ * <element name="ServiceIdentification" type="{}ServiceIdentification" minOccurs="0"/>
+ * </sequence>
+ * </restriction>
+ * </complexContent>
+ * </complexType>
+ *
+ */
+public class AdminRequest {
+
+ /** The Service identification. */
+ @JsonProperty("ServiceIdentification")
+ @Schema(description = "Identification of the administrative service to process.")
+ protected String serviceIdentification;
+
+ /**
+ * Gets the value of the serviceIdentification property.
+ *
+ * @return possible object is {@link String }
+ */
+ public String getServiceIdentification() {
+ return serviceIdentification;
+ }
+
+ /**
+ * Sets the value of the serviceIdentification property.
+ *
+ * @param value allowed object is {@link String }
+ */
+ public void setServiceIdentification(String value) {
+ this.serviceIdentification = value;
+ }
+}
diff --git a/src/main/java/com/adyen/model/clouddevice/AdminResponse.java b/src/main/java/com/adyen/model/clouddevice/AdminResponse.java
new file mode 100644
index 000000000..32a77e846
--- /dev/null
+++ b/src/main/java/com/adyen/model/clouddevice/AdminResponse.java
@@ -0,0 +1,57 @@
+package com.adyen.model.clouddevice;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+/**
+ * Content of the Custom Admin Response message. -- Usage: It conveys the result of the Custom
+ * Admin.
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonPropertyOrder({"Response"})
+public class AdminResponse {
+
+ /** The Response. */
+ @JsonProperty("Response")
+ @Schema(description = "Result of a message request processing.")
+ protected Response response;
+
+ /**
+ * Gets the value of the response property.
+ *
+ * @return possible object is {@link Response }
+ */
+ public Response getResponse() {
+ return response;
+ }
+
+ /**
+ * Sets the value of the response property.
+ *
+ * @param value allowed object is {@link Response }
+ */
+ public void setResponse(Response value) {
+ this.response = value;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("class AdminResponse {\n");
+ sb.append(" response: ").append(toIndentedString(response)).append("\n");
+ sb.append("}\n");
+ return sb.toString();
+ }
+
+ /**
+ * Convert the given object to string with each line indented by 4 spaces (except the first line).
+ */
+ private String toIndentedString(Object o) {
+ if (o == null) {
+ return "null";
+ }
+ return o.toString().replace("\n", "\n ");
+ }
+}
diff --git a/src/main/java/com/adyen/model/clouddevice/AlgorithmIdentifier.java b/src/main/java/com/adyen/model/clouddevice/AlgorithmIdentifier.java
new file mode 100644
index 000000000..327220aa9
--- /dev/null
+++ b/src/main/java/com/adyen/model/clouddevice/AlgorithmIdentifier.java
@@ -0,0 +1,53 @@
+package com.adyen.model.clouddevice;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Definition: Identification of a cryptographic algorithm -- Reference: RFC 3880: Internet X.509
+ * Public Key Infrastructure Certificate and Certificate -- Usage: This data structure contains: the
+ * algorithm identifier associated parameters
+ */
+public class AlgorithmIdentifier {
+
+ @JsonProperty("Parameter")
+ protected Parameter parameter;
+
+ @JsonProperty("Algorithm")
+ protected AlgorithmType algorithm;
+
+ /**
+ * Gets the value of the parameter property.
+ *
+ * @return possible object is {@link Parameter }
+ */
+ public Parameter getParameter() {
+ return parameter;
+ }
+
+ /**
+ * Sets the value of the parameter property.
+ *
+ * @param value allowed object is {@link Parameter }
+ */
+ public void setParameter(Parameter value) {
+ this.parameter = value;
+ }
+
+ /**
+ * Gets the value of the algorithm property.
+ *
+ * @return possible object is {@link AlgorithmType }
+ */
+ public AlgorithmType getAlgorithm() {
+ return algorithm;
+ }
+
+ /**
+ * Sets the value of the algorithm property.
+ *
+ * @param value allowed object is {@link AlgorithmType }
+ */
+ public void setAlgorithm(AlgorithmType value) {
+ this.algorithm = value;
+ }
+}
diff --git a/src/main/java/com/adyen/model/clouddevice/AlgorithmType.java b/src/main/java/com/adyen/model/clouddevice/AlgorithmType.java
new file mode 100644
index 000000000..9709f86b9
--- /dev/null
+++ b/src/main/java/com/adyen/model/clouddevice/AlgorithmType.java
@@ -0,0 +1,87 @@
+package com.adyen.model.clouddevice;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+import java.util.Arrays;
+
+/** Type of cryptographic algorithm */
+public enum AlgorithmType {
+
+ /**
+ * Retail CBC-MAC (cf. ISO 9807, ANSI X9.19) - (OID: iso(1) member-body(2) fr(250) type-org (1)
+ * gie-cb(79) algorithm(10) epas(1) 2)
+ */
+ ID_RETAIL_CBC_MAC("id-retail-cbc-mac"),
+
+ /**
+ * Retail-CBC-MAC with SHA-256 - (OID: iso(1) member-body(2) fr(250) type-org (1) gie-cb(79)
+ * algorithm(10) epas(1) 3)
+ */
+ ID_RETAIL_CBC_MAC_SHA_256("id-retail-cbc-mac-sha-256"),
+
+ /**
+ * The UKPT or Master Session Key key encryption - (OID: iso(1) member-body(2) fr(250) type-org
+ * (1) gie-cb(79) algorithm(10) epas(1) 4)
+ */
+ ID_UKPT_WRAP("id-ukpt-wrap "),
+
+ /**
+ * DUKPT is specified in ANS X9.24-2004, Annex A, and ISO/DIS 13492-2006. - (OID: iso(1)
+ * member-body(2) fr(250) type-org (1) gie-cb(79) algorithm(10) epas(1) 1)
+ */
+ ID_DUKPT_WRAP("id-dukpt-wrap"),
+
+ /**
+ * Triple DES ECB encryption with double length key (112 Bit) as defined in FIPS PUB 46-3 - (OID:
+ * iso(1) member-body(2) fr(250) type-org (1) gie-cb(79)
+ */
+ DES_EDE_3_ECB("des-ede3-ecb"),
+
+ /**
+ * Triple DES CBC encryption with double length key (112 Bit) as defined in FIPS PUB 46-3 - (OID:
+ * iso(1) member-body(2) us(840) rsadsi(113549)
+ */
+ DES_EDE_3_CBC("des-ede3-cbc"),
+
+ /** Message Digest Algorithm SHA-256 as defined in FIPS 180-1 and 2 - (ISO20022 Label: SHA256) */
+ ID_SHA_256("id-sha256"),
+
+ /**
+ * Signature Algorithms SHA-256 with RSA - (OID: iso(1) member-body(2) us(840) rsadsi(113549)
+ * pkcs(1) pkcs-1(1) 11)
+ */
+ SHA_256_WITH_RSA_ENCRYPTION("sha256WithRSAEncryption"),
+
+ /**
+ * Key Transport Algorithm RSA - (OID: iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1)
+ * pkcs-1(1) 1)
+ */
+ RSA_ENCRYPTION("rsaEncryption");
+
+ private final String value;
+
+ AlgorithmType(String v) {
+ value = v;
+ }
+
+ /**
+ * Returns the string representation of the AlgorithmType.
+ *
+ * @return the string value
+ */
+ @JsonValue
+ public String value() {
+ return value;
+ }
+
+ /**
+ * Creates an AlgorithmType from a string value.
+ *
+ * @param v the string value
+ * @return the corresponding AlgorithmType
+ */
+ @JsonCreator
+ public static AlgorithmType fromValue(String v) {
+ return Arrays.stream(values()).filter(s -> s.value.equals(v)).findFirst().orElse(null);
+ }
+}
diff --git a/src/main/java/com/adyen/model/clouddevice/AlignmentType.java b/src/main/java/com/adyen/model/clouddevice/AlignmentType.java
new file mode 100644
index 000000000..857e41540
--- /dev/null
+++ b/src/main/java/com/adyen/model/clouddevice/AlignmentType.java
@@ -0,0 +1,45 @@
+package com.adyen.model.clouddevice;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+import java.util.Arrays;
+
+/** Alignment of the text. */
+public enum AlignmentType {
+
+ /** Left alignment type. */
+ LEFT("Left"),
+ /** Right alignment type. */
+ RIGHT("Right"),
+ /** Centred alignment type. */
+ CENTRED("Centred"),
+ /** Justified alignment type. */
+ JUSTIFIED("Justified");
+
+ private final String value;
+
+ AlignmentType(String v) {
+ value = v;
+ }
+
+ /**
+ * Returns the string representation of the AlignmentType.
+ *
+ * @return the string value
+ */
+ @JsonValue
+ public String value() {
+ return value;
+ }
+
+ /**
+ * Creates an AlignmentType from a string value.
+ *
+ * @param v the string value
+ * @return the corresponding AlignmentType
+ */
+ @JsonCreator
+ public static AlignmentType fromValue(String v) {
+ return Arrays.stream(values()).filter(s -> s.value.equals(v)).findFirst().orElse(null);
+ }
+}
diff --git a/src/main/java/com/adyen/model/clouddevice/AllowedProduct.java b/src/main/java/com/adyen/model/clouddevice/AllowedProduct.java
new file mode 100644
index 000000000..a74ed21b8
--- /dev/null
+++ b/src/main/java/com/adyen/model/clouddevice/AllowedProduct.java
@@ -0,0 +1,98 @@
+package com.adyen.model.clouddevice;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+/** Product that are payable by the payment card. */
+@JsonPropertyOrder({"ProductLabel", "AdditionalProductInfo", "ProductCode", "EanUpc"})
+public class AllowedProduct {
+
+ @JsonProperty("ProductLabel")
+ @Schema(description = "Product name of an item purchased with the transaction.")
+ protected String productLabel;
+
+ @JsonProperty("AdditionalProductInfo")
+ @Schema(description = "Additionl information related to the line item.")
+ protected String additionalProductInfo;
+
+ @JsonProperty("ProductCode")
+ @Schema(description = "Product code of item purchased with the transaction.")
+ protected String productCode;
+
+ @JsonProperty("EanUpc")
+ @Schema(description = "Standard product code of item purchased with the transaction.")
+ protected String eanUpc;
+
+ /**
+ * Gets product label.
+ *
+ * @return the product label
+ */
+ public String getProductLabel() {
+ return productLabel;
+ }
+
+ /**
+ * Sets product label.
+ *
+ * @param productLabel the product label
+ */
+ public void setProductLabel(String productLabel) {
+ this.productLabel = productLabel;
+ }
+
+ /**
+ * Gets additional product info.
+ *
+ * @return the additional product info
+ */
+ public String getAdditionalProductInfo() {
+ return additionalProductInfo;
+ }
+
+ /**
+ * Sets additional product info.
+ *
+ * @param additionalProductInfo the additional product info
+ */
+ public void setAdditionalProductInfo(String additionalProductInfo) {
+ this.additionalProductInfo = additionalProductInfo;
+ }
+
+ /**
+ * Gets product code.
+ *
+ * @return the product code
+ */
+ public String getProductCode() {
+ return productCode;
+ }
+
+ /**
+ * Sets product code.
+ *
+ * @param productCode the product code
+ */
+ public void setProductCode(String productCode) {
+ this.productCode = productCode;
+ }
+
+ /**
+ * Gets ean upc.
+ *
+ * @return the ean upc
+ */
+ public String getEanUpc() {
+ return eanUpc;
+ }
+
+ /**
+ * Sets ean upc.
+ *
+ * @param eanUpc the ean upc
+ */
+ public void setEanUpc(String eanUpc) {
+ this.eanUpc = eanUpc;
+ }
+}
diff --git a/src/main/java/com/adyen/model/clouddevice/Amount.java b/src/main/java/com/adyen/model/clouddevice/Amount.java
new file mode 100644
index 000000000..40df98dc4
--- /dev/null
+++ b/src/main/java/com/adyen/model/clouddevice/Amount.java
@@ -0,0 +1,52 @@
+package com.adyen.model.clouddevice;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import java.math.BigDecimal;
+
+/** Common amount definition with currency. */
+@JsonPropertyOrder({"AmountValue", "Currency"})
+public class Amount {
+
+ @JsonProperty("AmountValue")
+ protected BigDecimal amountValue;
+
+ @JsonProperty("Currency")
+ protected String currency;
+
+ /**
+ * Gets amount value.
+ *
+ * @return the amount value
+ */
+ public BigDecimal getAmountValue() {
+ return amountValue;
+ }
+
+ /**
+ * Sets amount value.
+ *
+ * @param amountValue the amount value
+ */
+ public void setAmountValue(BigDecimal amountValue) {
+ this.amountValue = amountValue;
+ }
+
+ /**
+ * Gets currency.
+ *
+ * @return the currency
+ */
+ public String getCurrency() {
+ return currency;
+ }
+
+ /**
+ * Sets currency.
+ *
+ * @param currency the currency
+ */
+ public void setCurrency(String currency) {
+ this.currency = currency;
+ }
+}
diff --git a/src/main/java/com/adyen/model/clouddevice/AmountsReq.java b/src/main/java/com/adyen/model/clouddevice/AmountsReq.java
new file mode 100644
index 000000000..15b25b5de
--- /dev/null
+++ b/src/main/java/com/adyen/model/clouddevice/AmountsReq.java
@@ -0,0 +1,199 @@
+package com.adyen.model.clouddevice;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.math.BigDecimal;
+
+/** Various amounts related to the payment and loyalty request from the Sale System. */
+public class AmountsReq {
+
+ @JsonProperty("Currency")
+ @Schema(description = "Currency of a monetary amount.")
+ protected String currency;
+
+ @JsonProperty("RequestedAmount")
+ @Schema(
+ description =
+ "Amount requested by the Sale for the payment. --Rule: Absent if the maximum amount is unknown for a OneTimeReservationThe value has to be greater than 0.")
+ protected BigDecimal requestedAmount;
+
+ @JsonProperty("CashBackAmount")
+ @Schema(
+ description =
+ "The cash-back part of the amount requested by the Sale for the payment. --Rule: if payment with cash back. This data has to be performed by the POI. If cash back is not allowed, the payment is")
+ protected BigDecimal cashBackAmount;
+
+ @JsonProperty("TipAmount")
+ @Schema(
+ description =
+ "Amount paid for a tip. --Rule: If payment with tip requested by the Sale System.")
+ protected BigDecimal tipAmount;
+
+ @JsonProperty("PaidAmount")
+ @Schema(
+ description =
+ "Amount already paid amount in case of split payment. --Rule: If SplitPaymentFlag is True (split amount of a split payment).")
+ protected BigDecimal paidAmount;
+
+ @JsonProperty("MinimumAmountToDeliver")
+ @Schema(
+ description =
+ "Minimum amount the Sale System is allowed to deliver for this payment. --Rule: if unknown maximum amount for a OneTimeReservation or minimum amount requested by the Sale System")
+ protected BigDecimal minimumAmountToDeliver;
+
+ @JsonProperty("MaximumCashBackAmount")
+ @Schema(
+ description =
+ "Maximum amount which could be requested for cash-back to the Sale System. --Rule: Maximum amount which could be requested for cash-back to the Sale System.")
+ protected BigDecimal maximumCashBackAmount;
+
+ @JsonProperty("MinimumSplitAmount")
+ @Schema(
+ description =
+ "Minimum amount of a split, which could be requested by a Customer. --Rule: Minimum amount of a split, which could be requested.")
+ protected BigDecimal minimumSplitAmount;
+
+ /**
+ * Gets currency.
+ *
+ * @return the currency
+ */
+ public String getCurrency() {
+ return currency;
+ }
+
+ /**
+ * Sets currency.
+ *
+ * @param currency the currency
+ */
+ public void setCurrency(String currency) {
+ this.currency = currency;
+ }
+
+ /**
+ * Gets requested amount.
+ *
+ * @return the requested amount
+ */
+ public BigDecimal getRequestedAmount() {
+ return requestedAmount;
+ }
+
+ /**
+ * Sets requested amount.
+ *
+ * @param requestedAmount the requested amount
+ */
+ public void setRequestedAmount(BigDecimal requestedAmount) {
+ this.requestedAmount = requestedAmount;
+ }
+
+ /**
+ * Gets cash back amount.
+ *
+ * @return the cash back amount
+ */
+ public BigDecimal getCashBackAmount() {
+ return cashBackAmount;
+ }
+
+ /**
+ * Sets cash back amount.
+ *
+ * @param cashBackAmount the cash back amount
+ */
+ public void setCashBackAmount(BigDecimal cashBackAmount) {
+ this.cashBackAmount = cashBackAmount;
+ }
+
+ /**
+ * Gets tip amount.
+ *
+ * @return the tip amount
+ */
+ public BigDecimal getTipAmount() {
+ return tipAmount;
+ }
+
+ /**
+ * Sets tip amount.
+ *
+ * @param tipAmount the tip amount
+ */
+ public void setTipAmount(BigDecimal tipAmount) {
+ this.tipAmount = tipAmount;
+ }
+
+ /**
+ * Gets paid amount.
+ *
+ * @return the paid amount
+ */
+ public BigDecimal getPaidAmount() {
+ return paidAmount;
+ }
+
+ /**
+ * Sets paid amount.
+ *
+ * @param paidAmount the paid amount
+ */
+ public void setPaidAmount(BigDecimal paidAmount) {
+ this.paidAmount = paidAmount;
+ }
+
+ /**
+ * Gets minimum amount to deliver.
+ *
+ * @return the minimum amount to deliver
+ */
+ public BigDecimal getMinimumAmountToDeliver() {
+ return minimumAmountToDeliver;
+ }
+
+ /**
+ * Sets minimum amount to deliver.
+ *
+ * @param minimumAmountToDeliver the minimum amount to deliver
+ */
+ public void setMinimumAmountToDeliver(BigDecimal minimumAmountToDeliver) {
+ this.minimumAmountToDeliver = minimumAmountToDeliver;
+ }
+
+ /**
+ * Gets maximum cash back amount.
+ *
+ * @return the maximum cash back amount
+ */
+ public BigDecimal getMaximumCashBackAmount() {
+ return maximumCashBackAmount;
+ }
+
+ /**
+ * Sets maximum cash back amount.
+ *
+ * @param maximumCashBackAmount the maximum cash back amount
+ */
+ public void setMaximumCashBackAmount(BigDecimal maximumCashBackAmount) {
+ this.maximumCashBackAmount = maximumCashBackAmount;
+ }
+
+ /**
+ * Gets minimum split amount.
+ *
+ * @return the minimum split amount
+ */
+ public BigDecimal getMinimumSplitAmount() {
+ return minimumSplitAmount;
+ }
+
+ /**
+ * Sets minimum split amount.
+ *
+ * @param minimumSplitAmount the minimum split amount
+ */
+ public void setMinimumSplitAmount(BigDecimal minimumSplitAmount) {
+ this.minimumSplitAmount = minimumSplitAmount;
+ }
+}
diff --git a/src/main/java/com/adyen/model/clouddevice/AmountsResp.java b/src/main/java/com/adyen/model/clouddevice/AmountsResp.java
new file mode 100644
index 000000000..b0317968d
--- /dev/null
+++ b/src/main/java/com/adyen/model/clouddevice/AmountsResp.java
@@ -0,0 +1,147 @@
+package com.adyen.model.clouddevice;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.math.BigDecimal;
+
+/** Various amounts related to the payment response from the POI System. */
+public class AmountsResp {
+
+ @JsonProperty("Currency")
+ @Schema(description = "Currency of a monetary amount. --Rule: Mandatory for currency conversion.")
+ protected String currency;
+
+ @JsonProperty("AuthorizedAmount")
+ @Schema(description = "The amount authorized by the Acquirer for the payment transaction.")
+ protected BigDecimal authorizedAmount;
+
+ @JsonProperty("TotalRebatesAmount")
+ @Schema(
+ description =
+ "Sum of rebates in amount (total amount or line item amount) for all the loyalty programs.")
+ protected BigDecimal totalRebatesAmount;
+
+ @JsonProperty("TotalFeesAmount")
+ @Schema(description = "Total amount of financial fees.")
+ protected BigDecimal totalFeesAmount;
+
+ @JsonProperty("CashBackAmount")
+ @Schema(
+ description =
+ "The cash-back part of the amount requested by the Sale for the payment. --Rule: if payment with cash back")
+ protected BigDecimal cashBackAmount;
+
+ @JsonProperty("TipAmount")
+ @Schema(
+ description =
+ "Amount paid for a tip. --Rule: If payment with tip requested by the Sale System.")
+ protected BigDecimal tipAmount;
+
+ /**
+ * Gets currency.
+ *
+ * @return the currency
+ */
+ public String getCurrency() {
+ return currency;
+ }
+
+ /**
+ * Sets currency.
+ *
+ * @param currency the currency
+ */
+ public void setCurrency(String currency) {
+ this.currency = currency;
+ }
+
+ /**
+ * Gets authorized amount.
+ *
+ * @return the authorized amount
+ */
+ public BigDecimal getAuthorizedAmount() {
+ return authorizedAmount;
+ }
+
+ /**
+ * Sets authorized amount.
+ *
+ * @param authorizedAmount the authorized amount
+ */
+ public void setAuthorizedAmount(BigDecimal authorizedAmount) {
+ this.authorizedAmount = authorizedAmount;
+ }
+
+ /**
+ * Gets total rebates amount.
+ *
+ * @return the total rebates amount
+ */
+ public BigDecimal getTotalRebatesAmount() {
+ return totalRebatesAmount;
+ }
+
+ /**
+ * Sets total rebates amount.
+ *
+ * @param totalRebatesAmount the total rebates amount
+ */
+ public void setTotalRebatesAmount(BigDecimal totalRebatesAmount) {
+ this.totalRebatesAmount = totalRebatesAmount;
+ }
+
+ /**
+ * Gets total fees amount.
+ *
+ * @return the total fees amount
+ */
+ public BigDecimal getTotalFeesAmount() {
+ return totalFeesAmount;
+ }
+
+ /**
+ * Sets total fees amount.
+ *
+ * @param totalFeesAmount the total fees amount
+ */
+ public void setTotalFeesAmount(BigDecimal totalFeesAmount) {
+ this.totalFeesAmount = totalFeesAmount;
+ }
+
+ /**
+ * Gets cash back amount.
+ *
+ * @return the cash back amount
+ */
+ public BigDecimal getCashBackAmount() {
+ return cashBackAmount;
+ }
+
+ /**
+ * Sets cash back amount.
+ *
+ * @param cashBackAmount the cash back amount
+ */
+ public void setCashBackAmount(BigDecimal cashBackAmount) {
+ this.cashBackAmount = cashBackAmount;
+ }
+
+ /**
+ * Gets tip amount.
+ *
+ * @return the tip amount
+ */
+ public BigDecimal getTipAmount() {
+ return tipAmount;
+ }
+
+ /**
+ * Sets tip amount.
+ *
+ * @param tipAmount the tip amount
+ */
+ public void setTipAmount(BigDecimal tipAmount) {
+ this.tipAmount = tipAmount;
+ }
+}
diff --git a/src/main/java/com/adyen/model/clouddevice/AreaSize.java b/src/main/java/com/adyen/model/clouddevice/AreaSize.java
new file mode 100644
index 000000000..29b1e304a
--- /dev/null
+++ b/src/main/java/com/adyen/model/clouddevice/AreaSize.java
@@ -0,0 +1,52 @@
+package com.adyen.model.clouddevice;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+/** Size of an area. */
+public class AreaSize {
+
+ @JsonProperty("X")
+ @Schema(description = "Abscissa of a point coordinates.")
+ protected String x;
+
+ @JsonProperty("Y")
+ @Schema(description = "Ordinate of a point coordinates.")
+ protected String y;
+
+ /**
+ * Gets x.
+ *
+ * @return the x
+ */
+ public String getX() {
+ return x;
+ }
+
+ /**
+ * Sets x.
+ *
+ * @param x the x
+ */
+ public void setX(String x) {
+ this.x = x;
+ }
+
+ /**
+ * Gets y.
+ *
+ * @return the y
+ */
+ public String getY() {
+ return y;
+ }
+
+ /**
+ * Sets y.
+ *
+ * @param y the y
+ */
+ public void setY(String y) {
+ this.y = y;
+ }
+}
diff --git a/src/main/java/com/adyen/model/clouddevice/AttributeType.java b/src/main/java/com/adyen/model/clouddevice/AttributeType.java
new file mode 100644
index 000000000..f7d10b708
--- /dev/null
+++ b/src/main/java/com/adyen/model/clouddevice/AttributeType.java
@@ -0,0 +1,51 @@
+package com.adyen.model.clouddevice;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+import java.util.Arrays;
+
+/** Type of attribute of a distinguished name. */
+public enum AttributeType {
+
+ /** Common Name - (OID: joint-iso-ccitt(2) ds(5) 4 3) */
+ ID_AT_COMMON_NAME("id-at-commonName"),
+
+ /** Locality - (OID: joint-iso-ccitt(2) ds(5) 4 7) */
+ ID_AT_LOCALITY_NAME("id-at-localityName"),
+
+ /** Organization Name - (OID: joint-iso-ccitt(2) ds(5) 4 10) */
+ ID_AT_ORGANIZATION_NAME("id-at-organizationName"),
+
+ /** Organization Unit Name - (OID: joint-iso-ccitt(2) ds(5) 4 11) */
+ ID_AT_ORGANIZATIONAL_UNIT_NAME("id-at-organizationalUnitName"),
+
+ /** Country Name - (OID: joint-iso-ccitt(2) ds(5) 4 6) */
+ ID_AT_COUNTRY_NAME("id-at-countryName");
+
+ private final String value;
+
+ AttributeType(String v) {
+ value = v;
+ }
+
+ /**
+ * Returns the string representation of the AttributeType.
+ *
+ * @return the string value
+ */
+ @JsonValue
+ public String value() {
+ return value;
+ }
+
+ /**
+ * Creates an AttributeType from a string value.
+ *
+ * @param v the string value
+ * @return the corresponding AttributeType
+ */
+ @JsonCreator
+ public static AttributeType fromValue(String v) {
+ return Arrays.stream(values()).filter(s -> s.value.equals(v)).findFirst().orElse(null);
+ }
+}
diff --git a/src/main/java/com/adyen/model/clouddevice/AuthenticatedData.java b/src/main/java/com/adyen/model/clouddevice/AuthenticatedData.java
new file mode 100644
index 000000000..a2f3974c1
--- /dev/null
+++ b/src/main/java/com/adyen/model/clouddevice/AuthenticatedData.java
@@ -0,0 +1,128 @@
+package com.adyen.model.clouddevice;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Cryptographic Message Syntax (CMS) data structure containing MACed data with encryption key. */
+public class AuthenticatedData {
+
+ @JsonProperty("KeyTransport")
+ @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ protected ListThe following schema fragment specifies the expected content contained within this class. + * + *
+ * <simpleType name="GenericProfileType">
+ * <restriction base="{http://www.w3.org/2001/XMLSchema}string">
+ * <enumeration value="Basic"/>
+ * <enumeration value="Standard"/>
+ * <enumeration value="Extended"/>
+ * </restriction>
+ * </simpleType>
+ *
+ */
+@XmlType(name = "GenericProfileType")
+@XmlEnum
+public enum GenericProfileType {
+
+ /** Protocol services that needs to be implemented by all the Sale and POI */
+ @XmlEnumValue("Basic")
+ @Schema(description = "Protocol services that needs to be implemented by all the Sale and POI")
+ BASIC("Basic"),
+
+ /**
+ * Protocol services involving interaction between Sale System and POI System as devices shared
+ * between the two Systems.
+ */
+ @XmlEnumValue("Standard")
+ @Schema(
+ description =
+ "Protocol services involving interaction between Sale System and POI System as devices shared between the two Systems.")
+ STANDARD("Standard"),
+
+ /** Complete Protocol services */
+ @XmlEnumValue("Extended")
+ @Schema(description = "Complete Protocol services")
+ EXTENDED("Extended");
+ private final String value;
+
+ GenericProfileType(String v) {
+ value = v;
+ }
+
+ /**
+ * Value string.
+ *
+ * @return the string
+ */
+ public String value() {
+ return value;
+ }
+
+ /**
+ * Returns the enum constant matching the given string, or {@code null} if no match is found.
+ *
+ * @param v string value
+ * @return the GenericProfileType, or {@code null} if no match is found.
+ */
+ public static GenericProfileType fromValue(String v) {
+ return Arrays.stream(values()).filter(s -> s.value.equals(v)).findFirst().orElse(null);
+ }
+}
diff --git a/src/main/java/com/adyen/model/clouddevice/GeographicCoordinates.java b/src/main/java/com/adyen/model/clouddevice/GeographicCoordinates.java
new file mode 100644
index 000000000..2f4f8719d
--- /dev/null
+++ b/src/main/java/com/adyen/model/clouddevice/GeographicCoordinates.java
@@ -0,0 +1,57 @@
+package com.adyen.model.clouddevice;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+/** Location on the Earth specified by two numbers representing vertical and horizontal position. */
+@JsonPropertyOrder({"Latitude", "Longitude"})
+public class GeographicCoordinates {
+
+ @JsonProperty("Latitude")
+ @Schema(
+ description = "Angular distance of a location on the earth south or north of the equator.")
+ protected String latitude;
+
+ @JsonProperty("Longitude")
+ @Schema(
+ description =
+ "Angular measurement of the distance of a location on the earth east or west of the Greenwich observatory.")
+ protected String longitude;
+
+ /**
+ * Gets latitude.
+ *
+ * @return the latitude
+ */
+ public String getLatitude() {
+ return latitude;
+ }
+
+ /**
+ * Sets latitude.
+ *
+ * @param latitude the latitude
+ */
+ public void setLatitude(String latitude) {
+ this.latitude = latitude;
+ }
+
+ /**
+ * Gets longitude.
+ *
+ * @return the longitude
+ */
+ public String getLongitude() {
+ return longitude;
+ }
+
+ /**
+ * Sets longitude.
+ *
+ * @param longitude the longitude
+ */
+ public void setLongitude(String longitude) {
+ this.longitude = longitude;
+ }
+}
diff --git a/src/main/java/com/adyen/model/clouddevice/Geolocation.java b/src/main/java/com/adyen/model/clouddevice/Geolocation.java
new file mode 100644
index 000000000..51d6f6255
--- /dev/null
+++ b/src/main/java/com/adyen/model/clouddevice/Geolocation.java
@@ -0,0 +1,58 @@
+package com.adyen.model.clouddevice;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import io.swagger.v3.oas.annotations.media.Schema;
+
+/** Geographic location specified by geographic or UTM coordinates. */
+@JsonPropertyOrder({"GeographicCoordinates", "UTMCoordinates"})
+public class Geolocation {
+
+ @JsonProperty("GeographicCoordinates")
+ @Schema(
+ description =
+ "Location on the Earth specified by two numbers representing vertical and horizontal position.")
+ protected GeographicCoordinates geographicCoordinates;
+
+ @JsonProperty("UTMCoordinates")
+ @Schema(
+ description =
+ "Location on the Earth specified by the Universal Transverse Mercator coordinate system.")
+ protected UTMCoordinates utmCoordinates;
+
+ /**
+ * Gets geographic coordinates.
+ *
+ * @return the geographic coordinates
+ */
+ public GeographicCoordinates getGeographicCoordinates() {
+ return geographicCoordinates;
+ }
+
+ /**
+ * Sets geographic coordinates.
+ *
+ * @param geographicCoordinates the geographic coordinates
+ */
+ public void setGeographicCoordinates(GeographicCoordinates geographicCoordinates) {
+ this.geographicCoordinates = geographicCoordinates;
+ }
+
+ /**
+ * Gets utm coordinates.
+ *
+ * @return the utm coordinates
+ */
+ public UTMCoordinates getUTMCoordinates() {
+ return utmCoordinates;
+ }
+
+ /**
+ * Sets utm coordinates.
+ *
+ * @param utmCoordinates the utm coordinates
+ */
+ public void setUTMCoordinates(UTMCoordinates utmCoordinates) {
+ this.utmCoordinates = utmCoordinates;
+ }
+}
diff --git a/src/main/java/com/adyen/model/clouddevice/GetTotalsRequest.java b/src/main/java/com/adyen/model/clouddevice/GetTotalsRequest.java
new file mode 100644
index 000000000..dc14d27c0
--- /dev/null
+++ b/src/main/java/com/adyen/model/clouddevice/GetTotalsRequest.java
@@ -0,0 +1,61 @@
+package com.adyen.model.clouddevice;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Content of the Get Totals Request message. */
+@JsonPropertyOrder({"TotalDetails", "TotalFilter"})
+public class GetTotalsRequest {
+
+ @JsonProperty("TotalDetails")
+ @Schema(
+ description =
+ "Indicates the hierarchical structure of the reconciliation result of the Sale to POI reconciliation. --Rule: Require to present totals per value of element included in this cluster (POI Terminal, Sale Terminal, Cashier, Shift,")
+ protected ListJava class for Instalment complex type. + * + *
The following schema fragment specifies the expected content contained within this class. + * + *
+ * <complexType name="Instalment">
+ * <complexContent>
+ * <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+ * <attribute name="Instalment" type="{}InstalmentType" />
+ * <attribute name="SequenceNumber" type="{}SequenceNumber" />
+ * <attribute name="PlanID" type="{}PlanID" />
+ * <attribute name="Period" type="{}Period" />
+ * <attribute name="PeriodUnit" type="{}PeriodUnitType" />
+ * <attribute name="FirstPaymentDate" type="{}ISODate" />
+ * <attribute name="TotalNbOfPayments" type="{}TotalNbOfPayments" />
+ * <attribute name="CumulativeAmount" type="{}SimpleAmountType" />
+ * <attribute name="FirstAmount" type="{}SimpleAmountType" />
+ * <attribute name="Charges" type="{}SimpleAmountType" />
+ * </restriction>
+ * </complexContent>
+ * </complexType>
+ *
+ */
+@XmlAccessorType(XmlAccessType.FIELD)
+@XmlType(name = "Instalment")
+public class Instalment {
+
+ /** The Instalment type. */
+ @XmlElement(name = "InstalmentType")
+ @Schema(description = "Type of instalment transaction.")
+ protected InstalmentType instalmentType;
+
+ /** The Sequence number. */
+ @XmlElement(name = "SequenceNumber")
+ @Schema(description = "Sequence number of the instalment.")
+ protected BigInteger sequenceNumber;
+
+ /** The Plan id. */
+ @XmlElement(name = "PlanID")
+ @Schema(description = "Identification of an instalment plan.")
+ protected String planID;
+
+ /** The Period. */
+ @XmlElement(name = "Period")
+ @Schema(description = "Period of time with defined unit of time.")
+ protected BigInteger period;
+
+ /** The Period unit. */
+ @XmlElement(name = "PeriodUnit")
+ protected PeriodUnitType periodUnit;
+
+ /** The First payment date. */
+ @XmlElement(name = "FirstPaymentDate")
+ @Schema(description = "First date of a payment.")
+ protected String firstPaymentDate;
+
+ /** The Total nb of payments. */
+ @XmlElement(name = "TotalNbOfPayments")
+ @Schema(description = "Total number of payments.")
+ protected BigInteger totalNbOfPayments;
+
+ /** The Cumulative amount. */
+ @XmlElement(name = "CumulativeAmount")
+ @Schema(description = "Sum of a collection of amounts.")
+ protected BigDecimal cumulativeAmount;
+
+ /** The First amount. */
+ @XmlElement(name = "FirstAmount")
+ @Schema(description = "First amount of a payment.")
+ protected BigDecimal firstAmount;
+
+ /** The Charges. */
+ @XmlElement(name = "Charges")
+ @Schema(description = "Charges related to a transaction.")
+ protected BigDecimal charges;
+
+ /**
+ * Gets the value of the instalment property.
+ *
+ * @return possible object is {@link InstalmentType }
+ */
+ public InstalmentType getInstalmentType() {
+ return instalmentType;
+ }
+
+ /**
+ * Sets the value of the instalmentType property.
+ *
+ * @param value allowed object is {@link InstalmentType }
+ */
+ public void setInstalmentType(InstalmentType value) {
+ this.instalmentType = value;
+ }
+
+ /**
+ * Gets the value of the sequenceNumber property.
+ *
+ * @return possible object is {@link BigInteger }
+ */
+ public BigInteger getSequenceNumber() {
+ return sequenceNumber;
+ }
+
+ /**
+ * Sets the value of the sequenceNumber property.
+ *
+ * @param value allowed object is {@link BigInteger }
+ */
+ public void setSequenceNumber(BigInteger value) {
+ this.sequenceNumber = value;
+ }
+
+ /**
+ * Gets the value of the planID property.
+ *
+ * @return possible object is {@link String }
+ */
+ public String getPlanID() {
+ return planID;
+ }
+
+ /**
+ * Sets the value of the planID property.
+ *
+ * @param value allowed object is {@link String }
+ */
+ public void setPlanID(String value) {
+ this.planID = value;
+ }
+
+ /**
+ * Gets the value of the period property.
+ *
+ * @return possible object is {@link BigInteger }
+ */
+ public BigInteger getPeriod() {
+ return period;
+ }
+
+ /**
+ * Sets the value of the period property.
+ *
+ * @param value allowed object is {@link BigInteger }
+ */
+ public void setPeriod(BigInteger value) {
+ this.period = value;
+ }
+
+ /**
+ * Gets the value of the periodUnit property.
+ *
+ * @return possible object is {@link PeriodUnitType }
+ */
+ public PeriodUnitType getPeriodUnit() {
+ return periodUnit;
+ }
+
+ /**
+ * Sets the value of the periodUnit property.
+ *
+ * @param value allowed object is {@link PeriodUnitType }
+ */
+ public void setPeriodUnit(PeriodUnitType value) {
+ this.periodUnit = value;
+ }
+
+ /**
+ * Gets the value of the firstPaymentDate property.
+ *
+ * @return possible object is {@link String }
+ */
+ public String getFirstPaymentDate() {
+ return firstPaymentDate;
+ }
+
+ /**
+ * Sets the value of the firstPaymentDate property.
+ *
+ * @param value allowed object is {@link String }
+ */
+ public void setFirstPaymentDate(String value) {
+ this.firstPaymentDate = value;
+ }
+
+ /**
+ * Gets the value of the totalNbOfPayments property.
+ *
+ * @return possible object is {@link BigInteger }
+ */
+ public BigInteger getTotalNbOfPayments() {
+ return totalNbOfPayments;
+ }
+
+ /**
+ * Sets the value of the totalNbOfPayments property.
+ *
+ * @param value allowed object is {@link BigInteger }
+ */
+ public void setTotalNbOfPayments(BigInteger value) {
+ this.totalNbOfPayments = value;
+ }
+
+ /**
+ * Gets the value of the cumulativeAmount property.
+ *
+ * @return possible object is {@link BigDecimal }
+ */
+ public BigDecimal getCumulativeAmount() {
+ return cumulativeAmount;
+ }
+
+ /**
+ * Sets the value of the cumulativeAmount property.
+ *
+ * @param value allowed object is {@link BigDecimal }
+ */
+ public void setCumulativeAmount(BigDecimal value) {
+ this.cumulativeAmount = value;
+ }
+
+ /**
+ * Gets the value of the firstAmount property.
+ *
+ * @return possible object is {@link BigDecimal }
+ */
+ public BigDecimal getFirstAmount() {
+ return firstAmount;
+ }
+
+ /**
+ * Sets the value of the firstAmount property.
+ *
+ * @param value allowed object is {@link BigDecimal }
+ */
+ public void setFirstAmount(BigDecimal value) {
+ this.firstAmount = value;
+ }
+
+ /**
+ * Gets the value of the charges property.
+ *
+ * @return possible object is {@link BigDecimal }
+ */
+ public BigDecimal getCharges() {
+ return charges;
+ }
+
+ /**
+ * Sets the value of the charges property.
+ *
+ * @param value allowed object is {@link BigDecimal }
+ */
+ public void setCharges(BigDecimal value) {
+ this.charges = value;
+ }
+}
diff --git a/src/main/java/com/adyen/model/clouddevice/InstalmentType.java b/src/main/java/com/adyen/model/clouddevice/InstalmentType.java
new file mode 100644
index 000000000..d1f76e8db
--- /dev/null
+++ b/src/main/java/com/adyen/model/clouddevice/InstalmentType.java
@@ -0,0 +1,47 @@
+package com.adyen.model.clouddevice;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.util.Arrays;
+
+public enum InstalmentType {
+
+ /** The payment of the service or goods is deferred. */
+ @Schema(description = "The payment of the service or goods is deferred.")
+ DEFERRED_INSTALMENTS("DeferredInstalments"),
+
+ /** The payment is split in several instalments of equal amounts. */
+ @Schema(description = "The payment is split in several instalments of equal amounts.")
+ EQUAL_INSTALMENTS("EqualInstalments"),
+
+ /** The payment is split in several instalments of different amounts. */
+ @Schema(description = "The payment is split in several instalments of different amounts.")
+ INEQUAL_INSTALMENTS("InequalInstalments");
+ private final String value;
+
+ InstalmentType(String v) {
+ value = v;
+ }
+
+ /**
+ * Value string.
+ *
+ * @return the string
+ */
+ @JsonValue
+ public String value() {
+ return value;
+ }
+
+ /**
+ * Returns the enum constant matching the given string, or {@code null} if no match is found.
+ *
+ * @param v string value
+ * @return the InstalmentType, or {@code null} if no match is found.
+ */
+ @JsonCreator
+ public static InstalmentType fromValue(String v) {
+ return Arrays.stream(values()).filter(s -> s.value.equals(v)).findFirst().orElse(null);
+ }
+}
diff --git a/src/main/java/com/adyen/model/clouddevice/Issuer.java b/src/main/java/com/adyen/model/clouddevice/Issuer.java
new file mode 100644
index 000000000..1091ded53
--- /dev/null
+++ b/src/main/java/com/adyen/model/clouddevice/Issuer.java
@@ -0,0 +1,36 @@
+package com.adyen.model.clouddevice;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Certificate issuer name (see X.501-88). */
+@JsonPropertyOrder({"RelativeDistinguishedName"})
+public class Issuer {
+
+ @JsonProperty("RelativeDistinguishedName")
+ protected ListJava class for PaymentToken complex type. + * + *
The following schema fragment specifies the expected content contained within this class. + * + *
+ * <complexType name="PaymentToken">
+ * <complexContent>
+ * <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+ * <attribute name="TokenRequested" use="required" type="{}TokenRequestedType" />
+ * <attribute name="TokenValue" use="required" type="{}TokenValue" />
+ * <attribute name="ExpiryDateTime" type="{}ExpiryDateTime" />
+ * </restriction>
+ * </complexContent>
+ * </complexType>
+ *
+ */
+@XmlAccessorType(XmlAccessType.FIELD)
+@XmlType(name = "PaymentToken")
+public class PaymentToken {
+
+ /** The Token requested. */
+ @XmlElement(name = "TokenRequestedType", required = true)
+ @Schema(
+ description =
+ "Type of token replacing the PAN of a payment card to identify the payment mean of the customer.")
+ protected TokenRequestedType tokenRequestedType;
+
+ /** The Token value. */
+ @XmlElement(name = "TokenValue", required = true)
+ @Schema(
+ description =
+ "Payment token replacing the PAN of the payment card to identify the payment mean of the customer.")
+ protected String tokenValue;
+
+ /** The Expiry date time. */
+ @XmlElement(name = "ExpiryDateTime")
+ @Schema(description = "Expiry date and time.")
+ protected XMLGregorianCalendar expiryDateTime;
+
+ /**
+ * Gets the value of the tokenRequestedType property.
+ *
+ * @return possible object is {@link TokenRequestedType }
+ */
+ public TokenRequestedType getTokenRequestedType() {
+ return tokenRequestedType;
+ }
+
+ /**
+ * Sets the value of the tokenRequestedType property.
+ *
+ * @param value allowed object is {@link TokenRequestedType }
+ */
+ public void setTokenRequestedType(TokenRequestedType value) {
+ this.tokenRequestedType = value;
+ }
+
+ /**
+ * Gets the value of the tokenValue property.
+ *
+ * @return possible object is {@link String }
+ */
+ public String getTokenValue() {
+ return tokenValue;
+ }
+
+ /**
+ * Sets the value of the tokenValue property.
+ *
+ * @param value allowed object is {@link String }
+ */
+ public void setTokenValue(String value) {
+ this.tokenValue = value;
+ }
+
+ /**
+ * Gets the value of the expiryDateTime property.
+ *
+ * @return possible object is {@link XMLGregorianCalendar }
+ */
+ public XMLGregorianCalendar getExpiryDateTime() {
+ return expiryDateTime;
+ }
+
+ /**
+ * Sets the value of the expiryDateTime property.
+ *
+ * @param value allowed object is {@link XMLGregorianCalendar }
+ */
+ public void setExpiryDateTime(XMLGregorianCalendar value) {
+ this.expiryDateTime = value;
+ }
+}
diff --git a/src/main/java/com/adyen/model/clouddevice/PaymentTotals.java b/src/main/java/com/adyen/model/clouddevice/PaymentTotals.java
new file mode 100644
index 000000000..891ca1bf0
--- /dev/null
+++ b/src/main/java/com/adyen/model/clouddevice/PaymentTotals.java
@@ -0,0 +1,80 @@
+package com.adyen.model.clouddevice;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+/** Totals of the payment transaction during the reconciliation period. */
+@JsonPropertyOrder({"TransactionType", "TransactionCount", "TransactionAmount"})
+public class PaymentTotals {
+
+ @JsonProperty("TransactionType")
+ @Schema(
+ description =
+ "Type of transaction for which totals are grouped. --Rule: Debit, Credit, ReverseDebit, ReverseCredit, OneTimeReservation, CompletedDeffered, FirstReservation, UpdateReservation,")
+ protected TransactionType transactionType;
+
+ @JsonProperty("TransactionCount")
+ @Schema(description = "Number of processed transaction during the period.")
+ protected BigInteger transactionCount;
+
+ @JsonProperty("TransactionAmount")
+ @Schema(description = "Sum of amount of processed transaction during the period.")
+ protected BigDecimal transactionAmount;
+
+ /**
+ * Gets transaction type.
+ *
+ * @return the transaction type
+ */
+ public TransactionType getTransactionType() {
+ return transactionType;
+ }
+
+ /**
+ * Sets transaction type.
+ *
+ * @param transactionType the transaction type
+ */
+ public void setTransactionType(TransactionType transactionType) {
+ this.transactionType = transactionType;
+ }
+
+ /**
+ * Gets transaction count.
+ *
+ * @return the transaction count
+ */
+ public BigInteger getTransactionCount() {
+ return transactionCount;
+ }
+
+ /**
+ * Sets transaction count.
+ *
+ * @param transactionCount the transaction count
+ */
+ public void setTransactionCount(BigInteger transactionCount) {
+ this.transactionCount = transactionCount;
+ }
+
+ /**
+ * Gets transaction amount.
+ *
+ * @return the transaction amount
+ */
+ public BigDecimal getTransactionAmount() {
+ return transactionAmount;
+ }
+
+ /**
+ * Sets transaction amount.
+ *
+ * @param transactionAmount the transaction amount
+ */
+ public void setTransactionAmount(BigDecimal transactionAmount) {
+ this.transactionAmount = transactionAmount;
+ }
+}
diff --git a/src/main/java/com/adyen/model/clouddevice/PaymentTransaction.java b/src/main/java/com/adyen/model/clouddevice/PaymentTransaction.java
new file mode 100644
index 000000000..b407b57b5
--- /dev/null
+++ b/src/main/java/com/adyen/model/clouddevice/PaymentTransaction.java
@@ -0,0 +1,111 @@
+package com.adyen.model.clouddevice;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Data related to the payment and loyalty transaction. */
+@JsonPropertyOrder({"AmountsReq", "OriginalPOITransaction", "TransactionConditions", "SaleItem"})
+public class PaymentTransaction {
+
+ @JsonProperty("AmountsReq")
+ @Schema(
+ description =
+ "Various amounts related to the payment and loyalty request from the Sale System.")
+ protected AmountsReq amountsReq;
+
+ @JsonProperty("OriginalPOITransaction")
+ @Schema(
+ description =
+ "Identification of a previous POI transaction. --Rule: if UpdateReservation, Completion or Refund")
+ protected OriginalPOITransaction originalPOITransaction;
+
+ @JsonProperty("TransactionConditions")
+ @Schema(
+ description =
+ "Conditions on which the transaction must be processed. --Rule: If one data element is present")
+ protected TransactionConditions transactionConditions;
+
+ @JsonProperty("SaleItem")
+ @Schema(
+ description =
+ "Sale items of a transaction. --Rule: If purchased products are required for the payment")
+ protected List{@code
+ * PredefinedContentHelper helper = new PredefinedContentHelper("...&event=PIN_ENTERED");
+ * helper.getEvent().ifPresent(event -> System.out.println(event)); // Prints PIN_ENTERED
+ * }
+ */
+ public Optional+ * + *
This accessor method returns a reference to the live list, not a snapshot. Therefore any
+ * modification you make to the returned list will be present inside the JAXB object. This is why
+ * there is not a set method for the digestAlgorithm property.
+ *
+ *
+ * + *
For example, to add a new item, do as follows: + * + *
+ * getDigestAlgorithm().add(newItem); + *+ * + *
+ * + *
+ * + *
Objects of the following type(s) are allowed in the list {@link AlgorithmIdentifier }
+ *
+ * @return the digest algorithm
+ */
+ public List
+ *
+ * This accessor method returns a reference to the live list, not a snapshot. Therefore any
+ * modification you make to the returned list will be present inside the JAXB object. This is why
+ * there is not a
+ *
+ * For example, to add a new item, do as follows:
+ *
+ *
+ *
+ *
+ *
+ * Objects of the following type(s) are allowed in the list byte[]
+ *
+ * @return the certificate
+ */
+ public List
+ *
+ * This accessor method returns a reference to the live list, not a snapshot. Therefore any
+ * modification you make to the returned list will be present inside the JAXB object. This is why
+ * there is not a
+ *
+ * For example, to add a new item, do as follows:
+ *
+ *
+ *
+ *
+ *
+ * Objects of the following type(s) are allowed in the list {@link Signer }
+ *
+ * @return the signer
+ */
+ public List This accessor method returns a reference to the live list, not a snapshot. Therefore any
+ * modification you make to the returned list will be present inside the JAXB object. This is why
+ * there is not a For example, to add a new item, do as follows:
+ *
+ * Objects of the following type(s) are allowed in the list {@link EntryModeType }
+ */
+ public List
+ *
+ *
+ *
+ *
+ *
+ * This accessor method returns a reference to the live list, not a snapshot. Therefore any
+ * modification you make to the returned list will be present inside the JAXB object. This is why
+ * there is not a
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * Objects of the following type(s) are allowed in the list {@link StoredValueData }
+ *
+ * @return the stored value data
+ */
+ public List
+ *
+ * This accessor method returns a reference to the live list, not a snapshot. Therefore any
+ * modification you make to the returned list will be present inside the JAXB object. This is why
+ * there is not a
+ *
+ * For example, to add a new item, do as follows:
+ *
+ *
+ *
+ *
+ *
+ * Objects of the following type(s) are allowed in the list {@link StoredValueResult }
+ *
+ * @return the stored value result
+ */
+ public List
+ *
+ * This accessor method returns a reference to the live list, not a snapshot. Therefore any
+ * modification you make to the returned list will be present inside the JAXB object. This is why
+ * there is not a
+ *
+ * For example, to add a new item, do as follows:
+ *
+ *
+ *
+ *
+ *
+ * Objects of the following type(s) are allowed in the list {@link String }
+ *
+ * @return the allowed payment brand
+ */
+ public List
+ *
+ * This accessor method returns a reference to the live list, not a snapshot. Therefore any
+ * modification you make to the returned list will be present inside the JAXB object. This is why
+ * there is not a
+ *
+ * For example, to add a new item, do as follows:
+ *
+ *
+ *
+ *
+ *
+ * Objects of the following type(s) are allowed in the list {@link String }
+ *
+ * @return the acquirer id
+ */
+ public List
+ *
+ * This accessor method returns a reference to the live list, not a snapshot. Therefore any
+ * modification you make to the returned list will be present inside the JAXB object. This is why
+ * there is not a
+ *
+ * For example, to add a new item, do as follows:
+ *
+ *
+ *
+ *
+ *
+ * Objects of the following type(s) are allowed in the list {@link String }
+ *
+ * @return the allowed loyalty brand
+ */
+ public List
+ *
+ * This accessor method returns a reference to the live list, not a snapshot. Therefore any
+ * modification you make to the returned list will be present inside the JAXB object. This is why
+ * there is not a
+ *
+ * For example, to add a new item, do as follows:
+ *
+ *
+ *
+ *
+ *
+ * Objects of the following type(s) are allowed in the list {@link ForceEntryModeType }
+ *
+ * @return the force entry mode
+ */
+ public List
+ *
+ * This accessor method returns a reference to the live list, not a snapshot. Therefore any
+ * modification you make to the returned list will be present inside the JAXB object. This is why
+ * there is not a
+ *
+ * For example, to add a new item, do as follows:
+ *
+ *
+ *
+ *
+ *
+ * Objects of the following type(s) are allowed in the list {@link DocumentQualifierType }
+ *
+ * @return the document qualifier
+ */
+ public List
+ *
+ * This accessor method returns a reference to the live list, not a snapshot. Therefore any
+ * modification you make to the returned list will be present inside the JAXB object. This is why
+ * there is not a
+ *
+ * For example, to add a new item, do as follows:
+ *
+ *
+ *
+ *
+ *
+ * Objects of the following type(s) are allowed in the list {@link PaymentTotals }
+ *
+ * @return the payment totals
+ */
+ public List
+ *
+ * This accessor method returns a reference to the live list, not a snapshot. Therefore any
+ * modification you make to the returned list will be present inside the JAXB object. This is why
+ * there is not a
+ *
+ * For example, to add a new item, do as follows:
+ *
+ *
+ *
+ *
+ *
+ * Objects of the following type(s) are allowed in the list {@link LoyaltyTotals }
+ *
+ * @return the loyalty totals
+ */
+ public List Nexo derived keys is a 80 byte struct containing key data: a 32 byte cipher key, a 32 byte
+ * HMAC key and a 16 byte initialization vector (IV). These 80 bytes are derived from a passphrase.
+ */
+public class NexoDerivedKey {
+
+ public static final int NEXO_HMAC_KEY_LENGTH = 32;
+ public static final int NEXO_CIPHER_KEY_LENGTH = 32;
+ public static final int NEXO_IV_LENGTH = 16;
+
+ private byte[] hmacKey;
+ private byte[] cipherKey;
+ private byte[] iv;
+
+ public NexoDerivedKey() {
+ hmacKey = new byte[NEXO_HMAC_KEY_LENGTH];
+ cipherKey = new byte[NEXO_CIPHER_KEY_LENGTH];
+ iv = new byte[NEXO_IV_LENGTH];
+ }
+
+ public byte[] getHmacKey() {
+ return hmacKey;
+ }
+
+ public void setHmacKey(byte[] hmacKey) {
+ this.hmacKey = hmacKey;
+ }
+
+ public byte[] getCipherKey() {
+ return cipherKey;
+ }
+
+ public void setCipherKey(byte[] cipherKey) {
+ this.cipherKey = cipherKey;
+ }
+
+ public byte[] getIv() {
+ return iv;
+ }
+
+ public void setIv(byte[] iv) {
+ this.iv = iv;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ NexoDerivedKey that = (NexoDerivedKey) o;
+ return Arrays.equals(hmacKey, that.hmacKey)
+ && Arrays.equals(cipherKey, that.cipherKey)
+ && Arrays.equals(iv, that.iv);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = Arrays.hashCode(hmacKey);
+ result = 31 * result + Arrays.hashCode(cipherKey);
+ result = 31 * result + Arrays.hashCode(iv);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "NexoDerivedKey{"
+ + "hmacKey="
+ + Arrays.toString(hmacKey)
+ + ", cipherKey="
+ + Arrays.toString(cipherKey)
+ + ", iv="
+ + Arrays.toString(iv)
+ + '}';
+ }
+}
diff --git a/src/main/java/com/adyen/model/clouddevice/security/SecurityTrailer.java b/src/main/java/com/adyen/model/clouddevice/security/SecurityTrailer.java
new file mode 100644
index 000000000..3aa137bb2
--- /dev/null
+++ b/src/main/java/com/adyen/model/clouddevice/security/SecurityTrailer.java
@@ -0,0 +1,124 @@
+/*
+ * ######
+ * ######
+ * ############ ####( ###### #####. ###### ############ ############
+ * ############# #####( ###### #####. ###### ############# #############
+ * ###### #####( ###### #####. ###### ##### ###### ##### ######
+ * ###### ###### #####( ###### #####. ###### ##### ##### ##### ######
+ * ###### ###### #####( ###### #####. ###### ##### ##### ######
+ * ############# ############# ############# ############# ##### ######
+ * ############ ############ ############# ############ ##### ######
+ * ######
+ * #############
+ * ############
+ *
+ * Adyen Java API Library
+ *
+ * Copyright (c) 2019 Adyen B.V.
+ * This file is open source and available under the MIT license.
+ * See the LICENSE file for more info.
+ */
+
+package com.adyen.model.clouddevice.security;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.Arrays;
+import java.util.Objects;
+
+public class SecurityTrailer {
+ @JsonProperty("AdyenCryptoVersion")
+ private Integer adyenCryptoVersion;
+
+ @JsonProperty("KeyIdentifier")
+ private String keyIdentifier;
+
+ @JsonProperty("KeyVersion")
+ private Integer keyVersion;
+
+ @JsonProperty("Nonce")
+ private byte[] nonce;
+
+ @JsonProperty("Hmac")
+ private byte[] hmac;
+
+ public Integer getAdyenCryptoVersion() {
+ return adyenCryptoVersion;
+ }
+
+ public void setAdyenCryptoVersion(Integer adyenCryptoVersion) {
+ this.adyenCryptoVersion = adyenCryptoVersion;
+ }
+
+ public String getKeyIdentifier() {
+ return keyIdentifier;
+ }
+
+ public void setKeyIdentifier(String keyIdentifier) {
+ this.keyIdentifier = keyIdentifier;
+ }
+
+ public Integer getKeyVersion() {
+ return keyVersion;
+ }
+
+ public void setKeyVersion(Integer keyVersion) {
+ this.keyVersion = keyVersion;
+ }
+
+ public byte[] getNonce() {
+ return nonce;
+ }
+
+ public void setNonce(byte[] nonce) {
+ this.nonce = nonce;
+ }
+
+ public byte[] getHmac() {
+ return hmac;
+ }
+
+ public void setHmac(byte[] hmac) {
+ this.hmac = hmac;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ SecurityTrailer that = (SecurityTrailer) o;
+ return adyenCryptoVersion.equals(that.adyenCryptoVersion)
+ && keyIdentifier.equals(that.keyIdentifier)
+ && keyVersion.equals(that.keyVersion)
+ && Arrays.equals(nonce, that.nonce)
+ && Arrays.equals(hmac, that.hmac);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = Objects.hash(adyenCryptoVersion, keyIdentifier, keyVersion);
+ result = 31 * result + Arrays.hashCode(nonce);
+ result = 31 * result + Arrays.hashCode(hmac);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "SecurityTrailer{"
+ + "adyenCryptoVersion="
+ + adyenCryptoVersion
+ + ", keyIdentifier='"
+ + keyIdentifier
+ + '\''
+ + ", keyVersion="
+ + keyVersion
+ + ", nonce="
+ + Arrays.toString(nonce)
+ + ", hmac="
+ + Arrays.toString(hmac)
+ + '}';
+ }
+}
diff --git a/src/main/java/com/adyen/security/clouddevice/EncryptionCredentialDetails.java b/src/main/java/com/adyen/security/clouddevice/EncryptionCredentialDetails.java
new file mode 100644
index 000000000..bdb3245d9
--- /dev/null
+++ b/src/main/java/com/adyen/security/clouddevice/EncryptionCredentialDetails.java
@@ -0,0 +1,112 @@
+package com.adyen.security.clouddevice;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.Objects;
+
+/** Details of the encryption credential used for encrypting the request payload (nexoBlob) */
+public class EncryptionCredentialDetails {
+
+ /** The passphrase used to derive the encryption key. */
+ @JsonProperty("Passphrase")
+ private String passphrase;
+
+ /** The unique identifier of the key. */
+ @JsonProperty("KeyIdentifier")
+ private String keyIdentifier;
+
+ /** The version of the key. */
+ @JsonProperty("KeyVersion")
+ private Integer keyVersion;
+
+ /** The version of the Adyen-specific crypto implementation. */
+ @JsonProperty("AdyenCryptoVersion")
+ private Integer adyenCryptoVersion;
+
+ public String getPassphrase() {
+ return passphrase;
+ }
+
+ public void setPassphrase(String passphrase) {
+ this.passphrase = passphrase;
+ }
+
+ public String getKeyIdentifier() {
+ return keyIdentifier;
+ }
+
+ public void setKeyIdentifier(String keyIdentifier) {
+ this.keyIdentifier = keyIdentifier;
+ }
+
+ public Integer getKeyVersion() {
+ return keyVersion;
+ }
+
+ public void setKeyVersion(Integer keyVersion) {
+ this.keyVersion = keyVersion;
+ }
+
+ public Integer getAdyenCryptoVersion() {
+ return adyenCryptoVersion;
+ }
+
+ public void setAdyenCryptoVersion(Integer adyenCryptoVersion) {
+ this.adyenCryptoVersion = adyenCryptoVersion;
+ }
+
+ public EncryptionCredentialDetails passphrase(String passphrase) {
+ this.passphrase = passphrase;
+ return this;
+ }
+
+ public EncryptionCredentialDetails keyIdentifier(String keyIdentifier) {
+ this.keyIdentifier = keyIdentifier;
+ return this;
+ }
+
+ public EncryptionCredentialDetails keyVersion(Integer keyVersion) {
+ this.keyVersion = keyVersion;
+ return this;
+ }
+
+ public EncryptionCredentialDetails adyenCryptoVersion(Integer adyenCryptoVersion) {
+ this.adyenCryptoVersion = adyenCryptoVersion;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ EncryptionCredentialDetails that = (EncryptionCredentialDetails) o;
+ return Objects.equals(passphrase, that.passphrase)
+ && Objects.equals(keyIdentifier, that.keyIdentifier)
+ && Objects.equals(keyVersion, that.keyVersion)
+ && Objects.equals(adyenCryptoVersion, that.adyenCryptoVersion);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(passphrase, keyIdentifier, keyVersion, adyenCryptoVersion);
+ }
+
+ @Override
+ public String toString() {
+ return "EncryptionCredentialDetails{"
+ + "passphrase='"
+ + passphrase
+ + '\''
+ + ", keyIdentifier='"
+ + keyIdentifier
+ + '\''
+ + ", keyVersion="
+ + keyVersion
+ + ", adyenCryptoVersion="
+ + adyenCryptoVersion
+ + '}';
+ }
+}
diff --git a/src/main/java/com/adyen/security/clouddevice/NexoDerivedKeyGenerator.java b/src/main/java/com/adyen/security/clouddevice/NexoDerivedKeyGenerator.java
new file mode 100644
index 000000000..2922d57c6
--- /dev/null
+++ b/src/main/java/com/adyen/security/clouddevice/NexoDerivedKeyGenerator.java
@@ -0,0 +1,73 @@
+package com.adyen.security.clouddevice;
+
+/*
+ * ######
+ * ######
+ * ############ ####( ###### #####. ###### ############ ############
+ * ############# #####( ###### #####. ###### ############# #############
+ * ###### #####( ###### #####. ###### ##### ###### ##### ######
+ * ###### ###### #####( ###### #####. ###### ##### ##### ##### ######
+ * ###### ###### #####( ###### #####. ###### ##### ##### ######
+ * ############# ############# ############# ############# ##### ######
+ * ############ ############ ############# ############ ##### ######
+ * ######
+ * #############
+ * ############
+ *
+ * Adyen Java API Library
+ *
+ * Copyright (c) 2019 Adyen B.V.
+ * This file is open source and available under the MIT license.
+ * See the LICENSE file for more info.
+ */
+
+import static com.adyen.model.clouddevice.security.NexoDerivedKey.NEXO_CIPHER_KEY_LENGTH;
+import static com.adyen.model.clouddevice.security.NexoDerivedKey.NEXO_HMAC_KEY_LENGTH;
+import static com.adyen.model.clouddevice.security.NexoDerivedKey.NEXO_IV_LENGTH;
+
+import com.adyen.model.clouddevice.security.NexoDerivedKey;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Arrays;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+
+final class NexoDerivedKeyGenerator {
+
+ private NexoDerivedKeyGenerator() {}
+
+ /** Given a passphrase, compute 80 byte key of key material according to crypto.md */
+ static NexoDerivedKey deriveKeyMaterial(String passphrase)
+ throws NoSuchAlgorithmException, InvalidKeySpecException {
+ byte[] salt = "AdyenNexoV1Salt".getBytes();
+ int iterations = 4000;
+
+ PBEKeySpec spec =
+ new PBEKeySpec(
+ passphrase.toCharArray(),
+ salt,
+ iterations,
+ (NEXO_CIPHER_KEY_LENGTH + NEXO_HMAC_KEY_LENGTH + NEXO_IV_LENGTH) * 8);
+ SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
+ byte[] key = skf.generateSecret(spec).getEncoded();
+
+ return readKeyData(key);
+ }
+
+ /** Read a key material file of 80 bytes, splitting it in the hmacKey, cipherKey and iv */
+ private static NexoDerivedKey readKeyData(byte[] key) {
+ NexoDerivedKey nexoDerivedKey = new NexoDerivedKey();
+
+ nexoDerivedKey.setHmacKey(Arrays.copyOfRange(key, 0, NEXO_HMAC_KEY_LENGTH));
+ nexoDerivedKey.setCipherKey(
+ Arrays.copyOfRange(
+ key, NEXO_HMAC_KEY_LENGTH, NEXO_HMAC_KEY_LENGTH + NEXO_CIPHER_KEY_LENGTH));
+ nexoDerivedKey.setIv(
+ Arrays.copyOfRange(
+ key,
+ NEXO_HMAC_KEY_LENGTH + NEXO_CIPHER_KEY_LENGTH,
+ NEXO_CIPHER_KEY_LENGTH + NEXO_HMAC_KEY_LENGTH + NEXO_IV_LENGTH));
+
+ return nexoDerivedKey;
+ }
+}
diff --git a/src/main/java/com/adyen/security/clouddevice/NexoSecurityException.java b/src/main/java/com/adyen/security/clouddevice/NexoSecurityException.java
new file mode 100644
index 000000000..d9c75244b
--- /dev/null
+++ b/src/main/java/com/adyen/security/clouddevice/NexoSecurityException.java
@@ -0,0 +1,40 @@
+/*
+ * ######
+ * ######
+ * ############ ####( ###### #####. ###### ############ ############
+ * ############# #####( ###### #####. ###### ############# #############
+ * ###### #####( ###### #####. ###### ##### ###### ##### ######
+ * ###### ###### #####( ###### #####. ###### ##### ##### ##### ######
+ * ###### ###### #####( ###### #####. ###### ##### ##### ######
+ * ############# ############# ############# ############# ##### ######
+ * ############ ############ ############# ############ ##### ######
+ * ######
+ * #############
+ * ############
+ *
+ * Adyen Java API Library
+ *
+ * Copyright (c) 2019 Adyen B.V.
+ * This file is open source and available under the MIT license.
+ * See the LICENSE file for more info.
+ */
+package com.adyen.security.clouddevice;
+
+public class NexoSecurityException extends Exception {
+ public NexoSecurityException(String message) {
+ super(message);
+ }
+
+ public NexoSecurityException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public NexoSecurityException(Throwable cause) {
+ super(cause);
+ }
+
+ @Override
+ public String toString() {
+ return "NexoSecurityException{message=" + getMessage() + '}';
+ }
+}
diff --git a/src/main/java/com/adyen/security/clouddevice/NexoSecurityManager.java b/src/main/java/com/adyen/security/clouddevice/NexoSecurityManager.java
new file mode 100644
index 000000000..f2a33aabe
--- /dev/null
+++ b/src/main/java/com/adyen/security/clouddevice/NexoSecurityManager.java
@@ -0,0 +1,223 @@
+package com.adyen.security.clouddevice;
+
+import static com.adyen.model.clouddevice.security.NexoDerivedKey.NEXO_IV_LENGTH;
+
+import com.adyen.model.clouddevice.MessageHeader;
+import com.adyen.model.clouddevice.SaleToPOISecuredMessage;
+import com.adyen.model.clouddevice.security.NexoDerivedKey;
+import com.adyen.model.clouddevice.security.SecurityTrailer;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.Mac;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import org.apache.commons.codec.binary.Base64;
+
+/**
+ * Handles encryption, decryption, and integrity validation for Nexo SaleToPOI messages using AES
+ * and HMAC.
+ *
+ * - Derives keys from EncryptionCredentialDetails - Encrypts and decrypts Nexo messages
+ * (AES-256-CBC) - Generates and validates HMAC (SHA-256) - Constructs and validates SecurityTrailer
+ */
+public class NexoSecurityManager {
+
+ private final EncryptionCredentialDetails encryptionCredentialDetails;
+ private volatile NexoDerivedKey nexoDerivedKey;
+
+ public NexoSecurityManager(EncryptionCredentialDetails encryptionCredentialDetails)
+ throws NexoSecurityException {
+ validateSecurityKey(encryptionCredentialDetails);
+ this.encryptionCredentialDetails = encryptionCredentialDetails;
+ }
+
+ /**
+ * Encrypts the SaleToPOI message using the provided message header and security key.
+ *
+ * @param saleToPoiMessageJson the JSON string representing the SaleToPOI message
+ * @param messageHeader the message header for encryption
+ * @return encrypted SaleToPOISecuredMessage
+ * @throws NexoSecurityException if encryption fails
+ */
+ public SaleToPOISecuredMessage encrypt(String saleToPoiMessageJson, MessageHeader messageHeader)
+ throws NexoSecurityException {
+
+ try {
+ NexoDerivedKey derivedKey = getNexoDerivedKey();
+ byte[] saleToPoiMessageByteArray = saleToPoiMessageJson.getBytes(StandardCharsets.UTF_8);
+
+ // Generate a random initialization vector (IV) nonce
+ byte[] ivNonce = generateRandomIvNonce();
+
+ // Perform AES encryption
+ byte[] encryptedSaleToPoiMessage =
+ crypt(saleToPoiMessageByteArray, derivedKey, ivNonce, Cipher.ENCRYPT_MODE);
+
+ // Generate HMAC for message authentication
+ byte[] hmacSignature = hmac(saleToPoiMessageByteArray, derivedKey);
+
+ // Populate security trailer with metadata and HMAC
+ SecurityTrailer securityTrailer = new SecurityTrailer();
+ securityTrailer.setAdyenCryptoVersion(
+ this.encryptionCredentialDetails.getAdyenCryptoVersion());
+ securityTrailer.setKeyIdentifier(this.encryptionCredentialDetails.getKeyIdentifier());
+ securityTrailer.setKeyVersion(this.encryptionCredentialDetails.getKeyVersion());
+ securityTrailer.setNonce(Base64.encodeBase64String(ivNonce).getBytes());
+ securityTrailer.setHmac(Base64.encodeBase64String(hmacSignature).getBytes());
+
+ // Construct the secured message with the encrypted content and securityTrailer
+ SaleToPOISecuredMessage saleToPoiSecuredMessage = new SaleToPOISecuredMessage();
+ saleToPoiSecuredMessage.setMessageHeader(messageHeader);
+ saleToPoiSecuredMessage.setNexoBlob(Base64.encodeBase64String(encryptedSaleToPoiMessage));
+ saleToPoiSecuredMessage.setSecurityTrailer(securityTrailer);
+
+ return saleToPoiSecuredMessage;
+
+ } catch (Exception e) {
+ throw new NexoSecurityException("Cannot encrypt the SaleToPOISecuredMessage", e);
+ }
+ }
+
+ /**
+ * Decrypts the SaleToPOI secured message.
+ *
+ * @param saleToPoiSecuredMessage the encrypted message
+ * @return the decrypted SaleToPOI message as a JSON string
+ * @throws NexoSecurityException if decryption fails
+ */
+ public String decrypt(SaleToPOISecuredMessage saleToPoiSecuredMessage)
+ throws NexoSecurityException {
+ try {
+ NexoDerivedKey derivedKey = getNexoDerivedKey();
+
+ // Decode the encrypted blob
+ byte[] encryptedSaleToPoiMessageByteArray =
+ Base64.decodeBase64(saleToPoiSecuredMessage.getNexoBlob().getBytes());
+
+ // Retrieve the nonce (IV) from the securityTrailer
+ byte[] ivNonceB64 = saleToPoiSecuredMessage.getSecurityTrailer().getNonce();
+ String nonceString = new String(ivNonceB64, StandardCharsets.UTF_8);
+ byte[] ivNonce = Base64.decodeBase64(nonceString);
+
+ // Decrypt the message
+ byte[] decryptedSaleToPoiMessageByteArray =
+ crypt(encryptedSaleToPoiMessageByteArray, derivedKey, ivNonce, Cipher.DECRYPT_MODE);
+
+ // Validate HMAC to ensure message integrity
+ byte[] receivedHmac = saleToPoiSecuredMessage.getSecurityTrailer().getHmac();
+
+ String hmacString = new String(receivedHmac, StandardCharsets.UTF_8);
+ byte[] hmacBytes = Base64.decodeBase64(hmacString);
+ validateHmac(hmacBytes, decryptedSaleToPoiMessageByteArray, derivedKey);
+
+ return new String(decryptedSaleToPoiMessageByteArray, StandardCharsets.UTF_8);
+
+ } catch (Exception e) {
+ throw new NexoSecurityException("Cannot decrypt the SaleToPOISecuredMessage", e);
+ }
+ }
+
+ /**
+ * Validates the encryptionCredentialDetails to ensure all required fields are present.
+ *
+ * @param encryptionCredentialDetails the encryptionCredentialDetails to validate
+ * @throws NexoSecurityException if the security key is invalid
+ */
+ private void validateSecurityKey(EncryptionCredentialDetails encryptionCredentialDetails)
+ throws NexoSecurityException {
+ if (encryptionCredentialDetails == null
+ || encryptionCredentialDetails.getPassphrase() == null
+ || encryptionCredentialDetails.getPassphrase().isEmpty()
+ || encryptionCredentialDetails.getKeyIdentifier() == null
+ || encryptionCredentialDetails.getKeyVersion() == null
+ || encryptionCredentialDetails.getAdyenCryptoVersion() == null) {
+ throw new NexoSecurityException("Invalid Security Key");
+ }
+ }
+
+ /**
+ * Lazily initializes and retrieves the derived key material for encryption/decryption.
+ *
+ * @return the derived key material
+ */
+ NexoDerivedKey getNexoDerivedKey() throws GeneralSecurityException {
+ if (nexoDerivedKey == null) {
+ synchronized (this) {
+ if (nexoDerivedKey == null) {
+ nexoDerivedKey =
+ NexoDerivedKeyGenerator.deriveKeyMaterial(
+ this.encryptionCredentialDetails.getPassphrase());
+ }
+ }
+ }
+ return nexoDerivedKey;
+ }
+
+ /** Performs AES encryption/decryption using the derived key and provided IV. */
+ private byte[] crypt(byte[] bytes, NexoDerivedKey dk, byte[] ivNonce, int mode)
+ throws NoSuchAlgorithmException,
+ NoSuchPaddingException,
+ IllegalBlockSizeException,
+ BadPaddingException,
+ InvalidKeyException,
+ InvalidAlgorithmParameterException {
+
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ SecretKeySpec secretKeySpec = new SecretKeySpec(dk.getCipherKey(), "AES");
+
+ // Derive the actual IV by XORing the derived IV with the nonce
+ byte[] iv = dk.getIv();
+ byte[] actualIV = new byte[NEXO_IV_LENGTH];
+ for (int i = 0; i < NEXO_IV_LENGTH; i++) {
+ actualIV[i] = (byte) (iv[i] ^ ivNonce[i]);
+ }
+
+ IvParameterSpec ivParameterSpec = new IvParameterSpec(actualIV);
+ cipher.init(mode, secretKeySpec, ivParameterSpec);
+ return cipher.doFinal(bytes);
+ }
+
+ /** Generates an HMAC for message authentication. */
+ byte[] hmac(byte[] bytes, NexoDerivedKey derivedKey)
+ throws NoSuchAlgorithmException, InvalidKeyException {
+ Mac mac = Mac.getInstance("HmacSHA256");
+ SecretKeySpec s = new SecretKeySpec(derivedKey.getHmacKey(), "HmacSHA256");
+
+ mac.init(s);
+ return mac.doFinal(bytes);
+ }
+
+ /** Validates the HMAC of a decrypted message to ensure data integrity. */
+ void validateHmac(byte[] receivedHmac, byte[] decryptedMessage, NexoDerivedKey derivedKey)
+ throws NexoSecurityException, InvalidKeyException, NoSuchAlgorithmException {
+ byte[] hmac = hmac(decryptedMessage, derivedKey);
+ boolean valid = MessageDigest.isEqual(hmac, receivedHmac);
+
+ if (!valid) {
+ throw new NexoSecurityException("HMAC validation failed");
+ }
+ }
+
+ /** Generates a random IV nonce using a secure random number generator. */
+ private byte[] generateRandomIvNonce() {
+ byte[] ivNonce = new byte[NEXO_IV_LENGTH];
+ SecureRandom secureRandom;
+ try {
+ secureRandom = SecureRandom.getInstance("NativePRNGNonBlocking");
+ } catch (Exception NoSuchAlgorithmException) {
+ // Fallback to default SecureRandom implementation
+ secureRandom = new SecureRandom();
+ }
+ secureRandom.nextBytes(ivNonce);
+ return ivNonce;
+ }
+}
diff --git a/src/main/java/com/adyen/service/clouddevice/CloudDeviceApi.java b/src/main/java/com/adyen/service/clouddevice/CloudDeviceApi.java
new file mode 100644
index 000000000..b647db7fc
--- /dev/null
+++ b/src/main/java/com/adyen/service/clouddevice/CloudDeviceApi.java
@@ -0,0 +1,414 @@
+package com.adyen.service.clouddevice;
+
+import com.adyen.Client;
+import com.adyen.Service;
+import com.adyen.constants.ApiConstants;
+import com.adyen.model.clouddevice.*;
+import com.adyen.security.clouddevice.EncryptionCredentialDetails;
+import com.adyen.security.clouddevice.NexoSecurityManager;
+import com.adyen.service.exception.CloudDeviceException;
+import com.adyen.service.resource.Resource;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Cloud Device API service
+ *
+ * With the Cloud device API you can: - send Terminal API requests to the Adyen cloud endpoints.
+ * - check the cloud connection of a payment terminal or of a device used in a Mobile solution for
+ * in-person payments.
+ */
+public class CloudDeviceApi extends Service {
+
+ public static final String API_VERSION = "1";
+
+ protected String baseURL;
+
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ /**
+ * CloudDeviceApi constructor in {@link com.adyen.service.clouddevice package}.
+ *
+ * @param client {@link Client } (required)
+ */
+ public CloudDeviceApi(Client client) {
+ super(client);
+ this.baseURL = createBaseURL("https://device-api-test.adyen.com/v" + API_VERSION);
+ }
+
+ /**
+ * Send a synchronous payment request.
+ *
+ * @param merchantAccount The unique identifier of the merchant account
+ * @param deviceId The unique identifier of the payment device that you send this request to (must
+ * match POIID in the MessageHeader).
+ * @param cloudDeviceApiRequest The request to send.
+ * @return instance of CloudDeviceApiResponse
+ * @throws CloudDeviceException when an error occurs
+ */
+ public CloudDeviceApiResponse sendSync(
+ String merchantAccount, String deviceId, CloudDeviceApiRequest cloudDeviceApiRequest)
+ throws CloudDeviceException {
+
+ try {
+ // Add path params
+ Map Define in src/test/resources the configuration for the tests
+ *
+ * ``` ADYEN_API_KEY= ADYEN_MERCHANT_ACCOUNT= ADYEN_TERMINAL_DEVICE_ID=
+ * ADYEN_TERMINAL_DEVICE_KEY_IDENTIFIER= ADYEN_TERMINAL_DEVICE_PASSPHRASE= ```
+ */
+public class BaseIntegrationTest {
+
+ private static Properties properties = null;
+
+ protected Client getClient() {
+ return new Client(new Config().apiKey(getApiKey()).environment(Environment.TEST));
+ }
+
+ protected String getApiKey() {
+ return getProperty("ADYEN_API_KEY");
+ }
+
+ protected String getMerchantAccount() {
+ return getProperty("ADYEN_MERCHANT_ACCOUNT");
+ }
+
+ protected String getTerminalDeviceId() {
+ return getProperty("ADYEN_TERMINAL_DEVICE_ID");
+ }
+
+ protected String getTerminalDeviceKeyIdentifier() {
+ return getProperty("ADYEN_TERMINAL_DEVICE_KEY_IDENTIFIER");
+ }
+
+ protected String getTerminalDevicePassphrase() {
+ return getProperty("ADYEN_TERMINAL_DEVICE_PASSPHRASE");
+ }
+
+ private Properties getProperties() {
+ if (properties == null) {
+ properties = new Properties();
+ try (InputStream inputStream =
+ BaseIntegrationTest.class.getClassLoader().getResourceAsStream("config.properties")) {
+ if (inputStream != null) {
+ properties.load(inputStream);
+ }
+ } catch (IOException e) {
+ // Do nothing, properties will be empty
+ }
+ }
+
+ return properties;
+ }
+
+ private String getProperty(String name) {
+ String property = System.getenv(name);
+
+ if (property != null && !property.isEmpty()) {
+ return property;
+ }
+ property = getProperties().getProperty(name);
+
+ if (property == null || property.isEmpty()) {
+ throw new RuntimeException("Property " + name + " not defined");
+ }
+
+ return property;
+ }
+}
diff --git a/src/test/java/com/adyen/clouddevice/CloudDeviceApiTerminalTest.java b/src/test/java/com/adyen/clouddevice/CloudDeviceApiTerminalTest.java
new file mode 100644
index 000000000..8b1a7d1d0
--- /dev/null
+++ b/src/test/java/com/adyen/clouddevice/CloudDeviceApiTerminalTest.java
@@ -0,0 +1,163 @@
+package com.adyen.clouddevice;
+
+import static org.junit.Assert.assertNotNull;
+
+import com.adyen.BaseIntegrationTest;
+import com.adyen.model.clouddevice.*;
+import com.adyen.security.clouddevice.EncryptionCredentialDetails;
+import com.adyen.service.clouddevice.CloudDeviceApi;
+import java.math.BigDecimal;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.util.UUID;
+import org.junit.Ignore;
+import org.junit.Test;
+
+/**
+ * Verify Terminal integration: tests to send API requests to the Cloud Device API and test the
+ * Terminal responds as expected.
+ *
+ * Don't forget to: - Enable the terminal - Enable the test to run (by removing/comment
+ * the @Ignore annotation) - Set required variables (ADYEN_API_KEY, ADYEN_MERCHANT_ACCOUNT,
+ * ADYEN_TERMINAL_DEVICE_ID) creating the src/main/resources/config.properties file:
+ *
+ * # Adyen Test Credentials ADYEN_API_KEY=##### ADYEN_MERCHANT_ACCOUNT=MyMerchantAccount
+ * ADYEN_TERMINAL_DEVICE_ID=V400m-1234567890 # Terminal configuration
+ * ADYEN_TERMINAL_DEVICE_KEY_IDENTIFIER==##### ADYEN_TERMINAL_DEVICE_PASSPHRASE==#####
+ *
+ * - Run one test at the time with `mvn test -Dtest=CloudDeviceApiTerminalTest#sendSync` -
+ * Disable the test again
+ */
+public class CloudDeviceApiTerminalTest extends BaseIntegrationTest {
+
+ @Ignore // enable when you want to test with the Terminal
+ @Test
+ public void sendSync() throws Exception {
+
+ CloudDeviceApi cloudDeviceApi = new CloudDeviceApi(getClient());
+
+ CloudDeviceApiRequest cloudDeviceApiRequest =
+ createCloudDeviceAPIPaymentRequest(getTerminalDeviceId());
+
+ var response =
+ cloudDeviceApi.sendSync(getMerchantAccount(), getTerminalDeviceId(), cloudDeviceApiRequest);
+
+ assertNotNull(response);
+ System.out.println("Response: " + response);
+ }
+
+ @Ignore // enable when you want to test with the Terminal
+ @Test
+ public void sendAsync() throws Exception {
+
+ CloudDeviceApi cloudDeviceApi = new CloudDeviceApi(getClient());
+
+ CloudDeviceApiRequest cloudDeviceApiRequest =
+ createCloudDeviceAPIPaymentRequest(getTerminalDeviceId());
+
+ cloudDeviceApiRequest.getSaleToPOIRequest().setPaymentRequest(null);
+
+ var response =
+ cloudDeviceApi.sendAsync(
+ getMerchantAccount(), getTerminalDeviceId(), cloudDeviceApiRequest);
+
+ assertNotNull(response);
+ System.out.println("Response: " + response);
+ }
+
+ @Ignore // enable when you want to test with the Terminal
+ @Test
+ public void sendEncryptedSync() throws Exception {
+
+ CloudDeviceApi cloudDeviceApi = new CloudDeviceApi(getClient());
+
+ CloudDeviceApiRequest cloudDeviceApiRequest =
+ createCloudDeviceAPIPaymentRequest(getTerminalDeviceId());
+
+ EncryptionCredentialDetails encryptionCredentialDetails =
+ new EncryptionCredentialDetails()
+ .adyenCryptoVersion(1)
+ .keyIdentifier(getTerminalDeviceKeyIdentifier())
+ .keyVersion(1)
+ .passphrase(getTerminalDevicePassphrase());
+
+ var response =
+ cloudDeviceApi.sendEncryptedSync(
+ getMerchantAccount(),
+ getTerminalDeviceId(),
+ cloudDeviceApiRequest,
+ encryptionCredentialDetails);
+
+ assertNotNull(response);
+ System.out.println("Response: " + response);
+ }
+
+ @Ignore // enable when you want to test with the Terminal
+ @Test
+ public void sendEncryptedAsync() throws Exception {
+
+ CloudDeviceApi cloudDeviceApi = new CloudDeviceApi(getClient());
+
+ CloudDeviceApiRequest cloudDeviceApiRequest =
+ createCloudDeviceAPIPaymentRequest(getTerminalDeviceId());
+
+ EncryptionCredentialDetails encryptionCredentialDetails =
+ new EncryptionCredentialDetails()
+ .adyenCryptoVersion(1)
+ .keyIdentifier(getTerminalDeviceKeyIdentifier())
+ .keyVersion(1)
+ .passphrase(getTerminalDevicePassphrase());
+
+ var response =
+ cloudDeviceApi.sendEncryptedAsync(
+ getMerchantAccount(),
+ getTerminalDeviceId(),
+ cloudDeviceApiRequest,
+ encryptionCredentialDetails);
+
+ assertNotNull(response);
+ System.out.println("Response: " + response);
+ }
+
+ protected CloudDeviceApiRequest createCloudDeviceAPIPaymentRequest(String deviceId) {
+ SaleToPOIRequest saleToPOIRequest = new SaleToPOIRequest();
+
+ var randomId = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 10);
+
+ MessageHeader messageHeader = new MessageHeader();
+ messageHeader.setProtocolVersion("3.0");
+ messageHeader.setMessageClass(MessageClassType.SERVICE);
+ messageHeader.setMessageCategory(MessageCategoryType.PAYMENT);
+ messageHeader.setMessageType(MessageType.REQUEST);
+ messageHeader.setSaleID(randomId);
+ messageHeader.setServiceID(randomId);
+ messageHeader.setPOIID(deviceId);
+
+ saleToPOIRequest.setMessageHeader(messageHeader);
+
+ PaymentRequest paymentRequest = new PaymentRequest();
+
+ SaleData saleData = new SaleData();
+ TransactionIdentification transactionIdentification = new TransactionIdentification();
+ transactionIdentification.setTransactionID(randomId);
+ OffsetDateTime timestamp = OffsetDateTime.now(ZoneOffset.UTC);
+ transactionIdentification.setTimeStamp(timestamp);
+ saleData.setSaleTransactionID(transactionIdentification);
+
+ PaymentTransaction paymentTransaction = new PaymentTransaction();
+ AmountsReq amountsReq = new AmountsReq();
+ amountsReq.setCurrency("EUR");
+ amountsReq.setRequestedAmount(BigDecimal.ONE);
+ paymentTransaction.setAmountsReq(amountsReq);
+
+ paymentRequest.setSaleData(saleData);
+ paymentRequest.setPaymentTransaction(paymentTransaction);
+
+ saleToPOIRequest.setPaymentRequest(paymentRequest);
+
+ CloudDeviceApiRequest cloudDeviceApiRequest = new CloudDeviceApiRequest();
+ cloudDeviceApiRequest.setSaleToPOIRequest(saleToPOIRequest);
+
+ return cloudDeviceApiRequest;
+ }
+}
diff --git a/src/test/java/com/adyen/clouddevice/CloudDeviceApiTest.java b/src/test/java/com/adyen/clouddevice/CloudDeviceApiTest.java
new file mode 100644
index 000000000..df86d7bdf
--- /dev/null
+++ b/src/test/java/com/adyen/clouddevice/CloudDeviceApiTest.java
@@ -0,0 +1,347 @@
+package com.adyen.clouddevice;
+
+import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.verify;
+
+import com.adyen.BaseTest;
+import com.adyen.Client;
+import com.adyen.constants.ApiConstants;
+import com.adyen.model.clouddevice.*;
+import com.adyen.security.clouddevice.EncryptionCredentialDetails;
+import com.adyen.service.clouddevice.CloudDeviceApi;
+import com.adyen.service.exception.CloudDeviceException;
+import java.math.BigDecimal;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.util.Map;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class CloudDeviceApiTest extends BaseTest {
+
+ @Test
+ public void sendSync() throws Exception {
+ Client client = createMockClientFromFile("mocks/clouddevice/payment-sync-success.json");
+
+ CloudDeviceApi cloudDeviceApi = new CloudDeviceApi(client);
+
+ CloudDeviceApiRequest cloudDeviceApiRequest = createCloudDeviceAPIPaymentRequest();
+
+ var response =
+ cloudDeviceApi.sendSync("myMerchant", "P400Plus-123456789", cloudDeviceApiRequest);
+
+ assertNotNull(response);
+ assertNotNull(response.getSaleToPOIResponse());
+ assertNotNull(response.getSaleToPOIResponse().getMessageHeader());
+ assertEquals(
+ "P400Plus-123456789", response.getSaleToPOIResponse().getMessageHeader().getPOIID());
+
+ verify(client.getHttpClient())
+ .request(
+ "https://device-api-test.adyen.com/v1/merchants/myMerchant/devices/P400Plus-123456789/sync",
+ cloudDeviceApiRequest.toJson(),
+ client.getConfig(),
+ false,
+ null,
+ ApiConstants.HttpMethod.POST,
+ null);
+ }
+
+ @Test
+ public void sendAsync() throws Exception {
+ Client client = createMockClientFromFile("mocks/clouddevice/payment-async-success.json");
+
+ CloudDeviceApi cloudDeviceApi = new CloudDeviceApi(client);
+
+ CloudDeviceApiRequest cloudDeviceApiRequest = createCloudDeviceAPIPaymentRequest();
+
+ var response =
+ cloudDeviceApi.sendAsync("myMerchant", "P400Plus-123456789", cloudDeviceApiRequest);
+
+ assertNotNull(response);
+ assertNotNull("ok", response);
+ assertNull(response.getSaleToPOIRequest());
+
+ verify(client.getHttpClient())
+ .request(
+ "https://device-api-test.adyen.com/v1/merchants/myMerchant/devices/P400Plus-123456789/async",
+ cloudDeviceApiRequest.toJson(),
+ client.getConfig(),
+ false,
+ null,
+ ApiConstants.HttpMethod.POST,
+ null);
+ }
+
+ @Test
+ public void sendAsyncReturningError() throws Exception {
+ Client client = createMockClientFromFile("mocks/clouddevice/payment-async-error.json");
+
+ CloudDeviceApi cloudDeviceApi = new CloudDeviceApi(client);
+
+ CloudDeviceApiRequest cloudDeviceApiRequest = createCloudDeviceAPIPaymentRequest();
+
+ var response =
+ cloudDeviceApi.sendAsync("myMerchant", "P400Plus-123456789", cloudDeviceApiRequest);
+
+ assertNotNull(response);
+ assertNull(response.getResult());
+ assertNotNull(response.getSaleToPOIRequest());
+
+ assertEquals(
+ "Invalid event",
+ EventToNotifyType.REJECT,
+ response.getSaleToPOIRequest().getEventNotification().getEventToNotify());
+
+ verify(client.getHttpClient())
+ .request(
+ "https://device-api-test.adyen.com/v1/merchants/myMerchant/devices/P400Plus-123456789/async",
+ cloudDeviceApiRequest.toJson(),
+ client.getConfig(),
+ false,
+ null,
+ ApiConstants.HttpMethod.POST,
+ null);
+ }
+
+ @Test
+ public void sendEncryptedSync() throws Exception {
+ Client client =
+ createMockClientFromFile("mocks/clouddevice/payment-sync-encrypted-success.json");
+
+ CloudDeviceApi cloudDeviceApi = new CloudDeviceApi(client);
+
+ CloudDeviceApiRequest cloudDeviceApiRequest = createCloudDeviceAPIPaymentRequest();
+
+ EncryptionCredentialDetails encryptionCredentialDetails =
+ new EncryptionCredentialDetails()
+ .adyenCryptoVersion(0)
+ .keyIdentifier("CryptoKeyIdentifier12345")
+ .keyVersion(0)
+ .passphrase("p@ssw0rd123456");
+
+ var response =
+ cloudDeviceApi.sendEncryptedSync(
+ "TestMerchantAccount",
+ "MX915-284251016",
+ cloudDeviceApiRequest,
+ encryptionCredentialDetails);
+
+ assertNotNull(response);
+ assertNotNull(response.getSaleToPOIResponse());
+ assertNotNull(response.getSaleToPOIResponse().getMessageHeader());
+ assertEquals("MX915-284251016", response.getSaleToPOIResponse().getMessageHeader().getPOIID());
+
+ verify(client.getHttpClient())
+ .request(
+ eq(
+ "https://device-api-test.adyen.com/v1/merchants/TestMerchantAccount/devices/MX915-284251016/sync"),
+ argThat(
+ json -> {
+ assertTrue(
+ json.contains("\"NexoBlob\":"), "JSON payload should contain NexoBlob field");
+ return true;
+ }),
+ eq(client.getConfig()),
+ eq(false),
+ isNull(),
+ eq(ApiConstants.HttpMethod.POST),
+ isNull());
+ }
+
+ @Test
+ public void sendEncryptedAsync() throws Exception {
+ Client client = createMockClientFromFile("mocks/clouddevice/payment-async-success.json");
+
+ CloudDeviceApi cloudDeviceApi = new CloudDeviceApi(client);
+
+ CloudDeviceApiRequest cloudDeviceApiRequest = createCloudDeviceAPIPaymentRequest();
+
+ EncryptionCredentialDetails encryptionCredentialDetails =
+ new EncryptionCredentialDetails()
+ .adyenCryptoVersion(0)
+ .keyIdentifier("CryptoKeyIdentifier12345")
+ .keyVersion(0)
+ .passphrase("p@ssw0rd123456");
+
+ var response =
+ cloudDeviceApi.sendEncryptedAsync(
+ "TestMerchantAccount",
+ "MX915-284251016",
+ cloudDeviceApiRequest,
+ encryptionCredentialDetails);
+
+ assertNotNull(response);
+ assertEquals("ok", response);
+
+ verify(client.getHttpClient())
+ .request(
+ eq(
+ "https://device-api-test.adyen.com/v1/merchants/TestMerchantAccount/devices/MX915-284251016/async"),
+ argThat(
+ json -> {
+ assertTrue(
+ json.contains("\"NexoBlob\":"), "JSON payload should contain NexoBlob field");
+ return true;
+ }),
+ eq(client.getConfig()),
+ eq(false),
+ isNull(),
+ eq(ApiConstants.HttpMethod.POST),
+ isNull());
+ }
+
+ @Test
+ public void getConnectedDevices() throws Exception {
+ Client client = createMockClientFromFile("mocks/clouddevice/connected-devices.json");
+ CloudDeviceApi cloudDeviceApi = new CloudDeviceApi(client);
+
+ ConnectedDevicesResponse response = cloudDeviceApi.getConnectedDevices("myMerchant");
+
+ assertNotNull(response);
+ assertEquals(2, response.getUniqueDeviceIds().size());
+ assertEquals("P400Plus-123456789", response.getUniqueDeviceIds().get(0));
+ assertEquals("V400m-123456789", response.getUniqueDeviceIds().get(1));
+
+ verify(client.getHttpClient())
+ .request(
+ "https://device-api-test.adyen.com/v1/merchants/myMerchant/connectedDevices",
+ null,
+ client.getConfig(),
+ false,
+ null,
+ ApiConstants.HttpMethod.GET,
+ null);
+ }
+
+ @Test
+ public void getConnectedDevicesWithStore() throws Exception {
+ Client client = createMockClientFromFile("mocks/clouddevice/connected-devices.json");
+ CloudDeviceApi cloudDeviceApi = new CloudDeviceApi(client);
+
+ ConnectedDevicesResponse response = cloudDeviceApi.getConnectedDevices("myMerchant", "myStore");
+
+ assertNotNull(response);
+ assertEquals(2, response.getUniqueDeviceIds().size());
+ assertEquals("P400Plus-123456789", response.getUniqueDeviceIds().get(0));
+ assertEquals("V400m-123456789", response.getUniqueDeviceIds().get(1));
+
+ verify(client.getHttpClient())
+ .request(
+ "https://device-api-test.adyen.com/v1/merchants/myMerchant/connectedDevices",
+ null,
+ client.getConfig(),
+ false,
+ null,
+ ApiConstants.HttpMethod.GET,
+ Map.of("store", "myStore"));
+ }
+
+ @Test
+ public void getDeviceStatus() throws Exception {
+ Client client = createMockClientFromFile("mocks/clouddevice/status-device.json");
+ CloudDeviceApi cloudDeviceApi = new CloudDeviceApi(client);
+
+ DeviceStatusResponse response =
+ cloudDeviceApi.getDeviceStatus("myMerchant", "AMS1-000168242800763");
+
+ assertNotNull(response);
+ assertEquals("AMS1-000168242800763", response.getDeviceId());
+ assertEquals(DeviceStatus.ONLINE, response.getStatus());
+
+ verify(client.getHttpClient())
+ .request(
+ "https://device-api-test.adyen.com/v1/merchants/myMerchant/devices/AMS1-000168242800763/status",
+ null,
+ client.getConfig(),
+ false,
+ null,
+ ApiConstants.HttpMethod.GET,
+ null);
+ }
+
+ @Test
+ public void decryptNotification() throws Exception {
+
+ Client client = createMockClientFromResponse(""); // nop client
+ CloudDeviceApi cloudDeviceApi = new CloudDeviceApi(client);
+
+ String payload = getFileContents("mocks/clouddevice/encrypted-event-notification.json");
+
+ EncryptionCredentialDetails encryptionCredentialDetails =
+ new EncryptionCredentialDetails()
+ .adyenCryptoVersion(1)
+ .keyIdentifier("Key123456789crypt")
+ .keyVersion(1)
+ .passphrase("P@ssw0rd123456");
+
+ var response = cloudDeviceApi.decryptNotification(payload, encryptionCredentialDetails);
+
+ assertNotNull(response);
+ assertFalse(response.contains("\"NexoBlob\":"));
+ assertTrue(response.contains("\"PaymentResponse\":"));
+ }
+
+ @Test
+ public void decryptNotificationInvalidPayload() throws Exception {
+
+ Client client = createMockClientFromResponse(""); // nop client
+ CloudDeviceApi cloudDeviceApi = new CloudDeviceApi(client);
+
+ String payload = "{...}";
+
+ EncryptionCredentialDetails encryptionCredentialDetails =
+ new EncryptionCredentialDetails()
+ .adyenCryptoVersion(1)
+ .keyIdentifier("Key123456789crypt")
+ .keyVersion(1)
+ .passphrase("P@ssw0rd123456");
+
+ Assert.assertThrows(
+ CloudDeviceException.class,
+ () -> cloudDeviceApi.decryptNotification(payload, encryptionCredentialDetails));
+ }
+
+ protected CloudDeviceApiRequest createCloudDeviceAPIPaymentRequest() {
+ SaleToPOIRequest saleToPOIRequest = new SaleToPOIRequest();
+
+ MessageHeader messageHeader = new MessageHeader();
+ messageHeader.setProtocolVersion("3.0");
+ messageHeader.setMessageClass(MessageClassType.SERVICE);
+ messageHeader.setMessageCategory(MessageCategoryType.PAYMENT);
+ messageHeader.setMessageType(MessageType.REQUEST);
+ messageHeader.setSaleID("001");
+ messageHeader.setServiceID("001");
+ messageHeader.setPOIID("P400Plus-123456789");
+
+ saleToPOIRequest.setMessageHeader(messageHeader);
+
+ PaymentRequest paymentRequest = new PaymentRequest();
+
+ SaleData saleData = new SaleData();
+ TransactionIdentification transactionIdentification = new TransactionIdentification();
+ transactionIdentification.setTransactionID("001");
+ OffsetDateTime timestamp = OffsetDateTime.now(ZoneOffset.UTC);
+ transactionIdentification.setTimeStamp(timestamp);
+ saleData.setSaleTransactionID(transactionIdentification);
+
+ PaymentTransaction paymentTransaction = new PaymentTransaction();
+ AmountsReq amountsReq = new AmountsReq();
+ amountsReq.setCurrency("EUR");
+ amountsReq.setRequestedAmount(BigDecimal.ONE);
+ paymentTransaction.setAmountsReq(amountsReq);
+
+ paymentRequest.setSaleData(saleData);
+ paymentRequest.setPaymentTransaction(paymentTransaction);
+
+ saleToPOIRequest.setPaymentRequest(paymentRequest);
+
+ CloudDeviceApiRequest cloudDeviceApiRequest = new CloudDeviceApiRequest();
+ cloudDeviceApiRequest.setSaleToPOIRequest(saleToPOIRequest);
+
+ return cloudDeviceApiRequest;
+ }
+}
diff --git a/src/test/java/com/adyen/security/clouddevice/NexoSecurityManagerTest.java b/src/test/java/com/adyen/security/clouddevice/NexoSecurityManagerTest.java
new file mode 100644
index 000000000..475754914
--- /dev/null
+++ b/src/test/java/com/adyen/security/clouddevice/NexoSecurityManagerTest.java
@@ -0,0 +1,152 @@
+package com.adyen.security.clouddevice;
+
+import static org.junit.Assert.*;
+
+import com.adyen.BaseTest;
+import com.adyen.model.clouddevice.*;
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import org.apache.commons.codec.binary.Base64;
+import org.junit.Test;
+
+public class NexoSecurityManagerTest extends BaseTest {
+
+ @Test
+ public void testEncryptDecrypt() throws Exception {
+ EncryptionCredentialDetails encryptionCredentialDetails =
+ new EncryptionCredentialDetails()
+ .adyenCryptoVersion(1)
+ .keyIdentifier("SpecV2TestMACKey")
+ .keyVersion(1)
+ .passphrase("12345678901234567890123456789012");
+
+ NexoSecurityManager nexoSecurityManager = new NexoSecurityManager(encryptionCredentialDetails);
+
+ CloudDeviceApiRequest cloudDeviceApiRequest = createCloudDeviceAPIPaymentRequest();
+
+ // encrypt CloudDeviceApiRequest
+ SaleToPOISecuredMessage saleToPoiSecuredMessage =
+ nexoSecurityManager.encrypt(
+ cloudDeviceApiRequest.toJson(),
+ cloudDeviceApiRequest.getSaleToPOIRequest().getMessageHeader());
+
+ assertNotNull(saleToPoiSecuredMessage.getMessageHeader());
+ assertNotNull(saleToPoiSecuredMessage.getNexoBlob());
+ assertNotNull(saleToPoiSecuredMessage.getSecurityTrailer());
+ assertNotNull(saleToPoiSecuredMessage.getSecurityTrailer().getHmac());
+
+ // decrypt SaleToPOISecuredMessage
+ String decryptedSaleToPoiMessageJson = nexoSecurityManager.decrypt(saleToPoiSecuredMessage);
+ assertEquals(cloudDeviceApiRequest.toJson(), decryptedSaleToPoiMessageJson);
+ }
+
+ @Test
+ public void testInvalidSecurityKey() {
+ try {
+ new NexoSecurityManager(null);
+ fail("Expected exception");
+ } catch (NexoSecurityException e) {
+ assertEquals("Invalid Security Key", e.getMessage());
+ }
+
+ try {
+ new NexoSecurityManager(new EncryptionCredentialDetails());
+ fail("Expected exception");
+ } catch (NexoSecurityException e) {
+ assertEquals("Invalid Security Key", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testInvalidHmac() throws Exception {
+ EncryptionCredentialDetails encryptionCredentialDetails =
+ new EncryptionCredentialDetails()
+ .adyenCryptoVersion(1)
+ .keyIdentifier("SpecV2TestMACKey")
+ .keyVersion(1)
+ .passphrase("12345678901234567890123456789012");
+
+ NexoSecurityManager nexoSecurityManager = new NexoSecurityManager(encryptionCredentialDetails);
+
+ CloudDeviceApiRequest cloudDeviceApiRequest = createCloudDeviceAPIPaymentRequest();
+
+ SaleToPOISecuredMessage saleToPoiSecuredMessage =
+ nexoSecurityManager.encrypt(
+ cloudDeviceApiRequest.toJson(),
+ cloudDeviceApiRequest.getSaleToPOIRequest().getMessageHeader());
+
+ // Tamper with the HMAC
+ saleToPoiSecuredMessage.getSecurityTrailer().setHmac("invalidhmac".getBytes());
+
+ try {
+ nexoSecurityManager.decrypt(saleToPoiSecuredMessage);
+ fail("Expected exception");
+ } catch (NexoSecurityException e) {
+ assertEquals("Cannot decrypt the SaleToPOISecuredMessage", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testHmac() throws Exception {
+ EncryptionCredentialDetails encryptionCredentialDetails =
+ new EncryptionCredentialDetails()
+ .adyenCryptoVersion(1)
+ .keyIdentifier("SpecV2TestMACKey")
+ .keyVersion(1)
+ .passphrase("12345678901234567890123456789012");
+
+ NexoSecurityManager nexoSecurityManager = new NexoSecurityManager(encryptionCredentialDetails);
+
+ byte[] message = "0123456789abcdef0123456789abcdef".getBytes(StandardCharsets.UTF_8);
+
+ byte[] actualHmac = nexoSecurityManager.hmac(message, nexoSecurityManager.getNexoDerivedKey());
+
+ assertEquals("HMAC-SHA256 should produce 32 bytes", 32, actualHmac.length);
+ assertEquals(
+ "HMAC signagtures don't match",
+ "GWYcM3JVYrY3b9CSLCuJ+THMxclBG9jJ05n+RR6DkQE=",
+ Base64.encodeBase64String(actualHmac));
+ }
+
+ protected CloudDeviceApiRequest createCloudDeviceAPIPaymentRequest() {
+ SaleToPOIRequest saleToPOIRequest = new SaleToPOIRequest();
+
+ MessageHeader messageHeader = new MessageHeader();
+ messageHeader.setProtocolVersion("3.0");
+ messageHeader.setMessageClass(MessageClassType.SERVICE);
+ messageHeader.setMessageCategory(MessageCategoryType.PAYMENT);
+ messageHeader.setMessageType(MessageType.REQUEST);
+ messageHeader.setSaleID("001");
+ messageHeader.setServiceID("001");
+ messageHeader.setPOIID("P400Plus-123456789");
+
+ saleToPOIRequest.setMessageHeader(messageHeader);
+
+ PaymentRequest paymentRequest = new PaymentRequest();
+
+ SaleData saleData = new SaleData();
+ TransactionIdentification transactionIdentification = new TransactionIdentification();
+ transactionIdentification.setTransactionID("001");
+ OffsetDateTime timestamp = OffsetDateTime.now(ZoneOffset.UTC);
+ transactionIdentification.setTimeStamp(timestamp);
+ saleData.setSaleTransactionID(transactionIdentification);
+
+ PaymentTransaction paymentTransaction = new PaymentTransaction();
+ AmountsReq amountsReq = new AmountsReq();
+ amountsReq.setCurrency("EUR");
+ amountsReq.setRequestedAmount(BigDecimal.ONE);
+ paymentTransaction.setAmountsReq(amountsReq);
+
+ paymentRequest.setSaleData(saleData);
+ paymentRequest.setPaymentTransaction(paymentTransaction);
+
+ saleToPOIRequest.setPaymentRequest(paymentRequest);
+
+ CloudDeviceApiRequest cloudDeviceApiRequest = new CloudDeviceApiRequest();
+ cloudDeviceApiRequest.setSaleToPOIRequest(saleToPOIRequest);
+
+ return cloudDeviceApiRequest;
+ }
+}
diff --git a/src/test/resources/mocks/clouddevice/connected-devices.json b/src/test/resources/mocks/clouddevice/connected-devices.json
new file mode 100644
index 000000000..1b46d7c29
--- /dev/null
+++ b/src/test/resources/mocks/clouddevice/connected-devices.json
@@ -0,0 +1,6 @@
+{
+ "uniqueDeviceIds": [
+ "P400Plus-123456789",
+ "V400m-123456789"
+ ]
+}
\ No newline at end of file
diff --git a/src/test/resources/mocks/clouddevice/encrypted-event-notification.json b/src/test/resources/mocks/clouddevice/encrypted-event-notification.json
new file mode 100644
index 000000000..7c8827fe9
--- /dev/null
+++ b/src/test/resources/mocks/clouddevice/encrypted-event-notification.json
@@ -0,0 +1,21 @@
+{
+ "SaleToPOIResponse": {
+ "MessageHeader": {
+ "MessageCategory": "Payment",
+ "MessageClass": "Service",
+ "MessageType": "Response",
+ "POIID": "V400m-347374578",
+ "ProtocolVersion": "3.0",
+ "SaleID": "406aa21f8b",
+ "ServiceID": "406aa21f8b"
+ },
+ "NexoBlob": "MnqvlI21WRfLyhkHUz3WkoKtn2+jq2zlb8ACsEj59vDSQmMMw5QHCyknuqP85scrhU6DsHkyjq3LWN7XxB9lADzfIIO\/6IklAfVP+aqz7zADlT6erP2Kq+\/uDKILQEAg6jikpCkCEhHQmQ+E2P7rswKyYU5I\/wfcGs46AkZ9iaBq\/DkvMDkv+3WGfN6qf\/sYmom\/zJWcWXArvsRub1\/sBsAI5QyxE+2Thsl4wfNBslrHFiIWi3akLXpTnU8wnUAykCxEjYH6kqTu2NI00iN1mNU6lhEbFyC6Nm7byjHSNkVGuHve4yHE0HMV080pyCD3ZkHGuzle54ktumLh+UdlXhZt6jNHvKZzMu4MuHjhUJuLtN1Mw\/bIEtUu5uevGyZHWZzR59PuyjBiyLF1XluNG342lyjZwTXQ\/x9OP+mx7dmINQGsNp0RKk1gC1wpKRIIHkwrmbmqSoPo6oBwg3t0V\/\/IsHLETCPXF4Ekvy\/XEw+4noMtcQMiA2x6DZK0t9Y3c+QfAYTGCWLRwXqxom+6xucZqfTtjosJXDrLTWcfHgnpwlplNv+wfzJN1RWxEY2ZV\/6gXXkuZymdlJEMtP6hF70Db2uD9MetM\/FCWxGvGQTrRA4AErPJgpA4+YnMA6K41e7DszfZNRMGs+6IJPsyvEBby5T1El1z4v1nEkPeFASDmuWICYxc6Ki8i7jdYgxmD9ztZSiKY8zrtGvIna4nxZoMbgxp6UFN0ws+Rqg++mJUkvQY0S9e1A\/CLhrxIICpOaqGPMPAWzf6nhz5HOKpdMPs2ZUrjgN7KSW28QsAtIcBfQEjZYKCvn3f68ERTEEU68vp+FfQUnXNKFQ7vH4ytuDzEqkgzD+bCbkh7VVzrJ1GdzuH+N1aBi+Hh73Qnk06\/LVdSvCp7L9CI\/8gG3oZDTk2t5BVNuHzbTsV5NWamgzJ0wtRqu6ya3qohtQNIvCMiYfNpT936hbiUyudbqAtakrgGBLo9xKWGLjjOALRMM8+URzK9kazCYg5ENFR7A8DGKpfVZEEwonOK2ISKT1\/9UbUHMrFS4UrVrginXmakwvVmCBXHgOrhMWQWFU1pDGZFaCMRMaa+2M87SGVMh0rIH+jihbfBmmTpu\/iMbjP2Qc5ZqgCnTYAr4ZECzR4cbxYxKDtvL0yWc5FxZGGxQuM6Hk+K7IfOfbUitVeyUFyGAA3t3SYbv8CCHQGCXeE6OZI92tP0T09HpbJB70s8rFYmAHR6k\/hcGNojOuuWj1hbRIdIVvdaAnazYvizQEGr\/cfZvP0Fu6WfPcggYHbGVdmVE+s61yaqJTKoFEpnrvcwaHLhIrdFD62KErll9Of46bH1s3FQokRnN\/\/lCQwjjFjP6cHWfZTVaXZLhwJ1QVXk4+567+e4mtnUk1Zx0WOQNjqfjorLPfsafmfrFLUALZYHvNYdPjtshON8Mo1QiQLtMMqzW6RmMl5VDmIlDE5m0L6QiV9SwVbo15hu0ga+ZzjffvglZzQXJbdmxq9orZspsk4NeSjE5rwQHpIamE5AtIjd+oFZwM4x13fCmECQTExY5FuUcBbRT7t0SIHRGnlFJUdzzFCPJqqvotBiVQKmx34K6+kdS5BRe2JMmyQcWd3uO11k8QWIPQMa5TzKhsDnvYeOy\/IxPPteTbAoJJfuPJ9ms7MS0YTK91abEUhVwUJVGcpQBuSz8IsHClw11iBETnC3kDqF+HaneXj6PtjkQP2ewP1Uys4qHLFpEdUKh7aQB+LYulfzzcjx3CyucH3DmB44UtqzwQEXzUu4LlSBuAwEfI8eZQNJ6SJ8hu9G2ixx4mCJJPGAtifO32s\/vq6VHUZMIxyEcHaMjy7YGigJcSfcYGbwfcChOJwyd3YMFkEtPHDbfg6eU1uC1DSfQ4s1DmJFO0i3rFGbUtLT2UaSIQWhregVOQxibREl5QUEUl\/HBboY+Hwnsj8gUTlVV4ue7X28EceaNewbKuvYSzx8H5zDR\/4UetXMqDPJgAZMlxBEYkefztAPQNlcPu95T5nb\/gQeYDNt\/lfy3yYhs1DAkLvI7uRDaPVV30uDYhMIuO0prvRWevqlIBkhvJvD99\/0VA8ccA+ophH8wC\/xpBVrE318+GjhJ8cfTvvhSPuYbcxdCg1Erl1249B4+f4arRKiOVYvfvG5YIA6FPCpNL1rP6YDNdGHEO0EP3CAPN7onMri7FVCcUzV2iI8ozgoA0J+0\/IVBbT86E0sDiLATWdKgEXOc1Bz+GNoVf3G5PYbmYCV9nZVeEOMiCeWNo1vNSIjtC407Fh9a3s3HtsGxcLYR4w5jUYByUOCkC51BTUJvmT5d6EDXInZsTxcT5Ynqp\/MwBKHYqwpFXUpL3x4w\/uya0NmoamRKHei2IwbmJvhYg4uENJigbeH7Ecsga6S91YiTaA6ZmVnyvuG+7X25lSQdn2q\/Us5wMGTQNSJLHtsEK2E9VMt1Lzk8aAjB+EgIvimPKTYtndFpZ\/sLxt\/j0McvddD8SlqGwKp84YQPNkdl9nJfdWc+rCF9sKW6zN1hW7iVvj6m8\/AsZbVb+pZAJx5wT+jmgHFeuSCpvOeDBgQqP\/rpnF84MaA9VC457ZJCbzuRZ\/a0OFdJxKNCyAL9s267W7DXaJhFi9sJRUc3\/zLwPIzgP1\/2m8OZmBZk1Tj\/SoAMuYnMMimDKU\/ktisHvhl0kLPFhSwSGx376edIfnsI9H8ocxzLZH6ypPhovlKF8XpGC1Yaz4EPVDp0tEzxYP03Hfp7s9r2kaKVqUxPkzSSSuECOrVEXXlOL5JJypewhZaGBNo2Lt3vxGtrYSO6DBFpGN8thZbECjgD5W8NLs7ZAxaT3i+QQJfbsONgYXWMyw04KKQzxXAg1qdXiqttIlQUf\/7NzKH7Vk60KIQQCbMZhgEcHgjY0S8FR+g4LmMeArXsfefcsG4nGt\/D2XVYeFmWX0S7CA5+iFRxhbMBLFlJmXAiChdluTuNY8PI2pRR3GJN0EMAYq9itxzOW6gbG5dqaSL5rj56zK2s0Oe8kXO\/+RDAkIBz9CnCXqNHyaaGW4psEuULbsfnnPfVnYHd4aokYb198C6oHSSrgSMpyd4M962xDeDqowVeyelEcs3yqJyurHgQZ3fb4m3DiL9xDtS1o140XcFs31E3TcHK81l0P6Fqhn4LhTpM+a2ofJXkjFvKF3pZpyvof9kPUi\/lZ2OVYKraTPonbh1j+zK7vdhba39oZ4taCRCRA32TvJXUlCE73R0apqUGDALrO2fC6sV3Pgx0sM5sYyYIys7Hkb8P\/eOiTJYf2PyFk5dyRY91lRI+JGtWEy7weHspKpQ9JbpI8nnmcbuOV4Dmn4yGjXW22l8beFg6eUAKWN1cNMwSakIChHqk9BobamPTnNYUOKDOsJoZ7Y\/jfrRk3iUgp3d9540s+yOSrhzHjRLs12cMcdLB6VaoeEjO0HVMghbkfirPd6S2j6s8t1PKjgZTam51TBHmQOKb4tIyVkhvQSQr5GaFR1BjqIH34UfZHPlmTaYq2LutoafOHJNPHMoKMuReq8g+xAw\/2i2jV9USz1s+\/H59sLrbLPYvkD1p4UaJISJOYNM5GIstdPHqn0DwKJ3LRmI3+kp6lbNA6LQcUKhMyKrO4\/pNhVnKF5x3GscNogwbfUOGAEjGlB+G7xu3SQh2Y5J62v8cRdepC+s0iMI2YtzjqxDHo7mMxgXCPRGVGZKBC3d7pz1mtDigXBV6zxaOWZDNPEQpElD2by7AOM6fkIsbtDoY9a\/tCH635EoNqUS\/j2IljX1TzFnzuDQm7rj41V5Rho++qgOGCtw7c3RzZWti5CuvnasiMHNdy7ceJtrVyp4Y0bNtHfQ3RxJqDj8hy\/t3DGWu5Sokeyc8Sbyl7ac0I3UqsqvmGm2V\/WfiFIkH5DhGv8kE67JLTNHOw\/42c+h+FRkTFShHnyYNQiduSqHy8pmDFwdhAs4hpNA2oGWN49ppqdoKu5cm\/WM9ZrZCeFIHnhHMY6EsYOEE8tTLF545bDZEkWKO5wBDTyxnX3Wz4amc9plEDtd9aGH7JdAU5Fnx5vHo8WZW\/OzMGW\/J2r2tClwmqLLIOEeAt4AA4E\/182HCNABlSr3NzNFzJwjR3PmnKL7kTmfj7pw\/Ic1OUBGbgJlTSHhS3+hcHZG1Ikig3XQpb1UI6Ijo4WDPkeoDwBrbIDNYdA6qPrwyUVAhM2\/4yboWDhl06tnUCiS2f+WpHy9cfD0n1GAA0HwrNK4PwnawvqpM0HeAhWecXt1s+bWixlO4bjDePeQv5HMGUX\/oyfRJcdIoEtHtruSl63DMEmHyw2NyCogtYNoMU8qedqOcyhkg5TsG1o\/KOG68isg50XIJzsYve\/xBp9t6yz9MYXMPCae1zJTWJ1F3MnonczBXN0B6xYlZT5nhH9dYCcFKWt0Hw\/nLw3dTisLbIHa\/B2tesLSxL303CW457QtRjkBDw8Zm88wF4kRrIxu9ePxGh5yvU\/\/9R+lyiDL+Dot6txOwbcp4TLnmgQ+iIcCbQnE5k1x9g7RwQlOavspGL54fWFx\/PWdsJALkWXm1NNalzDOCgtLa66xDNXcfHVWconokoMIGV66\/PPuEkiBHu1TM9wW+mFpKgmnUnzs8mAl16D\/hG95XI9UcLxATGTw52q7MblOmhmgpAIf6VP+boi2tHVVH9Dfc5EAi+\/GMQ36WKJ2cxR414wjEPAAb9OAyXvPzv4HWMp1Ws21o3EJhg\/LrHMjTzWB\/NQlFGn+bShwpcZ1tfDnfDWfK0jXLuz0IDuJYMD\/7A\/pIGvqwzYEA4QdpimmU+FJP29iSmd5v6DZ4l9VP0k5tObpw18\/2dZioPyO1SBGkwBxOgOJZOvpyYzm2tRCruZxIgO6NO+ZPe8S0s+9pcz+zIrP3C9CrF3o7+C\/c3laZ6mCj+05mcJnvaD6PqXhilQ3eI4jyk5dhobZWXFU8jQDyADFPlFA3vsdlWi210mWcxZzJ7F1s47MWZpvKu0MxrpVbRKVQSQsycWajrEXxBxHbde4heuowChips5mxZeGxbMpdvPvGxNZE2RlpuM\/m\/EJrSDFVB+RRyebAdCgI1d2kKgwytgMYs1TWsM+geiRBrSkEQ9A4hIWEo6Qe1J\/p+vm+IgU0eGTeZT6mNnDV73jyhGdTjhBXfuD4F6d4x2gQS+UVd5t9A5fQpQslYJA9uFLSQDLOAPXC6h7A\/TniKvXfzt19S9BePqNez8FOqWG7sOhYYel5iU8G+4scS3ERbC2RqtDFbhwg7IMDNiP+H\/Nu6\/4FjWMk\/M7mX1W5Yzcdq8orTryaPSpSV7+d79cfvWSFMoIjVFvL9QRdTdxq0TXTa+AfV40Ey\/DtMzgp5c4hP\/O0foFd3F75YfZey4DUIIThrkC71KJhAgiGxUNlw5U\/h4tjXl+iJfI1Flnw5DPlPdctMYF3niG+Ao7E6qcyDlreZChdgiUTa9RpsZLy75h+pKheQUTrzuqaIRFBdW3TfXBepZXedCsYwKnBxKN25Zo+PGvfxQgK0+G21BKFsOWqPVI3mDlEKbSDZfkTyH7tJGzchdQhIzJaNAmPp3uw36Klm5YP7niWjVeLFkKoD2r71krwkoIisqhmYyEXjciYN7MX0do8kdSPhVCUZd+oUha7qoSaNIM6Ud7p9XxjN0Q3Q8t68NAw1Brsnk2NmQqwBm71CIZ8oz4Du3yTFyAKS0dkxuwRn5o6K44taMGN33B\/PBdBn9rRfkk86ZKuNFaNvOhO9UC00P4A7Zg0ZJiIBggEq3o\/D1GR+SUzgo9MnlGFrRpaQ83VqBt07X2tc2E1VUPUZftkuCAIIB7L93sEshxKkQjkVsrE1m+7LnvOs1dSrke9dItqSZ9CnakMAYMR7AGAH5eHN6qSGEypaZ0wZqBr8SR1tRq9TPJVwgrym9JPqtBCJMMT5yd8GoAxqv4iuauMXs1HrTk6BzH9bb8zWV21N4YKxiHmIJBS\/Diq6ip4PWcTa5nnDik+1xeg4ACME8\/rNn+xttx0rRJ1ENBOKH8DNe5Wgyy1zqIursQ7vXQE7c685KGk7L\/4rdZYThxnON8maaedi0CRA6JBrRPtmRjpBiLulkglP6buQfeEMllJk\/q1PtwyEt\/WDisy4P27oBEBAEItJ+8zgdx4JJsf5bNwuw8+Wjg3JKcH4GVEzhtzh81gYVqs7E9wRz3GRyS7V\/eALEXW2qsfnu\/DlC11kXREw72tkWIUtXZPjQB+jJhxN1fLov4UelK8\/BBE+PzxE+jaCm48p08jGdjfm79GHhYCDQ5YLldWPdvUcYRdtMRmKDhjMxwlV3UcxH6r8Nc0MYcvCrUBokdJfwQFN7m+Qna+AwH7Bu51gUmuMy66XJepxrrJsFOWIdLiWgpZxm3\/oEs7I2DEGzHSgzOH4Zn2jU5Yep1Fh30lpaeScPYP60cCS6sAF1Dp4O1qZ57l\/O8Fqvi3f8Ng8w00aQEM9RsBBSg2Ee29eqZzEx4++\/1qtRlXmgkCkLWjBlpr3CAj3tMJJlAugQkSYq08xac9I4nqOwHGQLyOX10FPSbZcBYQqq77GpH40K9ur5PvKcXT488qgpUj8Os++tkA4fMINQTw9pdWavQmhmwogeFgP5ByQnS7zt0ocIK2OPyTbBVvVsTaQ\/wCmRqjUHmvIDIkAv7bG99wvAxksvt80OGkzaUTblEPn+TWuZ4p8yF\/DQmmiaklvZ5lPcN63Nnr6kb4KM9rxDbxiXNBRT2IMp8FeLlCs\/a+tbp+xlnIJwy34VMhva+mpZl9vmQFhICTgDwuojt5q35D9Qq6JjKisxfSY3Krb+Lao4jGi1mKzq\/pzaTahKvSIv8TshvxyIs\/ULHscvPl\/zKmTf8B54y6qrpw\/S+ILNrMop0Yib7BUAAibLNhle7bYmAAGC6nrSYcE1LF+GZvSCCDHMwsX4Y\/DQT1uNhuHsnu5PKnCjm9H6P2G9hviriHTw0VpkzqExNnLUkfkmmWYkhlKFSV0eXGZPKIib47B3n2tdNB1cLprlfdwX1mtw0TZINbHrX6mTNF+X5ErEeGVZIRwxNoeN9TPVsnhhe0aNpOZ8bACZRgpbrEWREuLP1M+oKJZWp17uy3LI6E5EdArrAR13eKw7jdCiXYYK7PCzBjOuMYcIgfF4uYPiC2FE3mbLmGUxCPBUozrwGWu3A38Pik\/mfIKXyzVem2bP\/z0xGlefa4VkxAXzL5aesbrekxPjEGUaDT+c6mH\/TU3e9i2VoZs\/dFNdgCcHIUFh1lNFMBIRHrA2b9SEM5lBV09wL\/CM107zZRwZI5wmHeR1BhcsduVPlduB7+Gujmpnm5bNTTM3a53KBMa4aQE+SLpJGsixr42mxxDyUuvlgLwg43d4xbNgwWPf\/SCaxSvAWAFWHcVlWUpaMB54al6fAPrqKJeRxU6djRqB7WHZRuodNTIJDBqOl6r3NzcPvAiOtvfAuf4kea8WYqdNcBG7q6TMTrA\/\/VQss2dhaRxTpvEoqfZpS+O2xMCqv768CBA8pCgwy3i+TU5C4e+T4CtR4SJwjRL4yfHTL8bw4fMB4Ll3v+mNxhyCaJJWPv\/RH4QLivaqb7VojpzjGIoEdma77oUVNuTU1Uh0QPKElcNmAC8UzxNTzXv+U0fmmbu3XVsyF4TDMe8mT5HgMpeSoMEAaRkllnrS62x+itTiwUl2PH+eu0BYZHxZwYSyNwlCg2d5F7HgMrEqiTKi62wkHoPthQh484AkLQu6AwTQQUSsePA8qKJT5me4x4n3HcqKxD97xd5vqT08v4wCnSmG\/rDrPaZZEfB0wouCn0z3TAKcA\/zR9kCcwlRMJwkhaOjoxRcqmAzgjhNhJr8DbhEMmoiFWVqXj7WccGGi82xrx2JdrQ5WxzzEbjOfGwV033WrrA1sGFfW7+bM2w+Dj7FK0m98eS6shV0nDv6qVDDh+HRjQfU9CKCBpZ9fSfFpWam8PqWLCdXoSMlBDx1zUw5OQBlib\/LW884zbZmQFykZ4x6A5bdbXAJIxUwft8W0zpc4rw6dG9n9D8gcaiTnllq3kel\/hQUcMPCPmUH7B5c8xCO6r7wWVHyxV7FjOKiNZiPJZUcWFGMsdnaCK\/nf4FfUV5nKZebmX9BL5nVzxiICQ06vJgVgui3jGKtsT21FEmG8p7mulX7exYp115J8qbcxjzo4Jnbb5HtZrF5ScKRiMl30CYqT+H2iMOU57Fj8Ie8CGphebVEgjMVnH2iO34yG4oOHkNah2kFjWFfVapLrsQZtgUUB74rhHCuxVMg4i8romRpGv3\/l0TDRymb97K7b59bD0MgQHL7K+ya9ghzPKU1TPFxmKHRDfgAX5b88OfKG+RML3elAWVo8VzNlApQ7tr2QzkqjBHlaegjwNKnKcYYRolSPVlndiWMf0LA2q4bbEuFaRnT9CVvuFm\/QK\/fErEIqc0fAJ\/xwzVQ\/xjp2NyWlwHnoAVscWiLcugeiNTIrC5QezTnkOHWZnIdWirxJq9hgj7WItZRGlGR9MxbXNpq1d1ILBTISz1sTFMq12ky0mK+SzVOJgpPJezsVS2j8Dooliapu8BcRTraJAgdGvyRMAJCFhr5LfiCjQxdeCu7qryY+LONUQr\/HJmKYa6r80MZehVC72TDaWmEpWBMaJKJGgPJwO0ugou9h45VMMUE4MOzkoszYMuoZidC6XrKLMwduzzxTbzvL\/4kd+621GLs5b9TdzU1S7PGtYu0ifX42eE4f+phPsny0nLZEmIH6QQ64BxfllBV1aEk8ddp2kpAlpsP78Ihetl2peeb5OAMK3Vb8PW2jrT0kv7c1eDxFw2p10e2ydO7wliz3e0WYZ5nRWZ9LQIDQrrdEt0NAgDI0aQFTIJZ2Rh\/ExomOUYovXaUJA4XJJV2ZzfdEO5mq7L3XEOmoy\/2969hEGZLZma3lyV1Q796Wjq1ZycmHLU6RA\/TqJlcx7jgOl4\/9tqsB3m+Y6BjLEvq26BOtVkDMFVQf5+GKmVfgQw+Odpp5Wv3qis1ApPKmsxnxDTYQlb2OU6yquihWazCoLjpbLcY8UJhDXLGHtm8rAHp1U7hyCih73eLFt5saB57NVA3vLISirK4Ng97M5afix8cf16PaCOtXk4vt5+nQ36dg\/SRQhqaGdeLCH3ip2FmeQ6mT2kKK6bKWa4wsKVVzFvVtw3ifa7\/tJfZD080z5xf3WWHf5ApKiafv6ep8SKTubu9u+HlVkNs9Q\/9lq9ooCMt1YRtbpKjjj6WGtT3jWWQSZXcB7EOLJntNP0xx7adJT+F6awDKswKKH8iEDbmCoVjJhid5O20Qz0zKFIXY4NEUcTyZn1yK\/yRZFENTGumypD1GAfoAcfSKgzY0IaP2F5Ep0u98RwEpqY3nnZzobI5R6GASFYxRMD6oBYWi+54OXgrMKdvGodGSBuFnBvjFR8vSqHhSW9vMwu89IAL6IvC5ikPd9A95W4dyHIMAa5DEMQFbXr4rFjQuD2Vg",
+ "SecurityTrailer": {
+ "AdyenCryptoVersion": 1,
+ "Hmac": "Q4GI5WPe8hduJbzvEgzqei2Dy1mZ4DAextLs6yDKVs0=",
+ "KeyIdentifier": "Key123456789crypt",
+ "KeyVersion": 1,
+ "Nonce": "bflPDJ8VceOR4JUFN5Ejdw=="
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/mocks/clouddevice/payment-async-error.json b/src/test/resources/mocks/clouddevice/payment-async-error.json
new file mode 100644
index 000000000..d1b92d279
--- /dev/null
+++ b/src/test/resources/mocks/clouddevice/payment-async-error.json
@@ -0,0 +1,20 @@
+{
+ "SaleToPOIRequest": {
+ "EventNotification": {
+ "EventToNotify": "Reject",
+ "EventDetails": "message=Did+not+receive+a+response+from+the+POI.",
+ "RejectedMessage": "ewoi...0KfQo=",
+ "TimeStamp": "2020-03-31T10:28:39.515Z"
+ },
+ "MessageHeader": {
+ "DeviceID": "666568147",
+ "MessageCategory": "Event",
+ "MessageClass": "Event",
+ "MessageType": "Notification",
+ "POIID": "P400Plus-123456789",
+ "ProtocolVersion": "3.0",
+ "SaleID": "saleid-4c32759faaa7",
+ "ServiceID": "31122609"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/mocks/clouddevice/payment-async-success.json b/src/test/resources/mocks/clouddevice/payment-async-success.json
new file mode 100644
index 000000000..b5754e203
--- /dev/null
+++ b/src/test/resources/mocks/clouddevice/payment-async-success.json
@@ -0,0 +1 @@
+ok
\ No newline at end of file
diff --git a/src/test/resources/mocks/clouddevice/payment-sync-encrypted-success.json b/src/test/resources/mocks/clouddevice/payment-sync-encrypted-success.json
new file mode 100644
index 000000000..5f3938a23
--- /dev/null
+++ b/src/test/resources/mocks/clouddevice/payment-sync-encrypted-success.json
@@ -0,0 +1,21 @@
+{
+ "SaleToPOIResponse": {
+ "SecurityTrailer": {
+ "AdyenCryptoVersion": 1,
+ "Nonce": "YIyJBzjWFCAZn31QfAvGLA==",
+ "KeyIdentifier": "CryptoKeyIdentifier12345",
+ "Hmac": "hpptt0KfxlALCMSo35tTwtgw6fDNbQEESOTdD0AD9Sg=",
+ "KeyVersion": 1
+ },
+ "NexoBlob": "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",
+ "MessageHeader": {
+ "ProtocolVersion": "3.0",
+ "SaleID": "John",
+ "MessageClass": "Service",
+ "MessageCategory": "Payment",
+ "ServiceID": "9739",
+ "POIID": "MX915-284251016",
+ "MessageType": "Response"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/mocks/clouddevice/payment-sync-success.json b/src/test/resources/mocks/clouddevice/payment-sync-success.json
new file mode 100644
index 000000000..637405767
--- /dev/null
+++ b/src/test/resources/mocks/clouddevice/payment-sync-success.json
@@ -0,0 +1,330 @@
+{
+ "SaleToPOIResponse": {
+ "PaymentResponse": {
+ "POIData": {
+ "POITransactionID": {
+ "TimeStamp": "2019-04-29T00:00:00.000Z",
+ "TransactionID": "4r7i001556529591000.8515565295894301"
+ },
+ "POIReconciliationID": "1000"
+ },
+ "SaleData": {
+ "SaleTransactionID": {
+ "TimeStamp": "2019-04-29T00:00:00.000Z",
+ "TransactionID": "001"
+ }
+ },
+ "PaymentReceipt": [
+ {
+ "RequiredSignatureFlag": false,
+ "DocumentQualifier": "CashierReceipt",
+ "OutputContent": {
+ "OutputFormat": "Text",
+ "OutputText": [
+ {
+ "CharacterStyle": "Bold",
+ "Text": "key=header1",
+ "EndOfLineFlag": true
+ },
+ {
+ "CharacterStyle": "Bold",
+ "Text": "key=header2",
+ "EndOfLineFlag": true
+ },
+ {
+ "CharacterStyle": "Bold",
+ "Text": "name=MERCHANT%20COPY&key=merchantTitle",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "key=filler",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=Date&value=29%2f04%2f19&key=txdate",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=Time&value=10%3a19%3a51&key=txtime",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "key=filler",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=Card&value=%2a%2a%2a%2a%2a%2a%2a%2a%2a%2a%2a%2a3511&key=pan",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=Pref.%20name&value=MCC%20351%20v1%202&key=preferredName",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=Card%20type&value=mc&key=cardType",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=Payment%20method&value=mc&key=paymentMethod",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=Payment%20variant&value=mc&key=paymentMethodVariant",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=Entry%20mode&value=Contactless%20swipe&key=posEntryMode",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "key=filler",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=AID&value=A0000000041010&key=aid",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=MID&value=1000&key=mid",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=TID&value=P400Plus-275039202&key=tid",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=PTID&value=75039202&key=ptid",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "key=filler",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=Auth.%20code&value=123456&key=authCode",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=Tender&value=4r7i001556529591000&key=txRef",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=Reference&value=003&key=mref",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "key=filler",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=Type&value=GOODS_SERVICES&key=txtype",
+ "EndOfLineFlag": true
+ },
+ {
+ "CharacterStyle": "Bold",
+ "Text": "name=TOTAL&value=%e2%82%ac%c2%a01.00&key=totalAmount",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "key=filler",
+ "EndOfLineFlag": true
+ },
+ {
+ "CharacterStyle": "Bold",
+ "Text": "name=APPROVED&key=approved",
+ "EndOfLineFlag": true
+ }
+ ]
+ }
+ },
+ {
+ "RequiredSignatureFlag": false,
+ "DocumentQualifier": "CustomerReceipt",
+ "OutputContent": {
+ "OutputFormat": "Text",
+ "OutputText": [
+ {
+ "CharacterStyle": "Bold",
+ "Text": "key=header1",
+ "EndOfLineFlag": true
+ },
+ {
+ "CharacterStyle": "Bold",
+ "Text": "key=header2",
+ "EndOfLineFlag": true
+ },
+ {
+ "CharacterStyle": "Bold",
+ "Text": "name=CARDHOLDER%20COPY&key=cardholderHeader",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "key=filler",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=Date&value=29%2f04%2f19&key=txdate",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=Time&value=10%3a19%3a51&key=txtime",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "key=filler",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=Card&value=%2a%2a%2a%2a%2a%2a%2a%2a%2a%2a%2a%2a3511&key=pan",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=Pref.%20name&value=MCC%20351%20v1%202&key=preferredName",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=Card%20type&value=mc&key=cardType",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=Payment%20method&value=mc&key=paymentMethod",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=Payment%20variant&value=mc&key=paymentMethodVariant",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=Entry%20mode&value=Contactless%20swipe&key=posEntryMode",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "key=filler",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=AID&value=A0000000041010&key=aid",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=MID&value=1000&key=mid",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=TID&value=P400Plus-275039202&key=tid",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=PTID&value=75039202&key=ptid",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "key=filler",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=Auth.%20code&value=123456&key=authCode",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=Tender&value=4r7i001556529591000&key=txRef",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=Reference&value=003&key=mref",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "key=filler",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=Type&value=GOODS_SERVICES&key=txtype",
+ "EndOfLineFlag": true
+ },
+ {
+ "CharacterStyle": "Bold",
+ "Text": "name=TOTAL&value=%e2%82%ac%c2%a01.00&key=totalAmount",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "key=filler",
+ "EndOfLineFlag": true
+ },
+ {
+ "CharacterStyle": "Bold",
+ "Text": "name=APPROVED&key=approved",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "key=filler",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=Please%20retain%20for%20your%20records&key=retain",
+ "EndOfLineFlag": true
+ },
+ {
+ "Text": "name=Thank%20you&key=thanks",
+ "EndOfLineFlag": true
+ }
+ ]
+ }
+ }
+ ],
+ "PaymentResult": {
+ "OnlineFlag": true,
+ "PaymentAcquirerData": {
+ "AcquirerPOIID": "P400Plus-123456789",
+ "ApprovalCode": "123456",
+ "AcquirerTransactionID": {
+ "TimeStamp": "2019-04-29T09:19:51.000Z",
+ "TransactionID": "8515565295894301"
+ },
+ "MerchantID": "TestMerchant"
+ },
+ "CurrencyConversion": [
+ {
+ "ConvertedAmount": {
+ "AmountValue": 48.32,
+ "Currency": "EUR"
+ },
+ "CustomerApprovedFlag": true,
+ "Markup": 3,
+ "Rate": 0.035
+ }
+ ],
+ "PaymentInstrumentData": {
+ "CardData": {
+ "EntryMode": [
+ "Tapped"
+ ],
+ "PaymentBrand": "mc",
+ "MaskedPan": "411111 **** 1111",
+ "SensitiveCardData": {
+ "ExpiryDate": "1225"
+ }
+ },
+ "PaymentInstrumentType": "Card"
+ },
+ "AmountsResp": {
+ "AuthorizedAmount": 1,
+ "Currency": "EUR"
+ }
+ },
+ "Response": {
+ "Result": "Success",
+ "AdditionalResponse": "tid=75039202&AID=A0000000041010&transactionType=GOODS_SERVICES&backendGiftcardIndicator=false&expiryYear=2025&acquirerAccountCode=TestPmmAcquirerAccount&alias=M900978995070104&posOriginalAmountCurrency=EUR&giftcardIndicator=false&authorisedAmountValue=100&pspReference=8515565295894301&paymentMethodVariant=mc&cardHolderName=N%2fA&refusalReasonRaw=APPROVED&authorisationMid=1000&expiryDate=12%2f2025&applicationPreferredName=MCC%20351%20v1%202&acquirerCode=TestPmmAcquirer&txtime=10%3a19%3a51&iso8601TxDate=2019-04-29T09%3a19%3a51.0000000%2b0000&cardType=mc&posOriginalAmountValue=100&offline=false&aliasType=Default&txdate=29-04-2019&paymentMethod=mc&cvcResult=0%20Unknown&avsResult=0%20Unknown&mid=1000&merchantReference=003&transactionReferenceNumber=8515565295894301&expiryMonth=12&cardSummary=3511&posTotalAmountValue=100&posAuthAmountCurrency=EUR&cardHolderVerificationMethodResults=3F0300&authCode=123456&shopperCountry=NL&posEntryMode=CLESS_SWIPE&cardScheme=mc&cardBin=541333&posAuthAmountValue=100"
+ }
+ },
+ "MessageHeader": {
+ "ProtocolVersion": "3.0",
+ "SaleID": "001",
+ "MessageClass": "Service",
+ "MessageCategory": "Payment",
+ "ServiceID": "1234567890",
+ "POIID": "P400Plus-123456789",
+ "MessageType": "Response"
+ }
+ }
+}
diff --git a/src/test/resources/mocks/clouddevice/status-device.json b/src/test/resources/mocks/clouddevice/status-device.json
new file mode 100644
index 000000000..fc23716f2
--- /dev/null
+++ b/src/test/resources/mocks/clouddevice/status-device.json
@@ -0,0 +1,4 @@
+{
+ "deviceId": "AMS1-000168242800763",
+ "status": "ONLINE"
+}
\ No newline at end of file
set method for the certificate property.
+ *
+ *
+ * getCertificate().add(newItem);
+ *
+ *
+ * set method for the signer property.
+ *
+ *
+ * getSigner().add(newItem);
+ *
+ *
+ * set method for the entryMode property.
+ *
+ *
+ * getEntryMode().add(newItem);
+ *
+ *
+ * set method for the storedValueData property.
+ *
+ * set method for the storedValueResult property.
+ *
+ *
+ * getStoredValueResult().add(newItem);
+ *
+ *
+ * set method for the allowedPaymentBrand property.
+ *
+ *
+ * getAllowedPaymentBrand().add(newItem);
+ *
+ *
+ * set method for the acquirerID property.
+ *
+ *
+ * getAcquirerID().add(newItem);
+ *
+ *
+ * set method for the allowedLoyaltyBrand property.
+ *
+ *
+ * getAllowedLoyaltyBrand().add(newItem);
+ *
+ *
+ * set method for the forceEntryMode property.
+ *
+ *
+ * getForceEntryMode().add(newItem);
+ *
+ *
+ * set method for the documentQualifier property.
+ *
+ *
+ * getDocumentQualifier().add(newItem);
+ *
+ *
+ * set method for the paymentTotals property.
+ *
+ *
+ * getPaymentTotals().add(newItem);
+ *
+ *
+ * set method for the loyaltyTotals property.
+ *
+ *
+ * getLoyaltyTotals().add(newItem);
+ *
+ *
+ *