diff --git a/.github/workflows/ci-main.yaml b/.github/workflows/ci-main.yaml index 2736a52..5db7151 100644 --- a/.github/workflows/ci-main.yaml +++ b/.github/workflows/ci-main.yaml @@ -52,3 +52,14 @@ jobs: # with: # push: true # tags: diegoneves/racha-pedido:latest + + +# - name: Build and push +# id: docker_build +# uses: docker/build-push-action@v4 +# with: +# push: true +# tags: diegoneves/racha-pedido:latest +# build-args: MY_ENV_VAR=${{ env.MY_ENV_VAR }} +# env: +# MY_ENV_VAR: my_value diff --git a/README.md b/README.md index 98c28c7..5eabc56 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,10 @@ Diego-Docker +--- [![Linkedin badge](https://img.shields.io/badge/-Linkedin-blue?flat-square&logo=Linkedin&logoColor=white&link=https://www.linkedin.com/in/diego-neves-224208177/)](https://www.linkedin.com/in/diego-neves-224208177/) -[![CI Racha Pedido](https://github.com/diegosneves/racha-pedido/actions/workflows/ci-main.yml/badge.svg)](https://github.com/diegosneves/racha-pedido/actions/workflows/ci-main.yml) +[![CI Racha Pedido](https://github.com/diegosneves/racha-pedido/actions/workflows/ci-main.yaml/badge.svg)](https://github.com/diegosneves/racha-pedido/actions/workflows/ci-main.yaml) ---- O **Racha Pedido** é uma solução inteligente para resolver o desafio comum enfrentado por equipes de trabalho ao dividir lanches ou refeições solicitados por meio de aplicativos de entrega como iFood ou Uber Eats. diff --git a/src/main/java/diegosneves/github/rachapedido/config/ControllerExceptionHandler.java b/src/main/java/diegosneves/github/rachapedido/config/ControllerExceptionHandler.java index 7fa8dbd..f9d9a58 100644 --- a/src/main/java/diegosneves/github/rachapedido/config/ControllerExceptionHandler.java +++ b/src/main/java/diegosneves/github/rachapedido/config/ControllerExceptionHandler.java @@ -1,11 +1,21 @@ package diegosneves.github.rachapedido.config; import diegosneves.github.rachapedido.dto.ExceptionDTO; +import diegosneves.github.rachapedido.exceptions.CalculateInvoiceException; +import diegosneves.github.rachapedido.exceptions.CloseOrderException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +/** + * A classe {@link ControllerExceptionHandler} é um manipulador de exceções global para controladores. + * Ela lida com as exceções lançadas durante o processamento de solicitações e gera respostas de erro apropriadas. + * A classe é anotada com {@link RestControllerAdvice} para aplicar o tratamento de exceção globalmente + * a todas as classes de controlador. + * + * @author diegosneves + */ @RestControllerAdvice public class ControllerExceptionHandler { @@ -15,4 +25,16 @@ public ResponseEntity generalError(Exception exception) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(dto); } + @ExceptionHandler(CloseOrderException.class) + public ResponseEntity orderRelatedFaileures(CloseOrderException exception) { + ExceptionDTO dto = new ExceptionDTO(exception.getMessage(), CloseOrderException.ERROR.getStatusCodeValue()); + return ResponseEntity.status(CloseOrderException.ERROR.getHttpStatusCode()).body(dto); + } + + @ExceptionHandler(CalculateInvoiceException.class) + public ResponseEntity orderRelatedFaileures(CalculateInvoiceException exception) { + ExceptionDTO dto = new ExceptionDTO(exception.getMessage(), CalculateInvoiceException.ERROR.getStatusCodeValue()); + return ResponseEntity.status(CalculateInvoiceException.ERROR.getHttpStatusCode()).body(dto); + } + } diff --git a/src/main/java/diegosneves/github/rachapedido/config/OpenApiConfig.java b/src/main/java/diegosneves/github/rachapedido/config/OpenApiConfig.java index d22dafc..f1df137 100644 --- a/src/main/java/diegosneves/github/rachapedido/config/OpenApiConfig.java +++ b/src/main/java/diegosneves/github/rachapedido/config/OpenApiConfig.java @@ -28,7 +28,7 @@ private Info getInfo() { } private List getTags() { // TODO - Ajustar as tags do swagger - return List.of(new Tag().name("Racha-Pedido").description("Operações relacionadas a divisão dos valores do pedido")); + return List.of(new Tag().name("Racha Pedido").description("Operações relacionadas a divisão dos valores do pedido")); } } diff --git a/src/main/java/diegosneves/github/rachapedido/controller/SplitInvoiceController.java b/src/main/java/diegosneves/github/rachapedido/controller/SplitInvoiceController.java new file mode 100644 index 0000000..3379ca8 --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/controller/SplitInvoiceController.java @@ -0,0 +1,34 @@ +package diegosneves.github.rachapedido.controller; + +import diegosneves.github.rachapedido.controller.contract.SplitInvoiceControllerContract; +import diegosneves.github.rachapedido.request.SplitInvoiceRequest; +import diegosneves.github.rachapedido.response.SplitInvoiceResponse; +import diegosneves.github.rachapedido.service.SplitInvoiceService; +import diegosneves.github.rachapedido.service.contract.SplitInvoiceServiceContract; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * A classe {@link SplitInvoiceController} é responsável por lidar com solicitações HTTP relacionadas à divisão de faturas. + * Implementa a interface {@link SplitInvoiceControllerContract}. + * + */ +@RestController +@RequestMapping("/split") +public class SplitInvoiceController implements SplitInvoiceControllerContract { + + private final SplitInvoiceServiceContract service; + + public SplitInvoiceController(@Autowired SplitInvoiceService service) { + this.service = service; + } + + + @Override + public ResponseEntity splitInvoice(SplitInvoiceRequest request) { + SplitInvoiceResponse response = this.service.splitInvoice(request); // TODO - Criar a regra de negocio + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/diegosneves/github/rachapedido/controller/contract/SplitInvoiceControllerContract.java b/src/main/java/diegosneves/github/rachapedido/controller/contract/SplitInvoiceControllerContract.java new file mode 100644 index 0000000..17e20ec --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/controller/contract/SplitInvoiceControllerContract.java @@ -0,0 +1,31 @@ +package diegosneves.github.rachapedido.controller.contract; + +import diegosneves.github.rachapedido.request.SplitInvoiceRequest; +import diegosneves.github.rachapedido.response.SplitInvoiceResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +/** + * A interface {@link SplitInvoiceControllerContract} representa o contrato para lidar com solicitações HTTP relacionadas à divisão de faturas. + */ +public interface SplitInvoiceControllerContract { + + @PostMapping(value = "/invoice", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = "Receber os dados para realizar a divisão da fatura", tags = "Racha Pedido", parameters = { + @Parameter(name = "referencia", description = "Tipos de descontos que devem ser aplicado no `discountType` do body", + schema = @Schema(enumAsRef = true, defaultValue = "cash", allowableValues = {"cash", "percentage", "no discount"})) + }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Divisão da Fatura realizada com sucesso", content = @Content) + }) + ResponseEntity splitInvoice(@RequestBody SplitInvoiceRequest request); + +} diff --git a/src/main/java/diegosneves/github/rachapedido/core/CashDiscountStrategy.java b/src/main/java/diegosneves/github/rachapedido/core/CashDiscountStrategy.java new file mode 100644 index 0000000..7f25079 --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/core/CashDiscountStrategy.java @@ -0,0 +1,20 @@ +package diegosneves.github.rachapedido.core; + +import diegosneves.github.rachapedido.dto.InvoiceDTO; +import diegosneves.github.rachapedido.enums.DiscountType; +import diegosneves.github.rachapedido.mapper.BuilderMapper; +import diegosneves.github.rachapedido.model.Invoice; +import diegosneves.github.rachapedido.utils.RoundUtil; + +public class CashDiscountStrategy extends DiscountStrategy { + + @Override + public Invoice calculateDiscount(InvoiceDTO dto, Double discountAmount, DiscountType type, Double total, Double deliveryFee) { + if(DiscountType.CASH.name().equals(type.name())) { + dto.setPercentageConsumedTotalBill(dto.getValueConsumed() / total); + dto.setTotalPayable(RoundUtil.round(((total - type.discountAmount(discountAmount) + deliveryFee) * dto.getPercentageConsumedTotalBill()))); + return BuilderMapper.builderMapper(Invoice.class, dto); + } + return this.checkNext(dto, discountAmount, type, total, deliveryFee); + } +} diff --git a/src/main/java/diegosneves/github/rachapedido/core/DiscountStrategy.java b/src/main/java/diegosneves/github/rachapedido/core/DiscountStrategy.java new file mode 100644 index 0000000..48eb7d4 --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/core/DiscountStrategy.java @@ -0,0 +1,28 @@ +package diegosneves.github.rachapedido.core; + +import diegosneves.github.rachapedido.dto.InvoiceDTO; +import diegosneves.github.rachapedido.enums.DiscountType; +import diegosneves.github.rachapedido.model.Invoice; + +public abstract class DiscountStrategy { + + protected DiscountStrategy next; + + public static DiscountStrategy link(DiscountStrategy first, DiscountStrategy... chain) { + DiscountStrategy current = first; + for (DiscountStrategy nextLink : chain) { + current.next = nextLink; + current = nextLink; + } + return first; + } + public abstract Invoice calculateDiscount(InvoiceDTO dto, Double discountAmount, DiscountType type, Double total, Double deliveryFee); + + protected Invoice checkNext(InvoiceDTO dto, Double discountAmount, DiscountType type, Double total, Double deliveryFee) { + if (this.next == null) { + return new Invoice(); + } + return next.calculateDiscount(dto, discountAmount, type, total, deliveryFee); + } + +} diff --git a/src/main/java/diegosneves/github/rachapedido/core/NoDiscountStrategy.java b/src/main/java/diegosneves/github/rachapedido/core/NoDiscountStrategy.java new file mode 100644 index 0000000..82a9bc5 --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/core/NoDiscountStrategy.java @@ -0,0 +1,20 @@ +package diegosneves.github.rachapedido.core; + +import diegosneves.github.rachapedido.dto.InvoiceDTO; +import diegosneves.github.rachapedido.enums.DiscountType; +import diegosneves.github.rachapedido.mapper.BuilderMapper; +import diegosneves.github.rachapedido.model.Invoice; +import diegosneves.github.rachapedido.utils.RoundUtil; + +public class NoDiscountStrategy extends DiscountStrategy { + + @Override + public Invoice calculateDiscount(InvoiceDTO dto, Double discountAmount, DiscountType type, Double total, Double deliveryFee) { + if (DiscountType.NO_DISCOUNT.name().equals(type.name())) { + dto.setPercentageConsumedTotalBill(dto.getValueConsumed() / total); + dto.setTotalPayable(RoundUtil.round((total - type.discountAmount(discountAmount) + deliveryFee) * dto.getPercentageConsumedTotalBill())); + return BuilderMapper.builderMapper(Invoice.class, dto); + } + return this.checkNext(dto, discountAmount, type, total, deliveryFee); + } +} diff --git a/src/main/java/diegosneves/github/rachapedido/core/PagBank.java b/src/main/java/diegosneves/github/rachapedido/core/PagBank.java new file mode 100644 index 0000000..66cd03c --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/core/PagBank.java @@ -0,0 +1,16 @@ +package diegosneves.github.rachapedido.core; + +import diegosneves.github.rachapedido.core.contract.PaymentStrategy; + +public class PagBank implements PaymentStrategy { + + @Override + public String generatedPaymentLink(Double paymentAmount) { + return String.format("Pagar: R$%,.2f", paymentAmount); + } + + @Override + public void collectPaymentDetails() { + //TODO - Escrever aqui... + } +} diff --git a/src/main/java/diegosneves/github/rachapedido/core/PercentageDiscountStrategy.java b/src/main/java/diegosneves/github/rachapedido/core/PercentageDiscountStrategy.java new file mode 100644 index 0000000..a178f17 --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/core/PercentageDiscountStrategy.java @@ -0,0 +1,21 @@ +package diegosneves.github.rachapedido.core; + +import diegosneves.github.rachapedido.dto.InvoiceDTO; +import diegosneves.github.rachapedido.enums.DiscountType; +import diegosneves.github.rachapedido.mapper.BuilderMapper; +import diegosneves.github.rachapedido.model.Invoice; +import diegosneves.github.rachapedido.utils.RoundUtil; + +public class PercentageDiscountStrategy extends DiscountStrategy { + + @Override + public Invoice calculateDiscount(InvoiceDTO dto, Double discountAmount, DiscountType type, Double total, Double deliveryFee) { + if (DiscountType.PERCENTAGE.name().equals(type.name())) { + dto.setPercentageConsumedTotalBill(dto.getValueConsumed() / total); + dto.setTotalPayable(RoundUtil.round((total - (total * type.discountAmount(discountAmount)) + deliveryFee) * dto.getPercentageConsumedTotalBill())); + return BuilderMapper.builderMapper(Invoice.class, dto); + } + return this.checkNext(dto, discountAmount, type, total, deliveryFee); + } + +} diff --git a/src/main/java/diegosneves/github/rachapedido/core/contract/PaymentStrategy.java b/src/main/java/diegosneves/github/rachapedido/core/contract/PaymentStrategy.java new file mode 100644 index 0000000..99e2e2b --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/core/contract/PaymentStrategy.java @@ -0,0 +1,7 @@ +package diegosneves.github.rachapedido.core.contract; + +public interface PaymentStrategy { + + String generatedPaymentLink(Double paymentAmount); + void collectPaymentDetails(); +} diff --git a/src/main/java/diegosneves/github/rachapedido/dto/ExceptionDTO.java b/src/main/java/diegosneves/github/rachapedido/dto/ExceptionDTO.java index 866d2f7..8c2c838 100644 --- a/src/main/java/diegosneves/github/rachapedido/dto/ExceptionDTO.java +++ b/src/main/java/diegosneves/github/rachapedido/dto/ExceptionDTO.java @@ -3,6 +3,8 @@ /** * Esta classe representa um Data Transfer Object (DTO) para exceções. * Contém a mensagem de exceção e o código de status. + * + * @see java.lang.Record */ public record ExceptionDTO(String message, int statusCode) { } diff --git a/src/main/java/diegosneves/github/rachapedido/dto/InvoiceDTO.java b/src/main/java/diegosneves/github/rachapedido/dto/InvoiceDTO.java new file mode 100644 index 0000000..956970b --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/dto/InvoiceDTO.java @@ -0,0 +1,17 @@ +package diegosneves.github.rachapedido.dto; + +import lombok.*; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Builder +public class InvoiceDTO { + + private String consumerName; + private Double valueConsumed; + private Double totalPayable; + private Double percentageConsumedTotalBill; + +} diff --git a/src/main/java/diegosneves/github/rachapedido/dto/PersonDTO.java b/src/main/java/diegosneves/github/rachapedido/dto/PersonDTO.java new file mode 100644 index 0000000..06da940 --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/dto/PersonDTO.java @@ -0,0 +1,27 @@ +package diegosneves.github.rachapedido.dto; + +import diegosneves.github.rachapedido.model.Item; +import lombok.*; + +import java.util.ArrayList; +import java.util.List; + +/** + * A classe {@link PersonDTO} é uma versão Data Transfer Object (DTO) de uma pessoa, contendo campos para {@link String nome}, {@link String e-mail} e uma lista de {@link Item itens}. + * + * Esta classe é usada principalmente para transferência de dados entre processos ou componentes e ajuda a evitar múltiplas chamadas ao projeto atual. + * + * @see diegosneves.github.rachapedido.model.Person Person + */ +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Builder +public class PersonDTO { + + private String personName; + private String email; + private List items = new ArrayList<>(); + +} diff --git a/src/main/java/diegosneves/github/rachapedido/enums/DiscountType.java b/src/main/java/diegosneves/github/rachapedido/enums/DiscountType.java new file mode 100644 index 0000000..a57caf0 --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/enums/DiscountType.java @@ -0,0 +1,29 @@ +package diegosneves.github.rachapedido.enums; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Representa os diferentes tipos de descontos que podem ser aplicados. + * + * @author diegosneves + */ +public enum DiscountType { + + @JsonProperty(value = "cash") + CASH(1.0), + @JsonProperty(value = "percentage") + PERCENTAGE(100.0), + @JsonProperty(value = "no discount") + NO_DISCOUNT(0.0); + + private final Double calculation; + + DiscountType(Double calculation) { + this.calculation = calculation; + } + + public Double discountAmount(Double value) { + return this.calculation == 0.0 ? 0.0 : value / this.calculation; + } + +} diff --git a/src/main/java/diegosneves/github/rachapedido/exceptions/CalculateInvoiceException.java b/src/main/java/diegosneves/github/rachapedido/exceptions/CalculateInvoiceException.java new file mode 100644 index 0000000..41a6a4a --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/exceptions/CalculateInvoiceException.java @@ -0,0 +1,33 @@ +package diegosneves.github.rachapedido.exceptions; + +/** + * A classe {@link CalculateInvoiceException} representa uma exceção que é acionada quando ocorre um erro + * durante o processo de fechamento da fatura. Esta classe é uma extensão da classe {@link RuntimeException}. + * + * @author diegosneves + * @see RuntimeException + */ +public class CalculateInvoiceException extends RuntimeException { + + public static final ErroHandler ERROR = ErroHandler.INVOICE_FAILED; + + /** + * Constrói uma nova instância de {@link CalculateInvoiceException} com a mensagem descritiva específica. + * + * @param message A mensagem detalhada. Deve fornecer informações complementares sobre a causa da exceção. + */ + public CalculateInvoiceException(String message) { + super(ERROR.errorMessage(message)); + } + + /** + * Cria uma nova instância de {@link CalculateInvoiceException} com a mensagem de detalhe especificada e a causa. + * + * @param message a mensagem de detalhes, fornecendo informações adicionais sobre a causa da exceção + * @param e a causa da exceção + */ + public CalculateInvoiceException(String message, Throwable e) { + super(ERROR.errorMessage(message), e); + } + +} diff --git a/src/main/java/diegosneves/github/rachapedido/exceptions/CloseOrderException.java b/src/main/java/diegosneves/github/rachapedido/exceptions/CloseOrderException.java new file mode 100644 index 0000000..f110687 --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/exceptions/CloseOrderException.java @@ -0,0 +1,33 @@ +package diegosneves.github.rachapedido.exceptions; + +/** + * A classe {@link CloseOrderException} representa uma exceção que é acionada quando ocorre um erro + * durante o processo de fechamento de um pedido. Esta classe é uma extensão da classe {@link RuntimeException}. + * + * @author diegosneves + * @see RuntimeException + */ +public class CloseOrderException extends RuntimeException { + + public static final ErroHandler ERROR = ErroHandler.ORDER_FAILED; + + /** + * Constrói uma nova instância de {@link CloseOrderException} com a mensagem descritiva específica. + * + * @param message A mensagem detalhada. Deve fornecer informações complementares sobre a causa da exceção. + */ + public CloseOrderException(String message) { + super(ERROR.errorMessage(message)); + } + + /** + * Cria uma nova instância de {@link CloseOrderException} com a mensagem de detalhe especificada e a causa. + * + * @param message a mensagem de detalhes, fornecendo informações adicionais sobre a causa da exceção + * @param e a causa da exceção + */ + public CloseOrderException(String message, Throwable e) { + super(ERROR.errorMessage(message), e); + } + +} diff --git a/src/main/java/diegosneves/github/rachapedido/exceptions/ConstructorDefaultUndefinedException.java b/src/main/java/diegosneves/github/rachapedido/exceptions/ConstructorDefaultUndefinedException.java new file mode 100644 index 0000000..c8c9a2a --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/exceptions/ConstructorDefaultUndefinedException.java @@ -0,0 +1,22 @@ +package diegosneves.github.rachapedido.exceptions; + +/** + * A classe {@link ConstructorDefaultUndefinedException} é uma exceção que será lançada quando uma classe não possuir um construtor padrão. + * + * @see RuntimeException + * @author diegosneves + */ +public class ConstructorDefaultUndefinedException extends RuntimeException { + + public static final ErroHandler ERROR = ErroHandler.CONSTRUCTOR_DEFAULT_UNDEFINED; + + /** + * Cria uma nova instância da classe {@link ConstructorDefaultUndefinedException} com a mensagem e causa fornecidas. + * + * @param message A mensagem com as informações sobre o motivo do erro. + * @param e A causa do erro + */ + public ConstructorDefaultUndefinedException(String message, Throwable e) { + super(ERROR.errorMessage(message), e); + } +} diff --git a/src/main/java/diegosneves/github/rachapedido/exceptions/ErroHandler.java b/src/main/java/diegosneves/github/rachapedido/exceptions/ErroHandler.java new file mode 100644 index 0000000..1526fc4 --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/exceptions/ErroHandler.java @@ -0,0 +1,54 @@ +package diegosneves.github.rachapedido.exceptions; + +import org.springframework.http.HttpStatus; + +/** + * A enum {@link ErroHandler} representa diferentes manipuladores de erros, + * buscando padronizar as mensagens de erro. + * + * @author diegosneves + */ +public enum ErroHandler { + + CONSTRUCTOR_DEFAULT_UNDEFINED("Classe [ %s ] deve declarar um construtor padrão.", HttpStatus.NOT_IMPLEMENTED), + NULL_BUYER("O Objeto [ %s ] não deve ser nulo.", HttpStatus.BAD_REQUEST), + ORDER_FAILED("Houve um erro no fechamento do pedido: [ %s ].", HttpStatus.BAD_REQUEST), // TODO - Buscar deixar os demais nessa forma mais generica + INVOICE_FAILED("Ocorreu uma falha durante a execução do cálculo da fatura: [ %s ].", HttpStatus.BAD_REQUEST), // TODO - Buscar deixar os demais nessa forma mais generica + CLASS_MAPPING_FAILURE("Ocorreu um erro ao tentar mapear a classe [ %s ].", HttpStatus.INTERNAL_SERVER_ERROR); + + private final String message; + private final HttpStatus httpStatus; + + ErroHandler(String message, HttpStatus httpStatus) { + this.message = message; + this.httpStatus = httpStatus; + } + + /** + * Formata uma mensagem de erro com um valor de dados fornecido. + * + * @param data O valor dos dados a ser incluído na mensagem de erro. + * @return A mensagem de erro formatada. + */ + public String errorMessage(String data) { + return String.format(this.message, data); + } + + /** + * Retorna o código de status HTTP associado ao erro. + * + * @return O código numérico do status HTTP relacionado com o erro. + */ + public int getStatusCodeValue() { + return this.httpStatus.value(); + } + + /** + * Obtém o status HTTP associado ao erro. + * + * @return O código de status HTTP relacionado ao erro. + */ + public HttpStatus getHttpStatusCode() { + return this.httpStatus; + } +} diff --git a/src/main/java/diegosneves/github/rachapedido/exceptions/MapperFailureException.java b/src/main/java/diegosneves/github/rachapedido/exceptions/MapperFailureException.java new file mode 100644 index 0000000..a77c577 --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/exceptions/MapperFailureException.java @@ -0,0 +1,22 @@ +package diegosneves.github.rachapedido.exceptions; + +/** + * O {@link MapperFailureException} representa uma exceção que é lançada quando ocorre um erro durante o mapeamento de classes. + * + * @see RuntimeException + * @author diegosneves + */ +public class MapperFailureException extends RuntimeException { + + public static final ErroHandler ERROR = ErroHandler.CLASS_MAPPING_FAILURE; + + /** + * Cria uma nova instância da classe {@link MapperFailureException} com a mensagem de erro e a causa especificadas. + * + * @param message A mensagem de erro. + * @param e A causa do erro. + */ + public MapperFailureException(String message, Throwable e) { + super(ERROR.errorMessage(message), e); + } +} diff --git a/src/main/java/diegosneves/github/rachapedido/exceptions/NullBuyerException.java b/src/main/java/diegosneves/github/rachapedido/exceptions/NullBuyerException.java new file mode 100644 index 0000000..6492ed1 --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/exceptions/NullBuyerException.java @@ -0,0 +1,21 @@ +package diegosneves.github.rachapedido.exceptions; + +/** + * A classe {@link NullBuyerException} representa uma exceção lançada quando um objeto comprador é nulo. + * + * @see RuntimeException + * @author diegosneves + */ +public class NullBuyerException extends RuntimeException { + + public static final ErroHandler ERROR = ErroHandler.NULL_BUYER; + + /** + * Cria uma nova instância da classe {@link NullBuyerException} com a mensagem de erro e a causa especificadas. + * + * @param message A mensagem de erro. + */ + public NullBuyerException(String message) { + super(ERROR.errorMessage(message)); + } +} diff --git a/src/main/java/diegosneves/github/rachapedido/mapper/BuilderMapper.java b/src/main/java/diegosneves/github/rachapedido/mapper/BuilderMapper.java new file mode 100644 index 0000000..4c79098 --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/mapper/BuilderMapper.java @@ -0,0 +1,69 @@ +package diegosneves.github.rachapedido.mapper; + +import diegosneves.github.rachapedido.exceptions.ConstructorDefaultUndefinedException; +import diegosneves.github.rachapedido.exceptions.MapperFailureException; + +import java.lang.reflect.Field; + +/** + * A interface BuilderMapper fornece métodos para simplificar o processo de mapeamento entre objetos. + */ +public interface BuilderMapper { + + /** + * Mapeia os campos do objeto de origem para os campos da classe de destino. + * + * @param o tipo da classe de destino + * @param destinationClass a classe a ser mapeada + * @param source o objeto de origem que será convertido no objeto de destino + * @return uma instância da classe de destino com seus campos preenchidos + * @throws ConstructorDefaultUndefinedException se a classe de destino não tiver um construtor padrão + * @throws MapperFailureException se ocorrer um erro ao mapear os campos + */ + static T builderMapper(Class destinationClass, Object source) { + + var destinationFields = destinationClass.getDeclaredFields(); + + T mappedInstance = null; + + try { + mappedInstance = destinationClass.getConstructor().newInstance(); + } catch (NoSuchMethodException e) { + throw new ConstructorDefaultUndefinedException(destinationClass.getName(), e); + } catch (Exception e) { + throw new MapperFailureException(destinationClass.getName(), e); + } + + for(Field field : destinationFields) { + field.setAccessible(true); + try { + var sourceField = source.getClass().getDeclaredField(field.getName()); + sourceField.setAccessible(true); + field.set(mappedInstance, sourceField.get(source)); + } catch (Exception ignored) { + } + } + + return mappedInstance; + } + + /** + * Mapeia os campos do objeto de origem para os campos da classe de destino. + * + * @param o tipo da classe de destino + * @param o tipo do objeto de origem + * @param destinationClass a classe a ser mapeada + * @param source o objeto de origem a ser convertido no objeto de destino + * @param strategy a estratégia a ser usada para construir o objeto de destino (opcional) + * @return uma instância da classe destino com seus campos preenchidos + */ + static T builderMapper(Class destinationClass, E source, BuildingStrategy strategy) { + if (strategy == null) { + return BuilderMapper.builderMapper(destinationClass, source); + } + + return strategy.run(source); + } + + +} diff --git a/src/main/java/diegosneves/github/rachapedido/mapper/BuildingStrategy.java b/src/main/java/diegosneves/github/rachapedido/mapper/BuildingStrategy.java new file mode 100644 index 0000000..311343f --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/mapper/BuildingStrategy.java @@ -0,0 +1,19 @@ +package diegosneves.github.rachapedido.mapper; + +/** + * A interface {@link BuildingStrategy} define uma estratégia para executar operações de mapeamento de objetos. + * + * @param o tipo da classe de destino + * @param o tipo do objeto de origem + */ +public interface BuildingStrategy { + + /** + * Executa a estratégia para realizar uma operação de mapeamento entre objetos. + * + * @param origem o objeto de origem que será convertido no objeto de destino + * @return uma instância da classe de destino com seus campos preenchidos + */ + T run(E origem); + +} diff --git a/src/main/java/diegosneves/github/rachapedido/mapper/BuyerPersonMapper.java b/src/main/java/diegosneves/github/rachapedido/mapper/BuyerPersonMapper.java new file mode 100644 index 0000000..d36aff3 --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/mapper/BuyerPersonMapper.java @@ -0,0 +1,29 @@ +package diegosneves.github.rachapedido.mapper; + +import diegosneves.github.rachapedido.dto.PersonDTO; +import diegosneves.github.rachapedido.model.Person; + +/** + * A classe {@link BuyerPersonMapper} implementa a interface {@link BuildingStrategy} e é responsável por criar + * um novo objeto {@link Person} baseado no objeto {@link PersonDTO} fornecido usando o construtor + * padrão. + */ +public class BuyerPersonMapper implements BuildingStrategy { + + /** + * Executa o padrão de construtor para criar um novo objeto {@link Person} com base no {@link PersonDTO} fornecido. + * + * @param origem O objeto {@link PersonDTO} contendo os dados a serem usados no objeto {@link Person}. + * @return Um novo objeto {@link Person} com os campos definidos com base nos dados do {@link PersonDTO}. + * O campo {@link Boolean isBuyer} é sempre definido como {@link Boolean#TRUE verdadeiro}. + */ + @Override + public Person run(PersonDTO origem) { + return Person.builder() + .personName(origem.getPersonName()) + .isBuyer(Boolean.TRUE) + .items(origem.getItems()) + .email(origem.getEmail()) + .build(); + } +} diff --git a/src/main/java/diegosneves/github/rachapedido/model/BillSplit.java b/src/main/java/diegosneves/github/rachapedido/model/BillSplit.java new file mode 100644 index 0000000..1fcb76d --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/model/BillSplit.java @@ -0,0 +1,18 @@ +package diegosneves.github.rachapedido.model; + +import lombok.*; + +import java.util.ArrayList; +import java.util.List; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Builder +public class BillSplit { + + private List invoices = new ArrayList<>(); + private Double totalPayable; + +} diff --git a/src/main/java/diegosneves/github/rachapedido/model/Invoice.java b/src/main/java/diegosneves/github/rachapedido/model/Invoice.java new file mode 100644 index 0000000..4813058 --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/model/Invoice.java @@ -0,0 +1,18 @@ +package diegosneves.github.rachapedido.model; + +import lombok.*; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Builder +public class Invoice { + + private String consumerName; + private Double valueConsumed; + private Double totalPayable; + private Double percentageConsumedTotalBill; + private String paymentLink; + +} diff --git a/src/main/java/diegosneves/github/rachapedido/model/Item.java b/src/main/java/diegosneves/github/rachapedido/model/Item.java new file mode 100644 index 0000000..81348f7 --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/model/Item.java @@ -0,0 +1,15 @@ +package diegosneves.github.rachapedido.model; + +import lombok.*; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Builder +public class Item { + + private String name; + private Double price; + +} diff --git a/src/main/java/diegosneves/github/rachapedido/model/Order.java b/src/main/java/diegosneves/github/rachapedido/model/Order.java new file mode 100644 index 0000000..867bb8e --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/model/Order.java @@ -0,0 +1,21 @@ +package diegosneves.github.rachapedido.model; + +import lombok.*; + +/** + * A classe {@link Order} representa um pedido feito por um {@link Person consumidor}. + * Cada pedido contém o {@link String nome do consumidor} e o {@link Double valor total consumido}. + * + * @author diegosneves + */ +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Builder +public class Order { + + private String consumerName; + private Double valueConsumed; + +} diff --git a/src/main/java/diegosneves/github/rachapedido/model/Person.java b/src/main/java/diegosneves/github/rachapedido/model/Person.java new file mode 100644 index 0000000..8c755b7 --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/model/Person.java @@ -0,0 +1,26 @@ +package diegosneves.github.rachapedido.model; + +import lombok.*; + +import java.util.ArrayList; +import java.util.List; + +/** + * A classe {@link Person} é responsável por representar a abstração de uma pessoa. + * Cada instância dessa classe contém infomações como o nome e o email da pessoa. + * Além disso, possui um atributo do tipo {@link Boolean} que indica se a pessoa é uma compradora. + * Por fim, ela contém também uma lista de {@link Item} que representa os itens associados à pessoa. + */ +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Builder +public class Person { + + private String personName; + private String email; + private Boolean isBuyer = Boolean.FALSE; + private List items = new ArrayList<>(); + +} diff --git a/src/main/java/diegosneves/github/rachapedido/request/SplitInvoiceRequest.java b/src/main/java/diegosneves/github/rachapedido/request/SplitInvoiceRequest.java new file mode 100644 index 0000000..45beee7 --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/request/SplitInvoiceRequest.java @@ -0,0 +1,35 @@ +package diegosneves.github.rachapedido.request; + +import diegosneves.github.rachapedido.dto.PersonDTO; +import diegosneves.github.rachapedido.enums.DiscountType; +import lombok.*; + +import java.util.List; + +/** + * A classe {@link SplitInvoiceRequest} representa uma requisição para dividir um valor de fatura. + *

+ * Esta classe contém informações sobre: + *

    + *
  1. {@link PersonDTO comprador} - O indivíduo que fez a compra.
  2. + *
  3. {@link PersonDTO compradores} - Uma lista de pessoas com quem o comprador pretende dividir a fatura.
  4. + *
  5. {@link DiscountType discountType;} - O tipo de desconto aplicado (se houver).
  6. + *
  7. {@link Double discount;} - O valor do desconto (se houver).
  8. + *
  9. {@link Double deliveryFee;} - A taxa de entrega (se aplicável).
  10. + *
+ * Cada instância desta classe representa uma requisição única para dividir uma fatura. + * + * @author diegosneves + */ +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Builder +public class SplitInvoiceRequest { + private PersonDTO buyer; + private List splitInvoiceWith; + private DiscountType discountType; + private Double discount; + private Double deliveryFee; +} diff --git a/src/main/java/diegosneves/github/rachapedido/response/SplitInvoiceResponse.java b/src/main/java/diegosneves/github/rachapedido/response/SplitInvoiceResponse.java new file mode 100644 index 0000000..17d8312 --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/response/SplitInvoiceResponse.java @@ -0,0 +1,17 @@ +package diegosneves.github.rachapedido.response; + +import diegosneves.github.rachapedido.model.Invoice; +import lombok.*; + +import java.util.List; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@Builder +public class SplitInvoiceResponse { + + private List invoices; + private Double totalPayable; +} diff --git a/src/main/java/diegosneves/github/rachapedido/service/InvoiceService.java b/src/main/java/diegosneves/github/rachapedido/service/InvoiceService.java new file mode 100644 index 0000000..8665b10 --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/service/InvoiceService.java @@ -0,0 +1,75 @@ +package diegosneves.github.rachapedido.service; + +import diegosneves.github.rachapedido.core.CashDiscountStrategy; +import diegosneves.github.rachapedido.core.DiscountStrategy; +import diegosneves.github.rachapedido.core.NoDiscountStrategy; +import diegosneves.github.rachapedido.core.PercentageDiscountStrategy; +import diegosneves.github.rachapedido.dto.InvoiceDTO; +import diegosneves.github.rachapedido.enums.DiscountType; +import diegosneves.github.rachapedido.exceptions.CalculateInvoiceException; +import diegosneves.github.rachapedido.mapper.BuilderMapper; +import diegosneves.github.rachapedido.model.BillSplit; +import diegosneves.github.rachapedido.model.Invoice; +import diegosneves.github.rachapedido.model.Order; +import diegosneves.github.rachapedido.model.Person; +import diegosneves.github.rachapedido.service.contract.InvoiceServiceContract; +import diegosneves.github.rachapedido.service.contract.OrderServiceContract; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +import static java.util.Objects.isNull; + +@Service +public class InvoiceService implements InvoiceServiceContract { + + private static final String CALCULATION_ERROR_MESSAGE = "Houve um problema ao calcular o valor total do pedido."; + private static final String NULL_PARAMETER_ERROR_MESSAGE = "Um dos parâmetros necessários para a operação de cálculo da fatura está ausente ou nulo."; + private final OrderServiceContract orderService; + + @Autowired + public InvoiceService(OrderServiceContract orderService) { + this.orderService = orderService; + } + + @Override + public BillSplit generateInvoice(List consumers, DiscountType discountType, Double discount, Double deliveryFee) { + this.validateParameters(consumers, discountType, discount, deliveryFee); + List closeOrder = this.orderService.closeOrder(consumers); + List invoices = this.calculateDiscount(closeOrder, discountType, discount, deliveryFee); + return new BillSplit(); + } + + private void validateParameters(List consumers, DiscountType discountType, Double discount, Double deliveryFee) throws CalculateInvoiceException { + if (isNull(consumers) || isNull(discountType) || isNull(discount) || isNull(deliveryFee)) { + throw new CalculateInvoiceException(NULL_PARAMETER_ERROR_MESSAGE); + } + } + + private List calculateDiscount(List closeOrder, DiscountType discountType, Double discount, Double deliveryFee) throws CalculateInvoiceException { + List invoices = closeOrder.stream().map(this::convertToInvoiceDTO).toList(); + double total; + try { + total = invoices.stream().mapToDouble(InvoiceDTO::getValueConsumed).sum(); + } catch (Exception e) { + throw new CalculateInvoiceException(CALCULATION_ERROR_MESSAGE, e); + } + Double finalTotal = total; + return invoices.stream().map(dto -> this.calcDiscountForInvoice(dto, discountType, discount, finalTotal, deliveryFee)).toList(); + } + + private Invoice calcDiscountForInvoice(InvoiceDTO dto, DiscountType discountType, Double discount, Double total, Double deliveryFee){ + DiscountStrategy strategy = DiscountStrategy.link( + new CashDiscountStrategy(), + new PercentageDiscountStrategy(), + new NoDiscountStrategy()); + + return strategy.calculateDiscount(dto,discount, discountType, total, deliveryFee); + } + + private InvoiceDTO convertToInvoiceDTO(Order order) { + return BuilderMapper.builderMapper(InvoiceDTO.class, order); + } + +} diff --git a/src/main/java/diegosneves/github/rachapedido/service/OrderService.java b/src/main/java/diegosneves/github/rachapedido/service/OrderService.java new file mode 100644 index 0000000..774ed2b --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/service/OrderService.java @@ -0,0 +1,59 @@ +package diegosneves.github.rachapedido.service; + +import diegosneves.github.rachapedido.exceptions.CloseOrderException; +import diegosneves.github.rachapedido.model.Item; +import diegosneves.github.rachapedido.model.Order; +import diegosneves.github.rachapedido.model.Person; +import diegosneves.github.rachapedido.service.contract.OrderServiceContract; +import org.springframework.stereotype.Service; + +import java.util.List; + +import static java.util.Objects.isNull; + +/** + * A classe {@link OrderService} é responsável por fechar pedidos para cada consumidor fornecido. + * Ela implementa a interface {@link OrderServiceContract}. + * + * @author diegosneves + */ +@Service +public class OrderService implements OrderServiceContract { + + private static final String NULL_CONSTANT = "A lista de consumidores está nula, verifique se foram adicionados consumidores à lista."; + private static final String ORDER_CLOSE_FAILURE_MESSAGE = "Ao processar os cálculos do pedido, ocorreu um erro."; + + @Override + public List closeOrder(List allConsumers) throws CloseOrderException { + if (isNull(allConsumers)) { + throw new CloseOrderException(NULL_CONSTANT); + } + return allConsumers.stream().map(this::takeOrdersPerConsumers).toList(); + } + + /** + * Este método recebe um objeto {@link Person} como parâmetro e calcula o valor total de itens associados a essa pessoa. + * Em seguida, cria um objeto {@link Order} com o nome do consumidor e o valor total consumido e retorna esse objeto Order. + * Caso qualquer exceção ocorra durante o cálculo, uma exceção {@link CloseOrderException} será lançada com uma mensagem de erro apropriada. + * + * @param person O objeto {@link Person} para o qual o pedido precisa ser criado. + * @return Um objeto {@link Order} com o nome do consumidor e o valor total consumido. + * @throws CloseOrderException Se ocorrer alguma exceção durante o cálculo do valor total. + */ + private Order takeOrdersPerConsumers(Person person) throws CloseOrderException { + Double totalValue = null; + try { + totalValue = person.getItems().stream() + .mapToDouble(Item::getPrice) + .sum(); + } catch (Exception e) { + throw new CloseOrderException(ORDER_CLOSE_FAILURE_MESSAGE, e); + } + return Order.builder() + .consumerName(person.getPersonName()) + .valueConsumed(totalValue) + .build(); + } + + +} diff --git a/src/main/java/diegosneves/github/rachapedido/service/PersonService.java b/src/main/java/diegosneves/github/rachapedido/service/PersonService.java new file mode 100644 index 0000000..7d42b70 --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/service/PersonService.java @@ -0,0 +1,72 @@ +package diegosneves.github.rachapedido.service; + +import diegosneves.github.rachapedido.dto.PersonDTO; +import diegosneves.github.rachapedido.exceptions.NullBuyerException; +import diegosneves.github.rachapedido.mapper.BuilderMapper; +import diegosneves.github.rachapedido.mapper.BuyerPersonMapper; +import diegosneves.github.rachapedido.model.Person; +import diegosneves.github.rachapedido.service.contract.PersonServiceContract; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +import static java.util.Objects.isNull; +import static java.util.Objects.nonNull; + +/** + * A classe {@link PersonService} implementa a interface {@link PersonServiceContract} e fornece métodos + * para interagir com objetos do tipo Person. + * + * @author diegosneves + */ +@Service +public class PersonService implements PersonServiceContract { + + private static final String BUYER_ERROR = PersonDTO.class.getSimpleName(); + + @Override + public List getConsumers(PersonDTO buyer, List consumers) throws NullBuyerException { + if (isNull(buyer)) { + throw new NullBuyerException(BUYER_ERROR); + } + List personList = new ArrayList<>(); + personList.add(this.convertBuyerToPerson(buyer)); + if (nonNull(consumers)) { + personList.addAll(this.convertAllConsumersToPerson(consumers)); + } + + return personList; + } + + /** + * Converte uma lista de objetos {@link PersonDTO} em uma lista de objetos {@link Person}. + * + * @param consumer A lista de objetos {@link PersonDTO} a serem convertidos. + * @return A lista de objetos {@link Person} convertidos. + */ + private List convertAllConsumersToPerson(List consumer) { + return consumer.stream().map(this::convertToPerson).toList(); + } + + /** + * Converte um objeto {@link PersonDTO} em um objeto {@link Person}. + * + * @param consumer O objeto {@link PersonDTO} a ser convertido. + * @return O objeto {@link Person} convertido. + */ + private Person convertToPerson(PersonDTO consumer) { + return BuilderMapper.builderMapper(Person.class, consumer); + } + + /** + * Converte um objeto do tipo {@link PersonDTO} em um objeto do tipo {@link Person}. + * + * @param buyer O objeto {@link PersonDTO} que representa um consumidor. + * @return O objeto {@link Person} que representa um comprador, resultado da conversão. + */ + private Person convertBuyerToPerson(PersonDTO buyer) { + BuyerPersonMapper mapper = new BuyerPersonMapper(); + return BuilderMapper.builderMapper(Person.class, buyer, mapper); + } +} diff --git a/src/main/java/diegosneves/github/rachapedido/service/SplitInvoiceService.java b/src/main/java/diegosneves/github/rachapedido/service/SplitInvoiceService.java new file mode 100644 index 0000000..f35f46b --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/service/SplitInvoiceService.java @@ -0,0 +1,35 @@ +package diegosneves.github.rachapedido.service; + +import diegosneves.github.rachapedido.mapper.BuilderMapper; +import diegosneves.github.rachapedido.model.BillSplit; +import diegosneves.github.rachapedido.model.Person; +import diegosneves.github.rachapedido.request.SplitInvoiceRequest; +import diegosneves.github.rachapedido.response.SplitInvoiceResponse; +import diegosneves.github.rachapedido.service.contract.InvoiceServiceContract; +import diegosneves.github.rachapedido.service.contract.PersonServiceContract; +import diegosneves.github.rachapedido.service.contract.SplitInvoiceServiceContract; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class SplitInvoiceService implements SplitInvoiceServiceContract { + + private final PersonServiceContract personService; + private final InvoiceServiceContract invoiceService; + + @Autowired + public SplitInvoiceService(PersonServiceContract personService, InvoiceServiceContract invoiceService) { + this.personService = personService; + this.invoiceService = invoiceService; + } + + @Override + public SplitInvoiceResponse splitInvoice(SplitInvoiceRequest request) { + List consumers = this.personService.getConsumers(request.getBuyer(), request.getSplitInvoiceWith()); + BillSplit billSplit = this.invoiceService.generateInvoice(consumers, request.getDiscountType(), request.getDiscount(), request.getDeliveryFee()); + + return BuilderMapper.builderMapper(SplitInvoiceResponse.class, billSplit); + } +} diff --git a/src/main/java/diegosneves/github/rachapedido/service/contract/InvoiceServiceContract.java b/src/main/java/diegosneves/github/rachapedido/service/contract/InvoiceServiceContract.java new file mode 100644 index 0000000..e9931dc --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/service/contract/InvoiceServiceContract.java @@ -0,0 +1,13 @@ +package diegosneves.github.rachapedido.service.contract; + +import diegosneves.github.rachapedido.enums.DiscountType; +import diegosneves.github.rachapedido.model.BillSplit; +import diegosneves.github.rachapedido.model.Person; + +import java.util.List; + +public interface InvoiceServiceContract { + + BillSplit generateInvoice(List consumers, DiscountType discountType, Double discount, Double deliveryFee); + +} diff --git a/src/main/java/diegosneves/github/rachapedido/service/contract/OrderServiceContract.java b/src/main/java/diegosneves/github/rachapedido/service/contract/OrderServiceContract.java new file mode 100644 index 0000000..9ea78db --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/service/contract/OrderServiceContract.java @@ -0,0 +1,27 @@ +package diegosneves.github.rachapedido.service.contract; + +import diegosneves.github.rachapedido.exceptions.CloseOrderException; +import diegosneves.github.rachapedido.model.Order; +import diegosneves.github.rachapedido.model.Person; +import diegosneves.github.rachapedido.service.OrderService; + +import java.util.List; + +/** + * A interface {@link OrderServiceContract} representa o contrato para a classe {@link OrderService}. + * Fornece um método para fechar o {@link Order pedido} para cada {@link Person consumidor} fornecido. + * + * @author diegosneves + */ +public interface OrderServiceContract { + + /** + * Fecha o {@link Order pedido} para cada {@link Person consumidor} fornecido. + * + * @param allConsumers A lista de todos os {@link Person consumidores} para os quais o {@link Order pedido} precisa ser fechado. + * @return A lista contendo os {@link Order pedidos} fechados, com o valor total consumido por cada consumidor. + * @throws CloseOrderException se ocorrer algum erro durante o processo de finalização do pedido. + */ + List closeOrder(List allConsumers) throws CloseOrderException; + +} diff --git a/src/main/java/diegosneves/github/rachapedido/service/contract/PersonServiceContract.java b/src/main/java/diegosneves/github/rachapedido/service/contract/PersonServiceContract.java new file mode 100644 index 0000000..ea761ae --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/service/contract/PersonServiceContract.java @@ -0,0 +1,27 @@ +package diegosneves.github.rachapedido.service.contract; + +import diegosneves.github.rachapedido.dto.PersonDTO; +import diegosneves.github.rachapedido.exceptions.NullBuyerException; +import diegosneves.github.rachapedido.model.Person; + +import java.util.List; + +/** + * A interface {@link PersonServiceContract} estabelece o contrato para a classe PersonService. + * Ela provê um método para recuperar uma lista de {@link Person consumidores}, baseando-se nos dados do {@link PersonDTO comprador} e nos {@link PersonDTO participantes da divisão} fornecidos. + * + * @author diegosneves + */ +public interface PersonServiceContract { + + /** + * Recupera a lista de consumidores com base no {@link PersonDTO comprador} e nos {@link PersonDTO consumidores} fornecidos. + * + * @param buyer As informações do {@link PersonDTO comprador}. + * @param consumers A lista de {@link PersonDTO consumidores}. + * @return A lista com todos os {@link Person consumidores}. + * @throws NullBuyerException Se o comprador for nulo. + */ + List getConsumers(PersonDTO buyer, List consumers) throws NullBuyerException; + +} diff --git a/src/main/java/diegosneves/github/rachapedido/service/contract/SplitInvoiceServiceContract.java b/src/main/java/diegosneves/github/rachapedido/service/contract/SplitInvoiceServiceContract.java new file mode 100644 index 0000000..ac9377d --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/service/contract/SplitInvoiceServiceContract.java @@ -0,0 +1,10 @@ +package diegosneves.github.rachapedido.service.contract; + +import diegosneves.github.rachapedido.request.SplitInvoiceRequest; +import diegosneves.github.rachapedido.response.SplitInvoiceResponse; + +public interface SplitInvoiceServiceContract { + + SplitInvoiceResponse splitInvoice(SplitInvoiceRequest request); + +} diff --git a/src/main/java/diegosneves/github/rachapedido/utils/RoundUtil.java b/src/main/java/diegosneves/github/rachapedido/utils/RoundUtil.java new file mode 100644 index 0000000..e0acdd9 --- /dev/null +++ b/src/main/java/diegosneves/github/rachapedido/utils/RoundUtil.java @@ -0,0 +1,24 @@ +package diegosneves.github.rachapedido.utils; + +/** + * Esta classe fornece um conjunto de métodos de utilidade para arredondar números decimais. + * Os métodos de arredondamento presente aqui realizam um arredondamento para duas casas decimais. + * Como é uma classe utilitária, seu construtor é privado para prevenir a criação de instâncias. + * + * @author diegosneves + */ +public final class RoundUtil { + + private RoundUtil(){} + + /** + * Este método realiza o arredondamento do valor decimal fornecido para duas casas decimais. + * + * @param value o valor decimal a ser arredondado. + * @return O valor arredondado até duas casas decimais. + */ + public static Double round(Double value) { + return Math.round(value * 100.0) / 100.0; + } + +} \ No newline at end of file diff --git a/src/test/java/diegosneves/github/rachapedido/RachaPedidoApplicationTests.java b/src/test/java/diegosneves/github/rachapedido/RachaPedidoApplicationTests.java deleted file mode 100644 index e77a6ee..0000000 --- a/src/test/java/diegosneves/github/rachapedido/RachaPedidoApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package diegosneves.github.rachapedido; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class RachaPedidoApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/src/test/java/diegosneves/github/rachapedido/service/InvoiceServiceTest.java b/src/test/java/diegosneves/github/rachapedido/service/InvoiceServiceTest.java new file mode 100644 index 0000000..10d0784 --- /dev/null +++ b/src/test/java/diegosneves/github/rachapedido/service/InvoiceServiceTest.java @@ -0,0 +1,269 @@ +package diegosneves.github.rachapedido.service; + +import diegosneves.github.rachapedido.dto.InvoiceDTO; +import diegosneves.github.rachapedido.enums.DiscountType; +import diegosneves.github.rachapedido.exceptions.CalculateInvoiceException; +import diegosneves.github.rachapedido.model.*; +import diegosneves.github.rachapedido.service.contract.OrderServiceContract; +import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +@ExtendWith(SpringExtension.class) +class InvoiceServiceTest { + + private static final String CALCULATION_ERROR_MESSAGE = "Houve um problema ao calcular o valor total do pedido."; + private static final String NULL_PARAMETER_ERROR_MESSAGE = "Um dos parâmetros necessários para a operação de cálculo da fatura está ausente ou nulo."; + + @InjectMocks + private InvoiceService service; + + @Mock + private OrderServiceContract orderService; + + private Person consumerI; + private Person consumerII; + + private Item itemI; + private Item itemII; + private Item itemIII; + + private Order orderI; + private Order orderII; + + List orders; + @BeforeEach + void setUp() { + this.itemI = Item.builder() + .name("Hamburguer") + .price(40.0) + .build(); + + this.itemII = Item.builder() + .name("Sobremesa") + .price(2.0) + .build(); + + this.itemIII = Item.builder() + .name("Sanduíche") + .price(8.0) + .build(); + + this.consumerI = Person.builder() + .isBuyer(Boolean.TRUE) + .personName("Fulano") + .email("fulano@gmail.com") + .items(List.of(this.itemI, this.itemII)) + .build(); + + this.consumerII = Person.builder() + .personName("Amigo") + .email("amigo@gmail.com") + .items(List.of(this.itemIII)) + .build(); + + this.orderI = Order.builder() + .consumerName("Fulano") + .valueConsumed(42.0) + .build(); + + this.orderII = Order.builder() + .consumerName("Amigo") + .valueConsumed(8.0) + .build(); + + this.orders = List.of(this.orderI, this.orderII); + } + + @Test + void whenReceiveInvoiceDataThenReturnBillSplit() { + when(orderService.closeOrder(List.of(this.consumerI, this.consumerII))).thenReturn(orders); + + BillSplit actual = this.service.generateInvoice(List.of(this.consumerI, this.consumerII), DiscountType.CASH, 20.0, 8.0); + +// assertNull(actual); + assertNotNull(actual); +// assertEquals(2, actual.getInvoices().size()); +// assertEquals("Fulano", actual.getInvoices().get(0).getConsumerName()); +// assertEquals(42.0, actual.getInvoices().get(0).getValueConsumed()); +// assertEquals(31.92, actual.getInvoices().get(0).getTotalPayable()); +// assertEquals(84.0, actual.getInvoices().get(0).getPercentageConsumedTotalBill()); +// assertEquals("link", actual.getInvoices().get(0).getPaymentLink()); +// assertEquals("Amigo", actual.getInvoices().get(1).getConsumerName()); +// assertEquals(8.0, actual.getInvoices().get(1).getValueConsumed()); +// assertEquals(6.08, actual.getInvoices().get(1).getTotalPayable()); +// assertEquals(16.0, actual.getInvoices().get(1).getPercentageConsumedTotalBill()); +// assertEquals("link", actual.getInvoices().get(1).getPaymentLink()); +// assertEquals(38.0, actual.getTotalPayable()); + } + + + @Test + @SneakyThrows + void whenConvertToInvoiceReceiveOrderThenReturnInvoice() { + Method method = this.service.getClass().getDeclaredMethod("convertToInvoiceDTO", Order.class); + method.setAccessible(true); + + InvoiceDTO actual = (InvoiceDTO) method.invoke(this.service, this.orderI); + + assertNotNull(actual); + assertEquals("Fulano", actual.getConsumerName()); + assertEquals(42.0, actual.getValueConsumed()); + assertNull(actual.getTotalPayable()); + assertNull(actual.getPercentageConsumedTotalBill()); + + } + + @Test + @SneakyThrows + void whenCalculateDiscountReceiveInvoiceDataAndDiscountTypeCashThenApplyDiscount() { + List invoices = List.of(this.orderI, this.orderII); + + Method method = this.service.getClass().getDeclaredMethod("calculateDiscount", List.class, DiscountType.class, Double.class, Double.class); + method.setAccessible(true); + + List actual = (List) method.invoke(this.service, invoices, DiscountType.CASH, 20.0, 8.0); + + assertEquals("Fulano", actual.get(0).getConsumerName()); + assertEquals(42.0, actual.get(0).getValueConsumed()); + assertEquals(31.92, actual.get(0).getTotalPayable()); + assertEquals(0.84, actual.get(0).getPercentageConsumedTotalBill()); + assertNull(actual.get(0).getPaymentLink()); + assertEquals("Amigo", actual.get(1).getConsumerName()); + assertEquals(8.0, actual.get(1).getValueConsumed()); + assertEquals(6.08, actual.get(1).getTotalPayable()); + assertEquals(0.16, actual.get(1).getPercentageConsumedTotalBill()); + assertNull(actual.get(1).getPaymentLink()); + + } + + @Test + @SneakyThrows + void whenCalculateDiscountReceiveInvoiceDataAndDiscountTypePercentageThenApplyDiscount() { + List invoices = List.of(this.orderI, this.orderII); + + Method method = this.service.getClass().getDeclaredMethod("calculateDiscount", List.class, DiscountType.class, Double.class, Double.class); + method.setAccessible(true); + + List actual = (List) method.invoke(this.service, invoices, DiscountType.PERCENTAGE, 10.0, 8.0); + + assertEquals("Fulano", actual.get(0).getConsumerName()); + assertEquals(42.0, actual.get(0).getValueConsumed()); + assertEquals(44.52, actual.get(0).getTotalPayable()); + assertEquals(0.84, actual.get(0).getPercentageConsumedTotalBill()); + assertNull(actual.get(0).getPaymentLink()); + assertEquals("Amigo", actual.get(1).getConsumerName()); + assertEquals(8.0, actual.get(1).getValueConsumed()); + assertEquals(8.48, actual.get(1).getTotalPayable()); + assertEquals(0.16, actual.get(1).getPercentageConsumedTotalBill()); + assertNull(actual.get(1).getPaymentLink()); + + } + + @Test + @SneakyThrows + void whenCalculateDiscountReceiveInvoiceDataAndDiscountTypeNoDiscountThenNotApplyDiscount() { + List invoices = List.of(this.orderI, this.orderII); + + Method method = this.service.getClass().getDeclaredMethod("calculateDiscount", List.class, DiscountType.class, Double.class, Double.class); + method.setAccessible(true); + + List actual = (List) method.invoke(this.service, invoices, DiscountType.NO_DISCOUNT, 10.0, 8.0); + + assertEquals("Fulano", actual.get(0).getConsumerName()); + assertEquals(42.0, actual.get(0).getValueConsumed()); + assertEquals(48.72, actual.get(0).getTotalPayable()); + assertEquals(0.84, actual.get(0).getPercentageConsumedTotalBill()); + assertNull(actual.get(0).getPaymentLink()); + assertEquals("Amigo", actual.get(1).getConsumerName()); + assertEquals(8.0, actual.get(1).getValueConsumed()); + assertEquals(9.28, actual.get(1).getTotalPayable()); + assertEquals(0.16, actual.get(1).getPercentageConsumedTotalBill()); + assertNull(actual.get(1).getPaymentLink()); + + } + + @Test + @SneakyThrows + void whenCalculateDiscountReceiveInvoiceDataWithValueConsumedNullThenThrowsCalculateInvoiceException() { + this.orderI.setValueConsumed(null); + List invoices = List.of(this.orderI, this.orderII); + + Method method = this.service.getClass().getDeclaredMethod("calculateDiscount", List.class, DiscountType.class, Double.class, Double.class); + method.setAccessible(true); + + InvocationTargetException exception = assertThrows(InvocationTargetException.class, () -> method.invoke(this.service, invoices, DiscountType.NO_DISCOUNT, 10.0, 8.0)); + + assertInstanceOf(CalculateInvoiceException.class, exception.getTargetException()); + assertEquals(CalculateInvoiceException.ERROR.errorMessage(CALCULATION_ERROR_MESSAGE), exception.getTargetException().getMessage()); + + } + + @Test + @SneakyThrows + void whenValidateParametersReceiveInvoiceDataWithConsumerListNullThenThrowsCalculateInvoiceException() { + + Method method = this.service.getClass().getDeclaredMethod("validateParameters", List.class, DiscountType.class, Double.class, Double.class); + method.setAccessible(true); + + InvocationTargetException exception = assertThrows(InvocationTargetException.class, () -> method.invoke(this.service, null, DiscountType.NO_DISCOUNT, 10.0, 8.0)); + + assertInstanceOf(CalculateInvoiceException.class, exception.getTargetException()); + assertEquals(CalculateInvoiceException.ERROR.errorMessage(NULL_PARAMETER_ERROR_MESSAGE), exception.getTargetException().getMessage()); + + } + + @Test + @SneakyThrows + void whenValidateParametersReceiveInvoiceDataWithDiscountTypeNullThenThrowsCalculateInvoiceException() { + + Method method = this.service.getClass().getDeclaredMethod("validateParameters", List.class, DiscountType.class, Double.class, Double.class); + method.setAccessible(true); + + InvocationTargetException exception = assertThrows(InvocationTargetException.class, () -> method.invoke(this.service, this.orders, null, 10.0, 8.0)); + + assertInstanceOf(CalculateInvoiceException.class, exception.getTargetException()); + assertEquals(CalculateInvoiceException.ERROR.errorMessage(NULL_PARAMETER_ERROR_MESSAGE), exception.getTargetException().getMessage()); + + } + + @Test + @SneakyThrows + void whenValidateParametersReceiveInvoiceDataWithDiscountNullThenThrowsCalculateInvoiceException() { + + Method method = this.service.getClass().getDeclaredMethod("validateParameters", List.class, DiscountType.class, Double.class, Double.class); + method.setAccessible(true); + + InvocationTargetException exception = assertThrows(InvocationTargetException.class, () -> method.invoke(this.service, this.orders, DiscountType.CASH, null, 8.0)); + + assertInstanceOf(CalculateInvoiceException.class, exception.getTargetException()); + assertEquals(CalculateInvoiceException.ERROR.errorMessage(NULL_PARAMETER_ERROR_MESSAGE), exception.getTargetException().getMessage()); + + } + + @Test + @SneakyThrows + void whenValidateParametersReceiveInvoiceDataWithDeliveryFeeNullThenThrowsCalculateInvoiceException() { + + Method method = this.service.getClass().getDeclaredMethod("validateParameters", List.class, DiscountType.class, Double.class, Double.class); + method.setAccessible(true); + + InvocationTargetException exception = assertThrows(InvocationTargetException.class, () -> method.invoke(this.service, this.orders, DiscountType.CASH, 10.0, null)); + + assertInstanceOf(CalculateInvoiceException.class, exception.getTargetException()); + assertEquals(CalculateInvoiceException.ERROR.errorMessage(NULL_PARAMETER_ERROR_MESSAGE), exception.getTargetException().getMessage()); + + } + +} diff --git a/src/test/java/diegosneves/github/rachapedido/service/OrderServiceTest.java b/src/test/java/diegosneves/github/rachapedido/service/OrderServiceTest.java new file mode 100644 index 0000000..4366b78 --- /dev/null +++ b/src/test/java/diegosneves/github/rachapedido/service/OrderServiceTest.java @@ -0,0 +1,116 @@ +package diegosneves.github.rachapedido.service; + +import diegosneves.github.rachapedido.exceptions.CloseOrderException; +import diegosneves.github.rachapedido.model.Item; +import diegosneves.github.rachapedido.model.Order; +import diegosneves.github.rachapedido.model.Person; +import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(SpringExtension.class) +class OrderServiceTest { + + private static final String NULL_CONSTANT = "A lista de consumidores está nula, verifique se foram adicionados consumidores à lista."; + private static final String ORDER_CLOSE_FAILURE_MESSAGE = "Ao processar os cálculos do pedido, ocorreu um erro."; + + @InjectMocks + private OrderService service; + + private Person personI; + private Person personII; + private Item itemI; + private Item itemII; + private Item itemIII; + + @BeforeEach + void setUp() { + this.itemI = Item.builder() + .name("Item I") + .price(40.0) + .build(); + + this.itemII = Item.builder() + .name("Item II") + .price(2.0) + .build(); + + this.itemIII = Item.builder() + .name("Item III") + .price(8.0) + .build(); + + this.personI = Person.builder() + .personName("Buyer - Person I") + .isBuyer(Boolean.TRUE) + .email("buyer@teste.com") + .items(List.of(this.itemI, this.itemII)) + .build(); + + this.personII = Person.builder() + .personName("Consumer - Person II") + .email("consumer@teste.com") + .items(List.of(this.itemIII)) + .build(); + } + + @Test + void whenCloseOrderReceiveConsumerListThenReturnOrderListWithTotalValueConsumedPerConsumer(){ + List orders = this.service.closeOrder(List.of(this.personI, this.personII)); + + assertNotNull(orders); + assertEquals(2, orders.size()); + assertEquals("Buyer - Person I", orders.get(0).getConsumerName()); + assertEquals("Consumer - Person II", orders.get(1).getConsumerName()); + assertEquals(42.0, orders.get(0).getValueConsumed()); + assertEquals(8.0, orders.get(1).getValueConsumed()); + } + + @Test + @SneakyThrows + void whentakeOrdersPerConsumersReceivePersonThenReturnOrderWithTotalValueConsumedAndConsumerName(){ + Method method = this.service.getClass().getDeclaredMethod("takeOrdersPerConsumers", Person.class); + method.setAccessible(true); + + Order order = (Order) method.invoke(this.service, this.personI); + + assertNotNull(order); + assertEquals("Buyer - Person I", order.getConsumerName()); + assertEquals(42.0, order.getValueConsumed()); + } + + @Test + @SneakyThrows + void whentakeOrdersPerConsumersReceivePersonWithItemPriceNullThenThrowsNullPriceException(){ + this.itemI.setPrice(null); + + Method method = this.service.getClass().getDeclaredMethod("takeOrdersPerConsumers", Person.class); + method.setAccessible(true); + + + InvocationTargetException exception = assertThrows(InvocationTargetException.class, () -> method.invoke(this.service, this.personI)); + Throwable realException = exception.getTargetException(); + + assertInstanceOf(CloseOrderException.class, realException); + assertEquals(CloseOrderException.ERROR.errorMessage(ORDER_CLOSE_FAILURE_MESSAGE), realException.getMessage()); + } + + @Test + void whenCloseOrderReceiveConsumerListNullThenThrowsCloseOrderException(){ + + Exception exception = assertThrows(CloseOrderException.class, () -> this.service.closeOrder(null)); + + assertInstanceOf(CloseOrderException.class, exception); + assertEquals(CloseOrderException.ERROR.errorMessage(NULL_CONSTANT), exception.getMessage()); + } + +} diff --git a/src/test/java/diegosneves/github/rachapedido/service/PersonServiceTest.java b/src/test/java/diegosneves/github/rachapedido/service/PersonServiceTest.java new file mode 100644 index 0000000..edad00b --- /dev/null +++ b/src/test/java/diegosneves/github/rachapedido/service/PersonServiceTest.java @@ -0,0 +1,162 @@ +package diegosneves.github.rachapedido.service; + +import diegosneves.github.rachapedido.dto.PersonDTO; +import diegosneves.github.rachapedido.exceptions.NullBuyerException; +import diegosneves.github.rachapedido.model.Item; +import diegosneves.github.rachapedido.model.Person; +import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(SpringExtension.class) +class PersonServiceTest { + + @InjectMocks + private PersonService service; + + private PersonDTO personI; + private PersonDTO personII; + private Item itemI; + private Item itemII; + private Item itemIII; + + @BeforeEach + void setUp() { + this.itemI = Item.builder() + .name("Item I") + .price(40.0) + .build(); + + this.itemII = Item.builder() + .name("Item II") + .price(2.0) + .build(); + + this.itemIII = Item.builder() + .name("Item III") + .price(8.0) + .build(); + + this.personI = PersonDTO.builder() + .personName("Buyer - Person I") + .email("buyer@teste.com") + .items(List.of(this.itemI, this.itemII)) + .build(); + + this.personII = PersonDTO.builder() + .personName("Consumer - Person II") + .email("consumer@teste.com") + .items(List.of(this.itemIII)) + .build(); + } + + @Test + @SneakyThrows + void whenConvertBuyerToPersonReceivePersonDTOThenPersonBuyerMustBeReturn() { + String personNameExpect = "Buyer - Person I"; + String emailExpect = "buyer@teste.com"; + + + Method method = this.service.getClass().getDeclaredMethod("convertBuyerToPerson", PersonDTO.class); + method.setAccessible(true); + + Person actual = (Person) method.invoke(this.service, this.personI); + + assertNotNull(actual); + assertEquals(personNameExpect, actual.getPersonName()); + assertEquals(emailExpect, actual.getEmail()); + assertEquals(2, actual.getItems().size()); + assertEquals(this.itemI, actual.getItems().get(0)); + assertEquals(this.itemII, actual.getItems().get(1)); + assertTrue(actual.getIsBuyer()); + + } + + @Test + @SneakyThrows + void whenConvertToPersonReceivePersonDTOThenPersonConsumerMustBeReturn() { + String personNameExpect = "Consumer - Person II"; + String emailExpect = "consumer@teste.com"; + + + Method method = this.service.getClass().getDeclaredMethod("convertToPerson", PersonDTO.class); + method.setAccessible(true); + + Person actual = (Person) method.invoke(this.service, this.personII); + + assertNotNull(actual); + assertEquals(personNameExpect, actual.getPersonName()); + assertEquals(emailExpect, actual.getEmail()); + assertEquals(1, actual.getItems().size()); + assertEquals(this.itemIII, actual.getItems().get(0)); + assertFalse(actual.getIsBuyer()); + + } + + @Test + @SneakyThrows + void whenConvertAllToPersonReceivePersonDTOListThenPersonListMustBeReturn() { + List personsToBeInvoked = new ArrayList<>(); + personsToBeInvoked.add(this.personI); + personsToBeInvoked.add(this.personII); + + Method method = this.service.getClass().getDeclaredMethod("convertAllConsumersToPerson", List.class); + method.setAccessible(true); + + List actual = (List) method.invoke(this.service, personsToBeInvoked); + + assertNotNull(actual); + assertEquals(2, actual.size()); + assertFalse(actual.get(0).getIsBuyer()); + assertFalse(actual.get(1).getIsBuyer()); + } + + @Test + void whenGetConsumersReceivePersonDTOBuyerAndConsumersListThenPersonListMustBeReturn() { + List consumers = service.getConsumers(personI, List.of(personII)); + + assertEquals(2, consumers.size()); + assertEquals("Buyer - Person I", consumers.get(0).getPersonName()); + assertEquals("Consumer - Person II", consumers.get(1).getPersonName()); + assertEquals("buyer@teste.com", consumers.get(0).getEmail()); + assertEquals("consumer@teste.com", consumers.get(1).getEmail()); + assertEquals(2, consumers.get(0).getItems().size()); + assertEquals(1, consumers.get(1).getItems().size()); + assertEquals(itemI, consumers.get(0).getItems().get(0)); + assertEquals(itemII, consumers.get(0).getItems().get(1)); + assertEquals(itemIII, consumers.get(1).getItems().get(0)); + assertTrue(consumers.get(0).getIsBuyer()); + assertFalse(consumers.get(1).getIsBuyer()); + } + + @Test + void whenGetConsumersReceivePersonDTOBuyerAndConsumersListNullThenPersonListMustBeReturnWithAConsumer() { + List consumers = service.getConsumers(personI, null); + + assertEquals(1, consumers.size()); + assertEquals("Buyer - Person I", consumers.get(0).getPersonName()); + assertEquals("buyer@teste.com", consumers.get(0).getEmail()); + assertEquals(2, consumers.get(0).getItems().size()); + assertEquals(itemI, consumers.get(0).getItems().get(0)); + assertEquals(itemII, consumers.get(0).getItems().get(1)); + assertTrue(consumers.get(0).getIsBuyer()); + } + + @Test + void whenGetConsumersReceivePersonDTOBuyerNullThenThrowNullBuyerException() { + Exception exception = assertThrows(NullBuyerException.class, () -> service.getConsumers(null, List.of(personII))); + + assertInstanceOf(NullBuyerException.class, exception); + assertEquals(NullBuyerException.ERROR.errorMessage(PersonDTO.class.getSimpleName()), exception.getMessage()); + } + +} diff --git a/src/test/java/diegosneves/github/rachapedido/service/SplitInvoiceServiceTest.java b/src/test/java/diegosneves/github/rachapedido/service/SplitInvoiceServiceTest.java new file mode 100644 index 0000000..09ed47a --- /dev/null +++ b/src/test/java/diegosneves/github/rachapedido/service/SplitInvoiceServiceTest.java @@ -0,0 +1,133 @@ +package diegosneves.github.rachapedido.service; + +import diegosneves.github.rachapedido.dto.PersonDTO; +import diegosneves.github.rachapedido.enums.DiscountType; +import diegosneves.github.rachapedido.model.*; +import diegosneves.github.rachapedido.request.SplitInvoiceRequest; +import diegosneves.github.rachapedido.response.SplitInvoiceResponse; +import diegosneves.github.rachapedido.service.contract.InvoiceServiceContract; +import diegosneves.github.rachapedido.service.contract.PersonServiceContract; +import net.bytebuddy.implementation.bind.annotation.IgnoreForBinding; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@ExtendWith(SpringExtension.class) +class SplitInvoiceServiceTest { + + @InjectMocks + private SplitInvoiceService service; + @Mock + private InvoiceServiceContract invoiceService; + @Mock + private PersonServiceContract personService; + + private SplitInvoiceRequest request; + private Item item; + private PersonDTO buyer; + private PersonDTO friend; + + private Person consumerI; + private Person consumerII; + + private BillSplit billSplit; + private Invoice invoiceI; + private Invoice invoiceII; + + @BeforeEach + void setUp() { + this.buyer = PersonDTO.builder() + .personName("Fulano") + .email("fulano@gmail.com") + .items(List.of(new Item("Hamburguer", 40.0), new Item("Sobremesa", 2.0))) + .build(); + + this.friend = PersonDTO.builder() + .personName("Amigo") + .email("amigo@gmail.com") + .items(List.of(new Item("Sanduíche", 8.0))) + .build(); + + this.request = SplitInvoiceRequest.builder() + .buyer(this.buyer) + .splitInvoiceWith(List.of(this.friend)) + .deliveryFee(8.0) + .discount(20.0) + .discountType(DiscountType.CASH) + .build(); + + this.consumerI = Person.builder() + .isBuyer(Boolean.TRUE) + .personName("Fulano") + .email("fulano@gmail.com") + .items(List.of(new Item("Hamburguer", 40.0), new Item("Sobremesa", 2.0))) + .build(); + + this.consumerII = Person.builder() + .personName("Amigo") + .email("amigo@gmail.com") + .items(List.of(new Item("Sanduíche", 8.0))) + .build(); + + this.invoiceI = Invoice.builder() + .consumerName("Fulano") + .valueConsumed(42.0) + .totalPayable(31.92) + .percentageConsumedTotalBill(84.0) + .paymentLink("n/a") + .build(); + + this.invoiceII = Invoice.builder() + .consumerName("Amigo") + .valueConsumed(8.0) + .totalPayable(6.08) + .percentageConsumedTotalBill(16.0) + .paymentLink("link") + .build(); + + this.billSplit = BillSplit.builder() + .invoices(List.of(this.invoiceI, this.invoiceII)) + .totalPayable(38.0) + .build(); + + } + + @Test + void whenReceivingInvoiceThenDivisionMustBeCarriedOut() { + when(this.personService.getConsumers(this.buyer, List.of(this.friend))).thenReturn(List.of(this.consumerI, this.consumerII)); + when(this.invoiceService.generateInvoice(anyList(), eq(DiscountType.CASH), eq(20.0), eq(8.0))).thenReturn(this.billSplit); + + SplitInvoiceResponse response = this.service.splitInvoice(this.request); + + verify(personService, times(1)).getConsumers(eq(buyer), eq(List.of(friend))); + verify(invoiceService, times(1)).generateInvoice(eq(List.of(consumerI, consumerII)), eq(DiscountType.CASH), eq(20.0), eq(8.0)); + + assertNotNull(response); + Invoice buyerInvoice = response.getInvoices().stream().filter(p -> p.getConsumerName().equals(this.buyer.getPersonName())).findFirst().orElse(null); + Invoice friendInvoice = response.getInvoices().stream().filter(p -> p.getConsumerName().equals(this.friend.getPersonName())).findFirst().orElse(null); + assertNotNull(buyerInvoice); + assertNotNull(friendInvoice); + assertEquals(2, response.getInvoices().size()); + assertEquals(42.0,buyerInvoice.getValueConsumed()); + assertEquals(31.92,buyerInvoice.getTotalPayable()); + assertEquals(84.0, buyerInvoice.getPercentageConsumedTotalBill()); + assertEquals("n/a", buyerInvoice.getPaymentLink()); + assertEquals(8.0, friendInvoice.getValueConsumed()); + assertEquals(6.08, friendInvoice.getTotalPayable()); + assertEquals(16.0, friendInvoice.getPercentageConsumedTotalBill()); + assertEquals("link", friendInvoice.getPaymentLink()); + assertEquals(38.0, response.getTotalPayable()); + } + +}