├── files ├── customer-search.json ├── bill-search.json ├── bill.json ├── bill-file.json └── bill-items.json ├── .gitignore ├── Dockerfile ├── src ├── main │ ├── java │ │ └── br │ │ │ └── com │ │ │ └── emmanuelneri │ │ │ ├── exception │ │ │ ├── BusinessException.java │ │ │ └── ExceptionUtil.java │ │ │ ├── repository │ │ │ ├── CarrierRepository.java │ │ │ ├── BillFileRepository.java │ │ │ ├── BillRepositoryCustom.java │ │ │ ├── CustomerRepository.java │ │ │ ├── BillRepository.java │ │ │ ├── BillItemRepository.java │ │ │ └── impl │ │ │ │ └── BillRepositoryImpl.java │ │ │ ├── to │ │ │ ├── ExceptionTO.java │ │ │ ├── CustomerSearchTO.java │ │ │ └── BillSearchTO.java │ │ │ ├── model │ │ │ ├── ItemType.java │ │ │ ├── BillFile.java │ │ │ ├── Carrier.java │ │ │ ├── Customer.java │ │ │ ├── BillItem.java │ │ │ └── Bill.java │ │ │ ├── dto │ │ │ ├── CarrierDTO.java │ │ │ ├── CustomerDTO.java │ │ │ ├── NumberUseDTO.java │ │ │ ├── BillSummaryDTO.java │ │ │ ├── BillResumeDTO.java │ │ │ ├── BillDTO.java │ │ │ └── BillItemDTO.java │ │ │ ├── mapper │ │ │ ├── CarrierMapper.java │ │ │ ├── CustomerMapper.java │ │ │ ├── BillMapper.java │ │ │ └── BillItemMapper.java │ │ │ ├── service │ │ │ ├── CarrierService.java │ │ │ ├── BillFileService.java │ │ │ ├── CustomerService.java │ │ │ └── BillService.java │ │ │ ├── util │ │ │ └── YearMonthJpaConverter.java │ │ │ ├── controller │ │ │ ├── CarrierController.java │ │ │ ├── BillFileController.java │ │ │ ├── CustomerController.java │ │ │ ├── BillController.java │ │ │ └── GlobalExceptionHandler.java │ │ │ └── AppConfig.java │ └── resources │ │ ├── db │ │ └── migration │ │ │ ├── V2__script.sql │ │ │ └── V1__script.sql │ │ ├── ehcache.xml │ │ ├── application-docker.properties │ │ └── application.properties └── test │ ├── resources │ └── application.properties │ └── java │ └── br │ └── com │ └── emmanuelneri │ ├── test │ ├── AbstractWebTest.java │ ├── AbstractIntegrationTest.java │ └── data │ │ ├── ConstraintViolationMock.java │ │ └── BillDataMock.java │ ├── controller │ ├── CarrierControllerTest.java │ ├── BillFileControllerTest.java │ ├── CustomerControllerTest.java │ ├── GlobalExceptionHandlerTest.java │ └── BillControllerTest.java │ └── service │ ├── BillFileServiceTest.java │ ├── CustomerServiceTest.java │ └── BillServiceTest.java ├── docker-compose.yml ├── README.md └── pom.xml /files/customer-search.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"Customer" 3 | } -------------------------------------------------------------------------------- /files/bill-search.json: -------------------------------------------------------------------------------- 1 | { 2 | "initYearMonth":"2017-01", 3 | "endYearMonth":"2017-07" 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.jar 3 | *.war 4 | *.ear 5 | 6 | /target 7 | 8 | *.iml 9 | .idea/* 10 | .idea/codeStyleSettings.xml 11 | .idea/encodings.xml 12 | .idea/vcs.xml 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jdk-alpine 2 | VOLUME /tmp 3 | ADD target/productivity-with-spring-1.0-SNAPSHOT.jar app.jar 4 | ENTRYPOINT [ "sh", "-c", "java -Djava.security.egd=file:/dev/./urandom -Dspring.profiles.active=docker -jar /app.jar" ] -------------------------------------------------------------------------------- /files/bill.json: -------------------------------------------------------------------------------- 1 | { 2 | "customer":{ 3 | "id":1, 4 | "name":"Customer 1" 5 | }, 6 | "carrier":{ 7 | "id":1, 8 | "name":"TIM" 9 | }, 10 | "identifier":"29302", 11 | "yearMonth":"2017-07", 12 | "total":70.00 13 | } -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/exception/BusinessException.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.exception; 2 | 3 | public class BusinessException extends RuntimeException { 4 | 5 | public BusinessException(String message) { 6 | super(message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/repository/CarrierRepository.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.repository; 2 | 3 | import br.com.emmanuelneri.model.Carrier; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface CarrierRepository extends JpaRepository { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/to/ExceptionTO.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.to; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public final class ExceptionTO { 7 | 8 | private final String error; 9 | 10 | public ExceptionTO(String error) { 11 | this.error = error; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/repository/BillFileRepository.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.repository; 2 | 3 | import br.com.emmanuelneri.model.BillFile; 4 | import org.springframework.data.mongodb.repository.MongoRepository; 5 | 6 | public interface BillFileRepository extends MongoRepository { 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/repository/BillRepositoryCustom.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.repository; 2 | 3 | import br.com.emmanuelneri.dto.BillDTO; 4 | import br.com.emmanuelneri.to.BillSearchTO; 5 | import org.springframework.data.domain.Page; 6 | 7 | public interface BillRepositoryCustom { 8 | 9 | Page findAll(BillSearchTO searchTO); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/model/ItemType.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.model; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public enum ItemType { 7 | 8 | CALL("Call"), 9 | SERVICE("Service"); 10 | 11 | private final String description; 12 | 13 | ItemType(String description) { 14 | this.description = description; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/dto/CarrierDTO.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | @Getter @Setter 9 | @AllArgsConstructor 10 | @NoArgsConstructor 11 | public class CarrierDTO { 12 | 13 | private Long id; 14 | private String name; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/dto/CustomerDTO.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | @Getter @Setter 9 | @AllArgsConstructor 10 | @NoArgsConstructor 11 | public class CustomerDTO { 12 | 13 | private Long id; 14 | private String name; 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:postgresql://localhost:5432/productivity-with-spring-test 2 | spring.datasource.username=postgres 3 | spring.datasource.password=postgres 4 | spring.datasource.driver-class-name=org.postgresql.Driver 5 | 6 | spring.data.mongodb.host=localhost 7 | spring.data.mongodb.port=27017 8 | spring.data.mongodb.database=productivity-with-spring-test -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/dto/NumberUseDTO.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.dto; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public final class NumberUseDTO { 7 | 8 | private String number; 9 | private long duration; 10 | 11 | public NumberUseDTO(String number, long duration) { 12 | this.number = number; 13 | this.duration = duration; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/repository/CustomerRepository.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.repository; 2 | 3 | import br.com.emmanuelneri.model.Customer; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.querydsl.QueryDslPredicateExecutor; 6 | 7 | public interface CustomerRepository extends JpaRepository, QueryDslPredicateExecutor { 8 | } 9 | -------------------------------------------------------------------------------- /files/bill-file.json: -------------------------------------------------------------------------------- 1 | {"customer":{"id":1,"name":"Customer 1"},"carrier":{"id":1,"name":"TIM"},"identifier":"29302","yearMonth":"2017-07","items":[{"dataTime":"2017-07-01 15:30:00","description":"Ligação fora do plano","originNumber":"4499898484","destinationNumber":"1199848248","duration":"10","value":"50","type":"SERVICE"},{"dataTime":"2017-07-15 00:00:00","description":"Pacote","originNumber":"4499898484","value":"50","type":"SERVICE"}],"total":70.00} -------------------------------------------------------------------------------- /src/main/resources/db/migration/V2__script.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO customer (name) VALUES ('Customer 1'); 2 | INSERT INTO customer (name) VALUES ('Customer 2'); 3 | INSERT INTO customer (name) VALUES ('Customer 3'); 4 | INSERT INTO customer (name) VALUES ('Customer 4'); 5 | INSERT INTO customer (name) VALUES ('Customer 5'); 6 | 7 | INSERT INTO carrier (name) VALUES ('TIM'); 8 | INSERT INTO carrier (name) VALUES ('VIVO'); 9 | INSERT INTO carrier (name) VALUES ('OI'); 10 | INSERT INTO carrier (name) VALUES ('Claro'); -------------------------------------------------------------------------------- /src/main/resources/ehcache.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/test/java/br/com/emmanuelneri/test/AbstractWebTest.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.test; 2 | 3 | import org.junit.runner.RunWith; 4 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 10 | @AutoConfigureMockMvc 11 | public abstract class AbstractWebTest { 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/repository/BillRepository.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.repository; 2 | 3 | import br.com.emmanuelneri.model.Bill; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.querydsl.QueryDslPredicateExecutor; 6 | 7 | import java.time.YearMonth; 8 | 9 | public interface BillRepository extends JpaRepository, QueryDslPredicateExecutor, BillRepositoryCustom { 10 | 11 | Bill findByCustomerIdAndIdentifierAndYearMonth(Long customerId, String identifier, YearMonth yearMonth); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/dto/BillSummaryDTO.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.dto; 2 | 3 | import lombok.Getter; 4 | 5 | import java.math.BigDecimal; 6 | 7 | @Getter 8 | public final class BillSummaryDTO { 9 | 10 | private long itemsQuantity; 11 | private long totalDuration; 12 | private BigDecimal totalValue; 13 | 14 | public BillSummaryDTO(long itemsQuantity, long totalDuration, BigDecimal totalValue) { 15 | this.itemsQuantity = itemsQuantity; 16 | this.totalDuration = totalDuration; 17 | this.totalValue = totalValue; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | productivity-app: 4 | image: emmanuelneri/productivity-with-spring-app 5 | links: 6 | - productivity-postgres 7 | - productivity-mongodb 8 | ports: 9 | - "8080:8080" 10 | 11 | productivity-postgres: 12 | image: postgres:9.6 13 | ports: 14 | - "5432:5432" 15 | environment: 16 | - POSTGRES_DB=productivity-with-spring 17 | - POSTGRES_USER=postgres 18 | - POSTGRES_PASSWORD=postgres 19 | 20 | productivity-mongodb: 21 | image: mongo:3.5 22 | ports: 23 | - "27017:27017" -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/mapper/CarrierMapper.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.mapper; 2 | 3 | import br.com.emmanuelneri.dto.CarrierDTO; 4 | import br.com.emmanuelneri.model.Carrier; 5 | 6 | public final class CarrierMapper { 7 | 8 | private CarrierMapper(){ 9 | } 10 | 11 | public static Carrier fromDTO(CarrierDTO dto) { 12 | if(dto == null) { 13 | return null; 14 | } 15 | 16 | return new Carrier(dto.getId(), dto.getName()); 17 | } 18 | 19 | public static CarrierDTO toDTO(Carrier carrier) { 20 | return new CarrierDTO(carrier.getId(), carrier.getName()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/mapper/CustomerMapper.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.mapper; 2 | 3 | import br.com.emmanuelneri.dto.CustomerDTO; 4 | import br.com.emmanuelneri.model.Customer; 5 | 6 | public final class CustomerMapper { 7 | 8 | private CustomerMapper() { 9 | } 10 | 11 | public static Customer fromDTO(CustomerDTO dto) { 12 | if(dto == null) { 13 | return null; 14 | } 15 | 16 | return new Customer(dto.getId(), dto.getName()); 17 | } 18 | 19 | public static CustomerDTO toDTO(Customer customer) { 20 | return new CustomerDTO(customer.getId(), customer.getName()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/dto/BillResumeDTO.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.dto; 2 | 3 | import lombok.Getter; 4 | 5 | import java.util.List; 6 | 7 | @Getter 8 | public final class BillResumeDTO { 9 | 10 | private String identifier; 11 | private String customerName; 12 | private BillSummaryDTO summary; 13 | private List most10NumbersUse; 14 | 15 | public BillResumeDTO(String identifier, String customerName, BillSummaryDTO summary, List most10NumbersUse) { 16 | this.identifier = identifier; 17 | this.customerName = customerName; 18 | this.summary = summary; 19 | this.most10NumbersUse = most10NumbersUse; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /files/bill-items.json: -------------------------------------------------------------------------------- 1 | { 2 | "customer":{ 3 | "id":1, 4 | "name":"Customer 1" 5 | }, 6 | "carrier":{ 7 | "id":1, 8 | "name":"TIM" 9 | }, 10 | "identifier":"29302", 11 | "yearMonth":"2017-07", 12 | "items":[ 13 | { 14 | "dataTime":"2017-07-01 15:30:00", 15 | "description":"Ligação fora do plano", 16 | "originNumber":"4499898484", 17 | "destinationNumber":"1199848248", 18 | "duration":"10", 19 | "value":"50", 20 | "type":"SERVICE" 21 | }, 22 | { 23 | "dataTime":"2017-07-15 00:00:00", 24 | "description":"Pacote", 25 | "originNumber":"4499898484", 26 | "value":"50", 27 | "type":"SERVICE" 28 | } 29 | ], 30 | "total":70.00 31 | } -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/model/BillFile.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.model; 2 | 3 | import lombok.Getter; 4 | import org.springframework.data.annotation.Id; 5 | import org.springframework.data.mongodb.core.mapping.Document; 6 | 7 | import java.time.LocalDateTime; 8 | import java.time.format.DateTimeFormatter; 9 | 10 | @Document(collection = "bill_file") 11 | @Getter 12 | public class BillFile { 13 | 14 | @Id 15 | private String id; 16 | private String date; 17 | private String content; 18 | 19 | public BillFile(String content) { 20 | this.content = content; 21 | this.date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm")); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/dto/BillDTO.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | import java.math.BigDecimal; 8 | import java.time.YearMonth; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | @Getter @Setter 13 | public final class BillDTO { 14 | 15 | private Long id; 16 | private CustomerDTO customer; 17 | private CarrierDTO carrier; 18 | private String identifier; 19 | private BigDecimal total = BigDecimal.ZERO; 20 | private List items = new ArrayList<>(); 21 | 22 | @JsonFormat(pattern = "yyyy-MM") 23 | private YearMonth yearMonth; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/service/CarrierService.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.service; 2 | 3 | import br.com.emmanuelneri.model.Carrier; 4 | import br.com.emmanuelneri.repository.CarrierRepository; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.cache.annotation.Cacheable; 7 | import org.springframework.stereotype.Service; 8 | 9 | import java.util.List; 10 | 11 | @Service 12 | public class CarrierService { 13 | 14 | @Autowired 15 | private CarrierRepository carrierRepository; 16 | 17 | public Carrier save(Carrier carrier) { 18 | return carrierRepository.save(carrier); 19 | } 20 | 21 | @Cacheable("carriers") 22 | public List findAll() { 23 | return carrierRepository.findAll(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/util/YearMonthJpaConverter.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.util; 2 | 3 | import javax.persistence.AttributeConverter; 4 | import javax.persistence.Converter; 5 | import java.time.YearMonth; 6 | import java.time.format.DateTimeFormatter; 7 | 8 | @Converter(autoApply = true) 9 | public class YearMonthJpaConverter implements AttributeConverter { 10 | 11 | private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM"); 12 | 13 | @Override 14 | public String convertToDatabaseColumn(YearMonth yearMonth) { 15 | return yearMonth.format(FORMATTER); 16 | } 17 | 18 | @Override 19 | public YearMonth convertToEntityAttribute(String yearMonth) { 20 | return YearMonth.parse(yearMonth); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/service/BillFileService.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.service; 2 | 3 | import br.com.emmanuelneri.model.BillFile; 4 | import br.com.emmanuelneri.repository.BillFileRepository; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.util.List; 9 | 10 | @Service 11 | public class BillFileService { 12 | 13 | @Autowired 14 | private BillFileRepository billFileRepository; 15 | 16 | public void save(String content) { 17 | billFileRepository.save(new BillFile(content)); 18 | } 19 | 20 | public List findAll() { 21 | return billFileRepository.findAll(); 22 | } 23 | 24 | public void delete(String id) { 25 | billFileRepository.delete(id); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/controller/CarrierController.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.controller; 2 | 3 | import br.com.emmanuelneri.model.Carrier; 4 | import br.com.emmanuelneri.service.CarrierService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RequestMethod; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | import java.util.List; 11 | 12 | @RestController 13 | @RequestMapping("/carriers") 14 | public class CarrierController { 15 | 16 | @Autowired 17 | private CarrierService carrierService; 18 | 19 | @RequestMapping(method = RequestMethod.GET) 20 | public List findAll() { 21 | return carrierService.findAll(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/dto/BillItemDTO.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.dto; 2 | 3 | import br.com.emmanuelneri.model.ItemType; 4 | import com.fasterxml.jackson.annotation.JsonFormat; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | import lombok.Setter; 9 | 10 | import java.math.BigDecimal; 11 | import java.time.LocalDateTime; 12 | 13 | @Getter @Setter 14 | @AllArgsConstructor 15 | @NoArgsConstructor 16 | public class BillItemDTO { 17 | 18 | private Long id; 19 | private String description; 20 | private String originNumber; 21 | private String destinationNumber; 22 | private Long duration; 23 | private BigDecimal value; 24 | private ItemType type; 25 | 26 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") 27 | private LocalDateTime dataTime; 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/br/com/emmanuelneri/test/AbstractIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.test; 2 | 3 | import org.junit.runner.RunWith; 4 | import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest; 5 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; 6 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 7 | import org.springframework.test.annotation.DirtiesContext; 8 | import org.springframework.test.context.junit4.SpringRunner; 9 | 10 | import static org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace.NONE; 11 | import static org.springframework.test.annotation.DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD; 12 | 13 | @RunWith(SpringRunner.class) 14 | @DataJpaTest 15 | @DataMongoTest 16 | @AutoConfigureTestDatabase(replace = NONE) 17 | @DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) 18 | public abstract class AbstractIntegrationTest { 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/model/Carrier.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | import org.hibernate.validator.constraints.NotEmpty; 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.SequenceGenerator; 15 | import javax.validation.constraints.Size; 16 | 17 | @Entity 18 | @Getter @Setter 19 | @Builder 20 | @AllArgsConstructor 21 | @NoArgsConstructor 22 | public class Carrier { 23 | 24 | @Id 25 | @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "carrier_id_seq") 26 | @SequenceGenerator(name = "carrier_id_seq", sequenceName = "carrier_id_seq", allocationSize = 1) 27 | private Long id; 28 | 29 | @NotEmpty 30 | @Size(max = 200) 31 | private String name; 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/model/Customer.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | import org.hibernate.validator.constraints.NotEmpty; 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.SequenceGenerator; 15 | import javax.validation.constraints.Size; 16 | 17 | @Entity 18 | @Getter @Setter 19 | @Builder 20 | @AllArgsConstructor 21 | @NoArgsConstructor 22 | public class Customer { 23 | 24 | @Id 25 | @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "customer_id_seq") 26 | @SequenceGenerator(name = "customer_id_seq", sequenceName = "customer_id_seq", allocationSize = 1) 27 | private Long id; 28 | 29 | @NotEmpty 30 | @Size(max = 200) 31 | private String name; 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/AppConfig.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri; 2 | 3 | import br.com.emmanuelneri.model.Bill; 4 | import br.com.emmanuelneri.util.YearMonthJpaConverter; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.boot.autoconfigure.domain.EntityScan; 8 | import org.springframework.cache.annotation.EnableCaching; 9 | import org.springframework.context.annotation.ComponentScan; 10 | import org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters; 11 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 12 | 13 | @SpringBootApplication 14 | @EntityScan(basePackageClasses = {Bill.class, Jsr310JpaConverters.class, YearMonthJpaConverter.class}) 15 | @ComponentScan 16 | @EnableJpaRepositories 17 | @EnableCaching 18 | public class AppConfig { 19 | 20 | public static void main(String[] args) { 21 | SpringApplication.run(AppConfig.class, args); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/exception/ExceptionUtil.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.exception; 2 | 3 | import com.google.common.base.Throwables; 4 | import org.springframework.dao.DataIntegrityViolationException; 5 | 6 | public final class ExceptionUtil { 7 | 8 | private static final String SQL_VIOLATION_CODE_UNIQUE_CONSTRAINT = "23505"; 9 | 10 | private ExceptionUtil() { 11 | } 12 | 13 | public static boolean isUniqueConstraintError(DataIntegrityViolationException divex) { 14 | return Throwables.getCausalChain(divex).stream().anyMatch(ex -> { 15 | if (ex instanceof org.hibernate.exception.ConstraintViolationException) { 16 | org.hibernate.exception.ConstraintViolationException cvex = (org.hibernate.exception.ConstraintViolationException) ex; 17 | if (cvex.getSQLState().equals(SQL_VIOLATION_CODE_UNIQUE_CONSTRAINT)) { 18 | return true; 19 | } 20 | } 21 | return false; 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/controller/BillFileController.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.controller; 2 | 3 | import br.com.emmanuelneri.model.BillFile; 4 | import br.com.emmanuelneri.service.BillFileService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.web.bind.annotation.RequestBody; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RequestMethod; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | import java.util.List; 12 | 13 | @RestController 14 | @RequestMapping("/files/bills") 15 | public class BillFileController { 16 | 17 | @Autowired 18 | private BillFileService billFileService; 19 | 20 | @RequestMapping(method = RequestMethod.POST) 21 | public void save(@RequestBody String content) { 22 | billFileService.save(content); 23 | } 24 | 25 | @RequestMapping(method = RequestMethod.GET) 26 | public List findAll() { 27 | return billFileService.findAll(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/repository/BillItemRepository.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.repository; 2 | 3 | import br.com.emmanuelneri.dto.BillSummaryDTO; 4 | import br.com.emmanuelneri.dto.NumberUseDTO; 5 | import br.com.emmanuelneri.model.BillItem; 6 | import org.springframework.data.domain.Pageable; 7 | import org.springframework.data.jpa.repository.JpaRepository; 8 | import org.springframework.data.jpa.repository.Modifying; 9 | import org.springframework.data.jpa.repository.Query; 10 | import org.springframework.data.repository.query.Param; 11 | 12 | import java.util.List; 13 | 14 | public interface BillItemRepository extends JpaRepository { 15 | 16 | @Modifying 17 | @Query("delete from BillItem where bill.id = :#{#billId}") 18 | void deleteByBillItem(@Param("billId") Long billId); 19 | 20 | @Query(name = "BillItem.summarizeByBillId") 21 | BillSummaryDTO summarizeByBillId(@Param("billId") Long billId); 22 | 23 | @Query(name = "BillItem.numberGreaterUse") 24 | List numberGreaterUse(@Param("billId") Long billId, Pageable pageable); 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/to/CustomerSearchTO.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.to; 2 | 3 | import br.com.emmanuelneri.model.QCustomer; 4 | import com.google.common.base.Strings; 5 | import com.querydsl.core.BooleanBuilder; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | import lombok.Setter; 9 | 10 | @Getter @Setter 11 | @NoArgsConstructor 12 | public class CustomerSearchTO { 13 | 14 | private Long id; 15 | private String name; 16 | 17 | private int page; 18 | private int size = 10; 19 | 20 | public CustomerSearchTO(Long id, String name) { 21 | this.id = id; 22 | this.name = name; 23 | } 24 | 25 | public BooleanBuilder toPredicate() { 26 | final QCustomer qCustomer = QCustomer.customer; 27 | final BooleanBuilder predicate = new BooleanBuilder(); 28 | 29 | if(id != null) { 30 | predicate.and(qCustomer.id.eq(id)); 31 | } 32 | 33 | if(!Strings.isNullOrEmpty(name)) { 34 | predicate.and(qCustomer.name.containsIgnoreCase(name)); 35 | } 36 | 37 | return predicate; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/resources/db/migration/V1__script.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE customer ( 2 | id bigserial PRIMARY KEY, 3 | name VARCHAR(200) NOT NULL 4 | ); 5 | 6 | CREATE TABLE carrier ( 7 | id bigserial PRIMARY KEY, 8 | name VARCHAR(200) NOT NULL 9 | ); 10 | 11 | CREATE TABLE bill ( 12 | id bigserial PRIMARY KEY, 13 | customer_id BIGINT NOT NULL, 14 | carrier_id BIGINT NOT NULL, 15 | identifier VARCHAR(50) NOT NULL, 16 | year_month VARCHAR(7) NOT NULL, 17 | total NUMERIC(19, 2) NOT NULL, 18 | CONSTRAINT bill_uk UNIQUE (customer_id, identifier, year_month), 19 | CONSTRAINT bill_customer_id_fk FOREIGN KEY (customer_id) REFERENCES customer(id), 20 | CONSTRAINT bill_carrier_id_fk FOREIGN KEY (carrier_id) REFERENCES carrier(id) 21 | ); 22 | 23 | CREATE TABLE bill_item ( 24 | id bigserial PRIMARY KEY, 25 | bill_id BIGINT NOT NULL, 26 | date_time TIMESTAMP WITHOUT TIME ZONE NOT NULL, 27 | description VARCHAR(200) NOT NULL, 28 | origin_number VARCHAR(20) NOT NULL, 29 | destination_number VARCHAR(20), 30 | duration BIGINT, 31 | value NUMERIC(19, 2) NOT NULL, 32 | type VARCHAR(50) NOT NULL, 33 | CONSTRAINT bill_item_bill_id_fk FOREIGN KEY (bill_id) REFERENCES bill(id) 34 | ); -------------------------------------------------------------------------------- /src/main/resources/application-docker.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:postgresql://productivity-postgres:5432/productivity-with-spring 2 | spring.datasource.username=postgres 3 | spring.datasource.password=postgres 4 | spring.datasource.driver-class-name=org.postgresql.Driver 5 | 6 | spring.jpa.show-sql=true 7 | 8 | spring.cache.ehcache.config=ehcache.xml 9 | spring.jpa.properties.hibernate.cache.use_second_level_cache=true 10 | spring.jpa.properties.hibernate.cache.use_query_cache=true 11 | spring.jpa.properties.javax.persistence.sharedCache.mode=ENABLE_SELECTIVE 12 | spring.jpa.properties.hibernate.cache.provider_class=org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory 13 | spring.jpa.properties.hibernate.cache.region.factory_class=org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory 14 | 15 | spring.data.mongodb.host=productivity-mongodb 16 | spring.data.mongodb.port=27017 17 | spring.data.mongodb.database=productivity-with-spring 18 | 19 | management.context-path=/manage 20 | management.security.enabled=true 21 | security.user.name=admin 22 | security.user.password=admin 23 | security.basic.enabled=true 24 | 25 | info.app.name=@project.artifactId@ 26 | info.app.version=@project.version@ -------------------------------------------------------------------------------- /src/test/java/br/com/emmanuelneri/test/data/ConstraintViolationMock.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.test.data; 2 | 3 | import org.hibernate.validator.constraints.NotEmpty; 4 | 5 | import javax.validation.ConstraintViolation; 6 | import javax.validation.ConstraintViolationException; 7 | import javax.validation.Validation; 8 | import java.util.Set; 9 | 10 | public final class ConstraintViolationMock { 11 | 12 | private ConstraintViolationMock() { 13 | } 14 | 15 | public static ConstraintViolationException getFakeConstraintViolationException() { 16 | final Bill bill = new Bill(); 17 | 18 | final Set> constraintViolations = 19 | Validation.buildDefaultValidatorFactory().getValidator().validate(bill); 20 | 21 | return new ConstraintViolationException(constraintViolations); 22 | } 23 | 24 | static class Bill { 25 | 26 | @NotEmpty(message = "The identifier is required") 27 | private String identifier; 28 | 29 | public String getIdentifier() { 30 | return identifier; 31 | } 32 | 33 | public void setIdentifier(String identifier) { 34 | this.identifier = identifier; 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:postgresql://localhost:5432/productivity-with-spring 2 | spring.datasource.username=postgres 3 | spring.datasource.password=postgres 4 | spring.datasource.driver-class-name=org.postgresql.Driver 5 | 6 | spring.jpa.show-sql=true 7 | 8 | spring.cache.ehcache.config=ehcache.xml 9 | spring.jpa.properties.hibernate.cache.use_second_level_cache=true 10 | spring.jpa.properties.hibernate.cache.use_query_cache=true 11 | spring.jpa.properties.javax.persistence.sharedCache.mode=ENABLE_SELECTIVE 12 | spring.jpa.properties.hibernate.cache.provider_class=org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory 13 | spring.jpa.properties.hibernate.cache.region.factory_class=org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory 14 | 15 | spring.data.mongodb.host=localhost 16 | spring.data.mongodb.port=27017 17 | spring.data.mongodb.database=productivity-with-spring 18 | 19 | ## Ativar quando precisar de mais detalhes do Spring, por exemplo quando precisar debugar algum erro de serialize 20 | #logging.level.org.springframework.web=DEBUG 21 | 22 | management.context-path=/manage 23 | management.security.enabled=true 24 | security.user.name=admin 25 | security.user.password=admin 26 | security.basic.path=/manage/** 27 | security.basic.enabled=true 28 | 29 | info.app.name=@project.artifactId@ 30 | info.app.version=@project.version@ -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/mapper/BillMapper.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.mapper; 2 | 3 | import br.com.emmanuelneri.dto.BillDTO; 4 | import br.com.emmanuelneri.model.Bill; 5 | 6 | import java.util.List; 7 | import java.util.stream.Collectors; 8 | 9 | public final class BillMapper { 10 | 11 | private BillMapper() { 12 | } 13 | 14 | public static Bill fromDTO(BillDTO dto) { 15 | final Bill bill = new Bill(); 16 | bill.setId(dto.getId()); 17 | bill.setIdentifier(dto.getIdentifier()); 18 | bill.setYearMonth(dto.getYearMonth()); 19 | bill.setCarrier(CarrierMapper.fromDTO(dto.getCarrier())); 20 | bill.setCustomer(CustomerMapper.fromDTO(dto.getCustomer())); 21 | bill.setItems(BillItemMapper.fromDTO(dto.getItems(), bill)); 22 | bill.setTotal(dto.getTotal()); 23 | return bill; 24 | } 25 | 26 | public static BillDTO toDTO(Bill bill) { 27 | if(bill == null) { 28 | return null; 29 | } 30 | 31 | final BillDTO dto = new BillDTO(); 32 | dto.setId(bill.getId()); 33 | dto.setIdentifier(bill.getIdentifier()); 34 | dto.setYearMonth(bill.getYearMonth()); 35 | dto.setCarrier(CarrierMapper.toDTO(bill.getCarrier())); 36 | dto.setCustomer(CustomerMapper.toDTO(bill.getCustomer())); 37 | dto.setItems(BillItemMapper.toDTO(bill.getItems())); 38 | dto.setTotal(bill.getTotal()); 39 | return dto; 40 | } 41 | 42 | public static List toDTO(List bills) { 43 | return bills.stream().map(BillMapper::toDTO).collect(Collectors.toList()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/to/BillSearchTO.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.to; 2 | 3 | import br.com.emmanuelneri.model.QBill; 4 | import com.google.common.base.Strings; 5 | import com.querydsl.core.BooleanBuilder; 6 | import lombok.Getter; 7 | import lombok.Setter; 8 | import org.springframework.data.domain.PageRequest; 9 | import org.springframework.data.domain.Pageable; 10 | 11 | import java.time.YearMonth; 12 | 13 | @Getter @Setter 14 | public class BillSearchTO { 15 | 16 | private String identifier; 17 | private YearMonth initYearMonth; 18 | private YearMonth endYearMonth; 19 | private Long customerId; 20 | private Long carrierId; 21 | 22 | private int page; 23 | private int size = 10; 24 | 25 | public BooleanBuilder toPredicate() { 26 | final QBill qBill = QBill.bill; 27 | final BooleanBuilder predicate = new BooleanBuilder(); 28 | 29 | if(!Strings.isNullOrEmpty(identifier)) { 30 | predicate.and(qBill.identifier.eq(identifier)); 31 | } 32 | 33 | if(initYearMonth != null) { 34 | predicate.and(qBill.yearMonth.goe(initYearMonth)); 35 | } 36 | 37 | if(endYearMonth != null) { 38 | predicate.and(qBill.yearMonth.loe(endYearMonth)); 39 | } 40 | 41 | if(customerId != null) { 42 | predicate.and(qBill.customer.id.eq(customerId)); 43 | } 44 | 45 | if(carrierId != null) { 46 | predicate.and(qBill.carrier.id.eq(carrierId)); 47 | } 48 | 49 | return predicate; 50 | } 51 | 52 | public Pageable getPageable() { 53 | return new PageRequest(page, size); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/service/CustomerService.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.service; 2 | 3 | import br.com.emmanuelneri.model.Customer; 4 | import br.com.emmanuelneri.repository.CustomerRepository; 5 | import br.com.emmanuelneri.to.CustomerSearchTO; 6 | import com.google.common.collect.Lists; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.data.domain.Page; 9 | import org.springframework.data.domain.PageRequest; 10 | import org.springframework.data.domain.Sort; 11 | import org.springframework.stereotype.Service; 12 | 13 | import java.util.List; 14 | 15 | @Service 16 | public class CustomerService { 17 | 18 | @Autowired 19 | private CustomerRepository customerRepository; 20 | 21 | public Customer save(Customer customer) { 22 | return customerRepository.save(customer); 23 | } 24 | 25 | public List findAll() { 26 | return customerRepository.findAll(); 27 | } 28 | 29 | public Page findPaginable(int page, int size) { 30 | return customerRepository.findAll(new PageRequest(page, size, new Sort(Sort.Direction.DESC, "name"))); 31 | } 32 | 33 | public Page searchPagenable(CustomerSearchTO searchTO) { 34 | return customerRepository.findAll(searchTO.toPredicate(), 35 | new PageRequest(searchTO.getPage(), searchTO.getSize(), 36 | new Sort(Sort.Direction.DESC, "name") 37 | .and(new Sort(Sort.Direction.DESC, "id")))); 38 | } 39 | 40 | public List search(CustomerSearchTO searchTO) { 41 | return Lists.newArrayList(customerRepository.findAll(searchTO.toPredicate())); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/br/com/emmanuelneri/controller/CarrierControllerTest.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.controller; 2 | 3 | import br.com.emmanuelneri.model.Carrier; 4 | import br.com.emmanuelneri.service.CarrierService; 5 | import br.com.emmanuelneri.test.AbstractWebTest; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import org.junit.Test; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.mock.mockito.MockBean; 10 | import org.springframework.http.MediaType; 11 | import org.springframework.test.web.servlet.MockMvc; 12 | 13 | import javax.inject.Inject; 14 | import java.util.Arrays; 15 | import java.util.List; 16 | 17 | import static org.mockito.BDDMockito.given; 18 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 19 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 20 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 21 | 22 | public class CarrierControllerTest extends AbstractWebTest { 23 | 24 | @Autowired 25 | private MockMvc mockMvc; 26 | 27 | @MockBean 28 | private CarrierService carrierService; 29 | 30 | @Inject 31 | private ObjectMapper objectMapper; 32 | 33 | @Test 34 | public void findAll() throws Exception { 35 | final List carriers = Arrays.asList(new Carrier()); 36 | 37 | given(carrierService.findAll()) 38 | .willReturn(carriers); 39 | 40 | mockMvc.perform(get("/carriers") 41 | .accept(MediaType.APPLICATION_JSON)) 42 | .andExpect(status().isOk()) 43 | .andExpect(content().string(objectMapper.writeValueAsString(carriers))); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/br/com/emmanuelneri/service/BillFileServiceTest.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.service; 2 | 3 | import br.com.emmanuelneri.model.BillFile; 4 | import br.com.emmanuelneri.test.AbstractIntegrationTest; 5 | import org.junit.Assert; 6 | import org.junit.Test; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | 9 | import java.util.List; 10 | 11 | import static org.hamcrest.Matchers.*; 12 | import static org.hamcrest.beans.HasPropertyWithValue.hasProperty; 13 | 14 | public class BillFileServiceTest extends AbstractIntegrationTest { 15 | 16 | @Autowired 17 | private BillFileService billFileService; 18 | 19 | @Test 20 | public void save() { 21 | final String json = "{\"customer\":{\"id\":1,\"name\":\"Customer 1\"}," + 22 | "\"carrier\":{\"id\":1,\"name\":\"TIM\"},\"identifier\":\"29302\",\"yearMonth\":\"2017-07\"," + 23 | "\"items\":[" + 24 | "{\"dataTime\":\"2017-07-01 15:30:00\",\"description\":\"Ligação fora do plano\",\"originNumber\":\"4499898484\",\"destinationNumber\":\"1199848248\",\"duration\":\"10\",\"value\":\"50\",\"type\":\"SERVICE\"}," + 25 | "{\"dataTime\":\"2017-07-15 00:00:00\",\"description\":\"Pacote\",\"originNumber\":\"4499898484\",\"value\":\"50\",\"type\":\"SERVICE\"}]," + 26 | "\"total\":70.00}"; 27 | 28 | 29 | billFileService.save(json); 30 | 31 | final List billFiles = billFileService.findAll(); 32 | 33 | Assert.assertEquals(1, billFiles.size()); 34 | Assert.assertThat(billFiles, contains( 35 | allOf(hasProperty("id", notNullValue()), 36 | hasProperty("date", notNullValue()), 37 | hasProperty("content", is(json))))); 38 | 39 | billFiles.forEach(billFile -> billFileService.delete(billFile.getId())); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/controller/CustomerController.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.controller; 2 | 3 | import br.com.emmanuelneri.model.Customer; 4 | import br.com.emmanuelneri.service.CustomerService; 5 | import br.com.emmanuelneri.to.CustomerSearchTO; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.data.domain.Page; 8 | import org.springframework.web.bind.annotation.PathVariable; 9 | import org.springframework.web.bind.annotation.RequestBody; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RequestMethod; 12 | import org.springframework.web.bind.annotation.RequestParam; 13 | import org.springframework.web.bind.annotation.RestController; 14 | 15 | import java.util.List; 16 | 17 | @RestController 18 | @RequestMapping("/customers") 19 | public class CustomerController { 20 | 21 | @Autowired 22 | private CustomerService customerService; 23 | 24 | @RequestMapping(method = RequestMethod.GET) 25 | public List findAll() { 26 | return customerService.findAll(); 27 | } 28 | 29 | @RequestMapping(value = "/paged/{page}/{size}", method = RequestMethod.GET) 30 | public Page findAllPaged(@PathVariable("page") int page, @PathVariable("size") int size) { 31 | return customerService.findPaginable(page, size); 32 | } 33 | 34 | @RequestMapping(value = "/search/pagenable", method = RequestMethod.POST) 35 | public Page searchPagenable(@RequestBody CustomerSearchTO searchTO) { 36 | return customerService.searchPagenable(searchTO); 37 | } 38 | 39 | @RequestMapping(value = "/search", method = RequestMethod.GET) 40 | public List search(@RequestParam(value = "id", required = false) Long id, @RequestParam(value = "name", required = false) String name){ 41 | return customerService.search(new CustomerSearchTO(id, name)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/mapper/BillItemMapper.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.mapper; 2 | 3 | import br.com.emmanuelneri.dto.BillItemDTO; 4 | import br.com.emmanuelneri.exception.BusinessException; 5 | import br.com.emmanuelneri.model.Bill; 6 | import br.com.emmanuelneri.model.BillItem; 7 | 8 | import java.util.List; 9 | import java.util.stream.Collectors; 10 | 11 | public final class BillItemMapper { 12 | 13 | private BillItemMapper() { 14 | } 15 | 16 | public static List fromDTO(List items, Bill bill) { 17 | if(items == null) { 18 | throw new BusinessException("Items are required in the bill"); 19 | } 20 | 21 | return items.stream() 22 | .map(dto -> fromDTO(dto, bill)) 23 | .collect(Collectors.toList()); 24 | } 25 | 26 | public static BillItem fromDTO(BillItemDTO dto, Bill bill) { 27 | final BillItem billItem = new BillItem(); 28 | billItem.setId(dto.getId()); 29 | billItem.setBill(bill); 30 | billItem.setDataTime(dto.getDataTime()); 31 | billItem.setDescription(dto.getDescription()); 32 | billItem.setOriginNumber(dto.getOriginNumber()); 33 | billItem.setDestinationNumber(dto.getDestinationNumber()); 34 | billItem.setDuration(dto.getDuration()); 35 | billItem.setValue(dto.getValue()); 36 | billItem.setType(dto.getType()); 37 | 38 | return billItem; 39 | } 40 | 41 | public static List toDTO(List items) { 42 | return items.stream().map(BillItemMapper::toDTO).collect(Collectors.toList()); 43 | } 44 | 45 | public static BillItemDTO toDTO(BillItem billItem) { 46 | final BillItemDTO dto = new BillItemDTO(); 47 | dto.setId(billItem.getId()); 48 | dto.setDataTime(billItem.getDataTime()); 49 | dto.setDescription(billItem.getDescription()); 50 | dto.setOriginNumber(billItem.getOriginNumber()); 51 | dto.setDestinationNumber(billItem.getDestinationNumber()); 52 | dto.setDuration(billItem.getDuration()); 53 | dto.setValue(billItem.getValue()); 54 | dto.setType(billItem.getType()); 55 | 56 | return dto; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/br/com/emmanuelneri/controller/BillFileControllerTest.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.controller; 2 | 3 | import br.com.emmanuelneri.model.BillFile; 4 | import br.com.emmanuelneri.service.BillFileService; 5 | import br.com.emmanuelneri.test.AbstractWebTest; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import org.junit.Test; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.mock.mockito.MockBean; 10 | import org.springframework.http.MediaType; 11 | import org.springframework.test.web.servlet.MockMvc; 12 | 13 | import javax.inject.Inject; 14 | import java.util.Arrays; 15 | import java.util.List; 16 | 17 | import static org.mockito.BDDMockito.given; 18 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 19 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 20 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 21 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 22 | 23 | 24 | public class BillFileControllerTest extends AbstractWebTest { 25 | 26 | @Autowired 27 | private MockMvc mockMvc; 28 | 29 | @MockBean 30 | private BillFileService billFileService; 31 | 32 | @Inject 33 | private ObjectMapper objectMapper; 34 | 35 | @Test 36 | public void findAll() throws Exception { 37 | final List billFiles = Arrays.asList(new BillFile("content")); 38 | 39 | given(billFileService.findAll()) 40 | .willReturn(billFiles); 41 | 42 | mockMvc.perform(get("/files/bills") 43 | .accept(MediaType.APPLICATION_JSON)) 44 | .andExpect(status().isOk()) 45 | .andExpect(content().string(objectMapper.writeValueAsString(billFiles))); 46 | } 47 | 48 | @Test 49 | public void save() throws Exception { 50 | final BillFile billFile = new BillFile("content"); 51 | 52 | mockMvc.perform(post("/files/bills") 53 | .contentType(MediaType.APPLICATION_JSON) 54 | .content(objectMapper.writeValueAsBytes(billFile)) 55 | .accept(MediaType.APPLICATION_JSON)) 56 | .andExpect(status().isOk()); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/repository/impl/BillRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.repository.impl; 2 | 3 | import br.com.emmanuelneri.dto.BillDTO; 4 | import br.com.emmanuelneri.mapper.BillMapper; 5 | import br.com.emmanuelneri.model.Bill; 6 | import br.com.emmanuelneri.model.QBill; 7 | import br.com.emmanuelneri.repository.BillRepositoryCustom; 8 | import br.com.emmanuelneri.to.BillSearchTO; 9 | import com.querydsl.jpa.JPQLQuery; 10 | import com.querydsl.jpa.impl.JPAQuery; 11 | import org.springframework.data.domain.Page; 12 | import org.springframework.data.domain.PageImpl; 13 | import org.springframework.data.domain.Pageable; 14 | 15 | import javax.persistence.EntityManager; 16 | import javax.persistence.PersistenceContext; 17 | import java.util.List; 18 | 19 | public class BillRepositoryImpl implements BillRepositoryCustom { 20 | 21 | @PersistenceContext 22 | private EntityManager entityManager; 23 | 24 | @Override 25 | public Page findAll(BillSearchTO searchTO) { 26 | final long total = count(searchTO); 27 | final List bills = BillMapper.toDTO(find(searchTO)); 28 | return new PageImpl(bills, searchTO.getPageable(), total); 29 | } 30 | 31 | private List find(BillSearchTO searchTO) { 32 | final JPQLQuery jpqlQuery = findAllBaseQuery(searchTO); 33 | 34 | final Pageable pageable = searchTO.getPageable(); 35 | jpqlQuery.offset((long) pageable.getOffset()); 36 | jpqlQuery.limit((long) pageable.getPageSize()); 37 | 38 | jpqlQuery.orderBy(QBill.bill.yearMonth.desc(), QBill.bill.id.desc()); 39 | 40 | return jpqlQuery.fetch(); 41 | } 42 | 43 | private long count(BillSearchTO searchTO) { 44 | final JPQLQuery jpqlQuery = findAllBaseQuery(searchTO); 45 | return jpqlQuery 46 | .distinct() 47 | .fetchCount(); 48 | } 49 | 50 | private JPQLQuery findAllBaseQuery(BillSearchTO searchTO) { 51 | final QBill qBill = QBill.bill; 52 | 53 | final JPQLQuery jpaQuery = new JPAQuery(entityManager) 54 | .from(qBill) 55 | .join(qBill.carrier).fetchJoin() 56 | .join(qBill.customer).fetchJoin() 57 | .join(qBill.items).fetchJoin(); 58 | 59 | jpaQuery.where(searchTO.toPredicate()); 60 | 61 | return jpaQuery; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/test/java/br/com/emmanuelneri/service/CustomerServiceTest.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.service; 2 | 3 | import br.com.emmanuelneri.model.Customer; 4 | import br.com.emmanuelneri.test.AbstractIntegrationTest; 5 | import br.com.emmanuelneri.to.CustomerSearchTO; 6 | import org.junit.Assert; 7 | import org.junit.Test; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.data.domain.Page; 10 | 11 | import java.util.List; 12 | 13 | import static org.hamcrest.Matchers.*; 14 | import static org.hamcrest.beans.HasPropertyWithValue.hasProperty; 15 | 16 | public class CustomerServiceTest extends AbstractIntegrationTest { 17 | 18 | private static final int NUMBER_OF_CUSTOMER_INSERT_BY_FLYWAY = 5; 19 | private static final int SIZE_PAGINATION = 3; 20 | 21 | @Autowired 22 | private CustomerService customerService; 23 | 24 | @Test 25 | public void findPaged() { 26 | final Page customerPaged = customerService.findPaginable(1, SIZE_PAGINATION); 27 | Assert.assertEquals(NUMBER_OF_CUSTOMER_INSERT_BY_FLYWAY, customerPaged.getTotalElements()); 28 | Assert.assertEquals(2, customerPaged.getTotalPages()); 29 | Assert.assertEquals(2, customerPaged.getContent().size()); 30 | 31 | Assert.assertThat(customerPaged.getContent(), contains( 32 | allOf(hasProperty("name", is("Customer 2"))), 33 | allOf(hasProperty("name", is("Customer 1"))) 34 | )); 35 | } 36 | 37 | @Test 38 | public void findPagedByName() { 39 | final CustomerSearchTO searchByName = new CustomerSearchTO(null, "Customer 1"); 40 | 41 | final Page customerPaged = customerService.searchPagenable(searchByName); 42 | Assert.assertEquals(1, customerPaged.getTotalElements()); 43 | Assert.assertEquals(1, customerPaged.getTotalPages()); 44 | Assert.assertEquals(1, customerPaged.getContent().size()); 45 | 46 | Assert.assertThat(customerPaged.getContent(), contains( 47 | allOf(hasProperty("name", is("Customer 1"))) 48 | )); 49 | } 50 | 51 | @Test 52 | public void findByIdWithDynamicParams() { 53 | final CustomerSearchTO searchById = new CustomerSearchTO(1L, null); 54 | 55 | final List customers = customerService.search(searchById); 56 | Assert.assertEquals(1, customers.size()); 57 | 58 | Assert.assertThat(customers, contains( 59 | allOf(hasProperty("name", is("Customer 1"))) 60 | )); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/controller/BillController.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.controller; 2 | 3 | import br.com.emmanuelneri.dto.BillDTO; 4 | import br.com.emmanuelneri.dto.BillResumeDTO; 5 | import br.com.emmanuelneri.mapper.BillMapper; 6 | import br.com.emmanuelneri.service.BillService; 7 | import br.com.emmanuelneri.to.BillSearchTO; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.data.domain.Page; 10 | import org.springframework.web.bind.annotation.PathVariable; 11 | import org.springframework.web.bind.annotation.RequestBody; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | import org.springframework.web.bind.annotation.RequestMethod; 14 | import org.springframework.web.bind.annotation.RestController; 15 | 16 | import java.time.YearMonth; 17 | import java.util.List; 18 | 19 | @RestController 20 | @RequestMapping("/bills") 21 | public class BillController { 22 | 23 | @Autowired 24 | private BillService billService; 25 | 26 | @RequestMapping(method = RequestMethod.GET) 27 | public List findAll() { 28 | return BillMapper.toDTO(billService.findAll()); 29 | } 30 | 31 | @RequestMapping(value = "/search", method = RequestMethod.POST) 32 | public Page search(@RequestBody BillSearchTO searchTO) { 33 | return billService.search(searchTO); 34 | } 35 | 36 | @RequestMapping(value = "/byUk/{customerId}/{identifier}/{yearMonth}", method = RequestMethod.GET) 37 | public BillDTO findByUk(@PathVariable("customerId") Long customerId, @PathVariable("identifier") String identifier, @PathVariable("yearMonth") YearMonth yearMonth) { 38 | return BillMapper.toDTO(billService.findByUk(customerId, identifier, yearMonth)); 39 | } 40 | 41 | @RequestMapping(value = "/{id}", method = RequestMethod.GET) 42 | public BillDTO findById(@PathVariable("id") Long id) { 43 | return BillMapper.toDTO(billService.findById(id)); 44 | } 45 | 46 | @RequestMapping(method = RequestMethod.POST) 47 | public void save(@RequestBody BillDTO billDTO) { 48 | billService.save(BillMapper.fromDTO(billDTO)); 49 | } 50 | 51 | @RequestMapping(value = "/{id}", method = RequestMethod.DELETE) 52 | public void delete(@PathVariable("id") Long id) { 53 | billService.delete(id); 54 | } 55 | 56 | @RequestMapping(value = "/resume/{id}", method = RequestMethod.GET) 57 | public BillResumeDTO resume(@PathVariable("id") Long id) { 58 | return billService.findResume(id); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/model/BillItem.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Builder; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | import lombok.Setter; 9 | import org.hibernate.validator.constraints.NotBlank; 10 | import org.hibernate.validator.constraints.NotEmpty; 11 | 12 | import javax.persistence.Column; 13 | import javax.persistence.Entity; 14 | import javax.persistence.EnumType; 15 | import javax.persistence.Enumerated; 16 | import javax.persistence.FetchType; 17 | import javax.persistence.GeneratedValue; 18 | import javax.persistence.GenerationType; 19 | import javax.persistence.Id; 20 | import javax.persistence.JoinColumn; 21 | import javax.persistence.ManyToOne; 22 | import javax.persistence.NamedQueries; 23 | import javax.persistence.NamedQuery; 24 | import javax.persistence.SequenceGenerator; 25 | import javax.persistence.Table; 26 | import javax.validation.constraints.NotNull; 27 | import javax.validation.constraints.Size; 28 | import java.math.BigDecimal; 29 | import java.time.LocalDateTime; 30 | 31 | @Entity 32 | @Table(name = "bill_item") 33 | @Getter @Setter 34 | @Builder 35 | @AllArgsConstructor 36 | @NoArgsConstructor 37 | @NamedQueries(value = { 38 | @NamedQuery(name="BillItem.summarizeByBillId", query = "select new br.com.emmanuelneri.dto.BillSummaryDTO(count(bi.id), sum(bi.duration), sum(bi.value)) from BillItem bi where bi.bill.id = :billId"), 39 | @NamedQuery(name="BillItem.numberGreaterUse", query = "select new br.com.emmanuelneri.dto.NumberUseDTO(bi.originNumber, sum(bi.duration)) from BillItem bi where bi.bill.id = :billId group by bi.originNumber order by sum(bi.duration) desc"), 40 | }) 41 | public class BillItem { 42 | 43 | @Id 44 | @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "bill_item_id_seq") 45 | @SequenceGenerator(name = "bill_item_id_seq", sequenceName = "bill_item_id_seq", allocationSize = 1) 46 | private Long id; 47 | 48 | @NotNull 49 | @ManyToOne(optional = false, fetch = FetchType.LAZY) 50 | @JoinColumn(name = "bill_id") 51 | private Bill bill; 52 | 53 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") 54 | @NotNull 55 | @Column(name = "date_time") 56 | private LocalDateTime dataTime; 57 | 58 | @NotBlank 59 | @Size(max = 200) 60 | private String description; 61 | 62 | @NotEmpty 63 | @Size(max = 20) 64 | @Column(name = "origin_number") 65 | private String originNumber; 66 | 67 | @Size(max = 20) 68 | @Column(name = "destination_number") 69 | private String destinationNumber; 70 | 71 | private Long duration; 72 | 73 | @NotNull 74 | private BigDecimal value; 75 | 76 | @NotNull 77 | @Enumerated(EnumType.STRING) 78 | private ItemType type; 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/service/BillService.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.service; 2 | 3 | import br.com.emmanuelneri.dto.BillDTO; 4 | import br.com.emmanuelneri.dto.BillResumeDTO; 5 | import br.com.emmanuelneri.dto.BillSummaryDTO; 6 | import br.com.emmanuelneri.dto.NumberUseDTO; 7 | import br.com.emmanuelneri.model.Bill; 8 | import br.com.emmanuelneri.repository.BillItemRepository; 9 | import br.com.emmanuelneri.repository.BillRepository; 10 | import br.com.emmanuelneri.to.BillSearchTO; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.data.domain.Page; 14 | import org.springframework.data.domain.PageRequest; 15 | import org.springframework.stereotype.Service; 16 | import org.springframework.transaction.annotation.Transactional; 17 | 18 | import java.time.YearMonth; 19 | import java.util.List; 20 | import java.util.concurrent.CompletableFuture; 21 | import java.util.concurrent.ExecutionException; 22 | 23 | @Service 24 | @Slf4j 25 | public class BillService { 26 | 27 | @Autowired 28 | private BillRepository billRepository; 29 | 30 | @Autowired 31 | private BillItemRepository billItemRepository; 32 | 33 | public Bill save(Bill bill) { 34 | return billRepository.save(bill); 35 | } 36 | 37 | public List findAll() { 38 | return billRepository.findAll(); 39 | } 40 | 41 | public Bill findByUk(Long customerId, String identifier, YearMonth yearMonth) { 42 | return billRepository.findByCustomerIdAndIdentifierAndYearMonth(customerId, identifier, yearMonth); 43 | } 44 | 45 | public Bill findById(Long id) { 46 | return billRepository.getOne(id); 47 | } 48 | 49 | @Transactional 50 | public void delete(Long id) { 51 | billItemRepository.deleteByBillItem(id); 52 | billRepository.delete(id); 53 | } 54 | 55 | public Page search(BillSearchTO searchTO) { 56 | return billRepository.findAll(searchTO); 57 | } 58 | 59 | public BillResumeDTO findResume(Long billId) { 60 | 61 | final CompletableFuture billFuture = CompletableFuture.supplyAsync(() -> billRepository.findOne(billId)); 62 | final CompletableFuture summaryDTOCompletableFuture = CompletableFuture.supplyAsync(() -> billItemRepository.summarizeByBillId(billId)); 63 | final CompletableFuture> most10numbersUseFuture = CompletableFuture.supplyAsync(() -> billItemRepository.numberGreaterUse(billId, new PageRequest(0, 10))); 64 | 65 | try { 66 | final Bill bill = billFuture.get(); 67 | final BillSummaryDTO summary = summaryDTOCompletableFuture.get(); 68 | final List most10numbersUse = most10numbersUseFuture.get(); 69 | 70 | return new BillResumeDTO(bill.getIdentifier(), bill.getCustomer().getName(), summary, most10numbersUse); 71 | } catch (InterruptedException | ExecutionException e) { 72 | log.error("findResume process error" + e); 73 | throw new RuntimeException(); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/model/Bill.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | import org.hibernate.validator.constraints.NotEmpty; 9 | 10 | import javax.persistence.CascadeType; 11 | import javax.persistence.Column; 12 | import javax.persistence.Entity; 13 | import javax.persistence.GeneratedValue; 14 | import javax.persistence.GenerationType; 15 | import javax.persistence.Id; 16 | import javax.persistence.JoinColumn; 17 | import javax.persistence.ManyToOne; 18 | import javax.persistence.OneToMany; 19 | import javax.persistence.SequenceGenerator; 20 | import javax.persistence.Table; 21 | import javax.persistence.UniqueConstraint; 22 | import javax.validation.constraints.NotNull; 23 | import javax.validation.constraints.Size; 24 | import java.math.BigDecimal; 25 | import java.time.YearMonth; 26 | import java.util.ArrayList; 27 | import java.util.List; 28 | import java.util.Objects; 29 | 30 | @Entity 31 | @Table(uniqueConstraints = @UniqueConstraint(name = "bill_uk", columnNames = {"customer_id", "identifier", "year_month"})) 32 | @Getter @Setter 33 | @Builder 34 | @AllArgsConstructor 35 | @NoArgsConstructor 36 | public class Bill { 37 | 38 | @Id 39 | @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "bill_id_seq") 40 | @SequenceGenerator(name = "bill_id_seq", sequenceName = "bill_id_seq", allocationSize = 1) 41 | private Long id; 42 | 43 | @NotNull(message = "Customer is required") 44 | @ManyToOne(optional = false) 45 | @JoinColumn(name = "customer_id") 46 | private Customer customer; 47 | 48 | @NotNull(message = "Carrier is required") 49 | @ManyToOne(optional = false) 50 | @JoinColumn(name = "carrier_id") 51 | private Carrier carrier; 52 | 53 | @NotEmpty(message = "Bill identifier is required") 54 | @Size(max = 50, message = "The maximum length of identifier is {max} caracters ") 55 | private String identifier; 56 | 57 | @NotNull(message = "Year Month is required") 58 | @Column(name = "year_month") 59 | private YearMonth yearMonth; 60 | 61 | @NotNull(message = "Bill items is required") 62 | @Size(min = 1, message = "The Bill needs at least one item") 63 | @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "bill") 64 | private List items = new ArrayList<>(); 65 | 66 | @NotNull 67 | private BigDecimal total = BigDecimal.ZERO; 68 | 69 | 70 | @Override 71 | public boolean equals(Object o) { 72 | if (this == o) return true; 73 | if (!(o instanceof Bill)) return false; 74 | Bill bill = (Bill) o; 75 | return Objects.equals(customer, bill.customer) && 76 | Objects.equals(carrier, bill.carrier) && 77 | Objects.equals(identifier, bill.identifier) && 78 | Objects.equals(yearMonth, bill.yearMonth); 79 | } 80 | 81 | @Override 82 | public int hashCode() { 83 | return Objects.hash(customer, carrier, identifier, yearMonth); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/br/com/emmanuelneri/controller/GlobalExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.controller; 2 | 3 | import br.com.emmanuelneri.exception.BusinessException; 4 | import br.com.emmanuelneri.exception.ExceptionUtil; 5 | import br.com.emmanuelneri.to.ExceptionTO; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.dao.DataIntegrityViolationException; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.transaction.TransactionSystemException; 11 | import org.springframework.web.bind.annotation.ControllerAdvice; 12 | import org.springframework.web.bind.annotation.ExceptionHandler; 13 | 14 | import javax.persistence.RollbackException; 15 | import javax.validation.ConstraintViolationException; 16 | import java.util.Collections; 17 | import java.util.Set; 18 | import java.util.stream.Collectors; 19 | 20 | @Slf4j 21 | @ControllerAdvice 22 | public class GlobalExceptionHandler { 23 | 24 | @ExceptionHandler(BusinessException.class) 25 | public ResponseEntity> handleError(BusinessException ex) { 26 | return ResponseEntity.status(HttpStatus.BAD_REQUEST) 27 | .body(Collections.singleton(new ExceptionTO(ex.getMessage()))); 28 | } 29 | 30 | @ExceptionHandler(ConstraintViolationException.class) 31 | public ResponseEntity> handleError(ConstraintViolationException ex) { 32 | final Set erros = ex.getConstraintViolations().stream() 33 | .map(c -> new ExceptionTO(c.getMessage())) 34 | .collect(Collectors.toSet()); 35 | return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(erros); 36 | } 37 | 38 | @ExceptionHandler(DataIntegrityViolationException.class) 39 | public ResponseEntity> handleError(DataIntegrityViolationException divex) { 40 | return ExceptionUtil.isUniqueConstraintError(divex) 41 | ? ResponseEntity.status(HttpStatus.BAD_REQUEST) 42 | .body(Collections.singleton(new ExceptionTO("This registry already exists in the database."))) 43 | : ResponseEntity.status(HttpStatus.BAD_REQUEST) 44 | .body(Collections.singleton(new ExceptionTO(divex.getMessage()))); 45 | } 46 | 47 | @ExceptionHandler(TransactionSystemException.class) 48 | public ResponseEntity> handleError(TransactionSystemException tse) { 49 | if (tse.getCause() != null && tse.getCause() instanceof RollbackException) { 50 | final RollbackException re = (RollbackException) tse.getCause(); 51 | 52 | if (re.getCause() != null && re.getCause() instanceof ConstraintViolationException) { 53 | return handleError((ConstraintViolationException) re.getCause()); 54 | } 55 | } 56 | throw tse; 57 | } 58 | 59 | @ExceptionHandler(Exception.class) 60 | public ResponseEntity handleError(Exception ex) { 61 | log.error("Internal error server", ex); 62 | return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("The system is unavailable."); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/test/java/br/com/emmanuelneri/test/data/BillDataMock.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.test.data; 2 | 3 | import br.com.emmanuelneri.model.Bill; 4 | import br.com.emmanuelneri.model.BillItem; 5 | import br.com.emmanuelneri.model.Carrier; 6 | import br.com.emmanuelneri.model.Customer; 7 | import br.com.emmanuelneri.model.ItemType; 8 | 9 | import java.math.BigDecimal; 10 | import java.time.LocalDateTime; 11 | import java.time.YearMonth; 12 | import java.util.ArrayList; 13 | import java.util.Arrays; 14 | import java.util.List; 15 | 16 | public final class BillDataMock { 17 | 18 | private static final Carrier carrier = Carrier.builder().id(1L).name("Carrier").build(); 19 | private static final Customer customer = Customer.builder().id(1L).name("Customer").build(); 20 | 21 | public static Bill getFakeBill() { 22 | final List itemsBill123 = new ArrayList<>(); 23 | final Bill bill123 = Bill.builder() 24 | .carrier(carrier) 25 | .customer(customer) 26 | .identifier("123") 27 | .yearMonth(YearMonth.now()) 28 | .items(itemsBill123) 29 | .total(BigDecimal.valueOf(1.3)) 30 | .build(); 31 | 32 | itemsBill123.add(BillItem.builder() 33 | .bill(bill123) 34 | .dataTime(LocalDateTime.now()) 35 | .originNumber("4499898484") 36 | .destinationNumber("4499898400") 37 | .duration(100L) 38 | .description("Local call") 39 | .type(ItemType.CALL) 40 | .value(BigDecimal.valueOf(0.3)) 41 | .build()); 42 | 43 | itemsBill123.add(BillItem.builder() 44 | .bill(bill123) 45 | .dataTime(LocalDateTime.now()) 46 | .originNumber("4499898484") 47 | .destinationNumber("4499889400") 48 | .duration(250L) 49 | .description("Another carrier call") 50 | .type(ItemType.CALL) 51 | .value(BigDecimal.valueOf(1)) 52 | .build()); 53 | 54 | return bill123; 55 | } 56 | 57 | public static List getFakeBillList() { 58 | final Bill bill123 = getFakeBill(); 59 | 60 | final List items456 = new ArrayList<>(); 61 | final Bill bill456 = Bill.builder() 62 | .carrier(carrier) 63 | .customer(customer) 64 | .identifier("456") 65 | .yearMonth(YearMonth.now().plusMonths(1)) 66 | .items(items456) 67 | .total(BigDecimal.valueOf(1.3)) 68 | .build(); 69 | 70 | items456.add(BillItem.builder() 71 | .bill(bill456) 72 | .dataTime(LocalDateTime.now()) 73 | .originNumber("4499898484") 74 | .destinationNumber("4499088299") 75 | .duration(400L) 76 | .description("Local call") 77 | .type(ItemType.CALL) 78 | .value(BigDecimal.valueOf(10)) 79 | .build()); 80 | 81 | return Arrays.asList(bill123, bill456); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/test/java/br/com/emmanuelneri/controller/CustomerControllerTest.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.controller; 2 | 3 | import br.com.emmanuelneri.model.Customer; 4 | import br.com.emmanuelneri.service.CustomerService; 5 | import br.com.emmanuelneri.test.AbstractWebTest; 6 | import br.com.emmanuelneri.to.CustomerSearchTO; 7 | import com.fasterxml.jackson.databind.ObjectMapper; 8 | import org.junit.Test; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.test.mock.mockito.MockBean; 11 | import org.springframework.http.MediaType; 12 | import org.springframework.test.web.servlet.MockMvc; 13 | 14 | import javax.inject.Inject; 15 | 16 | import java.util.Arrays; 17 | import java.util.List; 18 | 19 | import static org.mockito.BDDMockito.given; 20 | import static org.mockito.Matchers.any; 21 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 22 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 23 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 24 | 25 | public class CustomerControllerTest extends AbstractWebTest { 26 | 27 | @Autowired 28 | private MockMvc mockMvc; 29 | 30 | @MockBean 31 | private CustomerService customerService; 32 | 33 | @Inject 34 | private ObjectMapper objectMapper; 35 | 36 | @Test 37 | public void findAll() throws Exception { 38 | final List customers = Arrays.asList(new Customer()); 39 | 40 | given(customerService.findAll()) 41 | .willReturn(customers); 42 | 43 | mockMvc.perform(get("/customers") 44 | .accept(MediaType.APPLICATION_JSON)) 45 | .andExpect(status().isOk()) 46 | .andExpect(content().string(objectMapper.writeValueAsString(customers))); 47 | } 48 | 49 | @Test 50 | public void searchWithoutParams() throws Exception { 51 | final List customers = Arrays.asList(new Customer()); 52 | 53 | given(customerService.search(any(CustomerSearchTO.class))) 54 | .willReturn(customers); 55 | 56 | mockMvc.perform(get("/customers/search") 57 | .accept(MediaType.APPLICATION_JSON)) 58 | .andExpect(status().isOk()) 59 | .andExpect(content().string(objectMapper.writeValueAsString(customers))); 60 | } 61 | 62 | @Test 63 | public void searchOneParams() throws Exception { 64 | final List customers = Arrays.asList(new Customer()); 65 | 66 | given(customerService.search(any(CustomerSearchTO.class))) 67 | .willReturn(customers); 68 | 69 | mockMvc.perform(get("/customers/search?id=1") 70 | .accept(MediaType.APPLICATION_JSON)) 71 | .andExpect(status().isOk()) 72 | .andExpect(content().string(objectMapper.writeValueAsString(customers))); 73 | } 74 | 75 | @Test 76 | public void searchAllParams() throws Exception { 77 | final List customers = Arrays.asList(new Customer()); 78 | 79 | given(customerService.search(any(CustomerSearchTO.class))) 80 | .willReturn(customers); 81 | 82 | mockMvc.perform(get("/customers/search") 83 | .param("id", "1") 84 | .param("name", "Customer") 85 | .accept(MediaType.APPLICATION_JSON)) 86 | .andExpect(status().isOk()) 87 | .andExpect(content().string(objectMapper.writeValueAsString(customers))); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/test/java/br/com/emmanuelneri/controller/GlobalExceptionHandlerTest.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.controller; 2 | 3 | import br.com.emmanuelneri.dto.BillDTO; 4 | import br.com.emmanuelneri.exception.BusinessException; 5 | import br.com.emmanuelneri.model.Bill; 6 | import br.com.emmanuelneri.service.BillService; 7 | import br.com.emmanuelneri.test.AbstractWebTest; 8 | import br.com.emmanuelneri.test.data.ConstraintViolationMock; 9 | import br.com.emmanuelneri.to.ExceptionTO; 10 | import com.fasterxml.jackson.databind.ObjectMapper; 11 | import org.junit.Test; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.boot.test.mock.mockito.MockBean; 14 | import org.springframework.dao.DataIntegrityViolationException; 15 | import org.springframework.http.MediaType; 16 | import org.springframework.test.web.servlet.MockMvc; 17 | 18 | import javax.inject.Inject; 19 | 20 | import java.util.Collections; 21 | 22 | import static org.mockito.BDDMockito.given; 23 | import static org.mockito.Matchers.any; 24 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 25 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 26 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 27 | 28 | public class GlobalExceptionHandlerTest extends AbstractWebTest { 29 | 30 | @Autowired 31 | private MockMvc mockMvc; 32 | 33 | @MockBean 34 | private BillService billService; 35 | 36 | @Inject 37 | private ObjectMapper objectMapper; 38 | 39 | @Test 40 | public void businessExceptionTest() throws Exception { 41 | given(billService.save(any(Bill.class))) 42 | .willThrow(new BusinessException("Erro de validação")); 43 | 44 | mockMvc.perform(post("/bills") 45 | .contentType(MediaType.APPLICATION_JSON) 46 | .content(objectMapper.writeValueAsBytes(new BillDTO())) 47 | .accept(MediaType.APPLICATION_JSON)) 48 | .andExpect(status().isBadRequest()) 49 | .andExpect(content().string(objectMapper.writeValueAsString( 50 | Collections.singleton(new ExceptionTO("Erro de validação"))))); 51 | } 52 | 53 | @Test 54 | public void constraintViolationExceptionTest() throws Exception { 55 | given(billService.save(any(Bill.class))) 56 | .willThrow(ConstraintViolationMock.getFakeConstraintViolationException()); 57 | 58 | mockMvc.perform(post("/bills") 59 | .contentType(MediaType.APPLICATION_JSON) 60 | .content(objectMapper.writeValueAsBytes(new BillDTO())) 61 | .accept(MediaType.APPLICATION_JSON)) 62 | .andExpect(status().isBadRequest()) 63 | .andExpect(content().string(objectMapper.writeValueAsString( 64 | Collections.singleton(new ExceptionTO("The identifier is required"))))); 65 | } 66 | 67 | @Test 68 | public void dataIntegrityViolationExceptionTest() throws Exception { 69 | given(billService.save(any(Bill.class))) 70 | .willThrow(new DataIntegrityViolationException("This registry already exists in the database.")); 71 | 72 | mockMvc.perform(post("/bills") 73 | .contentType(MediaType.APPLICATION_JSON) 74 | .content(objectMapper.writeValueAsBytes(new BillDTO())) 75 | .accept(MediaType.APPLICATION_JSON)) 76 | .andExpect(status().isBadRequest()) 77 | .andExpect(content().string(objectMapper.writeValueAsString( 78 | Collections.singleton(new ExceptionTO("This registry already exists in the database."))))); 79 | } 80 | 81 | @Test 82 | public void unexpectedExceptionTest() throws Exception { 83 | given(billService.save(any(Bill.class))) 84 | .willThrow(new RuntimeException()); 85 | 86 | mockMvc.perform(post("/bills") 87 | .contentType(MediaType.APPLICATION_JSON) 88 | .content(objectMapper.writeValueAsBytes(new BillDTO())) 89 | .accept(MediaType.APPLICATION_JSON)) 90 | .andExpect(status().is5xxServerError()) 91 | .andExpect(content().string("The system is unavailable.")); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/test/java/br/com/emmanuelneri/controller/BillControllerTest.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.controller; 2 | 3 | import br.com.emmanuelneri.dto.BillDTO; 4 | import br.com.emmanuelneri.mapper.BillMapper; 5 | import br.com.emmanuelneri.model.Bill; 6 | import br.com.emmanuelneri.service.BillService; 7 | import br.com.emmanuelneri.test.AbstractWebTest; 8 | import br.com.emmanuelneri.test.data.BillDataMock; 9 | import br.com.emmanuelneri.to.BillSearchTO; 10 | import com.fasterxml.jackson.databind.ObjectMapper; 11 | import org.junit.Test; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.boot.test.mock.mockito.MockBean; 14 | import org.springframework.data.domain.Page; 15 | import org.springframework.data.domain.PageImpl; 16 | import org.springframework.data.domain.PageRequest; 17 | import org.springframework.http.MediaType; 18 | import org.springframework.test.web.servlet.MockMvc; 19 | 20 | import javax.inject.Inject; 21 | import java.time.YearMonth; 22 | import java.util.List; 23 | 24 | import static org.mockito.BDDMockito.given; 25 | import static org.mockito.Matchers.any; 26 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 27 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 28 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; 29 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 30 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 31 | 32 | public class BillControllerTest extends AbstractWebTest { 33 | 34 | @Autowired 35 | private MockMvc mockMvc; 36 | 37 | @MockBean 38 | private BillService billService; 39 | 40 | @Inject 41 | private ObjectMapper objectMapper; 42 | 43 | @Test 44 | public void findAll() throws Exception { 45 | final List bills = BillDataMock.getFakeBillList(); 46 | 47 | given(billService.findAll()).willReturn(bills); 48 | 49 | mockMvc.perform(get("/bills") 50 | .accept(MediaType.APPLICATION_JSON)) 51 | .andExpect(status().isOk()) 52 | .andExpect(content().string(objectMapper.writeValueAsString(BillMapper.toDTO(bills)))); 53 | } 54 | 55 | @Test 56 | public void search() throws Exception { 57 | final List bills = BillDataMock.getFakeBillList(); 58 | 59 | final Page billsPaged = new PageImpl<>(BillMapper.toDTO(bills), 60 | new PageRequest(1, 10), 2L); 61 | 62 | given(billService.search(any(BillSearchTO.class))) 63 | .willReturn(billsPaged); 64 | 65 | mockMvc.perform(post("/bills/search") 66 | .contentType(MediaType.APPLICATION_JSON) 67 | .content(objectMapper.writeValueAsBytes(new BillSearchTO())) 68 | .accept(MediaType.APPLICATION_JSON)) 69 | .andExpect(status().isOk()) 70 | .andExpect(content().string(objectMapper.writeValueAsString(billsPaged))); 71 | } 72 | 73 | @Test 74 | public void findByUk() throws Exception { 75 | final Bill bill = BillDataMock.getFakeBill(); 76 | 77 | given(billService.findByUk(any(Long.class), any(String.class), any(YearMonth.class))) 78 | .willReturn(bill); 79 | 80 | mockMvc.perform(get("/bills/byUk/1/123/2017-06") 81 | .accept(MediaType.APPLICATION_JSON)) 82 | .andExpect(status().isOk()) 83 | .andExpect(content().string(objectMapper.writeValueAsString(BillMapper.toDTO(bill)))); 84 | } 85 | 86 | @Test 87 | public void findById() throws Exception { 88 | final Bill bill = BillDataMock.getFakeBill(); 89 | 90 | given(billService.findById(1L)) 91 | .willReturn(bill); 92 | 93 | mockMvc.perform(get("/bills/1") 94 | .accept(MediaType.APPLICATION_JSON)) 95 | .andExpect(status().isOk()) 96 | .andExpect(content().string(objectMapper.writeValueAsString(BillMapper.toDTO(bill)))); 97 | } 98 | 99 | @Test 100 | public void save() throws Exception { 101 | final Bill bill = BillDataMock.getFakeBill(); 102 | 103 | mockMvc.perform(post("/bills") 104 | .contentType(MediaType.APPLICATION_JSON) 105 | .content(objectMapper.writeValueAsBytes(BillMapper.toDTO(bill))) 106 | .accept(MediaType.APPLICATION_JSON)) 107 | .andExpect(status().isOk()); 108 | } 109 | 110 | @Test 111 | public void deleteBill() throws Exception { 112 | mockMvc.perform(delete("/bills/1") 113 | .accept(MediaType.APPLICATION_JSON)) 114 | .andExpect(status().isOk()); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Projeto utilizado na exemplificação da palestra no TDC São Paulo, segue o link da apresentação: https://pt.slideshare.net/emmanuelnerisouza/construindo-apis-de-forma-produtiva-com-spring-boot-spring-data-e-spring-mvc 2 | 3 | # Visão geral 4 | 5 | O projeto é uma aplicação back-end com objetivo de demonstrar a produtividade de construir APIs utilizando os frameworks [Spring Boot](https://projects.spring.io/spring-boot), [Spring MVC](https://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html) e [Spring Data](http://projects.spring.io/spring-data) em conjunto. 6 | 7 | ## Tecnologias 8 | 9 | - [Spring Boot](https://projects.spring.io/spring-boot) é uma ferramenta que simplifica a configuração e execução de aplicações Java stand-alone, com conceitos de dependências “starters”, auto configuração e servlet container embutidos é proporcionado uma grande produtividade desde o start-up da aplicação até sua ida a produção. 10 | 11 | - [Spring MVC](https://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html) é um framework já consolidado no mercado, que a partir da versão fornece mecanismos simplificados para a criação de APIs RESTful através de anotação, além disso, também possui recursos de serialização e deserialização de objetos de forma transparente 12 | 13 | - [Spring Data](http://projects.spring.io/spring-data/) é um framework que abstrai o acesso ao modelo de dados, independente a tecnologia de base de dados. 14 | 15 | 16 | # Setup da aplicação (local) 17 | 18 | ## Pré-requisito 19 | 20 | Antes de rodar a aplicação é preciso garantir que as seguintes dependências estejam corretamente instaladas: 21 | ``` 22 | Java 8 23 | PostgreSQL 9.6 24 | MongoDB 3.5.6 25 | Maven 3.3.3 26 | ``` 27 | 28 | ## Preparando ambiente 29 | 30 | É necessário a criação da base de dados relacional no Postgres 31 | 32 | ``` 33 | CREATE DATABASE "productivity-with-spring"; 34 | ``` 35 | 36 | Para os testes de integração também é necessario criar uma base de dados para os testes não interferirem na base de desenvolvimento. 37 | ``` 38 | CREATE DATABASE "productivity-with-spring-test"; 39 | ``` 40 | 41 | Observação: No MongoDB não é preciso criar as bases de dados, pois a aplicação cria caso não exista. 42 | 43 | ## Instalação da aplicação 44 | 45 | Primeiramente, faça o clone do repositório: 46 | ``` 47 | https://github.com/emmanuelneri/productivity-with-spring.git 48 | ``` 49 | Feito isso, acesse o projeto: 50 | ``` 51 | cd productivity-with-spring 52 | ``` 53 | É preciso compilar o código e baixar as dependências do projeto: 54 | ``` 55 | mvn clean package 56 | ``` 57 | Finalizado esse passo, vamos iniciar a aplicação: 58 | ``` 59 | mvn spring-boot:run 60 | ``` 61 | Pronto. A aplicação está disponível em http://localhost:8080 62 | ``` 63 | Tomcat started on port(s): 8080 (http) 64 | Started AppConfig in xxxx seconds (JVM running for xxxx) 65 | ``` 66 | 67 | # Setup da aplicação com docker 68 | 69 | ## Pré-requisito 70 | 71 | Antes de rodar a aplicação é preciso garantir que as seguintes dependências estejam corretamente instaladas: 72 | 73 | ``` 74 | Java 8 75 | Docker 17.06.0 76 | Maven 3.3.3 77 | ``` 78 | 79 | ## Preparando ambiente 80 | 81 | Criar e executar container do Posgres 82 | ``` 83 | docker run -d \ 84 | --name productivity-postgres \ 85 | -e POSTGRES_DB=productivity-with-spring \ 86 | -e POSTGRES_USER=postgres \ 87 | -e POSTGRES_PASSWORD=postgres \ 88 | postgres:9.6 89 | ``` 90 | 91 | Criar e executar container do MongoDB 92 | ``` 93 | docker run -d \ 94 | --name productivity-mongodb \ 95 | mongo:3.5 96 | ``` 97 | 98 | ## Instalação da aplicação 99 | 100 | Baixar as dependência e criar imagem da aplicação 101 | 102 | ``` 103 | mvn clean package -Dmaven.test.skip=true dockerfile:build 104 | ``` 105 | 106 | Executar container da aplicação 107 | 108 | ``` 109 | docker run -it \ 110 | --link productivity-postgres \ 111 | --link productivity-mongodb \ 112 | -p 8080:8080 \ 113 | emmanuelneri/productivity-with-spring-app 114 | ``` 115 | 116 | Pronto. A aplicação está disponível em http://localhost:8080 117 | 118 | # APIs 119 | 120 | O projeto disponibiliza algumas APIs em 3 contextos diferentes: Customer, Carriers e BilLs, onde utilizam o padrão Rest de comunicação, produzindo e consumindo arquivos no formato JSON. 121 | 122 | Segue abaixo as APIs disponíveis no projeto: 123 | 124 | #### Customer 125 | 126 | - /customers/search (GET) 127 | - /customers (GET) 128 | - /customers/paged/{page}/{size} (GET) 129 | - /customers/search/pagenable (POST) 130 | - Espera atributos para serem critérios de busca no body da requisição, exemplo: 131 | ``` 132 | { 133 | "name":"Customer" 134 | } 135 | ``` 136 | 137 | #### Carriers 138 | 139 | - /carriers (GET) 140 | 141 | #### Bills 142 | 143 | - /bills/{id} (DELETE) 144 | - /bills (GET) 145 | - /bills/byUk/{customerId}/{identifier}/{yearMonth} (GET) 146 | - /bills/{id} (GET) 147 | - /bills/search (POST) 148 | - Espera atributos para serem critérios de busca no body da requisição, exemplo: 149 | ``` 150 | { 151 | "initYearMonth":"2017-01", 152 | "endYearMonth":"2017-07" 153 | } 154 | ``` 155 | - /bills (POST) 156 | - Espera as informações do modelo de dados Bill, exemplo: 157 | ``` 158 | { 159 | "customer":{ 160 | "id":1, 161 | "name":"Customer 1" 162 | }, 163 | "carrier":{ 164 | "id":1, 165 | "name":"TIM" 166 | }, 167 | "identifier":"29302", 168 | "yearMonth":"2017-07", 169 | "items":[ 170 | { 171 | "dataTime":"2017-07-01 15:30:00", 172 | "description":"Ligação fora do plano", 173 | "originNumber":"4499898484", 174 | "destinationNumber":"1199848248", 175 | "duration":"10", 176 | "value":"50", 177 | "type":"SERVICE" 178 | }, 179 | { 180 | "dataTime":"2017-07-15 00:00:00", 181 | "description":"Pacote", 182 | "originNumber":"4499898484", 183 | "value":"50", 184 | "type":"SERVICE" 185 | } 186 | ], 187 | "total":70.00 188 | } 189 | ``` 190 | 191 | #### File Bills 192 | 193 | - /files/bills (GET) 194 | - /files/bills (POST) 195 | - Espera as informações de Bill como um arquivo, exemplo: 196 | ``` 197 | {"customer":{"id":1,"name":"Customer 1"},"carrier":{"id":1,"name":"TIM"},"identifier":"29302","yearMonth":"2017-07","items":[{"dataTime":"2017-07-01 15:30:00","description":"Ligação fora do plano","originNumber":"4499898484","destinationNumber":"1199848248","duration":"10","value":"50","type":"SERVICE"},{"dataTime":"2017-07-15 00:00:00","description":"Pacote","originNumber":"4499898484","value":"50","type":"SERVICE"}],"total":70.00} 198 | ``` 199 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | br.com.emmanuelneri 8 | productivity-with-spring 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 1.8 13 | 1.5.3.RELEASE 14 | 5.0.12.Final 15 | 9.4.1212 16 | 4.1.2 17 | 4.1.4 18 | 1.16.16 19 | 1.3 20 | 19.0 21 | 2.8.1 22 | 23 | UTF-8 24 | 25 | 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter 30 | ${spring-boot.version} 31 | 32 | org.springframework.boot 33 | spring-boot-starter-actuator 34 | ${spring-boot.version} 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-security 39 | ${spring-boot.version} 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-starter-data-jpa 44 | ${spring-boot.version} 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-starter-web 49 | ${spring-boot.version} 50 | 51 | 52 | org.springframework.boot 53 | spring-boot-starter-data-mongodb 54 | ${spring-boot.version} 55 | 56 | 57 | org.springframework.boot 58 | spring-boot-starter-test 59 | ${spring-boot.version} 60 | test 61 | 62 | 63 | org.springframework.boot 64 | spring-boot-starter-cache 65 | ${spring-boot.version} 66 | 67 | 68 | org.hibernate 69 | hibernate-ehcache 70 | ${hibernate.version} 71 | 72 | 73 | org.hamcrest 74 | hamcrest-all 75 | ${hamcrest.version} 76 | 77 | 78 | 79 | org.projectlombok 80 | lombok 81 | ${lombok.version} 82 | 83 | 84 | org.postgresql 85 | postgresql 86 | ${postgres.version} 87 | 88 | 89 | org.flywaydb 90 | flyway-core 91 | ${flyway.version} 92 | 93 | 94 | 95 | com.querydsl 96 | querydsl-apt 97 | ${queryDsl.version} 98 | 99 | 100 | com.querydsl 101 | querydsl-jpa 102 | ${queryDsl.version} 103 | 104 | 105 | 106 | com.fasterxml.jackson.core 107 | jackson-databind 108 | ${jackson.version} 109 | 110 | 111 | com.fasterxml.jackson.datatype 112 | jackson-datatype-jsr310 113 | ${jackson.version} 114 | 115 | 116 | 117 | com.google.guava 118 | guava 119 | ${guava.version} 120 | 121 | 122 | 123 | 124 | 125 | 126 | org.springframework.boot 127 | spring-boot-maven-plugin 128 | ${spring-boot.version} 129 | 130 | 131 | 132 | repackage 133 | 134 | 135 | 136 | 137 | 138 | maven-compiler-plugin 139 | 3.1 140 | 141 | ${java.version} 142 | ${java.version} 143 | 144 | 145 | 146 | com.spotify 147 | dockerfile-maven-plugin 148 | 1.3.4 149 | 150 | emmanuelneri/${project.artifactId}-app 151 | 152 | 153 | 154 | com.mysema.maven 155 | apt-maven-plugin 156 | 1.1.3 157 | 158 | 159 | generate-sources 160 | 161 | process 162 | 163 | 164 | target/generated-sources 165 | com.querydsl.apt.jpa.JPAAnnotationProcessor 166 | 167 | 168 | 169 | 170 | 171 | org.codehaus.mojo 172 | sonar-maven-plugin 173 | 2.7.1 174 | 175 | 176 | org.jacoco 177 | jacoco-maven-plugin 178 | 179 | true 180 | 181 | 182 | 183 | agent-for-ut 184 | 185 | prepare-agent 186 | 187 | 188 | 189 | agent-for-it 190 | 191 | prepare-agent-integration 192 | 193 | 194 | 195 | jacoco-site 196 | verify 197 | 198 | report 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | org.sonarsource.scanner.maven 209 | sonar-maven-plugin 210 | 3.2 211 | 212 | 213 | org.jacoco 214 | jacoco-maven-plugin 215 | 0.7.9 216 | 217 | 218 | 219 | 220 | 221 | 222 | src/main/resources 223 | true 224 | 225 | 226 | 227 | -------------------------------------------------------------------------------- /src/test/java/br/com/emmanuelneri/service/BillServiceTest.java: -------------------------------------------------------------------------------- 1 | package br.com.emmanuelneri.service; 2 | 3 | import br.com.emmanuelneri.dto.BillDTO; 4 | import br.com.emmanuelneri.model.Bill; 5 | import br.com.emmanuelneri.model.BillItem; 6 | import br.com.emmanuelneri.model.Carrier; 7 | import br.com.emmanuelneri.model.Customer; 8 | import br.com.emmanuelneri.model.ItemType; 9 | import br.com.emmanuelneri.test.AbstractIntegrationTest; 10 | import br.com.emmanuelneri.to.BillSearchTO; 11 | import org.junit.Assert; 12 | import org.junit.Before; 13 | import org.junit.Test; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.data.domain.Page; 16 | import org.springframework.orm.jpa.JpaObjectRetrievalFailureException; 17 | 18 | import java.math.BigDecimal; 19 | import java.time.LocalDateTime; 20 | import java.time.YearMonth; 21 | import java.util.ArrayList; 22 | import java.util.Collections; 23 | import java.util.List; 24 | 25 | import static org.hamcrest.Matchers.*; 26 | import static org.hamcrest.beans.HasPropertyWithValue.hasProperty; 27 | 28 | public class BillServiceTest extends AbstractIntegrationTest { 29 | 30 | @Autowired 31 | private CarrierService carrierService; 32 | 33 | @Autowired 34 | private CustomerService customerService; 35 | 36 | @Autowired 37 | private BillService billService; 38 | 39 | private Carrier carrier; 40 | private Customer customer; 41 | 42 | @Before 43 | public void init() { 44 | carrier = carrierService.save( 45 | Carrier.builder() 46 | .name("Carrier Test") 47 | .build()); 48 | 49 | customer = customerService.save( 50 | Customer.builder() 51 | .name("Customer Test") 52 | .build()); 53 | } 54 | 55 | @Test 56 | public void saveABillWithTwoCalls() { 57 | final List items = new ArrayList<>(); 58 | final Bill bill = Bill.builder() 59 | .carrier(carrier) 60 | .customer(customer) 61 | .identifier("123") 62 | .yearMonth(YearMonth.now()) 63 | .items(items) 64 | .total(BigDecimal.valueOf(1.3)) 65 | .build(); 66 | 67 | items.add(BillItem.builder() 68 | .bill(bill) 69 | .dataTime(LocalDateTime.now()) 70 | .originNumber("4499898484") 71 | .destinationNumber("4499898400") 72 | .duration(100L) 73 | .description("Local call") 74 | .type(ItemType.CALL) 75 | .value(BigDecimal.valueOf(0.3)) 76 | .build()); 77 | 78 | items.add(BillItem.builder() 79 | .bill(bill) 80 | .dataTime(LocalDateTime.now()) 81 | .originNumber("4499898484") 82 | .destinationNumber("4499889400") 83 | .duration(250L) 84 | .description("Another carrier call") 85 | .type(ItemType.CALL) 86 | .value(BigDecimal.valueOf(1)) 87 | .build()); 88 | 89 | final Bill savedBill = billService.save(bill); 90 | Assert.assertNotNull(savedBill.getId()); 91 | Assert.assertEquals(YearMonth.now(), savedBill.getYearMonth()); 92 | Assert.assertEquals(BigDecimal.valueOf(1.3), savedBill.getTotal()); 93 | 94 | Assert.assertEquals(2, savedBill.getItems().size()); 95 | Assert.assertThat(savedBill.getItems(), contains( 96 | allOf( 97 | hasProperty("bill", is(bill)), 98 | hasProperty("description", is("Local call"))), 99 | allOf( 100 | hasProperty("bill", is(bill)), 101 | hasProperty("description", is("Another carrier call"))) 102 | )); 103 | } 104 | 105 | @Test 106 | public void updatingABillWithOneMoreCall() { 107 | final List items = new ArrayList<>(); 108 | 109 | final Bill bill = Bill.builder() 110 | .carrier(carrier) 111 | .customer(customer) 112 | .identifier("123") 113 | .yearMonth(YearMonth.now()) 114 | .items(items) 115 | .total(BigDecimal.valueOf(1.3)) 116 | .build(); 117 | 118 | items.add(BillItem.builder() 119 | .bill(bill) 120 | .dataTime(LocalDateTime.now()) 121 | .originNumber("4499898639") 122 | .destinationNumber("4499898400") 123 | .duration(40L) 124 | .description("Local call") 125 | .type(ItemType.CALL) 126 | .value(BigDecimal.valueOf(0.63)) 127 | .build()); 128 | 129 | items.add(BillItem.builder() 130 | .bill(bill) 131 | .dataTime(LocalDateTime.now()) 132 | .originNumber("4499898484") 133 | .destinationNumber("4499999900") 134 | .duration(40L) 135 | .description("Another carrier call") 136 | .type(ItemType.CALL) 137 | .value(BigDecimal.valueOf(1.45)) 138 | .build()); 139 | 140 | final Bill savedBill = billService.save(bill); 141 | Assert.assertEquals(2, savedBill.getItems().size()); 142 | 143 | savedBill.getItems().add(BillItem.builder() 144 | .bill(savedBill) 145 | .dataTime(LocalDateTime.now()) 146 | .originNumber("4499898484") 147 | .destinationNumber("4499999900") 148 | .duration(100L) 149 | .description("Roaming Call") 150 | .type(ItemType.CALL) 151 | .value(BigDecimal.valueOf(3.98)) 152 | .build()); 153 | 154 | final Bill updatedBill = billService.save(savedBill); 155 | Assert.assertEquals(3, updatedBill.getItems().size()); 156 | Assert.assertThat(updatedBill.getItems(), contains( 157 | allOf( 158 | hasProperty("bill", is(bill)), 159 | hasProperty("description", is("Local call"))), 160 | allOf( 161 | hasProperty("bill", is(bill)), 162 | hasProperty("description", is("Another carrier call"))), 163 | allOf( 164 | hasProperty("bill", is(bill)), 165 | hasProperty("description", is("Roaming Call"))) 166 | )); 167 | } 168 | 169 | @Test 170 | public void findByUk() { 171 | final List itemsCurrentMonth = new ArrayList<>(); 172 | final Bill billCurrentMonth = Bill.builder() 173 | .carrier(carrier) 174 | .customer(customer) 175 | .identifier("123") 176 | .yearMonth(YearMonth.now()) 177 | .items(itemsCurrentMonth) 178 | .total(BigDecimal.valueOf(1)) 179 | .build(); 180 | 181 | itemsCurrentMonth.add(BillItem.builder() 182 | .bill(billCurrentMonth) 183 | .dataTime(LocalDateTime.now()) 184 | .originNumber("4499898484") 185 | .destinationNumber("4499898400") 186 | .duration(100L) 187 | .description("Local call") 188 | .type(ItemType.CALL) 189 | .value(BigDecimal.valueOf(1)) 190 | .build()); 191 | 192 | 193 | billService.save(billCurrentMonth); 194 | 195 | final List itemsLastMonth = new ArrayList<>(); 196 | 197 | final Bill billLastMonth = Bill.builder() 198 | .carrier(carrier) 199 | .customer(customer) 200 | .identifier("123") 201 | .yearMonth(YearMonth.now().minusMonths(1)) 202 | .items(itemsLastMonth) 203 | .total(BigDecimal.valueOf(256)) 204 | .build(); 205 | 206 | itemsLastMonth.add(BillItem.builder() 207 | .bill(billCurrentMonth) 208 | .dataTime(LocalDateTime.now()) 209 | .originNumber("4499898484") 210 | .destinationNumber("4499898400") 211 | .duration(800L) 212 | .description("Local call") 213 | .type(ItemType.CALL) 214 | .value(BigDecimal.valueOf(256)) 215 | .build()); 216 | 217 | billService.save(billLastMonth); 218 | 219 | final Bill bill = billService.findByUk(customer.getId(), "123", YearMonth.now()); 220 | Assert.assertEquals(YearMonth.now(), bill.getYearMonth()); 221 | 222 | } 223 | 224 | @Test(expected = JpaObjectRetrievalFailureException.class) 225 | public void delete() { 226 | 227 | final Bill bill = Bill.builder() 228 | .carrier(carrier) 229 | .customer(customer) 230 | .identifier("123") 231 | .yearMonth(YearMonth.now()) 232 | .total(BigDecimal.valueOf(1.3)) 233 | .build(); 234 | 235 | bill.setItems(Collections.singletonList( 236 | BillItem.builder() 237 | .bill(bill) 238 | .dataTime(LocalDateTime.now()) 239 | .originNumber("4499898484") 240 | .destinationNumber("4499898400") 241 | .duration(100L) 242 | .description("Local call") 243 | .type(ItemType.CALL) 244 | .value(BigDecimal.valueOf(0.3)) 245 | .build())); 246 | 247 | billService.save(bill); 248 | 249 | final Long billId = bill.getId(); 250 | billService.delete(billId); 251 | billService.findById(billId); 252 | } 253 | 254 | @Test 255 | public void findPaged() { 256 | final List itemsBill123 = new ArrayList<>(); 257 | final Bill bill123 = Bill.builder() 258 | .carrier(carrier) 259 | .customer(customer) 260 | .identifier("123") 261 | .yearMonth(YearMonth.now()) 262 | .items(itemsBill123) 263 | .total(BigDecimal.valueOf(1.3)) 264 | .build(); 265 | 266 | itemsBill123.add(BillItem.builder() 267 | .bill(bill123) 268 | .dataTime(LocalDateTime.now()) 269 | .originNumber("4499898484") 270 | .destinationNumber("4499898400") 271 | .duration(100L) 272 | .description("Local call") 273 | .type(ItemType.CALL) 274 | .value(BigDecimal.valueOf(0.3)) 275 | .build()); 276 | 277 | itemsBill123.add(BillItem.builder() 278 | .bill(bill123) 279 | .dataTime(LocalDateTime.now()) 280 | .originNumber("4499898484") 281 | .destinationNumber("4499889400") 282 | .duration(250L) 283 | .description("Another carrier call") 284 | .type(ItemType.CALL) 285 | .value(BigDecimal.valueOf(1)) 286 | .build()); 287 | 288 | billService.save(bill123); 289 | 290 | final List items456 = new ArrayList<>(); 291 | final Bill bill456 = Bill.builder() 292 | .carrier(carrier) 293 | .customer(customer) 294 | .identifier("456") 295 | .yearMonth(YearMonth.now().plusMonths(1)) 296 | .items(items456) 297 | .total(BigDecimal.valueOf(1.3)) 298 | .build(); 299 | 300 | items456.add(BillItem.builder() 301 | .bill(bill456) 302 | .dataTime(LocalDateTime.now()) 303 | .originNumber("4499898484") 304 | .destinationNumber("4499088299") 305 | .duration(400L) 306 | .description("Local call") 307 | .type(ItemType.CALL) 308 | .value(BigDecimal.valueOf(10)) 309 | .build()); 310 | 311 | billService.save(bill456); 312 | 313 | final BillSearchTO searchByIdentifier = new BillSearchTO(); 314 | searchByIdentifier.setIdentifier("456"); 315 | 316 | final Page billByIdentifierPaged = billService.search(searchByIdentifier); 317 | 318 | Assert.assertEquals(1, billByIdentifierPaged.getContent().size()); 319 | Assert.assertThat(billByIdentifierPaged.getContent(), contains( 320 | allOf(hasProperty("identifier", is("456"))))); 321 | 322 | 323 | final BillSearchTO searchByPeriod = new BillSearchTO(); 324 | searchByPeriod.setInitYearMonth(YearMonth.now()); 325 | searchByPeriod.setEndYearMonth(YearMonth.now()); 326 | 327 | final Page billByPeriodPaged = billService.search(searchByPeriod); 328 | 329 | Assert.assertEquals(1, billByPeriodPaged.getContent().size()); 330 | Assert.assertThat(billByPeriodPaged.getContent(), contains( 331 | allOf(hasProperty("identifier", is("123"))))); 332 | } 333 | 334 | } 335 | --------------------------------------------------------------------------------