diff --git a/backend-challenge/.gitignore b/backend-challenge/.gitignore new file mode 100644 index 0000000..153c933 --- /dev/null +++ b/backend-challenge/.gitignore @@ -0,0 +1,29 @@ +HELP.md +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ + +### VS Code ### +.vscode/ diff --git a/backend-challenge/README.md b/backend-challenge/README.md new file mode 100644 index 0000000..d6c05fe --- /dev/null +++ b/backend-challenge/README.md @@ -0,0 +1,19 @@ +Backend Challenge BIT - SP + +Esse projeto foi construído usando uma versão simplificada da Clean Architecture (http://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html). + +Utilizamos o Spring Boot como facilitador, na sua última versão disponível (2.1.3). + +Para construir a aplicação, utilize o comando: + +mvn clean package + +Desta forma, no diretório "target" será gerado o jar "backend-challenge-0.0.1-SNAPSHOT.jar", que pode ser executado com o comando: + +java -jar target\backend-challenge-0.0.1-SNAPSHOT.jar + +Ou, utilizando o plugin do maven do Spring Boot, sem precisar pré construir a aplicação: + +mvn spring-boot:run + + \ No newline at end of file diff --git a/backend-challenge/pom.xml b/backend-challenge/pom.xml new file mode 100644 index 0000000..7c8fb37 --- /dev/null +++ b/backend-challenge/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.1.3.RELEASE + + + br.com.bonaldo + backend-challenge + 0.0.1-SNAPSHOT + backend-challenge + backend-challenge + + + 1.8 + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/BackendChallengeApplication.java b/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/BackendChallengeApplication.java new file mode 100644 index 0000000..be0d760 --- /dev/null +++ b/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/BackendChallengeApplication.java @@ -0,0 +1,11 @@ +package br.com.bonaldo.backendchallenge; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class BackendChallengeApplication { + public static void main(String[] args) { + SpringApplication.run(BackendChallengeApplication.class, args); + } +} \ No newline at end of file diff --git a/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/config/RestTemplateConfig.java b/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/config/RestTemplateConfig.java new file mode 100644 index 0000000..9ac0b30 --- /dev/null +++ b/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/config/RestTemplateConfig.java @@ -0,0 +1,14 @@ +package br.com.bonaldo.backendchallenge.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/domains/Dimension.java b/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/domains/Dimension.java new file mode 100644 index 0000000..396dda0 --- /dev/null +++ b/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/domains/Dimension.java @@ -0,0 +1,23 @@ +package br.com.bonaldo.backendchallenge.domains; + +import br.com.bonaldo.backendchallenge.gateways.http.json.DimensionResponse; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.math.BigDecimal; + +@Data +@AllArgsConstructor +public class Dimension { + private BigDecimal weight; + private BigDecimal height; + private BigDecimal width; + private BigDecimal length; + + public Dimension(final DimensionResponse dimensionResponse) { + this.weight = dimensionResponse.getWeight(); + this.height = dimensionResponse.getHeight(); + this.length = dimensionResponse.getLength(); + this.width = dimensionResponse.getWidth(); + } +} \ No newline at end of file diff --git a/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/domains/OrderItem.java b/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/domains/OrderItem.java new file mode 100644 index 0000000..31205d2 --- /dev/null +++ b/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/domains/OrderItem.java @@ -0,0 +1,21 @@ +package br.com.bonaldo.backendchallenge.domains; + +import br.com.bonaldo.backendchallenge.gateways.http.json.OrderItemResponse; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class OrderItem { + private String name; + private String code; + private LocalDateTime date; + private Dimension dimension; + + public OrderItem(final OrderItemResponse orderItemResponse) { + this.name = orderItemResponse.getName(); + this.code = orderItemResponse.getCode(); + this.date = orderItemResponse.getDate(); + this.dimension = new Dimension(orderItemResponse.getDimension()); + } +} diff --git a/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/gateways/OrderGateway.java b/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/gateways/OrderGateway.java new file mode 100644 index 0000000..651cec6 --- /dev/null +++ b/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/gateways/OrderGateway.java @@ -0,0 +1,9 @@ +package br.com.bonaldo.backendchallenge.gateways; + +import br.com.bonaldo.backendchallenge.gateways.http.json.OrderItemResponse; + +import java.util.List; + +public interface OrderGateway { + List getOrderItems(); +} diff --git a/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/gateways/controllers/ItemController.java b/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/gateways/controllers/ItemController.java new file mode 100644 index 0000000..d3e5f05 --- /dev/null +++ b/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/gateways/controllers/ItemController.java @@ -0,0 +1,46 @@ +package br.com.bonaldo.backendchallenge.gateways.controllers; + +import br.com.bonaldo.backendchallenge.gateways.controllers.json.ItemResponse; +import br.com.bonaldo.backendchallenge.usecases.FilterOrderItems; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@RestController +@RequestMapping("/item") +@RequiredArgsConstructor +public class ItemController { + + private final FilterOrderItems filterOrderItems; + + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + public List findItems( + @RequestParam(value = "begindate") final String beginDate, + @RequestParam(value = "finaldate") final String finalDate) { + + log.info("Searching for items with date between {} and {}", beginDate, finalDate); + + LocalDate startDate = parseStringDate(beginDate); + LocalDate endDate = parseStringDate(finalDate); + + return filterOrderItems.execute(startDate, endDate) + .stream() + .map(ItemResponse::new) + .collect(Collectors.toList()); + } + + private LocalDate parseStringDate(final String stringDate) { + DateTimeFormatter dtf = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + return LocalDate.from(dtf.parse(stringDate)); + } +} \ No newline at end of file diff --git a/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/gateways/controllers/json/DimensionResponse.java b/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/gateways/controllers/json/DimensionResponse.java new file mode 100644 index 0000000..8273baf --- /dev/null +++ b/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/gateways/controllers/json/DimensionResponse.java @@ -0,0 +1,22 @@ +package br.com.bonaldo.backendchallenge.gateways.controllers.json; + +import br.com.bonaldo.backendchallenge.domains.Dimension; +import lombok.Data; + +import java.io.Serializable; +import java.math.BigDecimal; + +@Data +public class DimensionResponse implements Serializable { + private BigDecimal weight; + private BigDecimal height; + private BigDecimal width; + private BigDecimal length; + + public DimensionResponse(final Dimension dimension) { + this.weight = dimension.getWeight(); + this.height = dimension.getHeight(); + this.width = dimension.getWidth(); + this.length = dimension.getLength(); + } +} \ No newline at end of file diff --git a/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/gateways/controllers/json/ItemResponse.java b/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/gateways/controllers/json/ItemResponse.java new file mode 100644 index 0000000..8875a87 --- /dev/null +++ b/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/gateways/controllers/json/ItemResponse.java @@ -0,0 +1,22 @@ +package br.com.bonaldo.backendchallenge.gateways.controllers.json; + +import br.com.bonaldo.backendchallenge.domains.OrderItem; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +@Data +public class ItemResponse implements Serializable { + private String name; + private String code; + private LocalDateTime date; + private DimensionResponse dimension; + + public ItemResponse(final OrderItem orderItem) { + this.name = orderItem.getName(); + this.code = orderItem.getCode(); + this.date = orderItem.getDate(); + this.dimension = new DimensionResponse(orderItem.getDimension()); + } +} diff --git a/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/gateways/http/CustomExceptionHandler.java b/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/gateways/http/CustomExceptionHandler.java new file mode 100644 index 0000000..5af824e --- /dev/null +++ b/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/gateways/http/CustomExceptionHandler.java @@ -0,0 +1,25 @@ +package br.com.bonaldo.backendchallenge.gateways.http; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; + +import javax.servlet.http.HttpServletRequest; +import java.time.DateTimeException; + +@Slf4j +@ControllerAdvice +public class CustomExceptionHandler { + + @ExceptionHandler(DateTimeException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ResponseBody + public ResponseEntity handleInvalidDateException(HttpServletRequest req, Exception ex) { + log.info("Invalid date format on request: {}, with params: {}, exception: {}", req.getRequestURI(), req.getQueryString(), ex.getMessage()); + return ResponseEntity.badRequest().body("Invalid date format"); + } +} \ No newline at end of file diff --git a/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/gateways/http/OrderGatewayAPI.java b/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/gateways/http/OrderGatewayAPI.java new file mode 100644 index 0000000..94b2e4d --- /dev/null +++ b/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/gateways/http/OrderGatewayAPI.java @@ -0,0 +1,30 @@ +package br.com.bonaldo.backendchallenge.gateways.http; + +import br.com.bonaldo.backendchallenge.gateways.OrderGateway; +import br.com.bonaldo.backendchallenge.gateways.http.json.OrderItemResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class OrderGatewayAPI implements OrderGateway { + + @Value("${integration.order-api.url}") + private String API_URL; + + private final RestTemplate restTemplate; + + @Override + public List getOrderItems() { + ResponseEntity> response = restTemplate + .exchange(API_URL, HttpMethod.GET, null, new ParameterizedTypeReference>() {}); + return response.getBody(); + } +} \ No newline at end of file diff --git a/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/gateways/http/json/DimensionResponse.java b/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/gateways/http/json/DimensionResponse.java new file mode 100644 index 0000000..0f70e32 --- /dev/null +++ b/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/gateways/http/json/DimensionResponse.java @@ -0,0 +1,16 @@ +package br.com.bonaldo.backendchallenge.gateways.http.json; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.math.BigDecimal; + +@Data +@NoArgsConstructor +public class DimensionResponse implements Serializable { + private BigDecimal weight; + private BigDecimal height; + private BigDecimal width; + private BigDecimal length; +} diff --git a/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/gateways/http/json/OrderItemResponse.java b/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/gateways/http/json/OrderItemResponse.java new file mode 100644 index 0000000..bdb6145 --- /dev/null +++ b/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/gateways/http/json/OrderItemResponse.java @@ -0,0 +1,20 @@ +package br.com.bonaldo.backendchallenge.gateways.http.json; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class OrderItemResponse implements Serializable { + private String name; + private String code; + private LocalDateTime date; + private DimensionResponse dimension; +} diff --git a/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/usecases/FilterOrderItems.java b/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/usecases/FilterOrderItems.java new file mode 100644 index 0000000..b040618 --- /dev/null +++ b/backend-challenge/src/main/java/br/com/bonaldo/backendchallenge/usecases/FilterOrderItems.java @@ -0,0 +1,39 @@ +package br.com.bonaldo.backendchallenge.usecases; + +import br.com.bonaldo.backendchallenge.domains.OrderItem; +import br.com.bonaldo.backendchallenge.gateways.OrderGateway; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class FilterOrderItems { + + private final OrderGateway orderGateway; + + public List execute(final LocalDate startDate, final LocalDate finalDate) { + final List orders = fetchOrderItemsFromAPI(); + return filterOrderItemsByDate(startDate, finalDate, orders); + } + + private List fetchOrderItemsFromAPI() { + return orderGateway + .getOrderItems() + .stream() + .map(OrderItem::new) + .collect(Collectors.toList()); + } + + private List filterOrderItemsByDate(final LocalDate startDate, final LocalDate finalDate, final List orders) { + return orders + .stream() + .filter(orderItem -> orderItem.getDate().isAfter(startDate.atStartOfDay())) + .filter(orderItem -> orderItem.getDate().isBefore(finalDate.atTime(LocalTime.MAX))) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/backend-challenge/src/main/resources/application.yml b/backend-challenge/src/main/resources/application.yml new file mode 100644 index 0000000..f8b8787 --- /dev/null +++ b/backend-challenge/src/main/resources/application.yml @@ -0,0 +1,7 @@ +server: + servlet: + context-path: /challenge-backend + +integration: + order-api: + url: http://www.mocky.io/v2/5817803a1000007d01cc7fc9 \ No newline at end of file diff --git a/backend-challenge/src/test/java/br/com/bonaldo/backendchallenge/usecases/FilterOrderItemsTest.java b/backend-challenge/src/test/java/br/com/bonaldo/backendchallenge/usecases/FilterOrderItemsTest.java new file mode 100644 index 0000000..104c492 --- /dev/null +++ b/backend-challenge/src/test/java/br/com/bonaldo/backendchallenge/usecases/FilterOrderItemsTest.java @@ -0,0 +1,65 @@ +package br.com.bonaldo.backendchallenge.usecases; + +import br.com.bonaldo.backendchallenge.domains.OrderItem; +import br.com.bonaldo.backendchallenge.gateways.OrderGateway; +import br.com.bonaldo.backendchallenge.gateways.http.json.DimensionResponse; +import br.com.bonaldo.backendchallenge.gateways.http.json.OrderItemResponse; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@SpringBootTest +@RunWith(SpringRunner.class) +public class FilterOrderItemsTest { + + public static final String ITEM_DE_HOJE = "Today"; + + @InjectMocks + private FilterOrderItems filterOrderItems; + + @Mock + private OrderGateway orderGateway; + + @Test + public void FilterOrderItemsShouldReturnWithOnlyOneItemWithinDateRange() { + OrderItemResponse expectedItem = OrderItemResponse.builder() + .date(LocalDateTime.now()) + .name(ITEM_DE_HOJE) + .dimension(new DimensionResponse()) + .build(); + + OrderItemResponse futureItem = OrderItemResponse.builder() + .date(LocalDateTime.now().minusDays(1)) + .name("Tomorrow") + .dimension(new DimensionResponse()) + .build(); + + OrderItemResponse pastItem = OrderItemResponse.builder() + .date(LocalDateTime.now().plusDays(1)) + .name("Yesterday") + .dimension(new DimensionResponse()) + .build(); + + List response = new ArrayList<>(); + Collections.addAll(response, expectedItem, futureItem, pastItem); + + Mockito.when(orderGateway.getOrderItems()).thenReturn(response); + + List result = filterOrderItems.execute(LocalDate.now(), LocalDate.now()); + + Assert.assertEquals(1, result.size()); + Assert.assertNotNull(result.get(0)); + Assert.assertEquals(result.get(0).getName(), ITEM_DE_HOJE); + } +} \ No newline at end of file diff --git a/task2/query_events.sql b/task2/query_events.sql new file mode 100644 index 0000000..b2aa061 --- /dev/null +++ b/task2/query_events.sql @@ -0,0 +1,16 @@ +-- Os resultados da planilha nao estao em conformidade com a proposta do exercicio +-- Por exemplo, na base teste2, os dois registros mais antigos de event_type = 2 sao +-- 2015-05-09 12:42:00 com value = 5 e 2015-05-09 12:54:39 com value = 7, +-- ou seja, a diferenca e 7-5 = 2, e nao -3. Este cenario se repete nas outras bases + +select ev.event_type as EVENT_TYPE, + (select penultimate.value + from events penultimate + where penultimate.event_type = ev.event_type + order by time asc + limit 1 offset 1) + - + (select last.value from events last where last.event_type = ev.event_type order by time asc limit 1) as VALUE +from events ev +group by ev.event_type +having count(ev.event_type) > 1; \ No newline at end of file