├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── day1 └── README.md ├── day2 ├── README.md └── tests.http ├── day3 └── README.md ├── day6 └── README.md ├── docker-compose.yml ├── pom.xml └── src ├── main ├── java │ └── pizzalab │ │ ├── Main.java │ │ ├── config │ │ └── PizzaLabConfig.java │ │ ├── controller │ │ ├── CustomerController.java │ │ └── MenuController.java │ │ ├── domain │ │ ├── Deliverer.java │ │ ├── Order.java │ │ ├── PizzaType.java │ │ └── exchange │ │ │ ├── ExchangeApiResponse.java │ │ │ ├── ExchangeResponse.java │ │ │ └── Rates.java │ │ ├── dto │ │ └── CustomerOutputDTO.java │ │ ├── entity │ │ ├── Address.java │ │ ├── Customer.java │ │ ├── Menu.java │ │ ├── MenuItem.java │ │ ├── Pizza.java │ │ ├── Product.java │ │ └── Soda.java │ │ ├── exception │ │ └── CustomerNotFoundException.java │ │ ├── repository │ │ ├── CustomerRepository.java │ │ └── ProductRepository.java │ │ └── services │ │ ├── CustomerService.java │ │ ├── PantryService.java │ │ └── exchange │ │ ├── ApiExchangeService.java │ │ ├── CachedExchangeService.java │ │ ├── DockerExclusiveService.java │ │ ├── ExchangeConnectorProperties.java │ │ ├── ExchangeService.java │ │ └── LocalExchangeService.java └── resources │ ├── application-docker.properties │ ├── application-prod.properties │ ├── application.properties │ ├── db │ └── migration │ │ └── V1.0.1__Init.sql │ └── product-data.json └── test └── java └── pizzalab ├── integration └── CustomerIT.java └── services ├── CustomerServiceTest.java └── exchange └── ApiExchangeServiceTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | # IntelliJ IDEA 26 | *.iml 27 | .idea 28 | target/* 29 | .DS_Store 30 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jdk-alpine 2 | 3 | ARG JAR_FILE=target/*.jar 4 | COPY ${JAR_FILE} app.jar 5 | 6 | ENTRYPOINT ["java","-jar","/app.jar"] 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | mvn clean package spring-boot:repackage 3 | 4 | image: 5 | docker build -t pizza-lab:latest . 6 | 7 | push: 8 | docker push pizza-lab:latest 9 | 10 | start: 11 | docker run -p 8080:8080 pizza-lab:latest 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pizza-lab 2 | 3 | ## Schedule 4 | ### Day 1 5 | - Welcome + Intro 6 | - Initial Setup 7 | - Java Recap 8 | 9 | ### Day 2 10 | - Intro to Spring Boot and Web Services 11 | - Building REST APIs using Spring 12 | 13 | ### Day 3 14 | - Adding a database to the Spring Boot application 15 | - Hibernate, JPA, HQL/JPQL 16 | 17 | ### Day 4 18 | - Building the Service Layer 19 | - Design patterns 20 | - Http client 21 | 22 | ### Day 5 23 | - Testing your application 24 | - Unit tests, Mockito, integration/e2e tests 25 | 26 | ### Day 6 27 | - Deploying in containers 28 | - Docker, Logs 29 | 30 | ### Day 7 31 | 32 | - Deep dive into Web Services Suggestions 33 | - Best practices/ Spring Security 34 | - Cloud 35 | 36 | ### Day 8 37 | **DEMO DAY** 38 | 39 | -------------------------------------------------------------------------------- /day1/README.md: -------------------------------------------------------------------------------- 1 | ## Day 1 2 | ### Initial setup 3 | 4 | - Download and install [IntelliJ IDEA](https://www.jetbrains.com/idea/download/#section=windows) 5 | - Create a new GitHub repository using the following [tutorial](https://docs.github.com/en/get-started/quickstart/create-a-repo). If you don't have a github account yet, please create one using your 6 | personal email address. 7 | - Generate a new SSH key and add it to GITHUB [tutorial](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account) 8 | - Configure Java 1.8 [tutorial](https://www.jetbrains.com/help/idea/sdk.html#change-module-sdk) 9 | - Create new Maven project in your repository: use https://github.com/javacamp2022/pizza-lab as reference 10 | - Add lombok plugin [tutorial](https://projectlombok.org/setup/intellij) 11 | - [shortcuts intellij](https://cheatography.com/dmop/cheat-sheets/intellij-idea/) 12 | - Short tutorial Streams (map, filter) [more...](https://www.baeldung.com/java-8-streams) 13 | - Short tutorial design pattern (Builder, Singleton)[more...](https://sourcemaking.com/design_patterns/creational_patterns) 14 | 15 | - Create at least 8 domain objects 16 | - Create a dummy repository(storage) that stores in memory a few collections of your objects. The storage should be initialized at startup. 17 | - Create at least 5 operations to retrieve and add new objects to the dummy storage 18 | - Create a main class that will call the 5 operations 19 | 20 | Sugestii proiecte: 21 | 22 | - structura unei organizații (angajați, relații ierarhice, salarii) 23 | - o agendă personală (categorii, întâlniri, sarcini) 24 | - activitatea unei companii de transport (orașe, legături, mașini, rute) 25 | - credite (client, credit, rate) 26 | - cabinet medical (pacienți, medici, rețete) 27 | - admitere (candidat, facultate, examen) 28 | - vanzare de bilete online(client, eveniment, locatie) 29 | - software casa de marcat(metoda de plata, client, produs) 30 | - rezervare loc în sala de spectacol (spectacol, loc, client) 31 | -------------------------------------------------------------------------------- /day2/README.md: -------------------------------------------------------------------------------- 1 | ## Intro Web Services 2 | https://youtu.be/mKjvKPlb1rA 3 | 4 | ## Intro HTTP 5 | https://www.freecodecamp.org/news/http-and-everything-you-need-to-know-about-it/ 6 | 7 | ## REST 8 | 9 | https://restfulapi.net/ 10 | 11 | ``` 12 | # REST samples 13 | GET /customers - Retrieves a list of customers 14 | GET /customers/12 - Retrieves a specific customer 15 | POST /customers - Creates a new customer 16 | PUT /customers/12 - Updates customer #12 17 | PATCH /customers/12 - Partially updates customer #12 18 | DELETE /customers/12 - Deletes customer #12 19 | ``` 20 | 21 | ## Intellij Debug 22 | https://www.jetbrains.com/help/idea/debugging-your-first-java-application.html#examining-code 23 | 24 | ## Spring 25 | * [Using the "default" Package](https://docs.spring.io/spring-boot/docs/current/reference/html/using.html#using.structuring-your-code) 26 | 27 | ### Spring application.properties 28 | * https://www.javatpoint.com/spring-boot-properties 29 | 30 | ### Spring Exception handling 31 | * https://reflectoring.io/spring-boot-exception-handling/ 32 | 33 | * https://www.toptal.com/java/spring-boot-rest-api-error-handling 34 | 35 | ## Model Mapper 36 | https://www.baeldung.com/java-modelmapper 37 | 38 | http://modelmapper.org/getting-started/ 39 | 40 | ## Swagger UI 41 | ``` 42 | 43 | 44 | org.springdoc 45 | springdoc-openapi-ui 46 | 1.6.8 47 | 48 | ``` 49 | ## Chrome Extensions 50 | Live Reload 51 | [LiveReload](https://chrome.google.com/webstore/detail/livereload/jnihajbhpnppcggbcgedagnkighmdlei?hl=en) 52 | 53 | Json Formatter 54 | [Json Formatter](https://chrome.google.com/webstore/detail/json-formatter/bcjindcccaagfpapjjmafapmmgkkhgoa/related?hl=en) 55 | -------------------------------------------------------------------------------- /day2/tests.http: -------------------------------------------------------------------------------- 1 | POST http://127.0.0.1:8080/customers 2 | Content-Type: application/json 3 | 4 | { 5 | "name": "Alice", 6 | "phone": "071234567", 7 | "hashedPassword": "PRIVATE", 8 | "addresses": [ 9 | { 10 | "street": "string" 11 | } 12 | ] 13 | } 14 | 15 | ### GET customers 16 | GET http://127.0.0.1:8080/customers 17 | 18 | ### GET customers by name 19 | GET http://127.0.0.1:8080/customers/John 20 | 21 | ### GET customers by not existing name 22 | GET http://127.0.0.1:8080/customers/Beta 23 | -------------------------------------------------------------------------------- /day3/README.md: -------------------------------------------------------------------------------- 1 | ## Day 3 2 | 3 | ### Key takeaways 4 | 1. Decide on a database to use (RDBMS, NoSQL? In Memory? Cloud? etc.) 5 | 2. Decide on a testing infrastructure ([Docker](#docker)? In Memory? Locally installed) 6 | 3. Hibernate Annotations 7 | 1. Add [@Entity](https://docs.oracle.com/javaee/7/api/javax/persistence/Entity.html) annotations to the relevant domain objects. 8 | 2. Add [relationship annotations](https://www.objectdb.com/api/java/jpa/annotations/relationship) 9 | 3. Further notes below in [Hibernate section](#hibernate) 10 | 4. JPA Data 11 | 1. Create repositories for classes that will be processed https://www.baeldung.com/spring-data-repositories 12 | 2. Define any custom queries that you might need https://www.baeldung.com/the-persistence-layer-with-spring-data-jpa 13 | 5. Once you are satisfied, add versioning via [flyway](#flyway) or liquibase 14 | 1. Decide on a solution based on: 15 | * flexibility needed (+1 for liquibase) 16 | * ease of use (+1 for flyway) 17 | * other criteria 18 | 2. Dump existing DB 19 | 3. Migrate it to one of the formats 20 | 4. Add dependencies to the project POM 21 | 5. Change ```spring.jpa.hibernate.ddl-auto=validate``` 22 | 6. Further notes below in [Flyway section](#flyway) 23 | 24 | ### Docker 25 | #### Docker Installation 26 | https://docs.docker.com/desktop/windows/install/ 27 | #### Docker compose tutorial 28 | https://docs.docker.com/compose/gettingstarted/ 29 | 30 | To start the database from the docker file: 31 | ```docker compose down; docker compose up``` 32 | 33 | ### How to choose the right database for your service? 34 | https://medium.com/wix-engineering/how-to-choose-the-right-database-for-your-service-97b1670c5632 35 | 36 | ## Hibernate 37 | ### Most important annotations 38 | https://thorben-janssen.com/key-jpa-hibernate-annotations/ 39 | 40 | ### Ways of mapping inheritance 41 | The specific solution used is the one on paragraph 3, single table 42 | https://www.baeldung.com/hibernate-inheritance 43 | 44 | ### Many to Many Relationships 45 | https://www.baeldung.com/jpa-many-to-many 46 | 47 | ## Flyway 48 | ### Dumping the DB 49 | Run the following command: 50 | ``` 51 | mkdir -P /path/to/project/src/resources/db/migration 52 | docker exec -i pizzalab-postgres /bin/bash -c "PGPASSWORD=example pg_dump --username user pizzalab" > /path/to/project/src/resources/db/migration/V1.0.1__Init.sql 53 | ``` 54 | ### Add POM dependencies 55 | ``` 56 | 57 | org.flywaydb 58 | flyway-core 59 | 8.5.10 60 | 61 | ``` 62 | ### Check GIT 63 | Check commit `25fd49f22` for further info. 64 | You may use github, command line or any other GUI. 65 | #### Command line 66 | `git diff 25fd49f22~ 25fd49f22` 67 | ### Github 68 | Check it [here](https://github.com/javacamp2022/pizza-lab/commit/25fd49f22bde5eff31d8f120a6141513399ea1f7) 69 | 70 | -------------------------------------------------------------------------------- /day6/README.md: -------------------------------------------------------------------------------- 1 | ## Day 6 2 | 3 | ### Building the code 4 | We are using Maven as our build tool. Our goal is to run the tests and other plugins whenever building the project; more than this we want the application jar that is generated to be fully executable (have included the tomcat spring-boot runnables). 5 | 6 | There are a couple of options to configure maven to build the uber-jar (shaded, fat) and include all of the dependencies. 7 | https://www.baeldung.com/deployable-fat-jar-spring-boot 8 | https://www.baeldung.com/spring-boot-run-maven-vs-executable-jar 9 | 10 | The option we chose was to add a plugin in the pom.xml that will configure a spring-boot plugin that is used via the repackage goal to generate the jar. 11 | ```xml 12 | 13 | 14 | org.springframework.boot 15 | spring-boot-maven-plugin 16 | 17 | 18 | 19 | pizzalab.Main 20 | 21 | 22 | 23 | 24 | ``` 25 | 26 | 27 | ```bash 28 | $ mvn clean package spring-boot:repackage 29 | ``` 30 | 31 | If we now have the database running - we can simply start the jar; 32 | ```bash 33 | $ java -jar target/pizza-lab-0.0.1-SNAPSHOT.jar 34 | ``` 35 | 36 | ### Building the image 37 | Given our executable jar, we want to containerize the app and prepare a Docker Image that can be executed and have all the needed Java and OS prerequisites: 38 | 39 | The Dockerfile is build based on a openjdk-8 image; and our customization is only copying the previously build fat jar. 40 | ```dockerfile 41 | FROM openjdk:8-jdk-alpine 42 | 43 | ARG JAR_FILE=target/*.jar 44 | COPY ${JAR_FILE} app.jar 45 | 46 | ENTRYPOINT ["java","-jar","/app.jar"] 47 | ``` 48 | 49 | https://spring.io/guides/gs/spring-boot-docker/ 50 | 51 | ```bash 52 | $ docker build -t pizza-lab:latest . 53 | ``` 54 | 55 | ### Running docker 56 | We have a few options to run the Docker Image. One is to run the image directly; but we will need to override and config the app to use an H2 database. 57 | Another option is to repurpose the docker-compose.yaml file to start the database and web service together. 58 | 59 | https://stackoverflow.com/questions/64135291/how-to-connect-java-app-in-docker-container-with-database-in-another-container 60 | 61 | ```yaml 62 | version: '3.1' 63 | 64 | services: 65 | 66 | pizzalab: 67 | image: pizza-lab:v2 68 | environment: 69 | SPRING_PROFILES_ACTIVE: docker 70 | depends_on: 71 | - db 72 | ports: 73 | - "8080:8080" 74 | 75 | db: 76 | image: postgres 77 | container_name: pizzalab-postgres 78 | restart: always 79 | ports: 80 | - "8432:5432" 81 | expose: 82 | - 5432 83 | environment: 84 | POSTGRES_USER: user 85 | POSTGRES_PASSWORD: example 86 | POSTGRES_DB: pizzalab 87 | healthcheck: 88 | test: [ "CMD-SHELL", "pg_isready -d pizzalab -U user" ] 89 | interval: 30s 90 | timeout: 30s 91 | retries: 3 92 | 93 | adminer: 94 | image: adminer 95 | restart: always 96 | ports: 97 | - 9080:8080 98 | ``` 99 | 100 | Simply triggering the docker-compose commant we will startup the database, externally exposed on port 8432, along the application that will use spring-boot profile "docker". 101 | ```bash 102 | docker-compose up 103 | ``` 104 | 105 | The spring-boot profile is linked with a tweaked properties file that knows to connect to the dockerized database as well; 106 | ```properties 107 | spring.datasource.url=jdbc:postgresql://db:5432/pizzalab 108 | ``` 109 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Use postgres/example user/password credentials 2 | version: '3.1' 3 | 4 | services: 5 | 6 | pizzalab: 7 | image: pizza-lab:v2 8 | environment: 9 | SPRING_PROFILES_ACTIVE: docker 10 | depends_on: 11 | - db 12 | ports: 13 | - "8080:8080" 14 | 15 | db: 16 | image: postgres 17 | container_name: pizzalab-postgres 18 | restart: always 19 | ports: 20 | - "8432:5432" 21 | expose: 22 | - 5432 23 | environment: 24 | POSTGRES_USER: user 25 | POSTGRES_PASSWORD: example 26 | POSTGRES_DB: pizzalab 27 | healthcheck: 28 | test: [ "CMD-SHELL", "pg_isready -d pizzalab -U user" ] 29 | interval: 30s 30 | timeout: 30s 31 | retries: 3 32 | 33 | adminer: 34 | image: adminer 35 | restart: always 36 | ports: 37 | - 9080:8080 38 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | javabootcamp2022ro 6 | pizza-lab 7 | 0.0.1-SNAPSHOT 8 | pizza-lab 9 | 10 | jar 11 | 12 | 13 | 14 | 1.8 15 | 1.8 16 | 1.18.24 17 | 2.6.7 18 | 42.3.2 19 | 20 | 21 | 22 | 23 | org.projectlombok 24 | lombok 25 | ${lombok.version} 26 | provided 27 | 28 | 29 | 30 | com.github.ben-manes.caffeine 31 | caffeine 32 | 2.8.0 33 | 34 | 35 | 36 | 37 | com.konghq 38 | unirest-java 39 | 3.13.8 40 | 41 | 42 | 43 | 44 | com.github.peichhorn 45 | lombok-pg 46 | 0.11.0 47 | 48 | 49 | com.fasterxml.jackson.core 50 | jackson-databind 51 | 2.13.2 52 | 53 | 54 | 55 | org.springframework.boot 56 | spring-boot-starter-web 57 | ${spring.boot.version} 58 | 59 | 60 | org.springframework.boot 61 | spring-boot-starter-data-jpa 62 | ${spring.boot.version} 63 | 64 | 65 | org.springframework.data 66 | spring-data-jpa 67 | 2.6.4 68 | 69 | 70 | 71 | org.flywaydb 72 | flyway-core 73 | 8.5.10 74 | 75 | 76 | 77 | 78 | org.modelmapper 79 | modelmapper 80 | 3.1.0 81 | 82 | 83 | javax.persistence 84 | javax.persistence-api 85 | 2.2 86 | 87 | 88 | org.hibernate 89 | hibernate-core 90 | 5.6.8.Final 91 | 92 | 93 | org.postgresql 94 | postgresql 95 | ${postgresql.version} 96 | 97 | 98 | 99 | 100 | junit 101 | junit 102 | 4.13.2 103 | test 104 | 105 | 106 | org.mockito 107 | mockito-core 108 | 4.5.1 109 | test 110 | 111 | 112 | com.github.tomakehurst 113 | wiremock-jre8 114 | 2.32.0 115 | test 116 | 117 | 118 | org.springframework.boot 119 | spring-boot-starter-test 120 | 2.5.12 121 | test 122 | 123 | 124 | 125 | 126 | 127 | org.apache.maven.plugins 128 | maven-dependency-plugin 129 | 130 | 131 | 132 | copy-dependencies 133 | 134 | 135 | 136 | 137 | lombok 138 | 139 | 140 | 141 | org.springframework.boot 142 | spring-boot-maven-plugin 143 | 144 | 145 | 146 | pizzalab.Main 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /src/main/java/pizzalab/Main.java: -------------------------------------------------------------------------------- 1 | package pizzalab; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Main { 8 | public static void main(String[] args) { 9 | SpringApplication.run(Main.class, args); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/pizzalab/config/PizzaLabConfig.java: -------------------------------------------------------------------------------- 1 | package pizzalab.config; 2 | 3 | import com.github.benmanes.caffeine.cache.Cache; 4 | import com.github.benmanes.caffeine.cache.Caffeine; 5 | import org.modelmapper.ModelMapper; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.core.io.ClassPathResource; 9 | import org.springframework.core.io.Resource; 10 | import org.springframework.data.repository.init.Jackson2RepositoryPopulatorFactoryBean; 11 | 12 | import java.util.concurrent.TimeUnit; 13 | 14 | @Configuration 15 | public class PizzaLabConfig { 16 | 17 | @Bean 18 | public ModelMapper modelMapper() { 19 | return new ModelMapper(); 20 | } 21 | 22 | @Bean 23 | public Jackson2RepositoryPopulatorFactoryBean getRespositoryPopulator() { 24 | Jackson2RepositoryPopulatorFactoryBean factory = new Jackson2RepositoryPopulatorFactoryBean(); 25 | factory.setResources(new Resource[]{new ClassPathResource("product-data.json")}); 26 | return factory; 27 | } 28 | 29 | @Bean 30 | public Cache provideCache() { 31 | return Caffeine.newBuilder() 32 | .expireAfterWrite(1, TimeUnit.MINUTES) 33 | .maximumSize(100) 34 | .build(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/pizzalab/controller/CustomerController.java: -------------------------------------------------------------------------------- 1 | package pizzalab.controller; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | 5 | import org.modelmapper.ModelMapper; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.*; 8 | 9 | import pizzalab.services.CustomerService; 10 | import pizzalab.entity.Customer; 11 | import pizzalab.dto.CustomerOutputDTO; 12 | 13 | import java.util.List; 14 | import java.util.stream.Collectors; 15 | 16 | @Slf4j 17 | @RestController 18 | public class CustomerController { 19 | 20 | private final CustomerService customerService; 21 | private final ModelMapper modelMapper; 22 | 23 | public CustomerController(CustomerService deliveryService, ModelMapper modelMapper) { 24 | this.customerService = deliveryService; 25 | this.modelMapper = modelMapper; 26 | } 27 | 28 | @GetMapping("customers") 29 | public ResponseEntity getCustomers() { 30 | List customersWithPrivateStuff = customerService.findAll(); 31 | List customersDto = customersWithPrivateStuff.stream() 32 | .map(customer -> modelMapper.map(customer, CustomerOutputDTO.class)) 33 | .collect(Collectors.toList()); 34 | return ResponseEntity.ok(customersDto); 35 | } 36 | 37 | @PostMapping("customers") 38 | public ResponseEntity createCustomer(@RequestBody Customer customer) { 39 | // TODO validation 40 | Customer added = customerService.addCustomer(customer); 41 | return added != null ? ResponseEntity.ok(added) : ResponseEntity.badRequest().body("Customer already exists!"); 42 | } 43 | 44 | @GetMapping("customers/{customerId}") 45 | public ResponseEntity getCustomer(@PathVariable Long customerId) { 46 | return ResponseEntity.ok(modelMapper.map(customerService.findById(customerId), CustomerOutputDTO.class)); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/pizzalab/controller/MenuController.java: -------------------------------------------------------------------------------- 1 | package pizzalab.controller; 2 | 3 | 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.RestController; 7 | 8 | import pizzalab.services.PantryService; 9 | import pizzalab.entity.Menu; 10 | 11 | @RestController 12 | class MenuController { 13 | 14 | @Autowired 15 | PantryService pantryService; 16 | 17 | @GetMapping("/menu") 18 | public Menu getMenu() { 19 | return pantryService.listMenu(); 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /src/main/java/pizzalab/domain/Deliverer.java: -------------------------------------------------------------------------------- 1 | package pizzalab.domain; 2 | 3 | public class Deliverer { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/pizzalab/domain/Order.java: -------------------------------------------------------------------------------- 1 | package pizzalab.domain; 2 | 3 | import lombok.Data; 4 | import pizzalab.entity.Address; 5 | import pizzalab.entity.Customer; 6 | import pizzalab.entity.Product; 7 | 8 | import java.util.Date; 9 | import java.util.List; 10 | 11 | @Data 12 | public class Order { 13 | 14 | private Customer customer; 15 | private Address address; 16 | private Date createdAt; 17 | private Date expectedTime; 18 | private Date actualDeliveryTime; 19 | 20 | private Long total; 21 | private List items; 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/pizzalab/domain/PizzaType.java: -------------------------------------------------------------------------------- 1 | package pizzalab.domain; 2 | 3 | public enum PizzaType { 4 | QUATTRO_STAGIONI, 5 | CAPRICCIOSA, 6 | QUATTRO_FORMAGGI 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/pizzalab/domain/exchange/ExchangeApiResponse.java: -------------------------------------------------------------------------------- 1 | package pizzalab.domain.exchange; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class ExchangeApiResponse { 7 | private Rates rates; 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/pizzalab/domain/exchange/ExchangeResponse.java: -------------------------------------------------------------------------------- 1 | package pizzalab.domain.exchange; 2 | 3 | import lombok.Data; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | @Data 7 | @RequiredArgsConstructor 8 | public class ExchangeResponse { 9 | private final Double amount; 10 | private final Double curs; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/pizzalab/domain/exchange/Rates.java: -------------------------------------------------------------------------------- 1 | package pizzalab.domain.exchange; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class Rates { 7 | private Double EUR; 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/pizzalab/dto/CustomerOutputDTO.java: -------------------------------------------------------------------------------- 1 | package pizzalab.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | import lombok.Data; 6 | 7 | @Data 8 | public class CustomerOutputDTO { 9 | @JsonProperty 10 | private String name; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/pizzalab/entity/Address.java: -------------------------------------------------------------------------------- 1 | package pizzalab.entity; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.util.List; 9 | 10 | import javax.persistence.Entity; 11 | import javax.persistence.GeneratedValue; 12 | import javax.persistence.GenerationType; 13 | import javax.persistence.Id; 14 | import javax.persistence.ManyToMany; 15 | import javax.persistence.SequenceGenerator; 16 | 17 | @Data 18 | @Builder 19 | @NoArgsConstructor 20 | @AllArgsConstructor 21 | @Entity 22 | public class Address { 23 | 24 | @Id 25 | @GeneratedValue(strategy = GenerationType.AUTO, generator = "address_key_sequence_generator") 26 | @SequenceGenerator(name = "address_key_sequence_generator", sequenceName = "address_sequence", allocationSize = 1) 27 | private Long id; 28 | 29 | @ManyToMany(mappedBy = "addresses") 30 | private List customer; 31 | 32 | String street; 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/pizzalab/entity/Customer.java: -------------------------------------------------------------------------------- 1 | package pizzalab.entity; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.util.List; 9 | 10 | import javax.persistence.CascadeType; 11 | import javax.persistence.Entity; 12 | import javax.persistence.GeneratedValue; 13 | import javax.persistence.GenerationType; 14 | import javax.persistence.Id; 15 | import javax.persistence.JoinColumn; 16 | import javax.persistence.JoinTable; 17 | import javax.persistence.ManyToMany; 18 | import javax.persistence.SequenceGenerator; 19 | 20 | @Builder 21 | @Data 22 | @NoArgsConstructor 23 | @AllArgsConstructor 24 | @Entity 25 | public class Customer { 26 | 27 | @Id 28 | @GeneratedValue(strategy = GenerationType.AUTO, generator = "customer_key_sequence_generator") 29 | @SequenceGenerator(name = "customer_key_sequence_generator", sequenceName = "customer_sequence", allocationSize = 1) 30 | private Long id; 31 | 32 | private String name; 33 | private String phone; 34 | private String hashedPassword; 35 | private String creditCard; 36 | 37 | @ManyToMany(cascade = CascadeType.ALL) 38 | @JoinTable( 39 | name = "customer_to_address", 40 | joinColumns = @JoinColumn(name = "customer_id"), 41 | inverseJoinColumns = @JoinColumn(name = "address_id")) 42 | private List
addresses; 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/pizzalab/entity/Menu.java: -------------------------------------------------------------------------------- 1 | package pizzalab.entity; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.util.Date; 9 | import java.util.List; 10 | 11 | import javax.persistence.Entity; 12 | import javax.persistence.Id; 13 | import javax.persistence.JoinColumn; 14 | import javax.persistence.JoinTable; 15 | import javax.persistence.ManyToMany; 16 | 17 | @Builder 18 | @Data 19 | @Entity 20 | @NoArgsConstructor 21 | @AllArgsConstructor 22 | public class Menu { 23 | 24 | @Id 25 | private Long id; 26 | 27 | @ManyToMany 28 | @JoinTable( 29 | name = "menu_to_menu_items", 30 | joinColumns = @JoinColumn(name = "menu_item_id"), 31 | inverseJoinColumns = @JoinColumn(name = "menu_id")) 32 | private List items; 33 | private Date createdAt; 34 | private Date validUntil; 35 | private Date lastUpdatedAt; 36 | 37 | 38 | } 39 | 40 | -------------------------------------------------------------------------------- /src/main/java/pizzalab/entity/MenuItem.java: -------------------------------------------------------------------------------- 1 | package pizzalab.entity; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | import lombok.ToString; 8 | 9 | import java.util.List; 10 | 11 | import javax.persistence.Entity; 12 | import javax.persistence.Id; 13 | import javax.persistence.JoinColumn; 14 | import javax.persistence.JoinTable; 15 | import javax.persistence.ManyToMany; 16 | 17 | @Builder 18 | @ToString 19 | @Data 20 | @NoArgsConstructor 21 | @Entity 22 | @AllArgsConstructor 23 | public class MenuItem implements Comparable { 24 | 25 | @Id 26 | private Long id; 27 | 28 | private String description; 29 | private Double price; 30 | 31 | @ManyToMany 32 | @JoinTable( 33 | name = "menu_to_menu_items", 34 | joinColumns = @JoinColumn(name = "menu_id"), 35 | inverseJoinColumns = @JoinColumn(name = "menu_item_id")) 36 | private List menus; 37 | 38 | @Override 39 | public int compareTo(Object o) { 40 | return description.compareTo(((MenuItem) o).description); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/pizzalab/entity/Pizza.java: -------------------------------------------------------------------------------- 1 | package pizzalab.entity; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | import pizzalab.domain.PizzaType; 8 | import pizzalab.entity.Product; 9 | 10 | import javax.persistence.DiscriminatorValue; 11 | import javax.persistence.Entity; 12 | 13 | @Getter 14 | @Setter 15 | @Entity 16 | @NoArgsConstructor 17 | @DiscriminatorValue("1") 18 | public class Pizza extends Product { 19 | 20 | private PizzaType type; 21 | 22 | @Builder 23 | public Pizza(int quantity, double price, PizzaType type) { 24 | this.quantity = quantity; 25 | this.price = price; 26 | this.type = type; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/pizzalab/entity/Product.java: -------------------------------------------------------------------------------- 1 | package pizzalab.entity; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | 6 | import javax.persistence.DiscriminatorColumn; 7 | import javax.persistence.DiscriminatorType; 8 | import javax.persistence.Entity; 9 | import javax.persistence.GeneratedValue; 10 | import javax.persistence.GenerationType; 11 | import javax.persistence.Id; 12 | import javax.persistence.Inheritance; 13 | import javax.persistence.InheritanceType; 14 | import javax.persistence.SequenceGenerator; 15 | 16 | @Entity 17 | @Inheritance(strategy = InheritanceType.SINGLE_TABLE) 18 | @DiscriminatorColumn(name="product_type", discriminatorType = DiscriminatorType.INTEGER) 19 | @NoArgsConstructor 20 | @Getter 21 | public abstract class Product { 22 | 23 | @Id 24 | @GeneratedValue(strategy = GenerationType.AUTO, generator = "product_key_sequence_generator") 25 | @SequenceGenerator(name = "product_key_sequence_generator", sequenceName = "product_sequence", allocationSize = 1) 26 | private Long id; 27 | 28 | protected int quantity = 0; 29 | protected double price = -1; 30 | protected String description; 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/pizzalab/entity/Soda.java: -------------------------------------------------------------------------------- 1 | package pizzalab.entity; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | import pizzalab.entity.Product; 8 | 9 | import javax.persistence.DiscriminatorValue; 10 | import javax.persistence.Entity; 11 | 12 | @Getter 13 | @Setter 14 | @Entity 15 | @DiscriminatorValue("2") 16 | @NoArgsConstructor 17 | public class Soda extends Product { 18 | 19 | private String name; 20 | 21 | @Builder 22 | public Soda(int quantity, double price, String name) { 23 | this.quantity = quantity; 24 | this.price = price; 25 | this.name = name; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/pizzalab/exception/CustomerNotFoundException.java: -------------------------------------------------------------------------------- 1 | package pizzalab.exception; 2 | 3 | public class CustomerNotFoundException extends RuntimeException { 4 | 5 | public CustomerNotFoundException(String message) { 6 | super(message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/pizzalab/repository/CustomerRepository.java: -------------------------------------------------------------------------------- 1 | package pizzalab.repository; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | import pizzalab.entity.Customer; 6 | 7 | public interface CustomerRepository extends JpaRepository { 8 | 9 | Customer findFirstByName(String name); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/pizzalab/repository/ProductRepository.java: -------------------------------------------------------------------------------- 1 | package pizzalab.repository; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.data.jpa.repository.Modifying; 5 | import org.springframework.data.jpa.repository.Query; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import pizzalab.entity.Product; 9 | 10 | import java.util.List; 11 | 12 | @Repository 13 | public interface ProductRepository extends JpaRepository { 14 | 15 | List findAll(); 16 | 17 | @Query(value = "SELECT p FROM Product p WHERE p.quantity >0 ORDER BY type") 18 | List findMenuItems(); 19 | 20 | @Modifying 21 | @Query(value = "INSERT INTO Product (product_type, id, quantity, price, description) VALUES (?1, ?2, ?3, ?4, ?5)", nativeQuery = true) 22 | void insertProduct(int product_type, int id, int quantity, double price, String description); 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/pizzalab/services/CustomerService.java: -------------------------------------------------------------------------------- 1 | package pizzalab.services; 2 | 3 | import org.springframework.stereotype.Service; 4 | 5 | import org.springframework.transaction.annotation.Transactional; 6 | import pizzalab.entity.Customer; 7 | import pizzalab.repository.CustomerRepository; 8 | import pizzalab.repository.ProductRepository; 9 | import pizzalab.exception.CustomerNotFoundException; 10 | 11 | import java.sql.SQLException; 12 | import java.util.List; 13 | import java.util.Optional; 14 | 15 | @Service 16 | public class CustomerService { 17 | 18 | private final CustomerRepository customerRepository; 19 | private final ProductRepository productRepository; 20 | 21 | public CustomerService(CustomerRepository customerRepository, ProductRepository productRepository) { 22 | this.customerRepository = customerRepository; 23 | this.productRepository = productRepository; 24 | } 25 | 26 | @Transactional(rollbackFor = SQLException.class) 27 | public Customer addCustomer(Customer customer) { 28 | if (customerRepository.findFirstByName(customer.getName()) != null) { 29 | return null; 30 | } 31 | 32 | Customer saved = customerRepository.save(customer); 33 | return saved; 34 | // productRepository.insertProduct(1, 10, 20, 39, "Product 1"); 35 | } 36 | 37 | public List findAll() { 38 | return customerRepository.findAll(); 39 | } 40 | 41 | public Customer findById(Long customerId) { 42 | 43 | Optional customerOptional = customerRepository.findById(customerId); 44 | if (!customerOptional.isPresent()) { 45 | throw new CustomerNotFoundException("Customer not found"); 46 | } 47 | return customerOptional.get(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/pizzalab/services/PantryService.java: -------------------------------------------------------------------------------- 1 | package pizzalab.services; 2 | 3 | import org.springframework.beans.factory.annotation.Qualifier; 4 | import org.springframework.stereotype.Service; 5 | import pizzalab.entity.Menu; 6 | import pizzalab.entity.MenuItem; 7 | import pizzalab.repository.ProductRepository; 8 | import pizzalab.services.exchange.ExchangeService; 9 | 10 | import java.time.Instant; 11 | import java.time.temporal.ChronoUnit; 12 | import java.util.Date; 13 | import java.util.stream.Collectors; 14 | 15 | @Service 16 | public class PantryService { 17 | 18 | private final ProductRepository repository; 19 | private final ExchangeService exchangeService; 20 | 21 | public PantryService(ProductRepository repository, @Qualifier("cache") ExchangeService exchangeService) { 22 | this.repository = repository; 23 | this.exchangeService = exchangeService; 24 | } 25 | 26 | public Menu listMenu() { 27 | return Menu.builder() 28 | .items( 29 | repository.findAll().stream() 30 | .filter(p -> p.getQuantity() > 0) 31 | .map(p -> MenuItem.builder() 32 | .price(exchangeService.exchangeRonToEuro(p.getPrice()).getAmount()) 33 | .description(p.getDescription()) 34 | .id(p.getId()) 35 | .build()) 36 | // .sorted() 37 | .collect(Collectors.toList())) 38 | .createdAt(new Date()) 39 | .lastUpdatedAt(new Date()) 40 | .validUntil(Date.from(Instant.now().plus(2, ChronoUnit.DAYS))) 41 | .build(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/pizzalab/services/exchange/ApiExchangeService.java: -------------------------------------------------------------------------------- 1 | package pizzalab.services.exchange; 2 | 3 | import lombok.AllArgsConstructor; 4 | 5 | import kong.unirest.HttpResponse; 6 | import kong.unirest.Unirest; 7 | import org.springframework.beans.factory.annotation.Qualifier; 8 | import org.springframework.stereotype.Service; 9 | import pizzalab.domain.exchange.ExchangeApiResponse; 10 | import pizzalab.domain.exchange.ExchangeResponse; 11 | 12 | @Service 13 | @Qualifier("api") 14 | @AllArgsConstructor 15 | public class ApiExchangeService implements ExchangeService { 16 | 17 | private ExchangeConnectorProperties properties; 18 | 19 | public ExchangeResponse exchangeRonToEuro(Double amount) { 20 | HttpResponse response = Unirest.get(properties.getHostUrl() + "/api/latest") 21 | .queryString("access_key", "f7dbe1842278-43779b") 22 | .asObject(ExchangeApiResponse.class); 23 | 24 | if (response.isSuccess()) { 25 | Double curs = response.getBody().getRates().getEUR(); 26 | return new ExchangeResponse(amount / curs, curs); 27 | } 28 | 29 | return new ExchangeResponse(amount, 4.9); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/pizzalab/services/exchange/CachedExchangeService.java: -------------------------------------------------------------------------------- 1 | package pizzalab.services.exchange; 2 | 3 | import com.github.benmanes.caffeine.cache.Cache; 4 | import org.springframework.beans.factory.annotation.Qualifier; 5 | import org.springframework.stereotype.Service; 6 | import pizzalab.domain.exchange.ExchangeResponse; 7 | 8 | @Service 9 | @Qualifier("cache") 10 | public class CachedExchangeService implements ExchangeService { 11 | 12 | private final ExchangeService exchangeService; 13 | private final Cache cache; 14 | 15 | public CachedExchangeService(@Qualifier("api") ExchangeService exchangeService, 16 | Cache cache) { 17 | this.exchangeService = exchangeService; 18 | this.cache = cache; 19 | } 20 | 21 | @Override 22 | public ExchangeResponse exchangeRonToEuro(Double amount) { 23 | Double curs = cache.get("CURS", (key) -> exchangeService.exchangeRonToEuro(amount).getCurs()); 24 | return new ExchangeResponse(amount / curs, curs); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/pizzalab/services/exchange/DockerExclusiveService.java: -------------------------------------------------------------------------------- 1 | package pizzalab.services.exchange; 2 | 3 | import org.springframework.context.annotation.Profile; 4 | import org.springframework.stereotype.Service; 5 | 6 | import javax.annotation.PostConstruct; 7 | 8 | @Service 9 | @Profile("docker") 10 | public class DockerExclusiveService { 11 | 12 | @PostConstruct 13 | public void postContruct() { 14 | System.out.println("DockerExclusiveService started - only visible with running with docker profile"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/pizzalab/services/exchange/ExchangeConnectorProperties.java: -------------------------------------------------------------------------------- 1 | package pizzalab.services.exchange; 2 | 3 | import lombok.Data; 4 | 5 | import org.springframework.stereotype.Component; 6 | 7 | @Data 8 | @Component 9 | public class ExchangeConnectorProperties { 10 | 11 | private String hostUrl = "https://romanian-exchange-rate-bnr-api.herokuapp.com"; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/pizzalab/services/exchange/ExchangeService.java: -------------------------------------------------------------------------------- 1 | package pizzalab.services.exchange; 2 | 3 | import pizzalab.domain.exchange.ExchangeResponse; 4 | 5 | public interface ExchangeService { 6 | ExchangeResponse exchangeRonToEuro(Double amount); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/pizzalab/services/exchange/LocalExchangeService.java: -------------------------------------------------------------------------------- 1 | package pizzalab.services.exchange; 2 | 3 | import org.springframework.beans.factory.annotation.Qualifier; 4 | import org.springframework.stereotype.Service; 5 | import pizzalab.domain.exchange.ExchangeResponse; 6 | 7 | @Service 8 | @Qualifier("local") 9 | public class LocalExchangeService implements ExchangeService { 10 | 11 | public ExchangeResponse exchangeRonToEuro(Double amount) { 12 | // Take data from local file and return it 13 | return new ExchangeResponse(10d, 10d); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/resources/application-docker.properties: -------------------------------------------------------------------------------- 1 | 2 | spring.datasource.url=jdbc:postgresql://db:5432/pizzalab -------------------------------------------------------------------------------- /src/main/resources/application-prod.properties: -------------------------------------------------------------------------------- 1 | 2 | spring.datasource.url=jdbc:postgresql://localhost:8432/pizzalab 3 | spring.datasource.username=user 4 | spring.datasource.password=example 5 | spring.jpa.hibernate.ddl-auto=create-drop -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | logging.level.pizzalab=DEBUG 2 | 3 | spring.datasource.url=jdbc:postgresql://localhost:8432/pizzalab 4 | spring.datasource.username=user 5 | spring.datasource.password=example 6 | spring.jpa.hibernate.ddl-auto=create-drop 7 | spring.jpa.database=POSTGRESQL 8 | spring.jpa.show-sql=true 9 | spring.jpa.properties.hibernate.format_sql=true 10 | spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect 11 | 12 | spring.jpa.generate-ddl=true 13 | 14 | spring.sql.init.mode=always 15 | spring.datasource.initialize=true 16 | spring.sql.init.continue-on-error=true 17 | spring.flyway.enabled=true 18 | flyway.baselineOnMigrate = true 19 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V1.0.1__Init.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- PostgreSQL database dump 3 | -- 4 | 5 | -- Dumped from database version 14.2 (Debian 14.2-1.pgdg110+1) 6 | -- Dumped by pg_dump version 14.2 (Debian 14.2-1.pgdg110+1) 7 | 8 | SET statement_timeout = 0; 9 | SET lock_timeout = 0; 10 | SET idle_in_transaction_session_timeout = 0; 11 | SET client_encoding = 'UTF8'; 12 | SET standard_conforming_strings = on; 13 | SELECT pg_catalog.set_config('search_path', '', false); 14 | SET check_function_bodies = false; 15 | SET xmloption = content; 16 | SET client_min_messages = warning; 17 | SET row_security = off; 18 | 19 | SET default_tablespace = ''; 20 | 21 | SET default_table_access_method = heap; 22 | 23 | -- 24 | -- Name: address; Type: TABLE; Schema: public; Owner: user 25 | -- 26 | 27 | CREATE TABLE public.address ( 28 | id bigint NOT NULL, 29 | street character varying(255) 30 | ); 31 | 32 | 33 | ALTER TABLE public.address OWNER TO "user"; 34 | 35 | -- 36 | -- Name: address_sequence; Type: SEQUENCE; Schema: public; Owner: user 37 | -- 38 | 39 | CREATE SEQUENCE public.address_sequence 40 | START WITH 1 41 | INCREMENT BY 1 42 | NO MINVALUE 43 | NO MAXVALUE 44 | CACHE 1; 45 | 46 | 47 | ALTER TABLE public.address_sequence OWNER TO "user"; 48 | 49 | -- 50 | -- Name: customer; Type: TABLE; Schema: public; Owner: user 51 | -- 52 | 53 | CREATE TABLE public.customer ( 54 | id bigint NOT NULL, 55 | credit_card character varying(255), 56 | hashed_password character varying(255), 57 | name character varying(255), 58 | phone character varying(255) 59 | ); 60 | 61 | 62 | ALTER TABLE public.customer OWNER TO "user"; 63 | 64 | -- 65 | -- Name: customer_sequence; Type: SEQUENCE; Schema: public; Owner: user 66 | -- 67 | 68 | CREATE SEQUENCE public.customer_sequence 69 | START WITH 1 70 | INCREMENT BY 1 71 | NO MINVALUE 72 | NO MAXVALUE 73 | CACHE 1; 74 | 75 | 76 | ALTER TABLE public.customer_sequence OWNER TO "user"; 77 | 78 | -- 79 | -- Name: customer_to_address; Type: TABLE; Schema: public; Owner: user 80 | -- 81 | 82 | CREATE TABLE public.customer_to_address ( 83 | customer_id bigint NOT NULL, 84 | address_id bigint NOT NULL 85 | ); 86 | 87 | 88 | ALTER TABLE public.customer_to_address OWNER TO "user"; 89 | 90 | -- 91 | -- Name: menu; Type: TABLE; Schema: public; Owner: user 92 | -- 93 | 94 | CREATE TABLE public.menu ( 95 | id bigint NOT NULL, 96 | created_at timestamp without time zone, 97 | last_updated_at timestamp without time zone, 98 | valid_until timestamp without time zone 99 | ); 100 | 101 | 102 | ALTER TABLE public.menu OWNER TO "user"; 103 | 104 | -- 105 | -- Name: menu_item; Type: TABLE; Schema: public; Owner: user 106 | -- 107 | 108 | CREATE TABLE public.menu_item ( 109 | id bigint NOT NULL, 110 | description character varying(255), 111 | price bigint 112 | ); 113 | 114 | 115 | ALTER TABLE public.menu_item OWNER TO "user"; 116 | 117 | -- 118 | -- Name: menu_to_menu_items; Type: TABLE; Schema: public; Owner: user 119 | -- 120 | 121 | CREATE TABLE public.menu_to_menu_items ( 122 | menu_id bigint NOT NULL, 123 | menu_item_id bigint NOT NULL 124 | ); 125 | 126 | 127 | ALTER TABLE public.menu_to_menu_items OWNER TO "user"; 128 | 129 | -- 130 | -- Name: product; Type: TABLE; Schema: public; Owner: user 131 | -- 132 | 133 | CREATE TABLE public.product ( 134 | product_type integer NOT NULL, 135 | id bigint NOT NULL, 136 | description character varying(255), 137 | price bigint NOT NULL, 138 | quantity integer NOT NULL, 139 | type integer, 140 | name character varying(255) 141 | ); 142 | 143 | 144 | ALTER TABLE public.product OWNER TO "user"; 145 | 146 | -- 147 | -- Name: product_sequence; Type: SEQUENCE; Schema: public; Owner: user 148 | -- 149 | 150 | CREATE SEQUENCE public.product_sequence 151 | START WITH 1 152 | INCREMENT BY 1 153 | NO MINVALUE 154 | NO MAXVALUE 155 | CACHE 1; 156 | 157 | 158 | ALTER TABLE public.product_sequence OWNER TO "user"; 159 | 160 | -- 161 | -- Data for Name: address; Type: TABLE DATA; Schema: public; Owner: user 162 | -- 163 | 164 | COPY public.address (id, street) FROM stdin; 165 | 1 John's street 166 | \. 167 | 168 | 169 | -- 170 | -- Data for Name: customer; Type: TABLE DATA; Schema: public; Owner: user 171 | -- 172 | 173 | COPY public.customer (id, credit_card, hashed_password, name, phone) FROM stdin; 174 | 1 \N \N John 012345678 175 | \. 176 | 177 | 178 | -- 179 | -- Data for Name: customer_to_address; Type: TABLE DATA; Schema: public; Owner: user 180 | -- 181 | 182 | COPY public.customer_to_address (customer_id, address_id) FROM stdin; 183 | 1 1 184 | \. 185 | 186 | 187 | -- 188 | -- Data for Name: menu; Type: TABLE DATA; Schema: public; Owner: user 189 | -- 190 | 191 | COPY public.menu (id, created_at, last_updated_at, valid_until) FROM stdin; 192 | \. 193 | 194 | 195 | -- 196 | -- Data for Name: menu_item; Type: TABLE DATA; Schema: public; Owner: user 197 | -- 198 | 199 | COPY public.menu_item (id, description, price) FROM stdin; 200 | \. 201 | 202 | 203 | -- 204 | -- Data for Name: menu_to_menu_items; Type: TABLE DATA; Schema: public; Owner: user 205 | -- 206 | 207 | COPY public.menu_to_menu_items (menu_id, menu_item_id) FROM stdin; 208 | \. 209 | 210 | 211 | -- 212 | -- Data for Name: product; Type: TABLE DATA; Schema: public; Owner: user 213 | -- 214 | 215 | COPY public.product (product_type, id, description, price, quantity, type, name) FROM stdin; 216 | 1 1 Papa Joe's Quattro Stagioni 10 25 1 \N 217 | 1 2 Papa Joe's Capriciosa 12 10 1 \N 218 | 2 3 The Classic Coke 2 50 \N Coca Cola 219 | \. 220 | 221 | 222 | -- 223 | -- Name: address_sequence; Type: SEQUENCE SET; Schema: public; Owner: user 224 | -- 225 | 226 | SELECT pg_catalog.setval('public.address_sequence', 1, true); 227 | 228 | 229 | -- 230 | -- Name: customer_sequence; Type: SEQUENCE SET; Schema: public; Owner: user 231 | -- 232 | 233 | SELECT pg_catalog.setval('public.customer_sequence', 1, true); 234 | 235 | 236 | -- 237 | -- Name: product_sequence; Type: SEQUENCE SET; Schema: public; Owner: user 238 | -- 239 | 240 | SELECT pg_catalog.setval('public.product_sequence', 3, true); 241 | 242 | 243 | -- 244 | -- Name: address address_pkey; Type: CONSTRAINT; Schema: public; Owner: user 245 | -- 246 | 247 | ALTER TABLE ONLY public.address 248 | ADD CONSTRAINT address_pkey PRIMARY KEY (id); 249 | 250 | 251 | -- 252 | -- Name: customer customer_pkey; Type: CONSTRAINT; Schema: public; Owner: user 253 | -- 254 | 255 | ALTER TABLE ONLY public.customer 256 | ADD CONSTRAINT customer_pkey PRIMARY KEY (id); 257 | 258 | 259 | -- 260 | -- Name: menu_item menu_item_pkey; Type: CONSTRAINT; Schema: public; Owner: user 261 | -- 262 | 263 | ALTER TABLE ONLY public.menu_item 264 | ADD CONSTRAINT menu_item_pkey PRIMARY KEY (id); 265 | 266 | 267 | -- 268 | -- Name: menu menu_pkey; Type: CONSTRAINT; Schema: public; Owner: user 269 | -- 270 | 271 | ALTER TABLE ONLY public.menu 272 | ADD CONSTRAINT menu_pkey PRIMARY KEY (id); 273 | 274 | 275 | -- 276 | -- Name: product product_pkey; Type: CONSTRAINT; Schema: public; Owner: user 277 | -- 278 | 279 | ALTER TABLE ONLY public.product 280 | ADD CONSTRAINT product_pkey PRIMARY KEY (id); 281 | 282 | 283 | -- 284 | -- Name: menu_to_menu_items fk3wmxt0jo42khntpmnshogx9rg; Type: FK CONSTRAINT; Schema: public; Owner: user 285 | -- 286 | 287 | ALTER TABLE ONLY public.menu_to_menu_items 288 | ADD CONSTRAINT fk3wmxt0jo42khntpmnshogx9rg FOREIGN KEY (menu_id) REFERENCES public.menu_item(id); 289 | 290 | 291 | -- 292 | -- Name: customer_to_address fkgr5jqsuyc1lh4focsdaeindmu; Type: FK CONSTRAINT; Schema: public; Owner: user 293 | -- 294 | 295 | ALTER TABLE ONLY public.customer_to_address 296 | ADD CONSTRAINT fkgr5jqsuyc1lh4focsdaeindmu FOREIGN KEY (customer_id) REFERENCES public.customer(id); 297 | 298 | 299 | -- 300 | -- Name: customer_to_address fkrjijm2neogv5ver0goqff95oe; Type: FK CONSTRAINT; Schema: public; Owner: user 301 | -- 302 | 303 | ALTER TABLE ONLY public.customer_to_address 304 | ADD CONSTRAINT fkrjijm2neogv5ver0goqff95oe FOREIGN KEY (address_id) REFERENCES public.address(id); 305 | 306 | 307 | -- 308 | -- Name: menu_to_menu_items fksii5dul1t7aq43i4dmj3bmdwm; Type: FK CONSTRAINT; Schema: public; Owner: user 309 | -- 310 | 311 | ALTER TABLE ONLY public.menu_to_menu_items 312 | ADD CONSTRAINT fksii5dul1t7aq43i4dmj3bmdwm FOREIGN KEY (menu_item_id) REFERENCES public.menu(id); 313 | 314 | 315 | -- 316 | -- PostgreSQL database dump complete 317 | -- 318 | 319 | -------------------------------------------------------------------------------- /src/main/resources/product-data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "_class": "pizzalab.entity.Pizza", 4 | "type": 1, 5 | "price": 10, 6 | "quantity": 25, 7 | "description": "Papa Joe's Quattro Stagioni" 8 | }, 9 | { 10 | "_class": "pizzalab.entity.Pizza", 11 | "type": 1, 12 | "price": 12, 13 | "quantity": 10, 14 | "description": "Papa Joe's Capriciosa" 15 | }, 16 | { 17 | "_class": "pizzalab.entity.Soda", 18 | "price": 2, 19 | "quantity": 50, 20 | "name": "Coca Cola", 21 | "description": "The Classic Coke" 22 | } 23 | ] 24 | -------------------------------------------------------------------------------- /src/test/java/pizzalab/integration/CustomerIT.java: -------------------------------------------------------------------------------- 1 | package pizzalab.integration; 2 | 3 | 4 | import static org.apache.http.HttpStatus.SC_BAD_REQUEST; 5 | import static org.apache.http.HttpStatus.SC_OK; 6 | import static org.junit.Assert.assertEquals; 7 | import static org.junit.Assert.assertNotNull; 8 | import static org.junit.Assert.assertTrue; 9 | 10 | import org.junit.jupiter.api.Test; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.test.context.SpringBootTest; 13 | import org.springframework.http.ResponseEntity; 14 | 15 | import pizzalab.controller.CustomerController; 16 | import pizzalab.dto.CustomerOutputDTO; 17 | import pizzalab.entity.Customer; 18 | 19 | import java.util.List; 20 | 21 | @SpringBootTest 22 | public class CustomerIT { 23 | 24 | @Autowired 25 | private CustomerController customerController; 26 | 27 | @Test 28 | public void test_customersComponent() { 29 | // list all customers and check that there is no customer added 30 | ResponseEntity getCustomersResponse = customerController.getCustomers(); 31 | 32 | assertNotNull("response should not be null", getCustomersResponse); 33 | assertEquals("wrong http status code", SC_OK, getCustomersResponse.getStatusCode().value()); 34 | assertNotNull("response body should not be null", getCustomersResponse.getBody()); 35 | 36 | assertTrue("wrong response body type", getCustomersResponse.getBody() instanceof List); 37 | List result = (List) getCustomersResponse.getBody(); 38 | assertTrue("customers list should be empty", result.isEmpty()); 39 | 40 | // add new customer 41 | Customer ion = Customer.builder().name("Ion").phone("0790909090").build(); 42 | ResponseEntity addCustomerResponse = customerController.createCustomer(ion); 43 | 44 | assertNotNull("response should not be null", addCustomerResponse); 45 | assertEquals("wrong http status code", SC_OK, addCustomerResponse.getStatusCode().value()); 46 | assertTrue("wrong response body type", addCustomerResponse.getBody() instanceof Customer); 47 | 48 | Customer added = (Customer) addCustomerResponse.getBody(); 49 | assertEquals("wrong customer name", "Ion", added.getName()); 50 | assertEquals("wrong customer phone", "0790909090", added.getPhone()); 51 | 52 | // get the customer 53 | Long ionId = added.getId(); 54 | ResponseEntity getCustomerResponse = customerController.getCustomer(ionId); 55 | assertEquals("wrong http status code", SC_OK, getCustomerResponse.getStatusCode().value()); 56 | assertTrue("wrong response body type", getCustomerResponse.getBody() instanceof CustomerOutputDTO); 57 | 58 | CustomerOutputDTO getIon = (CustomerOutputDTO) getCustomerResponse.getBody(); 59 | assertEquals("wrong customer name", "Ion", getIon.getName()); 60 | 61 | // add the same customer and check that 400 is returned 62 | addCustomerResponse = customerController.createCustomer(ion); 63 | assertEquals("wrong http status code", SC_BAD_REQUEST, addCustomerResponse.getStatusCode().value()); 64 | assertEquals("wrong response body", "Customer already exists!", addCustomerResponse.getBody()); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/java/pizzalab/services/CustomerServiceTest.java: -------------------------------------------------------------------------------- 1 | package pizzalab.services; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertNotNull; 5 | import static org.junit.Assert.assertNull; 6 | import static org.junit.Assert.assertTrue; 7 | import static org.junit.Assert.fail; 8 | import static org.mockito.ArgumentMatchers.eq; 9 | import static org.mockito.Mockito.times; 10 | import static org.mockito.Mockito.verify; 11 | import static org.mockito.Mockito.verifyNoInteractions; 12 | import static org.mockito.Mockito.verifyNoMoreInteractions; 13 | import static org.mockito.Mockito.when; 14 | 15 | import org.junit.Test; 16 | import org.junit.runner.RunWith; 17 | import org.mockito.ArgumentCaptor; 18 | import org.mockito.Captor; 19 | import org.mockito.InjectMocks; 20 | import org.mockito.Mock; 21 | import org.mockito.Mockito; 22 | import org.mockito.junit.MockitoJUnitRunner; 23 | 24 | import pizzalab.entity.Customer; 25 | import pizzalab.exception.CustomerNotFoundException; 26 | import pizzalab.repository.CustomerRepository; 27 | import pizzalab.repository.ProductRepository; 28 | 29 | import java.util.LinkedList; 30 | import java.util.List; 31 | import java.util.Optional; 32 | 33 | @RunWith(MockitoJUnitRunner.class) 34 | public class CustomerServiceTest { 35 | 36 | @Mock 37 | private CustomerRepository customerRepository; 38 | 39 | @Mock 40 | private ProductRepository productRepository; 41 | 42 | @InjectMocks 43 | private CustomerService underTest; 44 | 45 | @Captor 46 | private ArgumentCaptor customerCaptor; 47 | 48 | @Test 49 | public void test_addCustomer_alreadyExists() { 50 | // given 51 | Customer customer = Customer.builder().name("Sergiu").build(); 52 | 53 | Mockito.when(customerRepository.findFirstByName("Sergiu")).thenReturn(customer); 54 | 55 | // when 56 | Customer result = underTest.addCustomer(customer); 57 | 58 | // then 59 | assertNull("result should be null", result); 60 | verify(customerRepository, times(1)).findFirstByName("Sergiu"); 61 | verifyNoMoreInteractions(customerRepository); 62 | verifyNoInteractions(productRepository); 63 | } 64 | 65 | @Test 66 | public void test_addCustomer_success() { 67 | // given 68 | Customer customer = Customer.builder().name("Sergiu").build(); 69 | Mockito.when(customerRepository.findFirstByName("Sergiu")).thenReturn(null); 70 | Mockito.when(customerRepository.save(eq(customer))).thenReturn(customer); 71 | 72 | // when 73 | Customer result = underTest.addCustomer(customer); 74 | 75 | // then 76 | assertNotNull("result should not be null", result); 77 | assertEquals("wrong result", customer, result); 78 | verify(customerRepository).findFirstByName("Sergiu"); 79 | verify(customerRepository).save(customerCaptor.capture()); 80 | verifyNoMoreInteractions(customerRepository); 81 | verifyNoInteractions(productRepository); 82 | 83 | assertEquals("wrong captured value", customer, customerCaptor.getValue()); 84 | } 85 | 86 | @Test 87 | public void test_findAll_success() { 88 | // given 89 | when(customerRepository.findAll()).thenReturn(new LinkedList<>()); 90 | 91 | // when 92 | List result = underTest.findAll(); 93 | 94 | // then 95 | assertNotNull("result should not be null", result); 96 | assertTrue("wrong list size", result.isEmpty()); 97 | verify(customerRepository).findAll(); 98 | verifyNoMoreInteractions(customerRepository); 99 | verifyNoInteractions(productRepository); 100 | } 101 | 102 | @Test //(expected = CustomerNotFoundException.class) 103 | public void test_findById_customerNotFound() { 104 | // given 105 | Long customerId = 1L; 106 | when(customerRepository.findById(customerId)).thenReturn(Optional.empty()); 107 | 108 | try { 109 | // when 110 | underTest.findById(customerId); 111 | fail("should have failed"); 112 | } catch (CustomerNotFoundException e) { 113 | // then 114 | assertEquals("wrong exception message", "Customer not found", e.getMessage()); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/test/java/pizzalab/services/exchange/ApiExchangeServiceTest.java: -------------------------------------------------------------------------------- 1 | package pizzalab.services.exchange; 2 | 3 | import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; 4 | import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; 5 | import static com.github.tomakehurst.wiremock.client.WireMock.get; 6 | import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; 7 | import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; 8 | import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; 9 | import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; 10 | import static com.github.tomakehurst.wiremock.client.WireMock.verify; 11 | import static org.apache.http.HttpStatus.SC_GATEWAY_TIMEOUT; 12 | import static org.apache.http.HttpStatus.SC_OK; 13 | import static org.junit.Assert.assertEquals; 14 | import static org.junit.Assert.assertNotNull; 15 | 16 | import com.github.tomakehurst.wiremock.WireMockServer; 17 | import com.github.tomakehurst.wiremock.client.WireMock; 18 | import com.github.tomakehurst.wiremock.core.WireMockConfiguration; 19 | 20 | import org.junit.AfterClass; 21 | import org.junit.Before; 22 | import org.junit.BeforeClass; 23 | import org.junit.Test; 24 | 25 | import pizzalab.domain.exchange.ExchangeResponse; 26 | 27 | public class ApiExchangeServiceTest { 28 | 29 | private static WireMockServer wireMockServer; 30 | private static String hostUrl; 31 | 32 | private ApiExchangeService underTest; 33 | 34 | @BeforeClass 35 | public static void initialize() { 36 | wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort()); 37 | wireMockServer.start(); 38 | int port = wireMockServer.port(); 39 | WireMock.configureFor("localhost", port); 40 | hostUrl = "http://localhost:" + port; 41 | } 42 | 43 | @AfterClass 44 | public static void destroy() { 45 | verify(2, getRequestedFor(urlEqualTo("/api/latest?access_key=f7dbe1842278-43779b"))); 46 | wireMockServer.stop(); 47 | } 48 | 49 | @Before 50 | public void setup() { 51 | ExchangeConnectorProperties properties = new ExchangeConnectorProperties(); 52 | properties.setHostUrl(hostUrl); 53 | underTest = new ApiExchangeService(properties); 54 | } 55 | 56 | @Test 57 | public void test_exchangeRonToEuro_success() { 58 | // given 59 | stubFor( 60 | get(urlPathMatching("/api/latest")) 61 | .withQueryParam("access_key", equalTo("f7dbe1842278-43779b")) 62 | .willReturn(aResponse() 63 | .withStatus(SC_OK) 64 | .withBody("{\n" 65 | + " \"rates\": {\n" 66 | + " \"AED\": 1.2863,\n" 67 | + " \"AUD\": 3.3196,\n" 68 | + " \"BGN\": 2.5297,\n" 69 | + " \"BRL\": 0.9335,\n" 70 | + " \"CAD\": 3.6853,\n" 71 | + " \"CHF\": 4.738,\n" 72 | + " \"CNY\": 0.6999,\n" 73 | + " \"CZK\": 0.2002,\n" 74 | + " \"DKK\": 0.6649,\n" 75 | + " \"EGP\": 0.2586,\n" 76 | + " \"EUR\": 4.9477,\n" 77 | + " \"GBP\": 5.881,\n" 78 | + " \"HRK\": 0.6578,\n" 79 | + " \"HUF\": 1.2761,\n" 80 | + " \"INR\": 0.0609,\n" 81 | + " \"JPY\": 3.6529,\n" 82 | + " \"KRW\": 0.3714,\n" 83 | + " \"MDL\": 0.2493,\n" 84 | + " \"MXN\": 0.2362,\n" 85 | + " \"NOK\": 0.4874,\n" 86 | + " \"NZD\": 3.0037,\n" 87 | + " \"PLN\": 1.0627,\n" 88 | + " \"RSD\": 0.0421,\n" 89 | + " \"RUB\": 0.0733,\n" 90 | + " \"SEK\": 0.4745,\n" 91 | + " \"THB\": 0.1368,\n" 92 | + " \"TRY\": 0.2976,\n" 93 | + " \"UAH\": 0.1599,\n" 94 | + " \"USD\": 4.7247,\n" 95 | + " \"XAU\": 277.4951,\n" 96 | + " \"XDR\": 6.3176,\n" 97 | + " \"ZAR\": 0.2938\n" 98 | + " },\n" 99 | + " \"base\": \"RON\",\n" 100 | + " \"date\": \"2022-05-17T00:00:00.000Z\"\n" 101 | + "}"))); 102 | 103 | // when 104 | ExchangeResponse result = underTest.exchangeRonToEuro(100D); 105 | 106 | // then 107 | assertNotNull("result should not be null", result); 108 | // verify(1, getRequestedFor(urlEqualTo("/api/latest?access_key=f7dbe1842278-43779b"))); 109 | } 110 | 111 | @Test 112 | public void test_exchangeRonToEuro_fail() { 113 | stubFor( 114 | get(urlPathMatching("/api/latest")) 115 | .withQueryParam("access_key", equalTo("f7dbe1842278-43779b")) 116 | .willReturn(aResponse() 117 | .withStatus(SC_GATEWAY_TIMEOUT))); 118 | 119 | ExchangeResponse result = underTest.exchangeRonToEuro(100D); 120 | 121 | assertNotNull("result should not be null", result); 122 | // verify(1, getRequestedFor(urlEqualTo("/api/latest?access_key=f7dbe1842278-43779b"))); 123 | 124 | assertEquals(100D, result.getAmount(), 0.000001); 125 | assertEquals(4.9, result.getCurs(), 0.000001); 126 | } 127 | } 128 | --------------------------------------------------------------------------------