├── .idea ├── dictionaries │ └── pabloperezgarcia.xml ├── encodings.xml ├── markdown-navigator.xml ├── scala_compiler.xml ├── uiDesigner.xml └── vcs.xml ├── .mvn └── wrapper │ ├── MavenWrapperDownloader.java │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── README.md ├── application ├── mvnw ├── mvnw.cmd ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── politrons │ │ │ └── application │ │ │ ├── handler │ │ │ ├── PaymentHandler.java │ │ │ └── impl │ │ │ │ └── PaymentHandlerImpl.java │ │ │ ├── model │ │ │ ├── command │ │ │ │ ├── AddPaymentCommand.java │ │ │ │ ├── PaymentCommand.java │ │ │ │ └── UpdatePaymentCommand.java │ │ │ ├── error │ │ │ │ └── ErrorPayload.java │ │ │ └── payload │ │ │ │ ├── payload │ │ │ │ ├── BeneficiaryPartyPayload.java │ │ │ │ ├── DebtorPartyPayload.java │ │ │ │ ├── PaymentInfoPayload.java │ │ │ │ ├── PaymentStatePayload.java │ │ │ │ └── SponsorPartyPayload.java │ │ │ │ └── response │ │ │ │ └── PaymentResponse.java │ │ │ ├── resources │ │ │ ├── PaymentHealthCheck.java │ │ │ └── PaymentResource.java │ │ │ └── service │ │ │ ├── PaymentService.java │ │ │ └── impl │ │ │ └── PaymentServiceImpl.java │ └── resources │ │ └── META-INF │ │ ├── beans.xml │ │ └── microprofile-config.properties │ └── test │ ├── java │ └── com │ │ └── politrons │ │ └── application │ │ ├── it │ │ ├── PaymentResourceBackendDownTest.java │ │ └── PaymentResourceTest.java │ │ ├── performance │ │ └── PaymentResourcePerformanceTest.java │ │ ├── unit │ │ ├── CommandsTest.java │ │ ├── PaymentHandlerTest.java │ │ └── PaymentServiceTest.java │ │ ├── utils │ │ └── PaymentResourceTestUtils.java │ │ └── volume │ │ └── PaymentResourceVolumeTest.java │ └── scala │ └── com │ └── politrons │ └── application │ └── JsonUtils.scala ├── domain ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── politrons │ │ │ └── domain │ │ │ ├── PaymentStateAggregateRoot.java │ │ │ └── entities │ │ │ ├── BeneficiaryParty.java │ │ │ ├── DebtorParty.java │ │ │ ├── PaymentInfo.java │ │ │ └── SponsorParty.java │ └── resources │ │ └── META-INF │ │ └── beans.xml │ └── test │ ├── java │ └── com │ │ └── politrons │ │ └── domain │ │ └── entities │ │ └── EntitiesTest.java │ └── scala │ └── com │ └── politrons │ └── domain │ └── entities │ └── JsonUtils.scala ├── img ├── ddd.png └── testPyramid.png ├── infrastructure ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── politrons │ │ │ └── infrastructure │ │ │ ├── dao │ │ │ ├── PaymentDAO.java │ │ │ └── impl │ │ │ │ └── PaymentDAOImpl.java │ │ │ ├── dto │ │ │ ├── BeneficiaryPartyDTO.java │ │ │ ├── DebtorPartyDTO.java │ │ │ ├── PaymentInfoDTO.java │ │ │ └── SponsorPartyDTO.java │ │ │ ├── events │ │ │ ├── PaymentAdded.java │ │ │ ├── PaymentDeleted.java │ │ │ ├── PaymentEvent.java │ │ │ └── PaymentUpdated.java │ │ │ └── repository │ │ │ ├── PaymentRepository.java │ │ │ └── impl │ │ │ └── PaymentRepositoryImpl.java │ ├── resources │ │ └── META-INF │ │ │ └── beans.xml │ └── scala │ │ └── com │ │ └── politrons │ │ └── infrastructure │ │ └── CassandraConnector.scala │ └── test │ └── java │ └── com │ └── politrons │ └── infrastructure │ ├── CassandraMock.java │ ├── dao │ └── impl │ │ ├── PaymentDAOBackendDownTest.java │ │ ├── PaymentDAOTest.java │ │ └── PaymentDAOUtilsTest.java │ └── repository │ └── impl │ └── PaymentRepositoryTest.java ├── paymentAPI.iml └── pom.xml /.idea/dictionaries/pabloperezgarcia.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | backends 5 | cqrs 6 | politrons 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/markdown-navigator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 36 | 37 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /.idea/scala_compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/uiDesigner.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.3"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + " .jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading"); 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | } 96 | 97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 99 | String username = System.getenv("MVNW_USERNAME"); 100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 101 | Authenticator.setDefault(new Authenticator() { 102 | @Override 103 | protected PasswordAuthentication getPasswordAuthentication() { 104 | return new PasswordAuthentication(username, password); 105 | } 106 | }); 107 | } 108 | URL website = new URL(urlString); 109 | ReadableByteChannel rbc; 110 | rbc = Channels.newChannel(website.openStream()); 111 | FileOutputStream fos = new FileOutputStream(destination); 112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 113 | fos.close(); 114 | rbc.close(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/politrons/PaymentAPI/2e12a671000d26cf34fe4afb3f96b99ffff7ce65/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.0/apache-maven-3.6.0-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.3/maven-wrapper-0.5.3.jar 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Architecture 3 | 4 | For the design of PaymentAPI I used DDD + CQRS + Event Sourcing, I will explain a little bit why the use of these three architectures patters. 5 | 6 | ### DDD 7 | 8 | Domain Driven Design it's a design pattern for dealing with highly complex domains that is based on making the domain itself the main focus of the project. 9 | 10 | Also it helps to decouple the different layers of your program(Application, Domain, Infrastructure) making it more extensible and easy to port each of the layers into another program if it's needed. 11 | 12 | In PaymentAPI I implemented three modules described like: 13 | 14 | * **Application**: Layer which normally is the entry point of your app. It has dependency with domain and infrastructure modules. 15 | * **Domain**: Layer where you implement the business logic of your application. It does not have any dependency. 16 | * **Infrastructure**: Layer where you implement the communication with the backends (Database, services, Brokers). It has dependency with domain. 17 | 18 | ![My image](img/ddd.png) 19 | 20 | ### CQRS 21 | 22 | Command Query Responsibility Segregation it's another Design pattern, mostly divulged by **Greg Young** which segregate the model of Commands and Queries 23 | in your architecture to do Writes and Reads separated. It can give you the chance to make the Queries more efficient since normally in a Cluster the 90% of the traffic 24 | is for Queries. 25 | 26 | In PaymentAPI I implemented Handler for Commands and Service to deal with Queries. 27 | 28 | * **Handler**: Receives commands and transforms the Command into Domain model, to pass the responsibility to the domain, and finally into the infrastructure. 29 | * **Service**: Receives Queries and invokes directly to the DAO in the infrastructure layer. 30 | 31 | ### Event Sourcing 32 | 33 | Event sourcing is a design pattern where you work with Events. The main idea of Event sourcing is to keep and persist the state of your program, without mutating previous states. 34 | 35 | That means that with Event sourcing I'm not deleting any data in the system, giving us the possibility to have an historical about the different state of one Payment. 36 | 37 | In PaymentAPI, Event sourcing together with CQRS, allow us persist events in order to keep state and have historical data. 38 | 39 | * **PaymentAdded**: Event that keep the state of the creation of a payment. 40 | * **PaymentUpdated**: Event that keep the state of the change of a payment. 41 | * **PaymentDeleted**: Event that keep the state of the deletion of a payment. 42 | 43 | ### API 44 | 45 | The design of the API is based on Restful design, so for Queries I used **GET**, for create **POST**, update **PUT** and delete **DELETE** 46 | 47 | Since the root of the endpoints is `/v1/payment` many of the endpoints do not require any extra word, only arguments as `Query param` 48 | 49 | ```.java 50 | @Path("/v1/payment") 51 | @Produces(MediaType.APPLICATION_JSON) 52 | @Consumes(MediaType.APPLICATION_JSON) 53 | public class PaymentResource { 54 | 55 | @GET 56 | @Path("/all") 57 | public CompletionStage> fetchAllPayment() 58 | 59 | @GET 60 | @Path("/{paymentId}") 61 | public CompletionStage> fetchPaymentById(@PathParam("paymentId") String id) 62 | 63 | @POST 64 | @Path("/") 65 | public CompletionStage> addPayment(AddPaymentCommand addPaymentCommand) 66 | 67 | @PUT 68 | @Path("/{paymentId}") 69 | public CompletionStage> updatePayment(@PathParam("paymentId") String paymentId, 70 | UpdatePaymentCommand updatePaymentCommand) 71 | @DELETE 72 | @Path("/{paymentId}") 73 | public CompletionStage> deletePayment(@PathParam("paymentId") String paymentId) 74 | } 75 | ``` 76 | The request body for Create and Update Payment it has this json format 77 | 78 | ``` 79 | { 80 | "amount": "amount", 81 | "currency": "currency", 82 | "paymentId": "paymentId", 83 | "debtorParty": { 84 | "accountName": "accountName", 85 | "accountNumber": "accountNumber", 86 | "accountType": 0.0, 87 | "address": "address", 88 | "bankId": "bankId", 89 | "name": "name" 90 | }, 91 | "sponsorParty": { 92 | "accountNumber": "accountName", 93 | "bankId": "bankId", 94 | "bankIdCode": "bankCode" 95 | }, 96 | "beneficiaryParty": { 97 | "accountName": "accountName", 98 | "accountNumber": "accountNumber", 99 | "accountType": 0.0, 100 | "address": "address", 101 | "bankId": "bankId", 102 | "name": "name" 103 | }, 104 | "paymentPurpose": "paymentPurpose", 105 | "paymentType": "paymentType", 106 | "processingDate": "processingDate", 107 | "reference": "reference", 108 | "schemePaymentSubType": "schemePaymentSubType", 109 | "schemePaymentType": "schemePaymentType" 110 | } 111 | ``` 112 | 113 | And the json response format for fetch payment has this one. 114 | 115 | ``` 116 | { 117 | "id": "id", 118 | "type": "created", 119 | "version": 1.0, 120 | "paymentInfo": { 121 | "amount": "amount", 122 | "currency": "currency", 123 | "paymentId": "paymentId", 124 | "paymentPurpose": "paymentPurpose", 125 | "paymentType": "paymentType", 126 | "processingDate": "processingDate", 127 | "reference": "reference", 128 | "schemePaymentSubType": "schemePaymentSubType", 129 | "schemePaymentType": "schemePaymentType", 130 | "debtorParty": { 131 | "accountName": "accountName", 132 | "accountNumber": "accountNumber", 133 | "accountType": 0.0, 134 | "address": "address", 135 | "bankId": "bankId", 136 | "name": "name" 137 | }, 138 | "sponsorParty": { 139 | "accountNumber": "accountName", 140 | "bankId": "bankId", 141 | "bankIdCode": "bankCode" 142 | }, 143 | "beneficiaryParty": { 144 | "accountName": "accountName", 145 | "accountNumber": "accountNumber", 146 | "accountType": 0.0, 147 | "address": "address", 148 | "bankId": "bankId", 149 | "name": "name" 150 | } 151 | } 152 | } 153 | ``` 154 | 155 | ## Testing 156 | 157 | ![My image](img/testPyramid.png) 158 | 159 | **To go fast you have to go well** this quote of Robert C. Martin express perfectly what TDD and BDD is. You should think first in all corner cases of your program, and then implement 160 | one by one committing every scenario to have a quick feedback about your program. 161 | 162 | In our application I invested around 70% of the time implementing the test framework, the type of testing implemented are described below. 163 | 164 | * **Unit**: I used JUnit5 together with Mockito to Mock external resources of your class. 165 | * **Integration**: I used Quarkus server, which include a very nice Test framework to do BDD and run the application and test all layers of your application. 166 | Just to be clear, the IT test are just a proof that our Unit test are well designed and the Mock behaves as I expect. None IT test should ever fail. And if it does, 167 | you have to reproduce it in Unit test. 168 | * **Performance**: Performance/Stress test are meant to be used to detect possible concurrency issue in your application, 169 | and also to have a control of the latency of your endpoints. 170 | Quarkus seems like it does not work with **Gatling** so I just use **@RepeatedTest** of JUnit5. 171 | So, no control of latency of endpoints available. 172 | * **Volume**: Volume test are meant to be used to have a load of traffic for a long period of time, to detect 173 | some possible memory leaks in your application that it might provoke that your application get 174 | out of memory and die. 175 | Quarkus seems like it does not work with **Gatling** so I just use **@RepeatedTest** of JUnit5. 176 | 177 | ## Technology Stack 178 | 179 | As Http Server I used **Quarkus**, a new Serverless framework implemented by RedHat team, as a new Power Vertx framework. Indeed a good choice to create Reactive systems. 180 | 181 | For the API I used **JAX-RS** standard. 182 | 183 | In order to have a reactive system, functional programing it's a powerful tool. Although Java is functional since Java 8 it's not Scala. **Vavr** is good library for functional 184 | programing that makes you feel like you're in Scala realm again. 185 | 186 | To make transformation of models between layer doing DDD, I used **Orika**. 187 | 188 | Finally as Connector and Database I chose Cassandra since it's a good choice for Event sourcing. 189 | 190 | * **Application**: Quarkus, JAX-RS, Vavr, Mockito 191 | 192 | * **Domain**: Vavr, Orika 193 | 194 | * **Infrastructure**: Orika, Vavr, Orika, Cassandra, Mockito. 195 | 196 | 197 | ## Use 198 | 199 | To run the Unit and Integration test, just go into the root program and run the command: 200 | 201 | ``` 202 | mvn clean install 203 | ``` 204 | 205 | Once that you have your project tested, you just need to run the application. Go to the application module and run the command: 206 | 207 | ``` 208 | ./mvnw compile quarkus:dev 209 | ``` 210 | 211 | Then you will have to run the Cassandra embedded that the project provide. 212 | 213 | ``` 214 | CassandraConnector.start() 215 | ``` 216 | 217 | 218 | ## Health check 219 | 220 | PaymentAPI provide a health check endpoint to know the state of the service and backends. 221 | 222 | ``` 223 | http://localhost:8080/health 224 | ``` 225 | 226 | ##### Response 227 | 228 | ``` 229 | { 230 | "outcome": "UP", 231 | "checks": [ 232 | { 233 | "name": "Payment API health check", 234 | "state": "UP", 235 | "data": { 236 | "Cassandra database": "running" 237 | } 238 | } 239 | ] 240 | } 241 | ``` 242 | -------------------------------------------------------------------------------- /application/mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | fi 118 | 119 | if [ -z "$JAVA_HOME" ]; then 120 | javaExecutable="`which javac`" 121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 122 | # readlink(1) is not available as standard on Solaris 10. 123 | readLink=`which readlink` 124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 125 | if $darwin ; then 126 | javaHome="`dirname \"$javaExecutable\"`" 127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 128 | else 129 | javaExecutable="`readlink -f \"$javaExecutable\"`" 130 | fi 131 | javaHome="`dirname \"$javaExecutable\"`" 132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 133 | JAVA_HOME="$javaHome" 134 | export JAVA_HOME 135 | fi 136 | fi 137 | fi 138 | 139 | if [ -z "$JAVACMD" ] ; then 140 | if [ -n "$JAVA_HOME" ] ; then 141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 142 | # IBM's JDK on AIX uses strange locations for the executables 143 | JAVACMD="$JAVA_HOME/jre/sh/java" 144 | else 145 | JAVACMD="$JAVA_HOME/bin/java" 146 | fi 147 | else 148 | JAVACMD="`which java`" 149 | fi 150 | fi 151 | 152 | if [ ! -x "$JAVACMD" ] ; then 153 | echo "Error: JAVA_HOME is not defined correctly." >&2 154 | echo " We cannot execute $JAVACMD" >&2 155 | exit 1 156 | fi 157 | 158 | if [ -z "$JAVA_HOME" ] ; then 159 | echo "Warning: JAVA_HOME environment variable is not set." 160 | fi 161 | 162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 163 | 164 | # traverses directory structure from process work directory to filesystem root 165 | # first directory with .mvn subdirectory is considered project base directory 166 | find_maven_basedir() { 167 | 168 | if [ -z "$1" ] 169 | then 170 | echo "Path not specified to find_maven_basedir" 171 | return 1 172 | fi 173 | 174 | basedir="$1" 175 | wdir="$1" 176 | while [ "$wdir" != '/' ] ; do 177 | if [ -d "$wdir"/.mvn ] ; then 178 | basedir=$wdir 179 | break 180 | fi 181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 182 | if [ -d "${wdir}" ]; then 183 | wdir=`cd "$wdir/.."; pwd` 184 | fi 185 | # end of workaround 186 | done 187 | echo "${basedir}" 188 | } 189 | 190 | # concatenates all lines of a file 191 | concat_lines() { 192 | if [ -f "$1" ]; then 193 | echo "$(tr -s '\n' ' ' < "$1")" 194 | fi 195 | } 196 | 197 | BASE_DIR=`find_maven_basedir "$(pwd)"` 198 | if [ -z "$BASE_DIR" ]; then 199 | exit 1; 200 | fi 201 | 202 | ########################################################################################## 203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 204 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 205 | ########################################################################################## 206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 207 | if [ "$MVNW_VERBOSE" = true ]; then 208 | echo "Found .mvn/wrapper/maven-wrapper.jar" 209 | fi 210 | else 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 213 | fi 214 | if [ -n "$MVNW_REPOURL" ]; then 215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.3/maven-wrapper-0.5.3.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.3/maven-wrapper-0.5.3.jar" 218 | fi 219 | while IFS="=" read key value; do 220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 221 | esac 222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 223 | if [ "$MVNW_VERBOSE" = true ]; then 224 | echo "Downloading from: $jarUrl" 225 | fi 226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 227 | if $cygwin; then 228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 229 | fi 230 | 231 | if command -v wget > /dev/null; then 232 | if [ "$MVNW_VERBOSE" = true ]; then 233 | echo "Found wget ... using wget" 234 | fi 235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 236 | wget "$jarUrl" -O "$wrapperJarPath" 237 | else 238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" 239 | fi 240 | elif command -v curl > /dev/null; then 241 | if [ "$MVNW_VERBOSE" = true ]; then 242 | echo "Found curl ... using curl" 243 | fi 244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 245 | curl -o "$wrapperJarPath" "$jarUrl" -f 246 | else 247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 248 | fi 249 | 250 | else 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo "Falling back to using Java to download" 253 | fi 254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 255 | # For Cygwin, switch paths to Windows format before running javac 256 | if $cygwin; then 257 | javaClass=`cygpath --path --windows "$javaClass"` 258 | fi 259 | if [ -e "$javaClass" ]; then 260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 261 | if [ "$MVNW_VERBOSE" = true ]; then 262 | echo " - Compiling MavenWrapperDownloader.java ..." 263 | fi 264 | # Compiling the Java class 265 | ("$JAVA_HOME/bin/javac" "$javaClass") 266 | fi 267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 268 | # Running the downloader 269 | if [ "$MVNW_VERBOSE" = true ]; then 270 | echo " - Running MavenWrapperDownloader.java ..." 271 | fi 272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 273 | fi 274 | fi 275 | fi 276 | fi 277 | ########################################################################################## 278 | # End of extension 279 | ########################################################################################## 280 | 281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 282 | if [ "$MVNW_VERBOSE" = true ]; then 283 | echo $MAVEN_PROJECTBASEDIR 284 | fi 285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 286 | 287 | # For Cygwin, switch paths to Windows format before running java 288 | if $cygwin; then 289 | [ -n "$M2_HOME" ] && 290 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 291 | [ -n "$JAVA_HOME" ] && 292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 293 | [ -n "$CLASSPATH" ] && 294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 295 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 297 | fi 298 | 299 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 300 | 301 | exec "$JAVACMD" \ 302 | $MAVEN_OPTS \ 303 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 304 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 305 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 306 | -------------------------------------------------------------------------------- /application/mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.3/maven-wrapper-0.5.3.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | echo Found %WRAPPER_JAR% 133 | ) else ( 134 | if not "%MVNW_REPOURL%" == "" ( 135 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.3/maven-wrapper-0.5.3.jar" 136 | ) 137 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 138 | echo Downloading from: %DOWNLOAD_URL% 139 | 140 | powershell -Command "&{"^ 141 | "$webclient = new-object System.Net.WebClient;"^ 142 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 143 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 144 | "}"^ 145 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 146 | "}" 147 | echo Finished downloading %WRAPPER_JAR% 148 | ) 149 | @REM End of extension 150 | 151 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 152 | if ERRORLEVEL 1 goto error 153 | goto end 154 | 155 | :error 156 | set ERROR_CODE=1 157 | 158 | :end 159 | @endlocal & set ERROR_CODE=%ERROR_CODE% 160 | 161 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 162 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 163 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 164 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 165 | :skipRcPost 166 | 167 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 168 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 169 | 170 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 171 | 172 | exit /B %ERROR_CODE% 173 | -------------------------------------------------------------------------------- /application/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | paymentAPI 7 | org.acme 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | application 13 | 14 | 15 | 16 | org.acme 17 | infrastructure 18 | 1.0-SNAPSHOT 19 | 20 | 21 | org.acme 22 | domain 23 | 1.0-SNAPSHOT 24 | 25 | 26 | 27 | org.jboss.resteasy 28 | resteasy-jackson2-provider 29 | 4.0.0.Final 30 | 31 | 32 | 33 | 34 | org.mockito 35 | mockito-core 36 | 2.23.0 37 | test 38 | 39 | 40 | org.mockito 41 | mockito-junit-jupiter 42 | 2.23.0 43 | test 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | net.alchim31.maven 59 | scala-maven-plugin 60 | 61 | 62 | scala-compile-first 63 | process-resources 64 | 65 | add-source 66 | compile 67 | 68 | 69 | 70 | scala-test-compile 71 | process-test-resources 72 | 73 | testCompile 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /application/src/main/java/com/politrons/application/handler/PaymentHandler.java: -------------------------------------------------------------------------------- 1 | package com.politrons.application.handler; 2 | 3 | import com.politrons.application.model.command.AddPaymentCommand; 4 | import com.politrons.application.model.command.UpdatePaymentCommand; 5 | import com.politrons.application.model.error.ErrorPayload; 6 | import io.vavr.concurrent.Future; 7 | import io.vavr.control.Either; 8 | 9 | import javax.enterprise.context.ApplicationScoped; 10 | 11 | @ApplicationScoped 12 | public interface PaymentHandler { 13 | 14 | Future> addPayment(AddPaymentCommand addPaymentCommand); 15 | 16 | Future> updatePayment(UpdatePaymentCommand addPaymentCommand); 17 | 18 | Future> deletePayment(String paymentId); 19 | } 20 | -------------------------------------------------------------------------------- /application/src/main/java/com/politrons/application/handler/impl/PaymentHandlerImpl.java: -------------------------------------------------------------------------------- 1 | package com.politrons.application.handler.impl; 2 | 3 | import com.politrons.application.handler.PaymentHandler; 4 | import com.politrons.application.model.command.AddPaymentCommand; 5 | import com.politrons.application.model.command.PaymentCommand; 6 | import com.politrons.application.model.command.UpdatePaymentCommand; 7 | import com.politrons.application.model.error.ErrorPayload; 8 | import com.politrons.domain.PaymentStateAggregateRoot; 9 | import com.politrons.domain.entities.BeneficiaryParty; 10 | import com.politrons.domain.entities.DebtorParty; 11 | import com.politrons.domain.entities.PaymentInfo; 12 | import com.politrons.domain.entities.SponsorParty; 13 | import com.politrons.infrastructure.repository.PaymentRepository; 14 | import io.vavr.API; 15 | import io.vavr.Function1; 16 | import io.vavr.concurrent.Future; 17 | import io.vavr.control.Either; 18 | import lombok.AllArgsConstructor; 19 | import lombok.NoArgsConstructor; 20 | 21 | import javax.enterprise.context.ApplicationScoped; 22 | import javax.inject.Inject; 23 | 24 | import static io.vavr.API.*; 25 | import static io.vavr.Patterns.$Left; 26 | import static io.vavr.Patterns.$Right; 27 | 28 | @NoArgsConstructor 29 | @AllArgsConstructor 30 | @ApplicationScoped 31 | public class PaymentHandlerImpl implements PaymentHandler { 32 | 33 | @Inject 34 | PaymentRepository paymentRepository; 35 | 36 | 37 | /** 38 | * Handler to receive a Command to add a payment and return the id of the operation. 39 | *

40 | * Since the operation to database it take time and it might not be part of the machine we 41 | * need to make this operation async. As personal level I like Vavr library for functional programing in Java.(Pretty much like Scala monads) 42 | *

43 | * Transform the [AddPaymentCommand] in a new [PaymentStateAggregateRoot] with [type] state as "created" 44 | *

45 | * Also since we have to deal with impure world, we need to control effects. So in order to control the possibility 46 | * of a Database problem, or network, we will catch the throwable and we will transform into Either of possible error [Left] 47 | * or the expected output [Right] 48 | * 49 | * @param addPaymentCommand to be transformed into Domain model to be persisted. 50 | * @return the id of the event to be modified or deleted in the future 51 | */ 52 | @Override 53 | public Future> addPayment(AddPaymentCommand addPaymentCommand) { 54 | return paymentRepository.addPayment(getPaymentAggregateRoot(addPaymentCommand, PaymentStateAggregateRoot::create)) 55 | .map(this::processRepositoryResponse); 56 | } 57 | 58 | /** 59 | * Handler to transform the [UpdatePaymentCommand] in a new [PaymentStateAggregateRoot] with [type] state as "changed" 60 | * 61 | * @param updatePaymentCommand command to be transformed into Domain model with state "changed" 62 | * @return a new id of the row of the new Event with the new state. 63 | */ 64 | @Override 65 | public Future> updatePayment(UpdatePaymentCommand updatePaymentCommand) { 66 | return paymentRepository.updatePayment(getPaymentAggregateRoot(updatePaymentCommand, PaymentStateAggregateRoot::update)) 67 | .map(this::processRepositoryResponse); 68 | } 69 | 70 | /** 71 | * Handler to get the the previous event created using the id, then we change the state as [deleted] and 72 | * finally we delegate the creation of the new event into the infrastructure layer." 73 | * 74 | * @param eventId from the previous event to fetch and change state. 75 | * @return a new id of the row of the new Event with the new state. 76 | */ 77 | @Override 78 | public Future> deletePayment(String eventId) { 79 | return paymentRepository.fetchPayment(eventId) 80 | .recover(API::Left) 81 | .flatMap(either -> Match(either).of( 82 | Case($Right($()), paymentStateAggregateRoot -> 83 | paymentRepository.deletePayment(PaymentStateAggregateRoot.delete(paymentStateAggregateRoot)) 84 | .map(this::processRepositoryResponse)), 85 | Case($Left($()), t -> Future.of(() -> Left(new ErrorPayload(500, t.getMessage())))) 86 | )); 87 | } 88 | 89 | private Either processRepositoryResponse(Either either) { 90 | return Match(either).of( 91 | Case($Right($()), API::Right), 92 | Case($Left($()), t -> Left(new ErrorPayload(500, t.getMessage())))); 93 | } 94 | 95 | //#############################// 96 | // DOMAIN FACTORY // 97 | //#############################// 98 | 99 | private PaymentStateAggregateRoot getPaymentAggregateRoot(PaymentCommand paymentCommand, 100 | Function1 changeStateFunc) { 101 | PaymentInfo paymentInfo = getPaymentInfo(paymentCommand); 102 | return changeStateFunc.apply(paymentInfo); 103 | } 104 | 105 | private PaymentInfo getPaymentInfo(PaymentCommand paymentCommand) { 106 | return PaymentInfo.create(paymentCommand.getAmount(), 107 | paymentCommand.getCurrency(), 108 | paymentCommand.getPaymentId(), 109 | paymentCommand.getPaymentPurpose(), 110 | paymentCommand.getPaymentType(), 111 | paymentCommand.getProcessingDate(), 112 | paymentCommand.getReference(), 113 | paymentCommand.getSchemePaymentSubType(), 114 | paymentCommand.getSchemePaymentType(), 115 | getDebtorParty(paymentCommand), 116 | getSponsorParty(paymentCommand), 117 | getBeneficiaryParty(paymentCommand)); 118 | } 119 | 120 | private SponsorParty getSponsorParty(PaymentCommand paymentCommand) { 121 | return SponsorParty.create(paymentCommand.getSponsorParty().getAccountNumber(), 122 | paymentCommand.getSponsorParty().getBankId(), 123 | paymentCommand.getSponsorParty().getBankIdCode()); 124 | } 125 | 126 | private DebtorParty getDebtorParty(PaymentCommand paymentCommand) { 127 | return DebtorParty.create(paymentCommand.getDebtorParty().getAccountName(), 128 | paymentCommand.getDebtorParty().getAccountNumber(), 129 | paymentCommand.getDebtorParty().getAccountType(), 130 | paymentCommand.getDebtorParty().getAddress(), 131 | paymentCommand.getDebtorParty().getBankId(), 132 | paymentCommand.getDebtorParty().getName()); 133 | } 134 | 135 | private BeneficiaryParty getBeneficiaryParty(PaymentCommand paymentCommand) { 136 | return BeneficiaryParty.create(paymentCommand.getBeneficiaryParty().getAccountName(), 137 | paymentCommand.getBeneficiaryParty().getAccountNumber(), 138 | paymentCommand.getBeneficiaryParty().getAccountType(), 139 | paymentCommand.getBeneficiaryParty().getAddress(), 140 | paymentCommand.getBeneficiaryParty().getBankId(), 141 | paymentCommand.getBeneficiaryParty().getName()); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /application/src/main/java/com/politrons/application/model/command/AddPaymentCommand.java: -------------------------------------------------------------------------------- 1 | package com.politrons.application.model.command; 2 | 3 | public class AddPaymentCommand extends PaymentCommand{ 4 | 5 | 6 | } 7 | -------------------------------------------------------------------------------- /application/src/main/java/com/politrons/application/model/command/PaymentCommand.java: -------------------------------------------------------------------------------- 1 | package com.politrons.application.model.command; 2 | 3 | import com.politrons.application.model.payload.payload.BeneficiaryPartyPayload; 4 | import com.politrons.application.model.payload.payload.DebtorPartyPayload; 5 | import com.politrons.application.model.payload.payload.SponsorPartyPayload; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | import lombok.Setter; 10 | 11 | @Getter 12 | @Setter 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | public abstract class PaymentCommand { 16 | protected String amount; 17 | protected String currency; 18 | protected String paymentId; 19 | protected String paymentPurpose; 20 | protected String paymentType; 21 | protected String processingDate; 22 | protected String reference; 23 | protected String schemePaymentType; 24 | protected String schemePaymentSubType; 25 | protected DebtorPartyPayload debtorParty; 26 | protected SponsorPartyPayload sponsorParty; 27 | protected BeneficiaryPartyPayload beneficiaryParty; 28 | 29 | } 30 | -------------------------------------------------------------------------------- /application/src/main/java/com/politrons/application/model/command/UpdatePaymentCommand.java: -------------------------------------------------------------------------------- 1 | package com.politrons.application.model.command; 2 | 3 | public class UpdatePaymentCommand extends PaymentCommand{ 4 | 5 | } 6 | -------------------------------------------------------------------------------- /application/src/main/java/com/politrons/application/model/error/ErrorPayload.java: -------------------------------------------------------------------------------- 1 | package com.politrons.application.model.error; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | @Getter 9 | @Setter 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class ErrorPayload { 13 | 14 | public int code; 15 | public String cause; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /application/src/main/java/com/politrons/application/model/payload/payload/BeneficiaryPartyPayload.java: -------------------------------------------------------------------------------- 1 | package com.politrons.application.model.payload.payload; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | @Getter 9 | @Setter 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class BeneficiaryPartyPayload { 13 | private String accountName; 14 | private String accountNumber; 15 | private float accountType; 16 | private String address; 17 | private String bankId; 18 | private String name; 19 | 20 | } 21 | 22 | -------------------------------------------------------------------------------- /application/src/main/java/com/politrons/application/model/payload/payload/DebtorPartyPayload.java: -------------------------------------------------------------------------------- 1 | package com.politrons.application.model.payload.payload; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | @Getter 9 | @Setter 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class DebtorPartyPayload { 13 | private String accountName; 14 | private String accountNumber; 15 | private float accountType; 16 | private String address; 17 | private String bankId; 18 | private String name; 19 | } 20 | -------------------------------------------------------------------------------- /application/src/main/java/com/politrons/application/model/payload/payload/PaymentInfoPayload.java: -------------------------------------------------------------------------------- 1 | package com.politrons.application.model.payload.payload; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | 9 | @Getter 10 | @Setter 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class PaymentInfoPayload { 14 | private String amount; 15 | private String currency; 16 | private String paymentId; 17 | private String paymentPurpose; 18 | private String paymentType; 19 | private String processingDate; 20 | private String reference; 21 | private String schemePaymentSubType; 22 | private String schemePaymentType; 23 | DebtorPartyPayload debtorParty; 24 | SponsorPartyPayload sponsorParty; 25 | BeneficiaryPartyPayload beneficiaryParty; 26 | 27 | public static PaymentInfoPayload create(String amount, 28 | String currency, 29 | String paymentId, 30 | String paymentPurpose, 31 | String paymentType, 32 | String processingDate, 33 | String reference, 34 | String schemePaymentSubType, 35 | String schemePaymentType, 36 | DebtorPartyPayload debtorParty, 37 | SponsorPartyPayload sponsorParty, 38 | BeneficiaryPartyPayload beneficiaryParty) { 39 | return new PaymentInfoPayload(amount, currency, paymentId, paymentPurpose, 40 | paymentType, processingDate, reference, schemePaymentSubType, 41 | schemePaymentType, debtorParty, sponsorParty, beneficiaryParty); 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /application/src/main/java/com/politrons/application/model/payload/payload/PaymentStatePayload.java: -------------------------------------------------------------------------------- 1 | package com.politrons.application.model.payload.payload; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | @Getter 9 | @Setter 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class PaymentStatePayload { 13 | 14 | private String id; 15 | private String type; 16 | private float version; 17 | PaymentInfoPayload paymentInfo; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /application/src/main/java/com/politrons/application/model/payload/payload/SponsorPartyPayload.java: -------------------------------------------------------------------------------- 1 | package com.politrons.application.model.payload.payload; 2 | 3 | 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | 9 | @Getter 10 | @Setter 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class SponsorPartyPayload { 14 | private String accountNumber; 15 | private String bankId; 16 | private String bankIdCode; 17 | } 18 | -------------------------------------------------------------------------------- /application/src/main/java/com/politrons/application/model/payload/response/PaymentResponse.java: -------------------------------------------------------------------------------- 1 | package com.politrons.application.model.payload.response; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | @Getter 9 | @Setter 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class PaymentResponse { 13 | 14 | int code; 15 | T payload; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /application/src/main/java/com/politrons/application/resources/PaymentHealthCheck.java: -------------------------------------------------------------------------------- 1 | package com.politrons.application.resources; 2 | 3 | import com.politrons.infrastructure.CassandraConnector; 4 | import org.eclipse.microprofile.health.Health; 5 | import org.eclipse.microprofile.health.HealthCheck; 6 | import org.eclipse.microprofile.health.HealthCheckResponse; 7 | 8 | import javax.enterprise.context.ApplicationScoped; 9 | 10 | /** 11 | * Health check implementation in Quarkus it's so simple enough as add the annotation 12 | * @Health and @ApplicationScoped to be injected and implement [HealthCheck] which it 13 | * will force you to implement [call] method which it will return a [HealthCheckResponse] 14 | */ 15 | @Health 16 | @ApplicationScoped 17 | public class PaymentHealthCheck implements HealthCheck { 18 | 19 | @Override 20 | public HealthCheckResponse call() { 21 | return HealthCheckResponse.named("Payment API health check").up() 22 | .withData("Cassandra database running:", CassandraConnector.isStarted()) 23 | .build(); 24 | } 25 | } -------------------------------------------------------------------------------- /application/src/main/java/com/politrons/application/resources/PaymentResource.java: -------------------------------------------------------------------------------- 1 | package com.politrons.application.resources; 2 | 3 | import com.politrons.application.handler.PaymentHandler; 4 | import com.politrons.application.model.command.AddPaymentCommand; 5 | import com.politrons.application.model.command.UpdatePaymentCommand; 6 | import com.politrons.application.model.error.ErrorPayload; 7 | import com.politrons.application.model.payload.response.PaymentResponse; 8 | import com.politrons.application.service.PaymentService; 9 | import io.vavr.control.Either; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | import javax.inject.Inject; 14 | import javax.ws.rs.*; 15 | import javax.ws.rs.core.MediaType; 16 | import java.util.concurrent.CompletionStage; 17 | 18 | import static io.vavr.API.*; 19 | import static io.vavr.Patterns.$Left; 20 | import static io.vavr.Patterns.$Right; 21 | 22 | @Path("/v1/payment") 23 | @Produces(MediaType.APPLICATION_JSON) 24 | @Consumes(MediaType.APPLICATION_JSON) 25 | public class PaymentResource { 26 | 27 | private Logger logger = LoggerFactory.getLogger(PaymentResource.class); 28 | 29 | @Inject 30 | PaymentService service; 31 | 32 | @Inject 33 | PaymentHandler handler; 34 | 35 | @GET 36 | @Path("/version") 37 | public String version() { 38 | logger.debug("Request to Version endpoint"); 39 | return "Payment API V1.0"; 40 | } 41 | 42 | /** 43 | * Endpoint to fetch a previous payment created. 44 | * We receive the id and we use to create the query to be passed into the service. 45 | * No transformation from Command into Domain into Event need it in Queries. 46 | * Only from domain into Payload to make our application not too much couple with domain. 47 | * 48 | * @param id of the payment to fetch 49 | * @return a PaymentResponse with the operation code and the payload as [PaymentStatePayload] 50 | */ 51 | @GET 52 | @Path("/{paymentId}") 53 | public CompletionStage> fetchPaymentById(@PathParam("paymentId") String id) { 54 | logger.debug("Request to get Payment with id " + id); 55 | return service.fetchPayment(id) 56 | .map(either -> Match(either).of( 57 | Case($Right($()), paymentStatePayload -> new PaymentResponse<>(200, paymentStatePayload)), 58 | Case($Left($()), errorPayload -> new PaymentResponse<>(errorPayload.code, errorPayload.cause)))) 59 | .toCompletableFuture(); 60 | } 61 | 62 | /** 63 | * Endpoint to fetch all payments. 64 | * @return a PaymentResponse with the operation code and the payload as List of PaymentStatePayload 65 | */ 66 | @GET 67 | @Path("/all") 68 | public CompletionStage> fetchAllPayment() { 69 | logger.debug("Request to get all Payments"); 70 | return service.fetchAllPayments() 71 | .map(either -> Match(either).of( 72 | Case($Right($()), paymentStatePayloadList -> new PaymentResponse<>(200, paymentStatePayloadList)), 73 | Case($Left($()), errorPayload -> new PaymentResponse<>(errorPayload.code, errorPayload.cause)))) 74 | .toCompletableFuture(); 75 | } 76 | 77 | /** 78 | * Endpoint to persist a payment. We receive a AddPaymentCommand which after being passed into the domain layer 79 | * it's persisted using the infra layer. 80 | * 81 | * @param addPaymentCommand that contains the information of the payment to be created 82 | * @return a PaymentResponse with the operation code and the payload as eventId 83 | */ 84 | @POST 85 | @Path("/") 86 | public CompletionStage> addPayment(AddPaymentCommand addPaymentCommand) { 87 | logger.debug("Request to add Payment with command " + addPaymentCommand); 88 | return handler.addPayment(addPaymentCommand) 89 | .map(this::matchEventIdResponse) 90 | .toCompletableFuture(); 91 | } 92 | 93 | /** 94 | * Endpoint where er receive the paymentId from the previous event as query param, and in the body of the request 95 | * the [UpdatePaymentCommand] to create a new Event with the new data updated. 96 | * 97 | * @param paymentId of the previous event created 98 | * @param updatePaymentCommand the new data to create a new event. 99 | * @return a PaymentResponse with the operation code and the payload as eventId 100 | */ 101 | @PUT 102 | @Path("/{paymentId}") 103 | public CompletionStage> updatePayment(@PathParam("paymentId") String paymentId, 104 | UpdatePaymentCommand updatePaymentCommand) { 105 | logger.debug("Request to create update Payment event for paymentId " + paymentId); 106 | updatePaymentCommand.setPaymentId(paymentId); 107 | return handler.updatePayment(updatePaymentCommand) 108 | .map(this::matchEventIdResponse) 109 | .toCompletableFuture(); 110 | } 111 | 112 | /** 113 | * Endpoint where er receive the paymentId from the previous event as query param to find the previous event 114 | * change the state to [deleted] and persist as a new Event. 115 | * 116 | * @param paymentId of the previous event created 117 | * @return a PaymentResponse with the operation code and the payload as eventId 118 | */ 119 | @DELETE 120 | @Path("/{paymentId}") 121 | public CompletionStage> deletePayment(@PathParam("paymentId") String paymentId) { 122 | logger.debug("Request to create delete Payment event for paymentId " + paymentId); 123 | return handler.deletePayment(paymentId) 124 | .map(this::matchEventIdResponse) 125 | .toCompletableFuture(); 126 | } 127 | 128 | 129 | private PaymentResponse matchEventIdResponse(Either either) { 130 | return Match(either).of( 131 | Case($Right($()), eventId -> new PaymentResponse<>(200, eventId)), 132 | Case($Left($()), errorPayload -> new PaymentResponse<>(errorPayload.code, errorPayload.cause))); 133 | } 134 | 135 | } -------------------------------------------------------------------------------- /application/src/main/java/com/politrons/application/service/PaymentService.java: -------------------------------------------------------------------------------- 1 | package com.politrons.application.service; 2 | 3 | import com.politrons.application.model.error.ErrorPayload; 4 | import com.politrons.application.model.payload.payload.PaymentStatePayload; 5 | import com.politrons.application.service.impl.PaymentServiceImpl; 6 | import io.vavr.concurrent.Future; 7 | import io.vavr.control.Either; 8 | import ma.glasnost.orika.MapperFactory; 9 | import ma.glasnost.orika.impl.DefaultMapperFactory; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | import javax.enterprise.context.ApplicationScoped; 14 | import java.util.List; 15 | 16 | @ApplicationScoped 17 | public interface PaymentService { 18 | 19 | Logger logger = LoggerFactory.getLogger(PaymentServiceImpl.class); 20 | 21 | MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build(); 22 | 23 | Future> fetchPayment(String id); 24 | 25 | Future>> fetchAllPayments(); 26 | } 27 | -------------------------------------------------------------------------------- /application/src/main/java/com/politrons/application/service/impl/PaymentServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.politrons.application.service.impl; 2 | 3 | 4 | import com.politrons.application.model.error.ErrorPayload; 5 | import com.politrons.application.model.payload.payload.PaymentStatePayload; 6 | import com.politrons.application.service.PaymentService; 7 | import com.politrons.domain.PaymentStateAggregateRoot; 8 | import com.politrons.infrastructure.dao.PaymentDAO; 9 | import com.politrons.infrastructure.events.PaymentAdded; 10 | import io.vavr.API; 11 | import io.vavr.collection.Stream; 12 | import io.vavr.concurrent.Future; 13 | import io.vavr.control.Either; 14 | import lombok.AllArgsConstructor; 15 | import lombok.NoArgsConstructor; 16 | import ma.glasnost.orika.MapperFacade; 17 | 18 | import javax.enterprise.context.ApplicationScoped; 19 | import javax.inject.Inject; 20 | 21 | import java.util.List; 22 | import java.util.stream.Collectors; 23 | 24 | import static io.vavr.API.*; 25 | import static io.vavr.Patterns.*; 26 | 27 | /** 28 | * Service to make Queries into the DAO. 29 | * Doing DDD + CQRS since we need to be fast in queries since 90% of traffic are queries, and there's no transformation need it here 30 | * we go though directly to the source instead to pass through the repository. 31 | */ 32 | @AllArgsConstructor 33 | @NoArgsConstructor 34 | @ApplicationScoped 35 | public class PaymentServiceImpl implements PaymentService { 36 | 37 | @Inject 38 | PaymentDAO paymentDAO; 39 | 40 | /** 41 | * Method to fetch a payment previously created/updated/deleted. 42 | * 43 | * @param id of the payment 44 | * @return the Domain model PaymentStateAggregateRoot 45 | */ 46 | public Future> fetchPayment(String id) { 47 | return paymentDAO.fetchPayment(id) 48 | .map(either -> Match(either).of( 49 | Case($Right($()), paymentStateAggregateRoot -> Right(transformPaymentStateAggregateRootToPayload(paymentStateAggregateRoot))), 50 | Case($Left($()), throwable -> { 51 | logger.error("Error in fetch payment Service. Caused by:" + throwable.getCause()); 52 | return Left(new ErrorPayload(500, throwable.getMessage())); 53 | }))); 54 | } 55 | 56 | /** 57 | * Method to fetch all payments made on the system 58 | * @return List of [PaymentStatePayload] 59 | */ 60 | @Override 61 | public Future>> fetchAllPayments() { 62 | return paymentDAO.fetchAllPayments() 63 | .map(either -> Match(either).of( 64 | Case($Right($()), paymentStateAggregateRootList -> Right(transformPaymentStateAggregateRootListToPayload(paymentStateAggregateRootList))), 65 | Case($Left($()), throwable -> { 66 | logger.error("Error in fetch payment Service. Caused by:" + throwable.getCause()); 67 | return Left(new ErrorPayload(500, throwable.getMessage())); 68 | }))); 69 | } 70 | 71 | /** 72 | * Get a list of [PaymentStateAggregateRoot] and transform into a list of [PaymentStatePayload] 73 | */ 74 | private List transformPaymentStateAggregateRootListToPayload(List paymentStateAggregateRootList) { 75 | return paymentStateAggregateRootList.stream() 76 | .map(this::transformPaymentStateAggregateRootToPayload) 77 | .collect(Collectors.toList()); 78 | } 79 | 80 | /** 81 | * Function to transform from the domain model [PaymentStateAggregateRoot] into payload type [PaymentStatePayload] 82 | */ 83 | private PaymentStatePayload transformPaymentStateAggregateRootToPayload(PaymentStateAggregateRoot paymentStateAggregateRoot) { 84 | mapperFactory.classMap(PaymentStateAggregateRoot.class, PaymentStatePayload.class); 85 | MapperFacade mapper = mapperFactory.getMapperFacade(); 86 | return mapper.map(paymentStateAggregateRoot, PaymentStatePayload.class); 87 | } 88 | 89 | 90 | } 91 | -------------------------------------------------------------------------------- /application/src/main/resources/META-INF/beans.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/politrons/PaymentAPI/2e12a671000d26cf34fe4afb3f96b99ffff7ce65/application/src/main/resources/META-INF/beans.xml -------------------------------------------------------------------------------- /application/src/main/resources/META-INF/microprofile-config.properties: -------------------------------------------------------------------------------- 1 | # DEBUG console logging 2 | quarkus.log.console.enable=true 3 | quarkus.log.console.format=%d{HH:mm:ss} %-5p [%c{2.}]] (%t) %s%e%n 4 | quarkus.log.console.level=INFO 5 | 6 | # DEBUG file logging 7 | quarkus.log.level=INFO 8 | quarkus.log.min-level=INFO 9 | quarkus.log.file.enable=true 10 | quarkus.log.file.path=/tmp/log.log 11 | quarkus.log.file.level=DEBUG 12 | quarkus.log.file.format=%d{HH:mm:ss} %-5p [%c{2.}]] (%t) %s%e%n 13 | quarkus.log.category."io.quarkus.smallrye.jwt".level=INFO 14 | quarkus.log.category."io.undertow.request.security".level=INFO 15 | quarkus.log.category."io.smallrye.jwt".level=INFO -------------------------------------------------------------------------------- /application/src/test/java/com/politrons/application/it/PaymentResourceBackendDownTest.java: -------------------------------------------------------------------------------- 1 | package com.politrons.application.it; 2 | 3 | import com.politrons.application.JsonUtils; 4 | import com.politrons.application.model.payload.response.PaymentResponse; 5 | import io.quarkus.test.junit.QuarkusTest; 6 | import io.restassured.http.Header; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import static io.restassured.RestAssured.given; 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | 12 | @QuarkusTest 13 | class PaymentResourceBackendDownTest { 14 | 15 | @Test 16 | void addPaymentEndpoint() { 17 | PaymentResponse response = given() 18 | .contentType("application/json") 19 | .header(new Header("Content-Type", "application/json")) 20 | .body(JsonUtils.paymentRequest()) 21 | .when().post("/v1/payment/") 22 | .then() 23 | .statusCode(200) 24 | .extract() 25 | .as(PaymentResponse.class); 26 | assertEquals(response.getCode(), 500); 27 | } 28 | 29 | @Test 30 | void updatePaymentEndpoint() { 31 | PaymentResponse response = given() 32 | .contentType("application/json") 33 | .header(new Header("Content-Type", "application/json")) 34 | .body(JsonUtils.paymentRequest()) 35 | .when().put("/v1/payment/" + "foo") 36 | .then() 37 | .statusCode(200) 38 | .extract() 39 | .as(PaymentResponse.class); 40 | assertEquals(response.getCode(), 500); 41 | } 42 | 43 | @Test 44 | void deletePaymentEndpoint() { 45 | PaymentResponse response = given() 46 | .contentType("application/json") 47 | .header(new Header("Content-Type", "application/json")) 48 | .when().delete("/v1/payment/" + "bla") 49 | .then() 50 | .statusCode(200) 51 | .extract() 52 | .as(PaymentResponse.class); 53 | assertEquals(response.getCode(), 500); 54 | } 55 | 56 | @Test 57 | void fetchPaymentEndpoint() { 58 | PaymentResponse response = given() 59 | .contentType("application/json") 60 | .header(new Header("Content-Type", "application/json")) 61 | .when().get("/v1/payment/" + "foo") 62 | .then() 63 | .statusCode(200) 64 | .extract() 65 | .as(PaymentResponse.class); 66 | assertEquals(response.getCode(), 500); 67 | } 68 | 69 | @Test 70 | void fetchAllPaymentEndpoint() { 71 | PaymentResponse response = given() 72 | .contentType("application/json") 73 | .header(new Header("Content-Type", "application/json")) 74 | .when().get("/v1/payment/all") 75 | .then() 76 | .statusCode(200) 77 | .extract() 78 | .as(PaymentResponse.class); 79 | assertEquals(response.getCode(), 500); 80 | } 81 | 82 | 83 | } -------------------------------------------------------------------------------- /application/src/test/java/com/politrons/application/it/PaymentResourceTest.java: -------------------------------------------------------------------------------- 1 | package com.politrons.application.it; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.core.type.TypeReference; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import com.politrons.application.JsonUtils; 7 | import com.politrons.application.model.payload.payload.PaymentStatePayload; 8 | import com.politrons.application.model.payload.response.PaymentResponse; 9 | import com.politrons.infrastructure.CassandraConnector; 10 | import com.politrons.infrastructure.dto.BeneficiaryPartyDTO; 11 | import com.politrons.infrastructure.dto.DebtorPartyDTO; 12 | import com.politrons.infrastructure.dto.PaymentInfoDTO; 13 | import com.politrons.infrastructure.dto.SponsorPartyDTO; 14 | import com.politrons.infrastructure.events.PaymentAdded; 15 | import io.quarkus.test.junit.QuarkusTest; 16 | import io.restassured.http.Header; 17 | import org.junit.jupiter.api.AfterAll; 18 | import org.junit.jupiter.api.BeforeAll; 19 | import org.junit.jupiter.api.Test; 20 | 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | import java.util.UUID; 24 | 25 | import static io.restassured.RestAssured.given; 26 | import static org.hamcrest.CoreMatchers.is; 27 | import static org.junit.jupiter.api.Assertions.*; 28 | 29 | @QuarkusTest 30 | class PaymentResourceTest { 31 | 32 | private ObjectMapper mapper = new ObjectMapper(); 33 | 34 | @BeforeAll 35 | static void init() { 36 | CassandraConnector.start(); 37 | } 38 | 39 | @AfterAll 40 | static void stop() { 41 | CassandraConnector.stop(); 42 | } 43 | 44 | @Test 45 | void versionEndpoint() { 46 | given() 47 | .when().get("/v1/payment/version") 48 | .then() 49 | .statusCode(200) 50 | .body(is("Payment API V1.0")); 51 | } 52 | 53 | @Test 54 | void addPaymentEndpoint() { 55 | PaymentResponse response = given() 56 | .contentType("application/json") 57 | .header(new Header("Content-Type", "application/json")) 58 | .body(JsonUtils.paymentRequest()) 59 | .when().post("/v1/payment/") 60 | .then() 61 | .statusCode(200) 62 | .extract() 63 | .as(PaymentResponse.class); 64 | assertEquals(response.getCode(), 200); 65 | assertTrue(response.getPayload() instanceof String); 66 | assertFalse(((String) response.getPayload()).isEmpty()); 67 | 68 | } 69 | 70 | @Test 71 | void updatePaymentEndpoint() throws JsonProcessingException { 72 | String uuid = addMockPayment(); 73 | PaymentResponse response = given() 74 | .contentType("application/json") 75 | .header(new Header("Content-Type", "application/json")) 76 | .body(JsonUtils.paymentRequest()) 77 | .when().put("/v1/payment/" + uuid) 78 | .then() 79 | .statusCode(200) 80 | .extract() 81 | .as(PaymentResponse.class); 82 | assertEquals(response.getCode(), 200); 83 | assertTrue(response.getPayload() instanceof String); 84 | assertFalse(((String) response.getPayload()).isEmpty()); 85 | } 86 | 87 | @Test 88 | void deletePaymentEndpoint() throws JsonProcessingException { 89 | String uuid = addMockPayment(); 90 | PaymentResponse response = given() 91 | .contentType("application/json") 92 | .header(new Header("Content-Type", "application/json")) 93 | .when().delete("/v1/payment/" + uuid) 94 | .then() 95 | .statusCode(200) 96 | .extract() 97 | .as(PaymentResponse.class); 98 | assertEquals(response.getCode(), 200); 99 | assertTrue(response.getPayload() instanceof String); 100 | assertFalse(((String) response.getPayload()).isEmpty()); 101 | } 102 | 103 | @Test 104 | void fetchPaymentEndpoint() throws JsonProcessingException { 105 | String uuid = addMockPayment(); 106 | PaymentResponse response = given() 107 | .contentType("application/json") 108 | .header(new Header("Content-Type", "application/json")) 109 | .when().get("/v1/payment/" + uuid) 110 | .then() 111 | .statusCode(200) 112 | .extract() 113 | .as(PaymentResponse.class); 114 | assertEquals(response.getCode(), 200); 115 | PaymentStatePayload paymentStatePayload = mapper.convertValue(response.getPayload(), PaymentStatePayload.class); 116 | assertEquals(paymentStatePayload.getType(), "created"); 117 | } 118 | 119 | @Test 120 | void fetchPaymentEndpointWithWrongEventId() { 121 | PaymentResponse response = given() 122 | .contentType("application/json") 123 | .header(new Header("Content-Type", "application/json")) 124 | .when().get("/v1/payment/" + "foo") 125 | .then() 126 | .statusCode(200) 127 | .extract() 128 | .as(PaymentResponse.class); 129 | assertEquals(response.getCode(), 500); 130 | } 131 | 132 | @Test 133 | void fetchAllPaymentEndpoint() throws JsonProcessingException { 134 | addMockPayment(); 135 | addMockPayment(); 136 | addMockPayment(); 137 | PaymentResponse response = given() 138 | .contentType("application/json") 139 | .header(new Header("Content-Type", "application/json")) 140 | .when().get("/v1/payment/all") 141 | .then() 142 | .statusCode(200) 143 | .extract() 144 | .as(PaymentResponse.class); 145 | assertEquals(response.getCode(), 200); 146 | List paymentStatePayloadList = mapper.convertValue(response.getPayload(), new TypeReference>() { 147 | }); 148 | assertEquals(paymentStatePayloadList.size(), 3); 149 | assertNotNull(paymentStatePayloadList.get(0).getPaymentInfo()); 150 | } 151 | 152 | private String addMockPayment() throws JsonProcessingException { 153 | String uuid = UUID.randomUUID().toString(); 154 | PaymentAdded paymentAddedEvent = getPaymentAddedEvent(); 155 | paymentAddedEvent.setId(uuid); 156 | String event = mapper.writeValueAsString(paymentAddedEvent); 157 | CassandraConnector.addPayment(getAddPaymentQuery(paymentAddedEvent, "12345", event)); 158 | return uuid; 159 | } 160 | 161 | 162 | String getAddPaymentQuery(PaymentAdded paymentAdded, String timeStampMillis, String event) { 163 | return "INSERT INTO " + "paymentsSchema.payment" + 164 | "(id, timestamp, event) " + 165 | "VALUES (" + paymentAdded.getId() + ", '" + 166 | timeStampMillis + "', '" + 167 | event + "');"; 168 | } 169 | 170 | PaymentAdded getPaymentAddedEvent() { 171 | return new PaymentAdded(UUID.randomUUID().toString(), "created", 0, getPaymentInfoDTO()); 172 | } 173 | 174 | PaymentInfoDTO getPaymentInfoDTO() { 175 | DebtorPartyDTO debtorParty = getDebtorParty(); 176 | BeneficiaryPartyDTO beneficiaryParty = getBeneficiaryParty(); 177 | SponsorPartyDTO sponsorParty = getSponsorParty(); 178 | return new PaymentInfoDTO("amount", "currency", 179 | "paymentId", 180 | "paymentPurpose", "paymentType", 181 | "processingDate", "reference", "schemePaymentSubType", 182 | "schemePaymentType", debtorParty, sponsorParty, beneficiaryParty); 183 | } 184 | 185 | private SponsorPartyDTO getSponsorParty() { 186 | return new SponsorPartyDTO("accountName", 187 | "bankId", "bankCode"); 188 | } 189 | 190 | private BeneficiaryPartyDTO getBeneficiaryParty() { 191 | return new BeneficiaryPartyDTO("accountName", 192 | "accountNumber", 0, "address", "bankId", "name"); 193 | } 194 | 195 | private DebtorPartyDTO getDebtorParty() { 196 | return new DebtorPartyDTO("accountName", "accountNumber", 197 | 0, "address", "bankId", "name"); 198 | } 199 | 200 | } -------------------------------------------------------------------------------- /application/src/test/java/com/politrons/application/performance/PaymentResourcePerformanceTest.java: -------------------------------------------------------------------------------- 1 | package com.politrons.application.performance; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.politrons.application.utils.PaymentResourceTestUtils; 5 | import com.politrons.infrastructure.CassandraConnector; 6 | import io.quarkus.test.junit.QuarkusTest; 7 | import org.junit.jupiter.api.AfterAll; 8 | import org.junit.jupiter.api.BeforeAll; 9 | import org.junit.jupiter.api.RepeatedTest; 10 | 11 | @QuarkusTest 12 | class PaymentResourcePerformanceTest extends PaymentResourceTestUtils { 13 | 14 | private final int numOfRequest = 100;//It should be at least 1000 15 | 16 | @BeforeAll 17 | static void init() { 18 | CassandraConnector.start(); 19 | } 20 | 21 | @AfterAll 22 | static void stop() { 23 | CassandraConnector.stop(); 24 | } 25 | 26 | @RepeatedTest(value = numOfRequest, name = RepeatedTest.LONG_DISPLAY_NAME) 27 | void performanceTest() throws JsonProcessingException { 28 | addPayment(); 29 | updatePayment(); 30 | deletePaymentEndpoint(); 31 | fetchPaymentEndpoint(); 32 | fetchAllPaymentEndpoint(); 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /application/src/test/java/com/politrons/application/unit/CommandsTest.java: -------------------------------------------------------------------------------- 1 | package com.politrons.application.unit; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.politrons.application.JsonUtils; 5 | import com.politrons.application.model.command.AddPaymentCommand; 6 | import com.politrons.application.model.command.UpdatePaymentCommand; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import java.io.IOException; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertNotNull; 12 | 13 | class CommandsTest { 14 | 15 | private ObjectMapper mapper = new ObjectMapper(); 16 | 17 | @Test 18 | void transformJsonToAddPaymentCommand() throws IOException { 19 | AddPaymentCommand command = mapper.readValue(JsonUtils.paymentRequest(), AddPaymentCommand.class); 20 | assertNotNull(command); 21 | } 22 | 23 | @Test 24 | void transformJsonToUpdatePaymentCommand() throws IOException { 25 | UpdatePaymentCommand command = mapper.readValue(JsonUtils.paymentRequest(), UpdatePaymentCommand.class); 26 | assertNotNull(command); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /application/src/test/java/com/politrons/application/unit/PaymentHandlerTest.java: -------------------------------------------------------------------------------- 1 | package com.politrons.application.unit; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.politrons.application.JsonUtils; 5 | import com.politrons.application.handler.PaymentHandler; 6 | import com.politrons.application.handler.impl.PaymentHandlerImpl; 7 | import com.politrons.application.model.command.AddPaymentCommand; 8 | import com.politrons.application.model.command.UpdatePaymentCommand; 9 | import com.politrons.application.model.error.ErrorPayload; 10 | import com.politrons.domain.PaymentStateAggregateRoot; 11 | import com.politrons.infrastructure.repository.PaymentRepository; 12 | import io.vavr.concurrent.Future; 13 | import io.vavr.control.Either; 14 | import org.junit.jupiter.api.BeforeEach; 15 | import org.junit.jupiter.api.Test; 16 | import org.junit.jupiter.api.extension.ExtendWith; 17 | import org.mockito.Mock; 18 | import org.mockito.junit.jupiter.MockitoExtension; 19 | 20 | import java.io.IOException; 21 | 22 | import static io.vavr.API.Right; 23 | import static org.junit.jupiter.api.Assertions.assertFalse; 24 | import static org.junit.jupiter.api.Assertions.assertTrue; 25 | import static org.mockito.ArgumentMatchers.any; 26 | import static org.mockito.Mockito.when; 27 | 28 | @ExtendWith(MockitoExtension.class) 29 | class PaymentHandlerTest { 30 | 31 | @Mock 32 | PaymentRepository paymentRepository; 33 | 34 | private PaymentHandler paymentHandler; 35 | 36 | @BeforeEach 37 | void setup() { 38 | paymentHandler = new PaymentHandlerImpl(paymentRepository); 39 | } 40 | 41 | private ObjectMapper mapper = new ObjectMapper(); 42 | 43 | @Test 44 | void addPaymentHandler() throws IOException { 45 | when(paymentRepository.addPayment(any(PaymentStateAggregateRoot.class))).thenReturn(Future.of(() -> Right("1981"))); 46 | AddPaymentCommand addPaymentCommand = mapper.readValue(JsonUtils.paymentRequest(), AddPaymentCommand.class); 47 | Future> eithers = paymentHandler.addPayment(addPaymentCommand); 48 | assertTrue(eithers.get().isRight()); 49 | assertFalse(eithers.get().right().get().isEmpty()); 50 | } 51 | 52 | @Test 53 | void updatePaymentHandler() throws IOException { 54 | when(paymentRepository.updatePayment(any(PaymentStateAggregateRoot.class))).thenReturn(Future.of(() -> Right("1981"))); 55 | UpdatePaymentCommand updatePaymentCommand = mapper.readValue(JsonUtils.paymentRequest(), UpdatePaymentCommand.class); 56 | Future> eithers = paymentHandler.updatePayment(updatePaymentCommand); 57 | assertTrue(eithers.get().isRight()); 58 | assertFalse(eithers.get().right().get().isEmpty()); 59 | } 60 | 61 | @Test 62 | void deletePaymentHandler() { 63 | when(paymentRepository.fetchPayment(any(String.class))).thenReturn(Future.of(() -> Right(new PaymentStateAggregateRoot()))); 64 | when(paymentRepository.deletePayment(any(PaymentStateAggregateRoot.class))).thenReturn(Future.of(() -> Right("1981"))); 65 | Future> eithers = paymentHandler.deletePayment("123"); 66 | assertTrue(eithers.get().isRight()); 67 | assertFalse(eithers.get().right().get().isEmpty()); 68 | } 69 | 70 | @Test 71 | void deletePaymentHandlerWithWrongEventId() { 72 | when(paymentRepository.fetchPayment(any(String.class))).thenReturn(Future.failed(new IllegalArgumentException())); 73 | Future> eithers = paymentHandler.deletePayment("123"); 74 | assertTrue(eithers.get().isLeft()); 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /application/src/test/java/com/politrons/application/unit/PaymentServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.politrons.application.unit; 2 | 3 | import com.politrons.application.model.error.ErrorPayload; 4 | import com.politrons.application.model.payload.payload.PaymentStatePayload; 5 | import com.politrons.application.model.payload.response.PaymentResponse; 6 | import com.politrons.application.service.PaymentService; 7 | import com.politrons.application.service.impl.PaymentServiceImpl; 8 | import com.politrons.domain.PaymentStateAggregateRoot; 9 | import com.politrons.infrastructure.dao.PaymentDAO; 10 | import io.vavr.concurrent.Future; 11 | import io.vavr.control.Either; 12 | import jodd.util.ArraysUtil; 13 | import org.junit.jupiter.api.BeforeEach; 14 | import org.junit.jupiter.api.Test; 15 | import org.junit.jupiter.api.extension.ExtendWith; 16 | import org.mockito.Mock; 17 | import org.mockito.junit.jupiter.MockitoExtension; 18 | 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | 22 | import static io.vavr.API.*; 23 | import static org.junit.jupiter.api.Assertions.assertEquals; 24 | import static org.junit.jupiter.api.Assertions.assertTrue; 25 | import static org.mockito.ArgumentMatchers.any; 26 | import static org.mockito.Mockito.when; 27 | 28 | @ExtendWith(MockitoExtension.class) 29 | class PaymentServiceTest { 30 | 31 | @Mock 32 | PaymentDAO paymentDAO; 33 | 34 | private PaymentService paymentService; 35 | 36 | @BeforeEach 37 | void setup() { 38 | paymentService = new PaymentServiceImpl(paymentDAO); 39 | } 40 | 41 | @Test 42 | void fetchPaymentService() { 43 | PaymentStateAggregateRoot paymentStateAggregateRoot = new PaymentStateAggregateRoot("myCustomUUID", "payment", 0, null); 44 | when(paymentDAO.fetchPayment(any(String.class))).thenReturn(Future.of(() -> Right(paymentStateAggregateRoot))); 45 | Future> eithers = paymentService.fetchPayment("myCustomUUID"); 46 | assertTrue(eithers.get().isRight()); 47 | assertEquals(eithers.get().right().get().getId(), "myCustomUUID"); 48 | } 49 | 50 | @Test 51 | void fetchPaymentServiceError() { 52 | when(paymentDAO.fetchPayment(any(String.class))).thenReturn(Future.of(() -> Left(new IllegalArgumentException()))); 53 | Future> eithers = paymentService.fetchPayment("1981"); 54 | assertTrue(eithers.get().isLeft()); 55 | assertTrue(eithers.get().left().get() instanceof ErrorPayload); 56 | } 57 | 58 | 59 | @Test 60 | void fetchAllPaymentService() { 61 | PaymentStateAggregateRoot paymentStateAggregateRoot = new PaymentStateAggregateRoot("myCustomUUID", "payment", 0, null); 62 | List paymentStateAggregateRootList = new ArrayList(); 63 | paymentStateAggregateRootList.add(paymentStateAggregateRoot); 64 | when(paymentDAO.fetchAllPayments()).thenReturn(Future.of(() -> Right(paymentStateAggregateRootList))); 65 | Future>> eithers = paymentService.fetchAllPayments(); 66 | assertTrue(eithers.get().isRight()); 67 | assertEquals(eithers.get().right().get().size(), 1); 68 | assertEquals(eithers.get().right().get().get(0).getId(), "myCustomUUID"); 69 | 70 | } 71 | 72 | @Test 73 | void fetchAllPaymentServiceError() { 74 | when(paymentDAO.fetchAllPayments()).thenReturn(Future.of(() -> Left(new IllegalArgumentException()))); 75 | Future>> eithers = paymentService.fetchAllPayments(); 76 | assertTrue(eithers.get().isLeft()); 77 | assertTrue(eithers.get().left().get() instanceof ErrorPayload); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /application/src/test/java/com/politrons/application/utils/PaymentResourceTestUtils.java: -------------------------------------------------------------------------------- 1 | package com.politrons.application.utils; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.core.type.TypeReference; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import com.politrons.application.JsonUtils; 7 | import com.politrons.application.model.payload.payload.PaymentStatePayload; 8 | import com.politrons.application.model.payload.response.PaymentResponse; 9 | import com.politrons.infrastructure.CassandraConnector; 10 | import com.politrons.infrastructure.dto.BeneficiaryPartyDTO; 11 | import com.politrons.infrastructure.dto.DebtorPartyDTO; 12 | import com.politrons.infrastructure.dto.PaymentInfoDTO; 13 | import com.politrons.infrastructure.dto.SponsorPartyDTO; 14 | import com.politrons.infrastructure.events.PaymentAdded; 15 | import io.restassured.http.Header; 16 | 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | import java.util.UUID; 20 | 21 | import static io.restassured.RestAssured.given; 22 | import static org.junit.jupiter.api.Assertions.*; 23 | import static org.junit.jupiter.api.Assertions.assertNotNull; 24 | 25 | public class PaymentResourceTestUtils { 26 | 27 | ObjectMapper mapper = new ObjectMapper(); 28 | 29 | protected void updatePayment() throws JsonProcessingException { 30 | String uuid = addMockPayment(); 31 | PaymentResponse response = given() 32 | .contentType("application/json") 33 | .header(new Header("Content-Type", "application/json")) 34 | .body(JsonUtils.paymentRequest()) 35 | .when().put("/v1/payment/" + uuid) 36 | .then() 37 | .statusCode(200) 38 | .extract() 39 | .as(PaymentResponse.class); 40 | assertEquals(response.getCode(), 200); 41 | assertTrue(response.getPayload() instanceof String); 42 | assertFalse(((String) response.getPayload()).isEmpty()); 43 | } 44 | 45 | protected void addPayment() { 46 | PaymentResponse response = given() 47 | .contentType("application/json") 48 | .header(new Header("Content-Type", "application/json")) 49 | .body(JsonUtils.paymentRequest()) 50 | .when().post("/v1/payment/") 51 | .then() 52 | .statusCode(200) 53 | .extract() 54 | .as(PaymentResponse.class); 55 | assertEquals(response.getCode(), 200); 56 | assertTrue(response.getPayload() instanceof String); 57 | assertFalse(((String) response.getPayload()).isEmpty()); 58 | } 59 | 60 | protected void deletePaymentEndpoint() throws JsonProcessingException { 61 | String uuid = addMockPayment(); 62 | PaymentResponse response = given() 63 | .contentType("application/json") 64 | .header(new Header("Content-Type", "application/json")) 65 | .when().delete("/v1/payment/" + uuid) 66 | .then() 67 | .statusCode(200) 68 | .extract() 69 | .as(PaymentResponse.class); 70 | assertEquals(response.getCode(), 200); 71 | assertTrue(response.getPayload() instanceof String); 72 | assertFalse(((String) response.getPayload()).isEmpty()); 73 | } 74 | 75 | protected void fetchPaymentEndpoint() throws JsonProcessingException { 76 | String uuid = addMockPayment(); 77 | PaymentResponse response = given() 78 | .contentType("application/json") 79 | .header(new Header("Content-Type", "application/json")) 80 | .when().get("/v1/payment/" + uuid) 81 | .then() 82 | .statusCode(200) 83 | .extract() 84 | .as(PaymentResponse.class); 85 | assertEquals(response.getCode(), 200); 86 | PaymentStatePayload paymentStatePayload = mapper.convertValue(response.getPayload(), PaymentStatePayload.class); 87 | assertEquals(paymentStatePayload.getType(), "created"); 88 | } 89 | 90 | protected void fetchAllPaymentEndpoint() { 91 | PaymentResponse response = given() 92 | .contentType("application/json") 93 | .header(new Header("Content-Type", "application/json")) 94 | .when().get("/v1/payment/all") 95 | .then() 96 | .statusCode(200) 97 | .extract() 98 | .as(PaymentResponse.class); 99 | assertEquals(response.getCode(), 200); 100 | List paymentStatePayloadList = mapper.convertValue(response.getPayload(), new TypeReference>() { 101 | }); 102 | assertTrue(paymentStatePayloadList.size() > 0); 103 | assertNotNull(paymentStatePayloadList.get(0).getPaymentInfo()); 104 | } 105 | 106 | 107 | private String addMockPayment() throws JsonProcessingException { 108 | String uuid = UUID.randomUUID().toString(); 109 | PaymentAdded paymentAddedEvent = getPaymentAddedEvent(); 110 | paymentAddedEvent.setId(uuid); 111 | String event = mapper.writeValueAsString(paymentAddedEvent); 112 | CassandraConnector.addPayment(getAddPaymentQuery(paymentAddedEvent, "12345", event)); 113 | return uuid; 114 | } 115 | 116 | 117 | String getAddPaymentQuery(PaymentAdded paymentAdded, String timeStampMillis, String event) { 118 | return "INSERT INTO " + "paymentsSchema.payment" + 119 | "(id, timestamp, event) " + 120 | "VALUES (" + paymentAdded.getId() + ", '" + 121 | timeStampMillis + "', '" + 122 | event + "');"; 123 | } 124 | 125 | PaymentAdded getPaymentAddedEvent() { 126 | return new PaymentAdded(UUID.randomUUID().toString(), "created", 0, getPaymentInfoDTO()); 127 | } 128 | 129 | PaymentInfoDTO getPaymentInfoDTO() { 130 | DebtorPartyDTO debtorParty = getDebtorParty(); 131 | BeneficiaryPartyDTO beneficiaryParty = getBeneficiaryParty(); 132 | SponsorPartyDTO sponsorParty = getSponsorParty(); 133 | return new PaymentInfoDTO("amount", "currency", 134 | "paymentId", 135 | "paymentPurpose", "paymentType", 136 | "processingDate", "reference", "schemePaymentSubType", 137 | "schemePaymentType", debtorParty, sponsorParty, beneficiaryParty); 138 | } 139 | 140 | private SponsorPartyDTO getSponsorParty() { 141 | return new SponsorPartyDTO("accountName", 142 | "bankId", "bankCode"); 143 | } 144 | 145 | private BeneficiaryPartyDTO getBeneficiaryParty() { 146 | return new BeneficiaryPartyDTO("accountName", 147 | "accountNumber", 0, "address", "bankId", "name"); 148 | } 149 | 150 | private DebtorPartyDTO getDebtorParty() { 151 | return new DebtorPartyDTO("accountName", "accountNumber", 152 | 0, "address", "bankId", "name"); 153 | } 154 | 155 | } 156 | -------------------------------------------------------------------------------- /application/src/test/java/com/politrons/application/volume/PaymentResourceVolumeTest.java: -------------------------------------------------------------------------------- 1 | package com.politrons.application.volume; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.core.type.TypeReference; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import com.politrons.application.JsonUtils; 7 | import com.politrons.application.model.payload.payload.PaymentStatePayload; 8 | import com.politrons.application.model.payload.response.PaymentResponse; 9 | import com.politrons.application.utils.PaymentResourceTestUtils; 10 | import com.politrons.infrastructure.CassandraConnector; 11 | import com.politrons.infrastructure.dto.BeneficiaryPartyDTO; 12 | import com.politrons.infrastructure.dto.DebtorPartyDTO; 13 | import com.politrons.infrastructure.dto.PaymentInfoDTO; 14 | import com.politrons.infrastructure.dto.SponsorPartyDTO; 15 | import com.politrons.infrastructure.events.PaymentAdded; 16 | import io.quarkus.test.junit.QuarkusTest; 17 | import io.restassured.http.Header; 18 | import org.junit.jupiter.api.AfterAll; 19 | import org.junit.jupiter.api.BeforeAll; 20 | import org.junit.jupiter.api.RepeatedTest; 21 | 22 | import java.util.ArrayList; 23 | import java.util.List; 24 | import java.util.UUID; 25 | 26 | import static io.restassured.RestAssured.given; 27 | import static org.junit.jupiter.api.Assertions.*; 28 | 29 | /** 30 | * Volume test are meant to be used to have a load of traffic for a big period of time to detect 31 | * some possible memory leaks in your application that, it might provoke that your application get 32 | * out of memory and die. 33 | */ 34 | @QuarkusTest 35 | class PaymentResourceVolumeTest extends PaymentResourceTestUtils { 36 | 37 | private final int numOfRequest = 100;//This it should be at least 10k 38 | 39 | @BeforeAll 40 | static void init() { 41 | CassandraConnector.start(); 42 | } 43 | 44 | @AfterAll 45 | static void stop() { 46 | CassandraConnector.stop(); 47 | } 48 | 49 | @RepeatedTest(value = numOfRequest, name = RepeatedTest.LONG_DISPLAY_NAME) 50 | void volumeTest() throws JsonProcessingException { 51 | addPayment(); 52 | updatePayment(); 53 | deletePaymentEndpoint(); 54 | fetchPaymentEndpoint(); 55 | fetchAllPaymentEndpoint(); 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /application/src/test/scala/com/politrons/application/JsonUtils.scala: -------------------------------------------------------------------------------- 1 | package com.politrons.application 2 | 3 | object JsonUtils { 4 | 5 | val paymentRequest = 6 | """ 7 | |{ 8 | | "amount": "amount", 9 | | "currency": "currency", 10 | | "paymentId": "paymentId", 11 | | "debtorParty": { 12 | | "accountName": "accountName", 13 | | "accountNumber": "accountNumber", 14 | | "accountType": 0.0, 15 | | "address": "address", 16 | | "bankId": "bankId", 17 | | "name": "name" 18 | | }, 19 | | "sponsorParty": { 20 | | "accountNumber": "accountName", 21 | | "bankId": "bankId", 22 | | "bankIdCode": "bankCode" 23 | | }, 24 | | "beneficiaryParty": { 25 | | "accountName": "accountName", 26 | | "accountNumber": "accountNumber", 27 | | "accountType": 0.0, 28 | | "address": "address", 29 | | "bankId": "bankId", 30 | | "name": "name" 31 | | }, 32 | | "paymentPurpose": "paymentPurpose", 33 | | "paymentType": "paymentType", 34 | | "processingDate": "processingDate", 35 | | "reference": "reference", 36 | | "schemePaymentSubType": "schemePaymentSubType", 37 | | "schemePaymentType": "schemePaymentType" 38 | |} 39 | """.stripMargin 40 | val hello = 41 | """ 42 | |{"message":"hello"} 43 | """.stripMargin 44 | 45 | } 46 | -------------------------------------------------------------------------------- /domain/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | paymentAPI 7 | org.acme 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | domain 13 | 14 | 15 | 16 | net.alchim31.maven 17 | scala-maven-plugin 18 | 19 | 20 | scala-compile-first 21 | process-resources 22 | 23 | add-source 24 | compile 25 | 26 | 27 | 28 | scala-test-compile 29 | process-test-resources 30 | 31 | testCompile 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /domain/src/main/java/com/politrons/domain/PaymentStateAggregateRoot.java: -------------------------------------------------------------------------------- 1 | package com.politrons.domain; 2 | 3 | import com.politrons.domain.entities.PaymentInfo; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | 9 | import java.util.UUID; 10 | 11 | @Getter 12 | @Setter 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | public class PaymentStateAggregateRoot { 16 | 17 | private String id; 18 | private String type; 19 | private float version; 20 | PaymentInfo paymentInfo; 21 | 22 | /** 23 | * Factory to create a PaymentStateAggregateRoot with state created. 24 | * Since this is the first event for a payment we set the rowId also as paymentId 25 | */ 26 | public static PaymentStateAggregateRoot create(PaymentInfo paymentInfo) { 27 | String id = UUID.randomUUID().toString(); 28 | paymentInfo.setPaymentId(id); 29 | return new PaymentStateAggregateRoot(id, "created", 0, paymentInfo); 30 | } 31 | 32 | /** 33 | * Factory to create a PaymentStateAggregateRoot with state changed 34 | */ 35 | public static PaymentStateAggregateRoot update(PaymentInfo paymentInfo) { 36 | String id = UUID.randomUUID().toString(); 37 | return new PaymentStateAggregateRoot(id, "changed", 0, paymentInfo); 38 | } 39 | 40 | /** 41 | * Factory to update a previous PaymentStateAggregateRoot with state deleted 42 | * In case of delete since we search previously the domain, we just need to update the rowId and set the state. 43 | */ 44 | public static PaymentStateAggregateRoot delete(PaymentStateAggregateRoot paymentStateAggregateRoot) { 45 | String id = UUID.randomUUID().toString(); 46 | paymentStateAggregateRoot.setId(id); 47 | paymentStateAggregateRoot.setType("deleted"); 48 | return paymentStateAggregateRoot; 49 | } 50 | 51 | 52 | } 53 | -------------------------------------------------------------------------------- /domain/src/main/java/com/politrons/domain/entities/BeneficiaryParty.java: -------------------------------------------------------------------------------- 1 | package com.politrons.domain.entities; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | @Getter 9 | @Setter 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class BeneficiaryParty { 13 | private String accountName; 14 | private String accountNumber; 15 | private float accountType; 16 | private String address; 17 | private String bankId; 18 | private String name; 19 | 20 | public static BeneficiaryParty create(String accountName, 21 | String accountNumber, 22 | float accountType, 23 | String address, 24 | String bankId, 25 | String name) { 26 | return new BeneficiaryParty(accountName, accountNumber, accountType, address, bankId, name); 27 | 28 | } 29 | } 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /domain/src/main/java/com/politrons/domain/entities/DebtorParty.java: -------------------------------------------------------------------------------- 1 | package com.politrons.domain.entities; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | @Getter 9 | @Setter 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class DebtorParty { 13 | private String accountName; 14 | private String accountNumber; 15 | private float accountType; 16 | private String address; 17 | private String bankId; 18 | private String name; 19 | 20 | public static DebtorParty create(String accountName, 21 | String accountNumber, 22 | float accountType, 23 | String address, 24 | String bankId, 25 | String name) { 26 | return new DebtorParty(accountName, accountNumber, accountType, address, bankId, name); 27 | 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /domain/src/main/java/com/politrons/domain/entities/PaymentInfo.java: -------------------------------------------------------------------------------- 1 | package com.politrons.domain.entities; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | 9 | @Getter 10 | @Setter 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class PaymentInfo { 14 | private String amount; 15 | private String currency; 16 | private String paymentId; 17 | private String paymentPurpose; 18 | private String paymentType; 19 | private String processingDate; 20 | private String reference; 21 | private String schemePaymentSubType; 22 | private String schemePaymentType; 23 | DebtorParty debtorParty; 24 | SponsorParty sponsorParty; 25 | BeneficiaryParty beneficiaryParty; 26 | 27 | public static PaymentInfo create(String amount, 28 | String currency, 29 | String paymentId, 30 | String paymentPurpose, 31 | String paymentType, 32 | String processingDate, 33 | String reference, 34 | String schemePaymentSubType, 35 | String schemePaymentType, 36 | DebtorParty debtorParty, 37 | SponsorParty sponsorParty, 38 | BeneficiaryParty beneficiaryParty) { 39 | return new PaymentInfo(amount, currency, paymentId, paymentPurpose, 40 | paymentType, processingDate, reference, schemePaymentSubType, 41 | schemePaymentType, debtorParty, sponsorParty, beneficiaryParty); 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /domain/src/main/java/com/politrons/domain/entities/SponsorParty.java: -------------------------------------------------------------------------------- 1 | package com.politrons.domain.entities; 2 | 3 | 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | 9 | @Getter 10 | @Setter 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class SponsorParty { 14 | private String accountNumber; 15 | private String bankId; 16 | private String bankIdCode; 17 | 18 | public static SponsorParty create(String accountNumber, 19 | String bankId, 20 | String bankIdCode) { 21 | return new SponsorParty(accountNumber, bankId, bankIdCode); 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /domain/src/main/resources/META-INF/beans.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/politrons/PaymentAPI/2e12a671000d26cf34fe4afb3f96b99ffff7ce65/domain/src/main/resources/META-INF/beans.xml -------------------------------------------------------------------------------- /domain/src/test/java/com/politrons/domain/entities/EntitiesTest.java: -------------------------------------------------------------------------------- 1 | package com.politrons.domain.entities; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.politrons.domain.PaymentStateAggregateRoot; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.io.IOException; 9 | 10 | import static org.junit.jupiter.api.Assertions.*; 11 | 12 | class EntitiesTest { 13 | 14 | private ObjectMapper mapper = new ObjectMapper(); 15 | 16 | @Test 17 | void createPaymentAggregateRoot() { 18 | PaymentInfo paymentInfo = getPaymentInfo(); 19 | PaymentStateAggregateRoot paymentStateAggregateRoot = new PaymentStateAggregateRoot("id", "type", 1, paymentInfo); 20 | assertEquals(paymentStateAggregateRoot.getId(), "id"); 21 | } 22 | 23 | @Test 24 | void transformDomainToJson() throws JsonProcessingException { 25 | PaymentInfo paymentInfo = getPaymentInfo(); 26 | PaymentStateAggregateRoot paymentStateAggregateRoot = new PaymentStateAggregateRoot("id", "type", 1, paymentInfo); 27 | String json = mapper.writeValueAsString(paymentStateAggregateRoot); 28 | assertNotNull(json); 29 | } 30 | 31 | @Test 32 | void transformJsonToDomain() throws IOException { 33 | PaymentInfo paymentInfo = mapper.readValue(JsonUtils.paymentInfoJson(), PaymentInfo.class); 34 | assertNotNull(paymentInfo); 35 | } 36 | 37 | @Test 38 | void debtorPartyFactory() { 39 | DebtorParty debtorParty = DebtorParty.create("accountName", "accountNumber", 40 | 0, "address", "bankId", "name"); 41 | assertNotNull(debtorParty); 42 | } 43 | 44 | @Test 45 | void beneficiaryPartyFactory() { 46 | BeneficiaryParty beneficiaryParty = BeneficiaryParty.create("accountName", 47 | "accountNumber", 0, "address", "bankId", "name"); 48 | assertNotNull(beneficiaryParty); 49 | 50 | } 51 | 52 | @Test 53 | void sponsorPartyFactory() { 54 | SponsorParty sponsorParty = SponsorParty.create("accountName", 55 | "bankId", "bankCode"); 56 | assertNotNull(sponsorParty); 57 | } 58 | 59 | @Test 60 | void paymentInfoFactory() { 61 | PaymentInfo paymentInfo = PaymentInfo.create("amount", "currency", 62 | "paymentId", 63 | "paymentPurpose", "paymentType", 64 | "processingDate", "reference", "schemePaymentSubType", 65 | "schemePaymentType", getDebtorParty(), getSponsorParty(), getBeneficiaryParty()); 66 | assertNotNull(paymentInfo); 67 | } 68 | 69 | private PaymentInfo getPaymentInfo() { 70 | DebtorParty debtorParty = getDebtorParty(); 71 | BeneficiaryParty beneficiaryParty = getBeneficiaryParty(); 72 | SponsorParty sponsorParty = getSponsorParty(); 73 | return new PaymentInfo("amount", "currency", 74 | "paymentId", 75 | "paymentPurpose", "paymentType", 76 | "processingDate", "reference", "schemePaymentSubType", 77 | "schemePaymentType", debtorParty, sponsorParty, beneficiaryParty); 78 | } 79 | 80 | private SponsorParty getSponsorParty() { 81 | return new SponsorParty("accountName", 82 | "bankId", "bankCode"); 83 | } 84 | 85 | private BeneficiaryParty getBeneficiaryParty() { 86 | return new BeneficiaryParty("accountName", 87 | "accountNumber", 0, "address", "bankId", "name"); 88 | } 89 | 90 | private DebtorParty getDebtorParty() { 91 | return new DebtorParty("accountName", "accountNumber", 92 | 0, "address", "bankId", "name"); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /domain/src/test/scala/com/politrons/domain/entities/JsonUtils.scala: -------------------------------------------------------------------------------- 1 | package com.politrons.domain.entities 2 | 3 | object JsonUtils { 4 | 5 | val paymentInfoJson = 6 | """ 7 | |{"amount":"amount","currency":"currency","paymentId":"paymentId","debtorParty":{"accountName":"accountName","accountNumber":"accountNumber","accountType":0.0,"address":"address","bankId":"bankId","name":"name"},"sponsorParty":{"accountNumber":"accountName","bankId":"bankId","bankIdCode":"bankCode"},"beneficiaryParty":{"accountName":"accountName","accountNumber":"accountNumber","accountType":0.0,"address":"address","bankId":"bankId","name":"name"},"paymentPurpose":"paymentPurpose","paymentType":"paymentType","processingDate":"processingDate","reference":"reference","schemePaymentSubType":"schemePaymentSubType","schemePaymentType":"schemePaymentType"} 8 | """.stripMargin 9 | 10 | } 11 | -------------------------------------------------------------------------------- /img/ddd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/politrons/PaymentAPI/2e12a671000d26cf34fe4afb3f96b99ffff7ce65/img/ddd.png -------------------------------------------------------------------------------- /img/testPyramid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/politrons/PaymentAPI/2e12a671000d26cf34fe4afb3f96b99ffff7ce65/img/testPyramid.png -------------------------------------------------------------------------------- /infrastructure/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | paymentAPI 7 | org.acme 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | infrastructure 13 | 14 | 15 | 16 | net.alchim31.maven 17 | scala-maven-plugin 18 | 19 | 20 | scala-compile-first 21 | process-resources 22 | 23 | add-source 24 | compile 25 | 26 | 27 | 28 | scala-test-compile 29 | process-test-resources 30 | 31 | testCompile 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | org.acme 41 | domain 42 | 1.0-SNAPSHOT 43 | 44 | 45 | ma.glasnost.orika 46 | orika-core 47 | 1.5.2 48 | 49 | 50 | 51 | com.datastax.cassandra 52 | cassandra-driver-core 53 | 55 | 3.7.1 56 | shaded 57 | 58 | 59 | * 60 | io.netty 61 | 62 | 63 | 64 | 65 | 66 | com.datastax.cassandra 67 | cassandra-driver-mapping 68 | 70 | 3.7.1 71 | 72 | 73 | * 74 | io.netty 75 | 76 | 77 | 78 | 79 | 80 | com.datastax.cassandra 81 | cassandra-driver-extras 82 | 84 | 3.7.1 85 | 86 | 87 | * 88 | io.netty 89 | 90 | 91 | 92 | 93 | 94 | com.github.nosan 95 | embedded-cassandra 96 | 2.0.1 97 | 98 | 99 | 100 | org.mockito 101 | mockito-core 102 | 2.23.0 103 | test 104 | 105 | 106 | org.mockito 107 | mockito-junit-jupiter 108 | 2.23.0 109 | test 110 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/politrons/infrastructure/dao/PaymentDAO.java: -------------------------------------------------------------------------------- 1 | package com.politrons.infrastructure.dao; 2 | 3 | import com.politrons.domain.PaymentStateAggregateRoot; 4 | import com.politrons.infrastructure.events.PaymentAdded; 5 | import com.politrons.infrastructure.events.PaymentDeleted; 6 | import com.politrons.infrastructure.events.PaymentUpdated; 7 | import io.vavr.concurrent.Future; 8 | import io.vavr.control.Either; 9 | 10 | import javax.enterprise.context.ApplicationScoped; 11 | import java.util.List; 12 | 13 | @ApplicationScoped 14 | public interface PaymentDAO { 15 | 16 | Future> persistPaymentAddedEvent(PaymentAdded paymentAdded); 17 | 18 | Future> persistPaymentUpdatedEvent(PaymentUpdated paymentUpdated); 19 | 20 | Future> persistPaymentDeletedEvent(PaymentDeleted paymentAdded); 21 | 22 | Future> fetchPayment(String id); 23 | 24 | Future>> fetchAllPayments(); 25 | } 26 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/politrons/infrastructure/dao/impl/PaymentDAOImpl.java: -------------------------------------------------------------------------------- 1 | package com.politrons.infrastructure.dao.impl; 2 | 3 | import com.datastax.driver.core.ResultSet; 4 | import com.datastax.driver.core.Row; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import com.politrons.domain.PaymentStateAggregateRoot; 7 | import com.politrons.infrastructure.CassandraConnector; 8 | import com.politrons.infrastructure.dao.PaymentDAO; 9 | import com.politrons.infrastructure.events.PaymentAdded; 10 | import com.politrons.infrastructure.events.PaymentDeleted; 11 | import com.politrons.infrastructure.events.PaymentEvent; 12 | import com.politrons.infrastructure.events.PaymentUpdated; 13 | import io.vavr.API; 14 | import io.vavr.concurrent.Future; 15 | import io.vavr.control.Either; 16 | import io.vavr.control.Try; 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | 20 | import javax.enterprise.context.ApplicationScoped; 21 | import java.time.Instant; 22 | import java.util.List; 23 | import java.util.UUID; 24 | import java.util.stream.Collectors; 25 | 26 | import static io.vavr.API.*; 27 | import static io.vavr.Patterns.$Failure; 28 | import static io.vavr.Patterns.$Success; 29 | 30 | @ApplicationScoped 31 | public class PaymentDAOImpl implements PaymentDAO { 32 | 33 | private Logger logger = LoggerFactory.getLogger(PaymentDAOImpl.class); 34 | 35 | private ObjectMapper mapper = new ObjectMapper(); 36 | 37 | @Override 38 | public Future> persistPaymentAddedEvent(PaymentAdded paymentAdded) { 39 | return upsertPayment(paymentAdded); 40 | } 41 | 42 | @Override 43 | public Future> persistPaymentUpdatedEvent(PaymentUpdated paymentUpdated) { 44 | return upsertPayment(paymentUpdated); 45 | } 46 | 47 | @Override 48 | public Future> persistPaymentDeletedEvent(PaymentDeleted paymentAdded) { 49 | return upsertPayment(paymentAdded); 50 | } 51 | 52 | /** 53 | * Method to find the Payment event in json format and transform into [PaymentStateAggregateRoot] 54 | * 55 | * @param id of the event 56 | * @return PaymentStateAggregateRoot 57 | */ 58 | @Override 59 | public Future> fetchPayment(String id) { 60 | return Future.of(() -> CassandraConnector.fetchPayment(fetchPaymentByIdQuery(id))) 61 | .map(this::transformResultSetToPaymentAggregateRoot); 62 | } 63 | 64 | @Override 65 | public Future>> fetchAllPayments() { 66 | return Future.of(() -> CassandraConnector.fetchAllPayments(fetchAllPaymentsQuery())) 67 | .map(this::transformResultSetToPaymentAggregateRoots); 68 | } 69 | 70 | /** 71 | * Function to receive the event [PaymentEvent] and we persist into Cassandra row adding the UUID, timestamp and Event in json format. 72 | * We are doing Event sourcing, which means the internal id the of event it will remain intact for future rehydrate, and we will create a new UUID per 73 | * transaction inserted in Cassandra. 74 | * 75 | * @param paymentEvent Event to be transform into json and being persisted. 76 | * @return the id of the row 77 | */ 78 | private Future> upsertPayment(PaymentEvent paymentEvent) { 79 | String uuid = createNewUUID(); 80 | return Match(Try(() -> mapper.writeValueAsString(paymentEvent)) 81 | .flatMap(event -> Try.of(() -> getAddPaymentQuery(uuid, getTimestampMillis(), event)) 82 | .map(query -> Future.of(() -> CassandraConnector.addPayment(query)) 83 | .map(maybeResultSet -> transformResultSetToId(maybeResultSet, uuid))))).of( 84 | Case($Success($()), future -> future), 85 | Case($Failure($()), throwable -> { 86 | logger.error("Error in add payment DAO mapping event to json. Caused by:" + throwable.getCause()); 87 | return Future.of(() -> Left(throwable)); 88 | })); 89 | } 90 | 91 | private Either> transformResultSetToPaymentAggregateRoots(Try maybeResultSet) { 92 | return Match(maybeResultSet).of( 93 | Case($Success($()), rs -> Right(getPaymentStateAggregateRoots(rs))), 94 | Case($Failure($()), throwable -> { 95 | logger.error("Error in fetch payment DAO transforming ResultSet. Caused by:" + throwable.getCause()); 96 | return Left(throwable); 97 | })); 98 | } 99 | 100 | private List getPaymentStateAggregateRoots(ResultSet rs) { 101 | return rs.all().stream() 102 | .map(this::transformIntoPaymentStateAggregateRoot) 103 | .collect(Collectors.toList()); 104 | } 105 | 106 | private PaymentStateAggregateRoot transformIntoPaymentStateAggregateRoot(Row row) { 107 | return Match(Try.of(() -> mapper.readValue(row.getString("event"), PaymentStateAggregateRoot.class))).of( 108 | Case($Success($()), paymentStateAggregateRoot -> paymentStateAggregateRoot), 109 | Case($Failure($()), throwable -> null)); 110 | } 111 | 112 | /** 113 | * Function to transform from the Try monad of ResultSet into The id of the transaction. 114 | * 115 | * @param maybeResultSet monad with maybe the resultSet or a Throwable 116 | * @param id of the row 117 | * @return id of the event 118 | */ 119 | private Either transformResultSetToId(Try maybeResultSet, String id) { 120 | return Match(maybeResultSet).of( 121 | Case($Success($()), resultSet -> Right(id)), 122 | Case($Failure($()), throwable -> { 123 | logger.error("Error in add payment DAO transforming ResultSet. Caused by:" + throwable.getCause()); 124 | return Left(throwable); 125 | })); 126 | } 127 | 128 | /** 129 | * Function to transform from the Try monad of ResultSet into The PaymentStateAggregateRoot of the transaction. 130 | * 131 | * @param maybeResultSet monad with maybe the resultSet or a Throwable 132 | */ 133 | private Either transformResultSetToPaymentAggregateRoot(Try maybeResultSet) { 134 | return Match(maybeResultSet).of( 135 | Case($Success($()), this::processResultSet), 136 | Case($Failure($()), throwable -> { 137 | logger.error("Error in fetch payment DAO transforming ResultSet. Caused by:" + throwable.getCause()); 138 | return Left(throwable); 139 | })); 140 | } 141 | 142 | private Either processResultSet(ResultSet resultSet) { 143 | return Match(Try.of(() -> mapper.readValue(resultSet.one().getString("event"), PaymentStateAggregateRoot.class))).of( 144 | Case($Success($()), API::Right), 145 | Case($Failure($()), throwable -> { 146 | logger.error("Error in add payment DAO transforming ResultSet. Caused by:" + throwable.getCause()); 147 | return Left(throwable); 148 | })); 149 | } 150 | 151 | private String getAddPaymentQuery(String id, String timeStampMillis, String event) { 152 | if (event == null || event.isEmpty() || event.equals("null")) throw new IllegalArgumentException(); 153 | return "INSERT INTO " + "paymentsSchema.payment" + 154 | "(id, timestamp, event) " + 155 | "VALUES (" + id + ", '" + 156 | timeStampMillis + "', '" + 157 | event + "');"; 158 | } 159 | 160 | private String fetchPaymentByIdQuery(String id) { 161 | return "SELECT * FROM paymentsSchema.payment WHERE id=" + id + " "; 162 | } 163 | 164 | private String fetchAllPaymentsQuery() { 165 | return "SELECT * FROM paymentsSchema.payment"; 166 | } 167 | 168 | 169 | private String getTimestampMillis() { 170 | Instant instant = Instant.now(); 171 | return String.valueOf(instant.toEpochMilli()); 172 | } 173 | 174 | private String createNewUUID() { 175 | return UUID.randomUUID().toString(); 176 | } 177 | 178 | } 179 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/politrons/infrastructure/dto/BeneficiaryPartyDTO.java: -------------------------------------------------------------------------------- 1 | package com.politrons.infrastructure.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | @Getter 9 | @Setter 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class BeneficiaryPartyDTO { 13 | private String accountName; 14 | private String accountNumber; 15 | private float accountType; 16 | private String address; 17 | private String bankId; 18 | private String name; 19 | 20 | } 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/politrons/infrastructure/dto/DebtorPartyDTO.java: -------------------------------------------------------------------------------- 1 | package com.politrons.infrastructure.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | @Getter 9 | @Setter 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class DebtorPartyDTO { 13 | private String accountName; 14 | private String accountNumber; 15 | private float accountType; 16 | private String address; 17 | private String bankId; 18 | private String name; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/politrons/infrastructure/dto/PaymentInfoDTO.java: -------------------------------------------------------------------------------- 1 | package com.politrons.infrastructure.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | 9 | @Getter 10 | @Setter 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class PaymentInfoDTO { 14 | private String amount; 15 | private String currency; 16 | private String paymentId; 17 | private String paymentPurpose; 18 | private String paymentType; 19 | private String processingDate; 20 | private String reference; 21 | private String schemePaymentSubType; 22 | private String schemePaymentType; 23 | DebtorPartyDTO debtorParty; 24 | SponsorPartyDTO sponsorParty; 25 | BeneficiaryPartyDTO beneficiaryParty; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/politrons/infrastructure/dto/SponsorPartyDTO.java: -------------------------------------------------------------------------------- 1 | package com.politrons.infrastructure.dto; 2 | 3 | 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | 9 | @Getter 10 | @Setter 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class SponsorPartyDTO { 14 | private String accountNumber; 15 | private String bankId; 16 | private String bankIdCode; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/politrons/infrastructure/events/PaymentAdded.java: -------------------------------------------------------------------------------- 1 | package com.politrons.infrastructure.events; 2 | 3 | import com.politrons.infrastructure.dto.PaymentInfoDTO; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | 9 | @Getter 10 | @Setter 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class PaymentAdded implements PaymentEvent{ 14 | String id; 15 | String type; 16 | float version; 17 | PaymentInfoDTO paymentInfo; 18 | } 19 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/politrons/infrastructure/events/PaymentDeleted.java: -------------------------------------------------------------------------------- 1 | package com.politrons.infrastructure.events; 2 | 3 | import com.politrons.infrastructure.dto.PaymentInfoDTO; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | 9 | @Getter 10 | @Setter 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class PaymentDeleted implements PaymentEvent{ 14 | String id; 15 | String type; 16 | float version; 17 | PaymentInfoDTO paymentInfo; 18 | } 19 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/politrons/infrastructure/events/PaymentEvent.java: -------------------------------------------------------------------------------- 1 | package com.politrons.infrastructure.events; 2 | 3 | public interface PaymentEvent { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/politrons/infrastructure/events/PaymentUpdated.java: -------------------------------------------------------------------------------- 1 | package com.politrons.infrastructure.events; 2 | 3 | import com.politrons.infrastructure.dto.PaymentInfoDTO; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | 9 | @Getter 10 | @Setter 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | public class PaymentUpdated implements PaymentEvent{ 14 | String id; 15 | String type; 16 | float version; 17 | PaymentInfoDTO paymentInfo; 18 | } 19 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/politrons/infrastructure/repository/PaymentRepository.java: -------------------------------------------------------------------------------- 1 | package com.politrons.infrastructure.repository; 2 | 3 | import com.politrons.domain.PaymentStateAggregateRoot; 4 | import io.vavr.concurrent.Future; 5 | import io.vavr.control.Either; 6 | 7 | import javax.enterprise.context.ApplicationScoped; 8 | 9 | @ApplicationScoped 10 | public interface PaymentRepository { 11 | 12 | Future> addPayment(PaymentStateAggregateRoot paymentStateAggregateRoot); 13 | 14 | Future> updatePayment(PaymentStateAggregateRoot paymentStateAggregateRoot); 15 | 16 | Future> deletePayment(PaymentStateAggregateRoot paymentStateAggregateRoot); 17 | 18 | Future> fetchPayment(String id); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /infrastructure/src/main/java/com/politrons/infrastructure/repository/impl/PaymentRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package com.politrons.infrastructure.repository.impl; 2 | 3 | import com.politrons.domain.PaymentStateAggregateRoot; 4 | import com.politrons.infrastructure.events.PaymentDeleted; 5 | import com.politrons.infrastructure.events.PaymentEvent; 6 | import com.politrons.infrastructure.events.PaymentUpdated; 7 | import com.politrons.infrastructure.repository.PaymentRepository; 8 | import com.politrons.infrastructure.dao.PaymentDAO; 9 | import com.politrons.infrastructure.events.PaymentAdded; 10 | import io.vavr.concurrent.Future; 11 | import io.vavr.control.Either; 12 | import lombok.NoArgsConstructor; 13 | import ma.glasnost.orika.MapperFacade; 14 | import ma.glasnost.orika.MapperFactory; 15 | import ma.glasnost.orika.impl.DefaultMapperFactory; 16 | 17 | import javax.enterprise.context.ApplicationScoped; 18 | import javax.inject.Inject; 19 | 20 | @NoArgsConstructor 21 | @ApplicationScoped 22 | public class PaymentRepositoryImpl implements PaymentRepository { 23 | 24 | @Inject 25 | PaymentDAO paymentDAO; 26 | 27 | private MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build(); 28 | 29 | PaymentRepositoryImpl(PaymentDAO paymentDAO) { 30 | this.paymentDAO = paymentDAO; 31 | } 32 | 33 | /** 34 | * Method responsible for the transformation from the AggregateRoot of the domain layer 35 | * into the PaymentAdded Event to be persisted. 36 | * 37 | * @param paymentStateAggregateRoot domain model to be transform into event. 38 | * @return The id of the transaction for futures fetch. 39 | */ 40 | @Override 41 | public Future> addPayment(PaymentStateAggregateRoot paymentStateAggregateRoot) { 42 | PaymentAdded paymentAdded = transformAggregateRootIntoEvent(paymentStateAggregateRoot, PaymentAdded.class); 43 | return paymentDAO.persistPaymentAddedEvent(paymentAdded); 44 | } 45 | 46 | /** 47 | * Method responsible for the transformation from the AggregateRoot of the domain layer 48 | * into the PaymentUpdated Event to be persisted. 49 | * 50 | * @param paymentStateAggregateRoot domain model to be transform into event. 51 | * @return The id of the transaction for futures fetch. 52 | */ 53 | @Override 54 | public Future> updatePayment(PaymentStateAggregateRoot paymentStateAggregateRoot) { 55 | PaymentUpdated paymentUpdated = transformAggregateRootIntoEvent(paymentStateAggregateRoot, PaymentUpdated.class); 56 | return paymentDAO.persistPaymentUpdatedEvent(paymentUpdated); 57 | } 58 | 59 | /** 60 | * Method responsible for the transformation from the AggregateRoot of the domain layer 61 | * into the PaymentDeleted Event to be persisted. 62 | * 63 | * @param paymentStateAggregateRoot domain model to be transform into event. 64 | * @return The id of the transaction for futures fetch. 65 | */ 66 | @Override 67 | public Future> deletePayment(PaymentStateAggregateRoot paymentStateAggregateRoot) { 68 | PaymentDeleted paymentDeleted = transformAggregateRootIntoEvent(paymentStateAggregateRoot, PaymentDeleted.class); 69 | return paymentDAO.persistPaymentDeletedEvent(paymentDeleted); 70 | } 71 | 72 | /** 73 | * Proxy method to be used from handler by delete Payment to get the payment first from the Database. 74 | * 75 | * @param id of the eventId 76 | * @return The PaymentStateAggregateRoot to change state as deleted and create a new Event. 77 | */ 78 | @Override 79 | public Future> fetchPayment(String id) { 80 | return paymentDAO.fetchPayment(id); 81 | } 82 | 83 | private T transformAggregateRootIntoEvent(PaymentStateAggregateRoot paymentStateAggregateRoot, 84 | Class paymentEventClass) { 85 | mapperFactory.classMap(PaymentStateAggregateRoot.class, paymentEventClass); 86 | MapperFacade mapper = mapperFactory.getMapperFacade(); 87 | return (T) mapper.map(paymentStateAggregateRoot, paymentEventClass); 88 | 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /infrastructure/src/main/resources/META-INF/beans.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/politrons/PaymentAPI/2e12a671000d26cf34fe4afb3f96b99ffff7ce65/infrastructure/src/main/resources/META-INF/beans.xml -------------------------------------------------------------------------------- /infrastructure/src/main/scala/com/politrons/infrastructure/CassandraConnector.scala: -------------------------------------------------------------------------------- 1 | package com.politrons.infrastructure 2 | 3 | import com.datastax.driver.core._ 4 | import com.github.nosan.embedded.cassandra.local.LocalCassandraFactory 5 | import com.github.nosan.embedded.cassandra.{Cassandra, Settings} 6 | import io.vavr.control.Try 7 | 8 | /** 9 | * Simple cassandra client, following the datastax documentation 10 | * (http://www.datastax.com/documentation/developer/java-driver/2.0/java-driver/quick_start/qsSimpleClientCreate_t.html). 11 | */ 12 | object CassandraConnector { 13 | 14 | lazy val defaultTable = 15 | s"""CREATE TABLE IF NOT EXISTS paymentsSchema.payment ( 16 | id uuid, 17 | timestamp varchar, 18 | event varchar, 19 | PRIMARY KEY (id ) 20 | );""" 21 | 22 | var session: Session = _ 23 | private var cluster: Cluster = _ 24 | private var cassandra: Cassandra = _ 25 | 26 | def isStarted(): Boolean = session != null && !session.isClosed 27 | 28 | def start(): Unit = { 29 | if(!isStarted()){ 30 | val cassandraFactory = new LocalCassandraFactory 31 | cassandra = cassandraFactory.create() 32 | cassandra.start() 33 | initCassandra() 34 | } 35 | } 36 | 37 | def stop() { 38 | session.close() 39 | cluster.close() 40 | cassandra.stop() 41 | } 42 | 43 | def addPayment(query: String): Try[ResultSet] = executeQuery(query) 44 | 45 | def fetchPayment(query: String): Try[ResultSet] = executeQuery(query) 46 | 47 | def fetchAllPayments(query: String): Try[ResultSet] = executeQuery(query) 48 | 49 | /** 50 | * Init the session/cluster to cassandra nd create the keyspace and table in case does not exist. 51 | */ 52 | def initCassandra(): Unit = { 53 | initCluster(cassandra.getSettings) 54 | cleanCassandra() 55 | createSchema() 56 | createTable() 57 | } 58 | 59 | private def initCluster(settings: Settings): Unit = { 60 | val socketOptions = new SocketOptions 61 | socketOptions.setConnectTimeoutMillis(30000) 62 | socketOptions.setReadTimeoutMillis(30000) 63 | cluster = Cluster.builder 64 | .addContactPoints(settings.getAddress) 65 | .withPort(settings.getPort) 66 | .withSocketOptions(socketOptions) 67 | .withoutJMXReporting 68 | .withoutMetrics 69 | .build 70 | session = cluster.connect 71 | } 72 | 73 | private def createSchema(keySpace: String = "paymentsSchema"): Unit = { 74 | session.execute(s"CREATE KEYSPACE IF NOT EXISTS $keySpace WITH replication = {'class':'SimpleStrategy', 'replication_factor':1};") 75 | println(s"Keyspace $keySpace created if not exists") 76 | } 77 | 78 | private def createTable(script: String = defaultTable): Unit = { 79 | session.execute(script) 80 | println(s"Table created if not exist") 81 | } 82 | 83 | /** 84 | * Clean the table data from the test 85 | */ 86 | private def cleanCassandra(keySpace: String = "paymentsSchema", table: String = "payment"): Unit = { 87 | executeQuery(s"""truncate $keySpace.$table""") 88 | } 89 | 90 | private def executeQuery(query: String): Try[ResultSet] = { 91 | Try.of(() => session.execute(query)) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /infrastructure/src/test/java/com/politrons/infrastructure/CassandraMock.java: -------------------------------------------------------------------------------- 1 | package com.politrons.infrastructure; 2 | 3 | import com.datastax.driver.core.Cluster; 4 | import com.datastax.driver.core.Session; 5 | import com.datastax.driver.core.SocketOptions; 6 | import com.github.nosan.embedded.cassandra.Cassandra; 7 | import com.github.nosan.embedded.cassandra.Settings; 8 | import com.github.nosan.embedded.cassandra.cql.CqlScript; 9 | import com.github.nosan.embedded.cassandra.local.LocalCassandraFactory; 10 | 11 | import java.util.List; 12 | 13 | public class CassandraMock { 14 | 15 | static private Cassandra cassandra; 16 | 17 | public static void start() { 18 | LocalCassandraFactory cassandraFactory = new LocalCassandraFactory(); 19 | cassandra = cassandraFactory.create(); 20 | cassandra.start(); 21 | Settings settings = cassandra.getSettings(); 22 | executeScripts(settings); 23 | } 24 | 25 | public static void stopCassandra(){ 26 | cassandra.stop(); 27 | } 28 | 29 | private static void executeScripts(Settings settings) { 30 | SocketOptions socketOptions = new SocketOptions(); 31 | socketOptions.setConnectTimeoutMillis(30000); 32 | socketOptions.setReadTimeoutMillis(30000); 33 | try (Cluster cluster = Cluster.builder().addContactPoints(settings.getAddress()) 34 | .withPort(settings.getPort()).withSocketOptions(socketOptions) 35 | .withoutJMXReporting().withoutMetrics() 36 | .build()) { 37 | Session session = cluster.connect(); 38 | // List statements = CqlScript.classpath("schema.cql").getStatements(); 39 | // statements.forEach(session::execute); 40 | } 41 | } 42 | 43 | 44 | } -------------------------------------------------------------------------------- /infrastructure/src/test/java/com/politrons/infrastructure/dao/impl/PaymentDAOBackendDownTest.java: -------------------------------------------------------------------------------- 1 | package com.politrons.infrastructure.dao.impl; 2 | 3 | import com.politrons.infrastructure.events.PaymentAdded; 4 | import io.vavr.concurrent.Future; 5 | import io.vavr.control.Either; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.util.UUID; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | import static org.junit.jupiter.api.Assertions.assertTrue; 12 | 13 | public class PaymentDAOBackendDownTest extends PaymentDAOUtilsTest{ 14 | 15 | private PaymentDAOImpl paymentDAO = new PaymentDAOImpl(); 16 | 17 | @Test 18 | void addPaymentEvenWithDatabaseDown() { 19 | Future> eithers = paymentDAO.persistPaymentAddedEvent(getPaymentAddedEvent()); 20 | assertTrue(eithers.get().isLeft()); 21 | } 22 | 23 | protected PaymentAdded getPaymentAddedEvent() { 24 | return new PaymentAdded(UUID.randomUUID().toString(), "payment", 0, getPaymentInfoDTO()); 25 | } 26 | 27 | 28 | } 29 | -------------------------------------------------------------------------------- /infrastructure/src/test/java/com/politrons/infrastructure/dao/impl/PaymentDAOTest.java: -------------------------------------------------------------------------------- 1 | package com.politrons.infrastructure.dao.impl; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.politrons.domain.PaymentStateAggregateRoot; 6 | import com.politrons.infrastructure.CassandraConnector; 7 | import com.politrons.infrastructure.events.PaymentAdded; 8 | import io.vavr.concurrent.Future; 9 | import io.vavr.control.Either; 10 | import org.junit.jupiter.api.AfterAll; 11 | import org.junit.jupiter.api.BeforeAll; 12 | import org.junit.jupiter.api.Test; 13 | 14 | import java.util.List; 15 | import java.util.UUID; 16 | 17 | import static org.junit.jupiter.api.Assertions.*; 18 | 19 | public class PaymentDAOTest extends PaymentDAOUtilsTest { 20 | 21 | private PaymentDAOImpl paymentDAO = new PaymentDAOImpl(); 22 | 23 | private ObjectMapper mapper = new ObjectMapper(); 24 | 25 | 26 | @BeforeAll 27 | static void init() { 28 | CassandraConnector.start(); 29 | } 30 | 31 | //####################// 32 | // Add payment // 33 | //####################// 34 | @Test 35 | void paymentAddedEvent() { 36 | Future> eithers = paymentDAO.persistPaymentAddedEvent(getPaymentAddedEvent()); 37 | assertTrue(eithers.get().isRight()); 38 | assertFalse(eithers.get().right().get().isEmpty()); 39 | } 40 | 41 | @Test 42 | void paymentAddedEventWithWrongJson() { 43 | Future> eithers = paymentDAO.persistPaymentAddedEvent(null); 44 | assertTrue(eithers.get().isLeft()); 45 | } 46 | 47 | //####################// 48 | // Update payment // 49 | //####################// 50 | @Test 51 | void paymentUpdatedEvent() { 52 | Future> eithers = paymentDAO.persistPaymentUpdatedEvent(getPaymentUpdatedEvent()); 53 | assertTrue(eithers.get().isRight()); 54 | assertFalse(eithers.get().right().get().isEmpty()); 55 | } 56 | 57 | @Test 58 | void paymentUpdatedEventWithWrongJson() { 59 | Future> eithers = paymentDAO.persistPaymentUpdatedEvent(null); 60 | assertTrue(eithers.get().isLeft()); 61 | } 62 | 63 | //####################// 64 | // Delete payment // 65 | //####################// 66 | @Test 67 | void paymentDeletedEvent() { 68 | Future> eithers = paymentDAO.persistPaymentDeletedEvent(getPaymentDeletedEvent()); 69 | assertTrue(eithers.get().isRight()); 70 | assertFalse(eithers.get().right().get().isEmpty()); 71 | } 72 | 73 | @Test 74 | void paymentDeletedEventWithWrongJson() { 75 | Future> eithers = paymentDAO.persistPaymentDeletedEvent(null); 76 | assertTrue(eithers.get().isLeft()); 77 | } 78 | 79 | //####################// 80 | // Fetch payment // 81 | //####################// 82 | @Test 83 | void fetchPayment() throws JsonProcessingException { 84 | String uuid = addPayment(); 85 | Future> eithers = paymentDAO.fetchPayment(uuid); 86 | assertTrue(eithers.get().isRight()); 87 | assertEquals(eithers.get().right().get().getId(), uuid); 88 | } 89 | 90 | @Test 91 | void fetchPaymentWithWrongId() { 92 | Future> eithers = paymentDAO.fetchPayment("foo"); 93 | assertTrue(eithers.get().isLeft()); 94 | } 95 | 96 | @Test 97 | void fetchAllPayment() throws JsonProcessingException { 98 | addPayment(); 99 | addPayment(); 100 | Future>> eithers = paymentDAO.fetchAllPayments(); 101 | assertTrue(eithers.get().isRight()); 102 | assertEquals(eithers.get().right().get().size(), 2); 103 | } 104 | 105 | String addPayment() throws JsonProcessingException { 106 | String uuid = UUID.randomUUID().toString(); 107 | PaymentAdded paymentAddedEvent = getPaymentAddedEvent(); 108 | paymentAddedEvent.setId(uuid); 109 | String event = mapper.writeValueAsString(paymentAddedEvent); 110 | CassandraConnector.addPayment(getAddPaymentQuery(paymentAddedEvent.getId(), "12345", event)); 111 | return uuid; 112 | } 113 | 114 | @AfterAll 115 | static void close() { 116 | CassandraConnector.stop(); 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /infrastructure/src/test/java/com/politrons/infrastructure/dao/impl/PaymentDAOUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.politrons.infrastructure.dao.impl; 2 | 3 | import com.politrons.infrastructure.dto.BeneficiaryPartyDTO; 4 | import com.politrons.infrastructure.dto.DebtorPartyDTO; 5 | import com.politrons.infrastructure.dto.PaymentInfoDTO; 6 | import com.politrons.infrastructure.dto.SponsorPartyDTO; 7 | import com.politrons.infrastructure.events.PaymentAdded; 8 | import com.politrons.infrastructure.events.PaymentDeleted; 9 | import com.politrons.infrastructure.events.PaymentUpdated; 10 | 11 | import java.util.UUID; 12 | 13 | public class PaymentDAOUtilsTest { 14 | 15 | 16 | String getAddPaymentQuery(String id, String timeStampMillis, String event) { 17 | return "INSERT INTO " + "paymentsSchema.payment" + 18 | "(id, timestamp, event) " + 19 | "VALUES (" + id + ", '" + 20 | timeStampMillis + "', '" + 21 | event + "');"; 22 | } 23 | 24 | protected PaymentAdded getPaymentAddedEvent() { 25 | return new PaymentAdded(UUID.randomUUID().toString(), "created", 0, getPaymentInfoDTO()); 26 | } 27 | 28 | protected PaymentUpdated getPaymentUpdatedEvent() { 29 | return new PaymentUpdated(UUID.randomUUID().toString(), "changed", 0, getPaymentInfoDTO()); 30 | } 31 | 32 | protected PaymentDeleted getPaymentDeletedEvent() { 33 | return new PaymentDeleted(UUID.randomUUID().toString(), "deleted", 0, getPaymentInfoDTO()); 34 | } 35 | 36 | PaymentInfoDTO getPaymentInfoDTO() { 37 | DebtorPartyDTO debtorParty = getDebtorParty(); 38 | BeneficiaryPartyDTO beneficiaryParty = getBeneficiaryParty(); 39 | SponsorPartyDTO sponsorParty = getSponsorParty(); 40 | return new PaymentInfoDTO("amount", "currency", 41 | "paymentId", 42 | "paymentPurpose", "paymentType", 43 | "processingDate", "reference", "schemePaymentSubType", 44 | "schemePaymentType", debtorParty, sponsorParty, beneficiaryParty); 45 | } 46 | 47 | private SponsorPartyDTO getSponsorParty() { 48 | return new SponsorPartyDTO("accountName", 49 | "bankId", "bankCode"); 50 | } 51 | 52 | private BeneficiaryPartyDTO getBeneficiaryParty() { 53 | return new BeneficiaryPartyDTO("accountName", 54 | "accountNumber", 0, "address", "bankId", "name"); 55 | } 56 | 57 | private DebtorPartyDTO getDebtorParty() { 58 | return new DebtorPartyDTO("accountName", "accountNumber", 59 | 0, "address", "bankId", "name"); 60 | } 61 | 62 | 63 | 64 | 65 | } 66 | -------------------------------------------------------------------------------- /infrastructure/src/test/java/com/politrons/infrastructure/repository/impl/PaymentRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.politrons.infrastructure.repository.impl; 2 | 3 | import com.politrons.domain.PaymentStateAggregateRoot; 4 | import com.politrons.domain.entities.BeneficiaryParty; 5 | import com.politrons.domain.entities.DebtorParty; 6 | import com.politrons.domain.entities.PaymentInfo; 7 | import com.politrons.domain.entities.SponsorParty; 8 | import com.politrons.infrastructure.dao.PaymentDAO; 9 | import com.politrons.infrastructure.events.PaymentAdded; 10 | import com.politrons.infrastructure.events.PaymentDeleted; 11 | import com.politrons.infrastructure.events.PaymentUpdated; 12 | import com.politrons.infrastructure.repository.PaymentRepository; 13 | import io.vavr.concurrent.Future; 14 | import io.vavr.control.Either; 15 | import org.junit.jupiter.api.BeforeEach; 16 | import org.junit.jupiter.api.Test; 17 | import org.junit.jupiter.api.extension.ExtendWith; 18 | import org.mockito.Mock; 19 | import org.mockito.junit.jupiter.MockitoExtension; 20 | 21 | import static io.vavr.API.Right; 22 | import static org.junit.jupiter.api.Assertions.assertFalse; 23 | import static org.junit.jupiter.api.Assertions.assertTrue; 24 | import static org.mockito.ArgumentMatchers.any; 25 | import static org.mockito.Mockito.when; 26 | 27 | @ExtendWith(MockitoExtension.class) 28 | public class PaymentRepositoryTest { 29 | 30 | @Mock 31 | PaymentDAO paymentDAO; 32 | 33 | private PaymentRepository paymentRepository; 34 | 35 | @BeforeEach 36 | void setup() { 37 | paymentRepository = new PaymentRepositoryImpl(paymentDAO); 38 | } 39 | 40 | @Test 41 | void addPaymentEvent() { 42 | PaymentStateAggregateRoot paymentStateAggregateRoot = new PaymentStateAggregateRoot("id", "payment", 0, getPaymentInfo()); 43 | when(paymentDAO.persistPaymentAddedEvent(any(PaymentAdded.class))).thenReturn(Future.of(() -> Right("1981"))); 44 | Future> eithers = paymentRepository.addPayment(paymentStateAggregateRoot); 45 | assertTrue(eithers.get().isRight()); 46 | assertFalse(eithers.get().right().get().isEmpty()); 47 | } 48 | 49 | @Test 50 | void updatePaymentEvent() { 51 | PaymentStateAggregateRoot paymentStateAggregateRoot = new PaymentStateAggregateRoot("id", "payment", 0, getPaymentInfo()); 52 | when(paymentDAO.persistPaymentUpdatedEvent(any(PaymentUpdated.class))).thenReturn(Future.of(() -> Right("1981"))); 53 | Future> eithers = paymentRepository.updatePayment(paymentStateAggregateRoot); 54 | assertTrue(eithers.get().isRight()); 55 | assertFalse(eithers.get().right().get().isEmpty()); 56 | } 57 | 58 | @Test 59 | void deletePaymentEvent() { 60 | PaymentStateAggregateRoot paymentStateAggregateRoot = new PaymentStateAggregateRoot("id", "payment", 0, getPaymentInfo()); 61 | when(paymentDAO.persistPaymentDeletedEvent(any(PaymentDeleted.class))).thenReturn(Future.of(() -> Right("1981"))); 62 | Future> eithers = paymentRepository.deletePayment(paymentStateAggregateRoot); 63 | assertTrue(eithers.get().isRight()); 64 | assertFalse(eithers.get().right().get().isEmpty()); 65 | } 66 | 67 | private PaymentInfo getPaymentInfo() { 68 | DebtorParty debtorParty = getDebtorParty(); 69 | BeneficiaryParty beneficiaryParty = getBeneficiaryParty(); 70 | SponsorParty sponsorParty = getSponsorParty(); 71 | return new PaymentInfo("amount", "currency", 72 | "paymentId", 73 | "paymentPurpose", "paymentType", 74 | "processingDate", "reference", "schemePaymentSubType", 75 | "schemePaymentType", debtorParty, sponsorParty, beneficiaryParty); 76 | } 77 | 78 | private SponsorParty getSponsorParty() { 79 | return new SponsorParty("accountName", 80 | "bankId", "bankCode"); 81 | } 82 | 83 | private BeneficiaryParty getBeneficiaryParty() { 84 | return new BeneficiaryParty("accountName", 85 | "accountNumber", 0, "address", "bankId", "name"); 86 | } 87 | 88 | private DebtorParty getDebtorParty() { 89 | return new DebtorParty("accountName", "accountNumber", 90 | 0, "address", "bankId", "name"); 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /paymentAPI.iml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | org.acme 7 | paymentAPI 8 | pom 9 | 1.0-SNAPSHOT 10 | 11 | application 12 | infrastructure 13 | domain 14 | 15 | 16 | 2.22.0 17 | 0.15.0 18 | UTF-8 19 | 1.8 20 | 1.8 21 | 22 | 23 | 24 | 25 | io.quarkus 26 | quarkus-bom 27 | ${quarkus.version} 28 | pom 29 | import 30 | 31 | 32 | 33 | 34 | 35 | 36 | io.quarkus 37 | quarkus-resteasy 38 | 39 | 40 | io.quarkus 41 | quarkus-vertx 42 | 43 | 44 | io.quarkus 45 | quarkus-smallrye-health 46 | 47 | 48 | io.quarkus 49 | quarkus-undertow-websockets 50 | 51 | 52 | io.quarkus 53 | quarkus-junit5 54 | test 55 | 56 | 57 | io.rest-assured 58 | rest-assured 59 | test 60 | 61 | 62 | io.quarkus 63 | quarkus-smallrye-openapi 64 | 65 | 66 | 67 | 68 | org.projectlombok 69 | lombok 70 | 1.18.6 71 | 72 | 73 | 74 | org.slf4j 75 | slf4j-api 76 | 1.7.26 77 | 78 | 79 | 80 | 81 | io.vavr 82 | vavr 83 | 0.10.0 84 | 85 | 86 | 87 | io.gatling 88 | gatling-app 89 | 2.3.1 90 | 91 | 92 | io.gatling.highcharts 93 | gatling-charts-highcharts 94 | 2.3.1 95 | 96 | 97 | io.netty 98 | * 99 | 100 | 101 | 102 | 103 | org.scala-lang 104 | scala-library 105 | 2.12.4 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | org.scalatest 114 | scalatest_2.12 115 | 3.0.1 116 | test 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | src/test/scala 126 | 127 | 128 | src/test/resources 129 | 130 | **/* 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | io.quarkus 139 | quarkus-maven-plugin 140 | ${quarkus.version} 141 | 142 | 143 | 144 | build 145 | 146 | 147 | 148 | 149 | 150 | maven-surefire-plugin 151 | ${surefire-plugin.version} 152 | 153 | 154 | org.jboss.logmanager.LogManager 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | native 163 | 164 | 165 | native 166 | 167 | 168 | 169 | 170 | 171 | io.quarkus 172 | quarkus-maven-plugin 173 | ${quarkus.version} 174 | 175 | 176 | 177 | native-image 178 | 179 | 180 | true 181 | 182 | 183 | 184 | 185 | 186 | maven-failsafe-plugin 187 | ${surefire-plugin.version} 188 | 189 | 190 | 191 | integration-test 192 | verify 193 | 194 | 195 | 196 | 197 | ${project.build.directory}/${project.build.finalName}-runner 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | net.alchim31.maven 208 | scala-maven-plugin 209 | 210 | 211 | scala-compile-first 212 | process-resources 213 | 214 | add-source 215 | compile 216 | 217 | 218 | 219 | scala-test-compile 220 | process-test-resources 221 | 222 | testCompile 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | org.scalatest 231 | scalatest-maven-plugin 232 | 1.0 233 | 234 | ${project.build.directory}/surefire-reports 235 | . 236 | WDF TestSuite.txt 237 | -Dcrypto.uuid.enabled=false 238 | 239 | 240 | 241 | 242 | test 243 | test 244 | 245 | test 246 | 247 | 248 | 249 | 250 | 251 | 252 | org.apache.maven.plugins 253 | maven-compiler-plugin 254 | 255 | 256 | compile 257 | 258 | compile 259 | 260 | 261 | 262 | 263 | 264 | io.gatling 265 | gatling-maven-plugin 266 | 2.2.2 267 | 268 | true 269 | ${project.basedir}/src/test/resources 270 | ${project.basedir}/src/test/resources/data 271 | ${project.basedir}/target/gatling/results 272 | ${project.basedir}/src/test/resources/bodies 273 | 274 | true 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | --------------------------------------------------------------------------------