From dc3e2013a9c9c2e3ac1b041b99b3b4365441ed42 Mon Sep 17 00:00:00 2001 From: Adnane Miliari Date: Fri, 29 Nov 2024 01:02:10 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8Fimplement=20exception=20ha?= =?UTF-8?q?ndling=20across=20microservices?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business/CustomerException.java | 10 ++ .../business/NotificationException.java | 9 ++ .../business/OrderException.java | 10 ++ .../business/PaymentException.java | 10 ++ .../business/ProductException.java | 9 ++ .../core/BadRequestException.java | 7 + .../exceptionhandler/core/BaseException.java | 7 + .../core/DuplicateResourceException.java | 8 + .../core/ResourceNotFoundException.java | 7 + .../core/ValidationException.java | 17 ++ .../RestResponseEntityExceptionHandler.java | 115 ++++++++++++++ .../exceptionhandler/payload/ErrorCode.java | 20 +++ .../payload/ErrorDetails.java | 18 +++ .../payload/ValidationError.java | 12 ++ .../dev/nano/customer/CustomerConstant.java | 5 +- .../dev/nano/customer/CustomerController.java | 75 ++++----- .../java/dev/nano/customer/CustomerDTO.java | 25 ++- .../dev/nano/customer/CustomerMapper.java | 11 +- .../customer/CustomerNotFoundException.java | 7 - .../dev/nano/customer/CustomerRepository.java | 1 + .../dev/nano/customer/CustomerService.java | 3 +- .../nano/customer/CustomerServiceImpl.java | 149 ++++++++++++------ notification/pom.xml | 4 + .../notification/NotificationConstant.java | 4 + .../notification/NotificationController.java | 28 +--- .../nano/notification/NotificationDTO.java | 21 +++ .../nano/notification/NotificationMapper.java | 2 - .../notification/NotificationService.java | 1 - .../notification/NotificationServiceImpl.java | 68 ++++---- .../java/dev/nano/order/OrderConstant.java | 5 +- .../java/dev/nano/order/OrderController.java | 23 +-- .../main/java/dev/nano/order/OrderDTO.java | 18 ++- .../main/java/dev/nano/order/OrderMapper.java | 3 - .../java/dev/nano/order/OrderService.java | 2 - .../java/dev/nano/order/OrderServiceImpl.java | 98 +++++++----- .../dev/nano/payment/PaymentConstant.java | 4 +- .../dev/nano/payment/PaymentController.java | 27 +--- .../java/dev/nano/payment/PaymentDTO.java | 12 ++ .../java/dev/nano/payment/PaymentMapper.java | 7 +- .../java/dev/nano/payment/PaymentService.java | 1 - .../dev/nano/payment/PaymentServiceImpl.java | 77 ++++++--- .../dev/nano/product/ProductConstant.java | 6 +- .../dev/nano/product/ProductController.java | 48 ++++-- .../java/dev/nano/product/ProductDTO.java | 28 +++- .../java/dev/nano/product/ProductMapper.java | 10 +- .../dev/nano/product/ProductServiceImpl.java | 60 ++++--- product/src/main/resources/db/data.sql | 13 +- 47 files changed, 782 insertions(+), 323 deletions(-) create mode 100644 common/src/main/java/exceptionhandler/business/CustomerException.java create mode 100644 common/src/main/java/exceptionhandler/business/NotificationException.java create mode 100644 common/src/main/java/exceptionhandler/business/OrderException.java create mode 100644 common/src/main/java/exceptionhandler/business/PaymentException.java create mode 100644 common/src/main/java/exceptionhandler/business/ProductException.java create mode 100644 common/src/main/java/exceptionhandler/core/BadRequestException.java create mode 100644 common/src/main/java/exceptionhandler/core/BaseException.java create mode 100644 common/src/main/java/exceptionhandler/core/DuplicateResourceException.java create mode 100644 common/src/main/java/exceptionhandler/core/ResourceNotFoundException.java create mode 100644 common/src/main/java/exceptionhandler/core/ValidationException.java create mode 100644 common/src/main/java/exceptionhandler/handler/RestResponseEntityExceptionHandler.java create mode 100644 common/src/main/java/exceptionhandler/payload/ErrorCode.java create mode 100644 common/src/main/java/exceptionhandler/payload/ErrorDetails.java create mode 100644 common/src/main/java/exceptionhandler/payload/ValidationError.java delete mode 100644 customer/src/main/java/dev/nano/customer/CustomerNotFoundException.java diff --git a/common/src/main/java/exceptionhandler/business/CustomerException.java b/common/src/main/java/exceptionhandler/business/CustomerException.java new file mode 100644 index 0000000..c250e9d --- /dev/null +++ b/common/src/main/java/exceptionhandler/business/CustomerException.java @@ -0,0 +1,10 @@ +package exceptionhandler.business; + +import exceptionhandler.core.BaseException; + +public class CustomerException extends BaseException { + public CustomerException(String message) { + super(message); + } +} + diff --git a/common/src/main/java/exceptionhandler/business/NotificationException.java b/common/src/main/java/exceptionhandler/business/NotificationException.java new file mode 100644 index 0000000..66eeb4f --- /dev/null +++ b/common/src/main/java/exceptionhandler/business/NotificationException.java @@ -0,0 +1,9 @@ +package exceptionhandler.business; + +import exceptionhandler.core.BaseException; + +public class NotificationException extends BaseException { + public NotificationException(String message) { + super(message); + } +} diff --git a/common/src/main/java/exceptionhandler/business/OrderException.java b/common/src/main/java/exceptionhandler/business/OrderException.java new file mode 100644 index 0000000..b9be7ac --- /dev/null +++ b/common/src/main/java/exceptionhandler/business/OrderException.java @@ -0,0 +1,10 @@ +package exceptionhandler.business; + +import exceptionhandler.core.BaseException; + +public class OrderException extends BaseException { + public OrderException(String message) { + super(message); + } +} + diff --git a/common/src/main/java/exceptionhandler/business/PaymentException.java b/common/src/main/java/exceptionhandler/business/PaymentException.java new file mode 100644 index 0000000..bc95612 --- /dev/null +++ b/common/src/main/java/exceptionhandler/business/PaymentException.java @@ -0,0 +1,10 @@ +package exceptionhandler.business; + +import exceptionhandler.core.BaseException; + +public class PaymentException extends BaseException { + public PaymentException(String message) { + super(message); + } +} + diff --git a/common/src/main/java/exceptionhandler/business/ProductException.java b/common/src/main/java/exceptionhandler/business/ProductException.java new file mode 100644 index 0000000..086bd16 --- /dev/null +++ b/common/src/main/java/exceptionhandler/business/ProductException.java @@ -0,0 +1,9 @@ +package exceptionhandler.business; + +import exceptionhandler.core.BaseException; + +public class ProductException extends BaseException { + public ProductException(String message) { + super(message); + } +} diff --git a/common/src/main/java/exceptionhandler/core/BadRequestException.java b/common/src/main/java/exceptionhandler/core/BadRequestException.java new file mode 100644 index 0000000..8a07376 --- /dev/null +++ b/common/src/main/java/exceptionhandler/core/BadRequestException.java @@ -0,0 +1,7 @@ +package exceptionhandler.core; + +public class BadRequestException extends BaseException { + public BadRequestException(String message) { + super(message); + } +} diff --git a/common/src/main/java/exceptionhandler/core/BaseException.java b/common/src/main/java/exceptionhandler/core/BaseException.java new file mode 100644 index 0000000..f4451b1 --- /dev/null +++ b/common/src/main/java/exceptionhandler/core/BaseException.java @@ -0,0 +1,7 @@ +package exceptionhandler.core; + +public abstract class BaseException extends RuntimeException { + public BaseException(String message) { + super(message); + } +} diff --git a/common/src/main/java/exceptionhandler/core/DuplicateResourceException.java b/common/src/main/java/exceptionhandler/core/DuplicateResourceException.java new file mode 100644 index 0000000..efa5510 --- /dev/null +++ b/common/src/main/java/exceptionhandler/core/DuplicateResourceException.java @@ -0,0 +1,8 @@ +package exceptionhandler.core; + +public class DuplicateResourceException extends BaseException { + public DuplicateResourceException(String message) { + super(message); + } +} + diff --git a/common/src/main/java/exceptionhandler/core/ResourceNotFoundException.java b/common/src/main/java/exceptionhandler/core/ResourceNotFoundException.java new file mode 100644 index 0000000..227f9bd --- /dev/null +++ b/common/src/main/java/exceptionhandler/core/ResourceNotFoundException.java @@ -0,0 +1,7 @@ +package exceptionhandler.core; + +public class ResourceNotFoundException extends BaseException { + public ResourceNotFoundException(String message) { + super(message); + } +} diff --git a/common/src/main/java/exceptionhandler/core/ValidationException.java b/common/src/main/java/exceptionhandler/core/ValidationException.java new file mode 100644 index 0000000..9939d57 --- /dev/null +++ b/common/src/main/java/exceptionhandler/core/ValidationException.java @@ -0,0 +1,17 @@ +package exceptionhandler.core; + +import exceptionhandler.payload.ValidationError; +import lombok.Getter; + +import java.util.List; + +@Getter +public class ValidationException extends BaseException { + private final List errors; + + public ValidationException(String message, List errors) { + super(message); + this.errors = errors; + } +} + diff --git a/common/src/main/java/exceptionhandler/handler/RestResponseEntityExceptionHandler.java b/common/src/main/java/exceptionhandler/handler/RestResponseEntityExceptionHandler.java new file mode 100644 index 0000000..437c3a5 --- /dev/null +++ b/common/src/main/java/exceptionhandler/handler/RestResponseEntityExceptionHandler.java @@ -0,0 +1,115 @@ +package exceptionhandler.handler; + +import exceptionhandler.business.CustomerException; +import exceptionhandler.business.NotificationException; +import exceptionhandler.business.OrderException; +import exceptionhandler.business.PaymentException; +import exceptionhandler.business.ProductException; +import exceptionhandler.core.BaseException; +import exceptionhandler.core.DuplicateResourceException; +import exceptionhandler.core.ResourceNotFoundException; +import exceptionhandler.core.ValidationException; +import exceptionhandler.payload.ErrorCode; +import exceptionhandler.payload.ErrorDetails; +import exceptionhandler.payload.ValidationError; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import java.time.LocalDateTime; +import java.util.List; + +@RestControllerAdvice +@Slf4j +public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler { + + @ExceptionHandler(ResourceNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ResponseEntity handleResourceNotFoundException( + ResourceNotFoundException ex, WebRequest request) { + return buildErrorResponse(ex.getMessage(), + ErrorCode.RESOURCE_NOT_FOUND, + HttpStatus.NOT_FOUND, + request); + } + + @ExceptionHandler(ValidationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity handleValidationException( + ValidationException ex, WebRequest request) { + return buildErrorResponse(ex.getMessage(), + ErrorCode.VALIDATION_FAILED, + HttpStatus.BAD_REQUEST, + request, + ex.getErrors()); + } + + @ExceptionHandler(DuplicateResourceException.class) + @ResponseStatus(HttpStatus.CONFLICT) + public ResponseEntity handleDuplicateResourceException( + DuplicateResourceException ex, WebRequest request) { + return buildErrorResponse(ex.getMessage(), + ErrorCode.DUPLICATE_RESOURCE, + HttpStatus.CONFLICT, + request); + } + + @ExceptionHandler({ + CustomerException.class, + OrderException.class, + PaymentException.class, + ProductException.class, + NotificationException.class + }) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity handleBusinessExceptions( + BaseException ex, WebRequest request) { + return buildErrorResponse(ex.getMessage(), + ErrorCode.BAD_REQUEST, + HttpStatus.BAD_REQUEST, + request); + } + + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ResponseEntity handleAllUncaughtException( + Exception ex, WebRequest request) { + return buildErrorResponse(ex.getMessage(), + ErrorCode.INTERNAL_ERROR, + HttpStatus.INTERNAL_SERVER_ERROR, + request); + } + + private ResponseEntity buildErrorResponse( + String message, + ErrorCode errorCode, + HttpStatus status, + WebRequest request) { + return buildErrorResponse(message, errorCode, status, request, null); + } + + private ResponseEntity buildErrorResponse( + String message, + ErrorCode errorCode, + HttpStatus status, + WebRequest request, + List errors) { + + ErrorDetails errorDetails = ErrorDetails.builder() + .timestamp(LocalDateTime.now()) + .code(errorCode.getCode()) + .message(message) + .path(((ServletWebRequest) request).getRequest().getRequestURI()) + .errors(errors) + .build(); + + return new ResponseEntity<>(errorDetails, status); + } +} + diff --git a/common/src/main/java/exceptionhandler/payload/ErrorCode.java b/common/src/main/java/exceptionhandler/payload/ErrorCode.java new file mode 100644 index 0000000..0831fa5 --- /dev/null +++ b/common/src/main/java/exceptionhandler/payload/ErrorCode.java @@ -0,0 +1,20 @@ +package exceptionhandler.payload; + +public enum ErrorCode { + RESOURCE_NOT_FOUND("ERR_001"), + VALIDATION_FAILED("ERR_002"), + DUPLICATE_RESOURCE("ERR_003"), + BAD_REQUEST("ERR_004"), + INTERNAL_ERROR("ERR_005"); + + private final String code; + + ErrorCode(String code) { + this.code = code; + } + + public String getCode() { + return code; + } +} + diff --git a/common/src/main/java/exceptionhandler/payload/ErrorDetails.java b/common/src/main/java/exceptionhandler/payload/ErrorDetails.java new file mode 100644 index 0000000..d953b77 --- /dev/null +++ b/common/src/main/java/exceptionhandler/payload/ErrorDetails.java @@ -0,0 +1,18 @@ +package exceptionhandler.payload; + +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@Builder +public class ErrorDetails { + private LocalDateTime timestamp; + private String code; + private String message; + private String path; + private List errors; +} + diff --git a/common/src/main/java/exceptionhandler/payload/ValidationError.java b/common/src/main/java/exceptionhandler/payload/ValidationError.java new file mode 100644 index 0000000..16cd427 --- /dev/null +++ b/common/src/main/java/exceptionhandler/payload/ValidationError.java @@ -0,0 +1,12 @@ +package exceptionhandler.payload; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class ValidationError { + private String field; + private String message; +} + diff --git a/customer/src/main/java/dev/nano/customer/CustomerConstant.java b/customer/src/main/java/dev/nano/customer/CustomerConstant.java index 5d93df2..7a01306 100644 --- a/customer/src/main/java/dev/nano/customer/CustomerConstant.java +++ b/customer/src/main/java/dev/nano/customer/CustomerConstant.java @@ -2,5 +2,8 @@ public class CustomerConstant { public static final String CUSTOMER_URI_REST_API = "/api/v1/customers"; - public static final String CUSTOMER_NOT_FOUND_EXCEPTION = "User not found"; + public static final String CUSTOMER_NOT_FOUND = "Customer with ID %d not found"; + public static final String CUSTOMER_EMAIL_EXISTS = "Customer with email %s already exists"; + public static final String NO_CUSTOMERS_FOUND = "No customers found"; } + diff --git a/customer/src/main/java/dev/nano/customer/CustomerController.java b/customer/src/main/java/dev/nano/customer/CustomerController.java index 0fb8e98..969cac6 100644 --- a/customer/src/main/java/dev/nano/customer/CustomerController.java +++ b/customer/src/main/java/dev/nano/customer/CustomerController.java @@ -4,13 +4,15 @@ import dev.nano.clients.order.OrderResponse; import dev.nano.clients.payment.PaymentRequest; import dev.nano.clients.payment.PaymentResponse; +import jakarta.validation.Valid; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.util.List; + import static dev.nano.customer.CustomerConstant.CUSTOMER_URI_REST_API; @@ -21,53 +23,54 @@ public class CustomerController { private final CustomerService customerService; - @GetMapping( - path = "/{customerId}", - produces={MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE} - ) - public ResponseEntity getCustomer(@PathVariable("customerId") Long customerId) - throws CustomerNotFoundException { + @GetMapping(path = "/{customerId}") + public ResponseEntity getCustomer(@PathVariable("customerId") Long customerId) { + log.info("Retrieving customer with ID: {}", customerId); + return ResponseEntity.ok(customerService.getCustomer(customerId)); + } - log.info("Retrieving customer with id {}", customerId); - return new ResponseEntity<>( - customerService.getCustomer(customerId), - HttpStatus.OK - ); + @GetMapping + public ResponseEntity> getAllCustomers() { + log.info("Retrieving all customers"); + return ResponseEntity.ok(customerService.getAllCustomers()); } - @PostMapping( - path = "/add", - consumes={MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE}, - produces={MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE} - ) - public ResponseEntity createNewCustomer(@RequestBody CustomerDTO customerDTO) { - log.info("Add new customer {}", customerDTO); + @PostMapping("/add") + public ResponseEntity createCustomer(@Valid @RequestBody CustomerDTO customerDTO) { + log.info("Creating new customer: {}", customerDTO); return new ResponseEntity<>( - customerService.createCustomer(customerDTO), - HttpStatus.CREATED + customerService.createCustomer(customerDTO), + HttpStatus.CREATED ); } - @PostMapping( - path = "/orders", - produces={MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE} - ) - public ResponseEntity customerOrders(@RequestBody OrderRequest orderRequest) { + @PutMapping("/{customerId}") + public ResponseEntity updateCustomer( + @PathVariable Long customerId, + @Valid @RequestBody CustomerDTO customerDTO) { + log.info("Updating customer with ID {}: {}", customerId, customerDTO); + return ResponseEntity.ok(customerService.updateCustomer(customerId, customerDTO)); + } + + @DeleteMapping("/{customerId}") + public ResponseEntity deleteCustomer(@PathVariable Long customerId) { + log.info("Deleting customer with ID: {}", customerId); + customerService.deleteCustomer(customerId); + return ResponseEntity.noContent().build(); + } - log.info("Customer orders {}", orderRequest); + @PostMapping("/orders") + public ResponseEntity customerOrders(@Valid @RequestBody OrderRequest orderRequest) { + log.info("Processing order for customer: {}", orderRequest); return new ResponseEntity<>( - customerService.customerOrders(orderRequest), - HttpStatus.CREATED + customerService.customerOrders(orderRequest), + HttpStatus.CREATED ); } - @PostMapping( - path = "/payment", - produces={MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE} - ) - public ResponseEntity customerPayment(@RequestBody PaymentRequest paymentRequest) { - - log.info("Customer payment {}", paymentRequest); + @PostMapping("/payment") + public ResponseEntity customerPayment(@Valid @RequestBody PaymentRequest paymentRequest) { + log.info("Processing payment for customer: {}", paymentRequest); return new ResponseEntity<>( customerService.customerPayment(paymentRequest), HttpStatus.CREATED diff --git a/customer/src/main/java/dev/nano/customer/CustomerDTO.java b/customer/src/main/java/dev/nano/customer/CustomerDTO.java index 80ba2cf..afee038 100644 --- a/customer/src/main/java/dev/nano/customer/CustomerDTO.java +++ b/customer/src/main/java/dev/nano/customer/CustomerDTO.java @@ -1,15 +1,32 @@ package dev.nano.customer; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; - -import java.io.Serializable; +import lombok.NoArgsConstructor; @Data -public class CustomerDTO implements Serializable { - private long id; +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class CustomerDTO { + private Long id; + + @NotBlank(message = "Name is required") + @Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters") private String name; + + @NotBlank(message = "Email is required") + @Email(message = "Invalid email format") + @Size(max = 255, message = "Email cannot exceed 255 characters") private String email; + private String phone; + + @Size(max = 255, message = "Address cannot exceed 255 characters") private String address; } diff --git a/customer/src/main/java/dev/nano/customer/CustomerMapper.java b/customer/src/main/java/dev/nano/customer/CustomerMapper.java index 383517d..9957a1c 100644 --- a/customer/src/main/java/dev/nano/customer/CustomerMapper.java +++ b/customer/src/main/java/dev/nano/customer/CustomerMapper.java @@ -1,15 +1,14 @@ package dev.nano.customer; import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; import java.util.List; @Mapper(componentModel = "spring") public interface CustomerMapper { - - CustomerEntity toEntity(CustomerDTO dto); - List toListEntity(List listDTO); - CustomerDTO toDTO(CustomerEntity entity); - List toListDTO(List listEntity); - + CustomerDTO toDTO(CustomerEntity entity); + CustomerEntity toEntity(CustomerDTO dto); + List toListDTO(List listEntity); + void updateCustomerFromDTO(CustomerDTO dto, @MappingTarget CustomerEntity entity); } diff --git a/customer/src/main/java/dev/nano/customer/CustomerNotFoundException.java b/customer/src/main/java/dev/nano/customer/CustomerNotFoundException.java deleted file mode 100644 index 0517c5f..0000000 --- a/customer/src/main/java/dev/nano/customer/CustomerNotFoundException.java +++ /dev/null @@ -1,7 +0,0 @@ -package dev.nano.customer; - -public class CustomerNotFoundException extends Throwable { - public CustomerNotFoundException(String message) { - super(message); - } -} diff --git a/customer/src/main/java/dev/nano/customer/CustomerRepository.java b/customer/src/main/java/dev/nano/customer/CustomerRepository.java index 1365ac9..fd710b3 100644 --- a/customer/src/main/java/dev/nano/customer/CustomerRepository.java +++ b/customer/src/main/java/dev/nano/customer/CustomerRepository.java @@ -5,4 +5,5 @@ @Repository public interface CustomerRepository extends JpaRepository { + boolean existsByEmail(String email); } diff --git a/customer/src/main/java/dev/nano/customer/CustomerService.java b/customer/src/main/java/dev/nano/customer/CustomerService.java index 38b19c2..718083e 100644 --- a/customer/src/main/java/dev/nano/customer/CustomerService.java +++ b/customer/src/main/java/dev/nano/customer/CustomerService.java @@ -8,8 +8,7 @@ import java.util.List; public interface CustomerService { - - CustomerDTO getCustomer(Long id) throws CustomerNotFoundException; + CustomerDTO getCustomer(Long id); List getAllCustomers(); CustomerDTO createCustomer(CustomerDTO customer); CustomerDTO updateCustomer(Long id, CustomerDTO customer); diff --git a/customer/src/main/java/dev/nano/customer/CustomerServiceImpl.java b/customer/src/main/java/dev/nano/customer/CustomerServiceImpl.java index 0a8b12d..c8da1ae 100644 --- a/customer/src/main/java/dev/nano/customer/CustomerServiceImpl.java +++ b/customer/src/main/java/dev/nano/customer/CustomerServiceImpl.java @@ -9,13 +9,20 @@ import dev.nano.clients.payment.PaymentRequest; import dev.nano.clients.payment.PaymentResponse; import dev.nano.clients.product.ProductClient; +import exceptionhandler.business.CustomerException; +import exceptionhandler.business.NotificationException; +import exceptionhandler.business.OrderException; +import exceptionhandler.business.PaymentException; +import exceptionhandler.core.DuplicateResourceException; +import exceptionhandler.core.ResourceNotFoundException; +import feign.FeignException; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.List; -import static dev.nano.customer.CustomerConstant.CUSTOMER_NOT_FOUND_EXCEPTION; +import static dev.nano.customer.CustomerConstant.*; @Service @AllArgsConstructor @@ -30,77 +37,125 @@ public class CustomerServiceImpl implements CustomerService { private final RabbitMQProducer rabbitMQProducer; @Override - public CustomerDTO getCustomer(Long id) throws CustomerNotFoundException { - - CustomerEntity customer = customerRepository.findById(id).orElse(null); - - if (customer == null) { - log.error("Customer not found {}", id); - throw new CustomerNotFoundException(CUSTOMER_NOT_FOUND_EXCEPTION); - } - - return customerMapper.toDTO(customer); + public CustomerDTO getCustomer(Long id) { + return customerRepository.findById(id) + .map(customerMapper::toDTO) + .orElseThrow(() -> new ResourceNotFoundException( + String.format(CUSTOMER_NOT_FOUND, id) + )); } @Override public List getAllCustomers() { - return null; + List customers = customerRepository.findAll(); + if (customers.isEmpty()) { + throw new ResourceNotFoundException(NO_CUSTOMERS_FOUND); + } + return customerMapper.toListDTO(customers); } @Override public CustomerDTO createCustomer(CustomerDTO customerDTO) { - - // save customer - CustomerEntity customer = customerRepository.save(customerMapper.toEntity(customerDTO)); - - // send notification to notification microservice - NotificationRequest notificationRequest = NotificationRequest.builder() - .customerId(customer.getId()) - .customerName(customer.getName()) - .customerEmail(customer.getEmail()) - .sender("nanodev") - .message("Hello " + customer.getName() + ". Welcome to nanodev demo project on microservices") - .build(); - - rabbitMQProducer.publish( - "internal.exchange", - "internal.notification.routing-key", - notificationRequest - ); - - log.info("New customer created successfully {}", customer); - - return customerMapper.toDTO(customer); + validateNewCustomer(customerDTO); + try { + CustomerEntity customer = customerMapper.toEntity(customerDTO); + CustomerEntity savedCustomer = customerRepository.save(customer); + sendWelcomeNotification(savedCustomer); + log.info("Customer created successfully: {}", savedCustomer); + return customerMapper.toDTO(savedCustomer); + } catch (Exception e) { + throw new CustomerException("Failed to create customer: %s" + e.getMessage()); + } } @Override - public CustomerDTO updateCustomer(Long id, CustomerDTO customer) { - return null; + public CustomerDTO updateCustomer(Long id, CustomerDTO customerDTO) { + CustomerEntity existingCustomer = customerRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException( + String.format(CUSTOMER_NOT_FOUND, id) + )); + + if (!existingCustomer.getEmail().equals(customerDTO.getEmail()) + && customerRepository.existsByEmail(customerDTO.getEmail())) { + throw new DuplicateResourceException( + String.format(CUSTOMER_EMAIL_EXISTS, customerDTO.getEmail()) + ); + } + + customerMapper.updateCustomerFromDTO(customerDTO, existingCustomer); + return customerMapper.toDTO(customerRepository.save(existingCustomer)); } + @Override public void deleteCustomer(Long id) { - + if (!customerRepository.existsById(id)) { + throw new ResourceNotFoundException( + String.format(CUSTOMER_NOT_FOUND, id) + ); + } + try { + customerRepository.deleteById(id); + } catch (Exception e) { + throw new CustomerException("Failed to delete customer: " + e.getMessage()); + } } @Override public OrderResponse customerOrders(OrderRequest orderRequest) { - customerRepository.findById(orderRequest.getCustomerId()) - .orElseThrow(() -> new IllegalStateException("Customer not found")); - - productClient.getProduct(orderRequest.getProductId()); - - OrderResponse orderResponse = orderClient.createOrder(orderRequest); - - return orderResponse; + .orElseThrow(() -> new ResourceNotFoundException( + String.format(CUSTOMER_NOT_FOUND, orderRequest.getCustomerId()) + )); + + try { + productClient.getProduct(orderRequest.getProductId()); + return orderClient.createOrder(orderRequest); + } catch (FeignException e) { + throw new OrderException("Failed to process order: " + e.getMessage()); + } } @Override public PaymentResponse customerPayment(PaymentRequest paymentRequest) { customerRepository.findById(paymentRequest.getCustomerId()) - .orElseThrow(() -> new IllegalStateException(CUSTOMER_NOT_FOUND_EXCEPTION)); + .orElseThrow(() -> new ResourceNotFoundException( + String.format(CUSTOMER_NOT_FOUND, paymentRequest.getCustomerId()) + )); + + try { + return paymentClient.createPayment(paymentRequest); + } catch (FeignException e) { + throw new PaymentException("Failed to process payment: " + e.getMessage()); + } + } - return paymentClient.createPayment(paymentRequest); + private void validateNewCustomer(CustomerDTO customerDTO) { + if (customerRepository.existsByEmail(customerDTO.getEmail())) { + throw new DuplicateResourceException( + String.format(CUSTOMER_EMAIL_EXISTS, customerDTO.getEmail()) + ); + } + } + + private void sendWelcomeNotification(CustomerEntity customer) { + try { + NotificationRequest notificationRequest = NotificationRequest.builder() + .customerId(customer.getId()) + .customerName(customer.getName()) + .customerEmail(customer.getEmail()) + .sender("nanodev") + .message("Welcome to nanodev demo project on microservices") + .build(); + + rabbitMQProducer.publish( + "internal.exchange", + "internal.notification.routing-key", + notificationRequest + ); + } catch (Exception e) { + log.error("Failed to send welcome notification: {}", e.getMessage()); + throw new NotificationException("Failed to send welcome notification"); + } } } diff --git a/notification/pom.xml b/notification/pom.xml index c09aad6..49214ff 100644 --- a/notification/pom.xml +++ b/notification/pom.xml @@ -25,6 +25,10 @@ org.springframework.boot spring-boot-starter-data-jpa + + org.springframework.boot + spring-boot-starter-validation + org.postgresql postgresql diff --git a/notification/src/main/java/dev/nano/notification/NotificationConstant.java b/notification/src/main/java/dev/nano/notification/NotificationConstant.java index e74cf6d..a67e76b 100644 --- a/notification/src/main/java/dev/nano/notification/NotificationConstant.java +++ b/notification/src/main/java/dev/nano/notification/NotificationConstant.java @@ -2,4 +2,8 @@ public class NotificationConstant { public static final String NOTIFICATION_URI_REST_API = "/api/v1/notifications"; + public static final String NOTIFICATION_NOT_FOUND = "Notification with ID %d not found"; + public static final String NOTIFICATION_SEND_ERROR = "Failed to send notification: %s"; + public static final String NO_NOTIFICATIONS_FOUND = "No notifications found"; } + diff --git a/notification/src/main/java/dev/nano/notification/NotificationController.java b/notification/src/main/java/dev/nano/notification/NotificationController.java index 0ff6d83..4ad5971 100644 --- a/notification/src/main/java/dev/nano/notification/NotificationController.java +++ b/notification/src/main/java/dev/nano/notification/NotificationController.java @@ -1,10 +1,9 @@ package dev.nano.notification; import dev.nano.clients.notification.NotificationRequest; +import jakarta.validation.Valid; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -19,33 +18,22 @@ public class NotificationController { private final NotificationService notificationService; - @GetMapping( - path = "/{notificationId}", - produces={MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE} - ) + @GetMapping("/{notificationId}") public ResponseEntity getNotification(@PathVariable("notificationId") Long notificationId) { log.info("Retrieving notification with id {}", notificationId); - return new ResponseEntity<>( - notificationService.getNotification(notificationId), - HttpStatus.OK - ); + return ResponseEntity.ok(notificationService.getNotification(notificationId)); } - @GetMapping( - path = "/all", - produces={MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE} - ) + @GetMapping("/all") public ResponseEntity> getAllNotification() { log.info("Retrieving all notifications"); - return new ResponseEntity<>( - notificationService.getAllNotification(), - HttpStatus.OK - ); + return ResponseEntity.ok(notificationService.getAllNotification()); } - @PostMapping(path = "/send") - public void sendNotification(@RequestBody NotificationRequest notificationRequest) { + @PostMapping("/send") + public ResponseEntity sendNotification(@Valid @RequestBody NotificationRequest notificationRequest) { log.info("Sending new notification {}", notificationRequest); notificationService.sendNotification(notificationRequest); + return ResponseEntity.ok().build(); } } diff --git a/notification/src/main/java/dev/nano/notification/NotificationDTO.java b/notification/src/main/java/dev/nano/notification/NotificationDTO.java index 0b11a9b..7ef117f 100644 --- a/notification/src/main/java/dev/nano/notification/NotificationDTO.java +++ b/notification/src/main/java/dev/nano/notification/NotificationDTO.java @@ -1,17 +1,38 @@ package dev.nano.notification; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import java.time.LocalDateTime; @Data +@Builder +@AllArgsConstructor +@NoArgsConstructor public class NotificationDTO { private Long id; + + @NotNull(message = "Customer ID is required") private Long customerId; + + @NotBlank(message = "Customer name is required") private String customerName; + + @NotBlank(message = "Customer email is required") + @Email(message = "Invalid email format") private String customerEmail; + + @NotBlank(message = "Sender is required") private String sender; + + @NotBlank(message = "Message is required") private String message; + private LocalDateTime sentAt; } diff --git a/notification/src/main/java/dev/nano/notification/NotificationMapper.java b/notification/src/main/java/dev/nano/notification/NotificationMapper.java index 9f116b2..26a265c 100644 --- a/notification/src/main/java/dev/nano/notification/NotificationMapper.java +++ b/notification/src/main/java/dev/nano/notification/NotificationMapper.java @@ -6,9 +6,7 @@ @Mapper(componentModel = "spring") public interface NotificationMapper { - NotificationEntity toEntity(NotificationDTO dto); - List toListEntity(List listDTO); NotificationDTO toDTO(NotificationEntity entity); List toListDTO(List listEntity); } diff --git a/notification/src/main/java/dev/nano/notification/NotificationService.java b/notification/src/main/java/dev/nano/notification/NotificationService.java index 308f01b..c01ad50 100644 --- a/notification/src/main/java/dev/nano/notification/NotificationService.java +++ b/notification/src/main/java/dev/nano/notification/NotificationService.java @@ -5,7 +5,6 @@ import java.util.List; public interface NotificationService { - NotificationDTO getNotification(Long notificationId); List getAllNotification(); void sendNotification(NotificationRequest notificationRequest); diff --git a/notification/src/main/java/dev/nano/notification/NotificationServiceImpl.java b/notification/src/main/java/dev/nano/notification/NotificationServiceImpl.java index 07f975a..49b21a1 100644 --- a/notification/src/main/java/dev/nano/notification/NotificationServiceImpl.java +++ b/notification/src/main/java/dev/nano/notification/NotificationServiceImpl.java @@ -2,14 +2,20 @@ import dev.nano.clients.notification.NotificationRequest; import dev.nano.notification.email.EmailService; +import exceptionhandler.business.NotificationException; +import exceptionhandler.core.ResourceNotFoundException; import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.util.List; +import static dev.nano.notification.NotificationConstant.*; + @Service @AllArgsConstructor +@Slf4j public class NotificationServiceImpl implements NotificationService { private final NotificationRepository notificationRepository; @@ -18,43 +24,51 @@ public class NotificationServiceImpl implements NotificationService { @Override public NotificationDTO getNotification(Long notificationId) { - - NotificationEntity product = notificationRepository.findById(notificationId).orElseThrow(() -> - new IllegalStateException("Notification not found")); - - return notificationMapper.toDTO(product); + return notificationRepository.findById(notificationId) + .map(notificationMapper::toDTO) + .orElseThrow(() -> new ResourceNotFoundException( + String.format(NOTIFICATION_NOT_FOUND, notificationId) + )); } @Override public List getAllNotification() { - List listProduct = notificationRepository.findAll(); - return notificationMapper.toListDTO(listProduct); + List notifications = notificationRepository.findAll(); + if (notifications.isEmpty()) { + throw new ResourceNotFoundException(NO_NOTIFICATIONS_FOUND); + } + return notificationMapper.toListDTO(notifications); } @Override public void sendNotification(NotificationRequest notificationRequest) { + try { + NotificationEntity notification = NotificationEntity.builder() + .customerId(notificationRequest.getCustomerId()) + .customerName(notificationRequest.getCustomerName()) + .customerEmail(notificationRequest.getCustomerEmail()) + .sender("nanodev") + .message(notificationRequest.getMessage()) + .sentAt(LocalDateTime.now()) + .build(); - notificationRepository.save(NotificationEntity.builder() - .customerId(notificationRequest.getCustomerId()) - .customerName(notificationRequest.getCustomerName()) - .customerEmail(notificationRequest.getCustomerEmail()) - .sender("nanodev") - .message(notificationRequest.getMessage()) - .sentAt(LocalDateTime.now()) - .build()); - - emailService.send( - notificationRequest.getCustomerEmail(), - buildEmail(notificationRequest.getCustomerName(), notificationRequest.getMessage()), - notificationRequest.getSender() - ); - } + notificationRepository.save(notification); - public String buildEmail(String name, String message) { - return """ - Mail sent to: ${name} -

${message}

- """; + emailService.send( + notificationRequest.getCustomerEmail(), + buildEmail(notificationRequest.getCustomerName(), notificationRequest.getMessage()), + notificationRequest.getSender() + ); + } catch (Exception e) { + log.error("Failed to send notification: {}", e.getMessage()); + throw new NotificationException(String.format(NOTIFICATION_SEND_ERROR, e.getMessage())); + } + } + private String buildEmail(String name, String message) { + return String.format(""" + Mail sent to: %s +

%s

+ """, name, message); } } diff --git a/order/src/main/java/dev/nano/order/OrderConstant.java b/order/src/main/java/dev/nano/order/OrderConstant.java index 12af13c..58e99cb 100644 --- a/order/src/main/java/dev/nano/order/OrderConstant.java +++ b/order/src/main/java/dev/nano/order/OrderConstant.java @@ -2,5 +2,8 @@ public class OrderConstant { public static final String ORDER_URI_REST_API = "/api/v1/orders"; - public static final String ORDER_NOT_FOUND_EXCEPTION = "Order not found"; + public static final String ORDER_NOT_FOUND = "Order with ID %d not found"; + public static final String ORDER_CREATE_ERROR = "Failed to create order: %s"; + public static final String NO_ORDERS_FOUND = "No orders found"; } + diff --git a/order/src/main/java/dev/nano/order/OrderController.java b/order/src/main/java/dev/nano/order/OrderController.java index 18e41f8..e399827 100644 --- a/order/src/main/java/dev/nano/order/OrderController.java +++ b/order/src/main/java/dev/nano/order/OrderController.java @@ -4,7 +4,6 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -22,29 +21,21 @@ public class OrderController { private final OrderService orderService; - @GetMapping(path = "/{orderId}") + @GetMapping("/{orderId}") public ResponseEntity getOrder(@PathVariable("orderId") Long orderId) { log.info("Retrieving order with id {}", orderId); - return new ResponseEntity<>( - orderService.getOrder(orderId), - HttpStatus.OK - ); + return ResponseEntity.ok(orderService.getOrder(orderId)); } - @GetMapping( - produces={MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE} - ) + @GetMapping public ResponseEntity> getAllOrders() { log.info("Retrieving all orders"); - return new ResponseEntity<>( - orderService.getAllOrders(), - HttpStatus.OK - ); + return ResponseEntity.ok(orderService.getAllOrders()); } - @PostMapping(path = "/add") - public ResponseEntity createOrder(@RequestBody OrderRequest orderRequest) { - + @PostMapping("/add") + public ResponseEntity createOrder(@Valid @RequestBody OrderRequest orderRequest) { + log.info("Creating new order: {}", orderRequest); return new ResponseEntity<>( orderService.createOrder(orderRequest), HttpStatus.CREATED diff --git a/order/src/main/java/dev/nano/order/OrderDTO.java b/order/src/main/java/dev/nano/order/OrderDTO.java index 300b0f8..d65845c 100644 --- a/order/src/main/java/dev/nano/order/OrderDTO.java +++ b/order/src/main/java/dev/nano/order/OrderDTO.java @@ -1,14 +1,30 @@ package dev.nano.order; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import java.time.LocalDateTime; @Data +@Builder +@AllArgsConstructor +@NoArgsConstructor public class OrderDTO { private Long id; + + @NotNull(message = "Customer ID is required") private Long customerId; + + @NotNull(message = "Product ID is required") private Long productId; - private Integer amount; + + @NotNull(message = "Amount is required") + @Min(value = 1, message = "Amount must be greater than 0") + private double amount; + private LocalDateTime createAt; } diff --git a/order/src/main/java/dev/nano/order/OrderMapper.java b/order/src/main/java/dev/nano/order/OrderMapper.java index 107da47..e8d2a97 100644 --- a/order/src/main/java/dev/nano/order/OrderMapper.java +++ b/order/src/main/java/dev/nano/order/OrderMapper.java @@ -6,10 +6,7 @@ @Mapper(componentModel = "spring") public interface OrderMapper { - OrderEntity toEntity(OrderDTO dto); - List toListEntity(List listDTO); OrderDTO toDTO(OrderEntity entity); List toListDTO(List listEntity); - } diff --git a/order/src/main/java/dev/nano/order/OrderService.java b/order/src/main/java/dev/nano/order/OrderService.java index 70df13b..818b61d 100644 --- a/order/src/main/java/dev/nano/order/OrderService.java +++ b/order/src/main/java/dev/nano/order/OrderService.java @@ -8,6 +8,4 @@ public interface OrderService { OrderDTO getOrder(Long id); List getAllOrders(); OrderDTO createOrder(OrderRequest order); - OrderDTO updateOrder(Long id, OrderDTO order); - void deleteOrder(Long id); } diff --git a/order/src/main/java/dev/nano/order/OrderServiceImpl.java b/order/src/main/java/dev/nano/order/OrderServiceImpl.java index ada438a..d0dedc6 100644 --- a/order/src/main/java/dev/nano/order/OrderServiceImpl.java +++ b/order/src/main/java/dev/nano/order/OrderServiceImpl.java @@ -4,74 +4,86 @@ import dev.nano.clients.notification.NotificationRequest; import dev.nano.clients.order.OrderRequest; import dev.nano.clients.product.ProductClient; +import exceptionhandler.business.NotificationException; +import exceptionhandler.business.OrderException; +import exceptionhandler.core.ResourceNotFoundException; +import feign.FeignException; import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.util.List; -import static dev.nano.order.OrderConstant.ORDER_NOT_FOUND_EXCEPTION; +import static dev.nano.order.OrderConstant.*; @Service @AllArgsConstructor +@Slf4j public class OrderServiceImpl implements OrderService { private final OrderRepository orderRepository; private final OrderMapper orderMapper; private final ProductClient productClient; private final RabbitMQProducer rabbitMQProducer; - @Override public OrderDTO getOrder(Long id) { - - OrderEntity order = orderRepository.findById(id).orElseThrow(() -> - new IllegalStateException(ORDER_NOT_FOUND_EXCEPTION)); - - return orderMapper.toDTO(order); + return orderRepository.findById(id) + .map(orderMapper::toDTO) + .orElseThrow(() -> new ResourceNotFoundException( + String.format(ORDER_NOT_FOUND, id) + )); } @Override public List getAllOrders() { - - List allOrders = orderRepository.findAll(); - return orderMapper.toListDTO(allOrders); - } - - @Override - public OrderDTO createOrder(OrderRequest order) { - - // Check if product is exist - productClient.getProduct(order.getProductId()); - - // Create order - OrderEntity orderEntity = orderRepository.save(OrderEntity.builder() - .customerId(order.getCustomerId()) - .productId(order.getProductId()) - .amount(order.getAmount()) - .createAt(LocalDateTime.now()).build()); - - // Create notificationRequest - NotificationRequest notificationRequest = NotificationRequest.builder() - .customerId(order.getCustomerId()) - .customerName(order.getCustomerName()) - .customerEmail(order.getCustomerEmail()) - .sender("NanoDev") - .message("Your order has been success.") - .build(); - - // Send notification - rabbitMQProducer.publish("internal.exchange", "internal.notification.routing-key", notificationRequest); - - return orderMapper.toDTO(orderEntity); + List orders = orderRepository.findAll(); + if (orders.isEmpty()) { + throw new ResourceNotFoundException(NO_ORDERS_FOUND); + } + return orderMapper.toListDTO(orders); } @Override - public OrderDTO updateOrder(Long id, OrderDTO order) { - return null; + public OrderDTO createOrder(OrderRequest orderRequest) { + try { + // Verify product exists + productClient.getProduct(orderRequest.getProductId()); + + OrderEntity order = OrderEntity.builder() + .customerId(orderRequest.getCustomerId()) + .productId(orderRequest.getProductId()) + .amount(orderRequest.getAmount()) + .createAt(LocalDateTime.now()) + .build(); + + OrderEntity savedOrder = orderRepository.save(order); + sendOrderNotification(orderRequest); + + return orderMapper.toDTO(savedOrder); + } catch (FeignException e) { + throw new OrderException(String.format(ORDER_CREATE_ERROR, e.getMessage())); + } } - @Override - public void deleteOrder(Long id) { - + private void sendOrderNotification(OrderRequest order) { + try { + NotificationRequest notificationRequest = NotificationRequest.builder() + .customerId(order.getCustomerId()) + .customerName(order.getCustomerName()) + .customerEmail(order.getCustomerEmail()) + .sender("NanoDev") + .message("Your order has been created successfully") + .build(); + + rabbitMQProducer.publish( + "internal.exchange", + "internal.notification.routing-key", + notificationRequest + ); + } catch (Exception e) { + log.error("Failed to send order notification: {}", e.getMessage()); + throw new NotificationException("Failed to send order notification"); + } } } diff --git a/payment/src/main/java/dev/nano/payment/PaymentConstant.java b/payment/src/main/java/dev/nano/payment/PaymentConstant.java index 13246cc..d6a6ec9 100644 --- a/payment/src/main/java/dev/nano/payment/PaymentConstant.java +++ b/payment/src/main/java/dev/nano/payment/PaymentConstant.java @@ -2,5 +2,7 @@ public class PaymentConstant { public static final String PAYMENT_URI_REST_API = "/api/v1/payments"; - public static final String PAYMENT_NOT_FOUND_EXCEPTION = "Payment not found"; + public static final String PAYMENT_NOT_FOUND = "Payment with ID %d not found"; + public static final String PAYMENT_CREATE_ERROR = "Failed to create payment: %s"; + public static final String NO_PAYMENTS_FOUND = "No payments found"; } diff --git a/payment/src/main/java/dev/nano/payment/PaymentController.java b/payment/src/main/java/dev/nano/payment/PaymentController.java index 02e9254..580fabd 100644 --- a/payment/src/main/java/dev/nano/payment/PaymentController.java +++ b/payment/src/main/java/dev/nano/payment/PaymentController.java @@ -1,10 +1,10 @@ package dev.nano.payment; import dev.nano.clients.payment.PaymentRequest; +import jakarta.validation.Valid; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -20,32 +20,21 @@ public class PaymentController { private final PaymentService paymentService; - @GetMapping( - path = "/{paymentId}", - produces={MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE} - ) + @GetMapping("/{paymentId}") public ResponseEntity getPayment(@PathVariable("paymentId") Long paymentId) { log.info("Retrieving payment with id {}", paymentId); - return new ResponseEntity<>( - paymentService.getPayment(paymentId), - HttpStatus.OK - ); + return ResponseEntity.ok(paymentService.getPayment(paymentId)); } - @GetMapping( - produces={MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE} - ) + @GetMapping public ResponseEntity> getAllPayments() { log.info("Retrieving all payments"); - return new ResponseEntity<>( - paymentService.getAllPayments(), - HttpStatus.OK - ); + return ResponseEntity.ok(paymentService.getAllPayments()); } - @PostMapping(path = "/make-new-payment") - public ResponseEntity createPayment(@RequestBody PaymentRequest payment) { - + @PostMapping("/make-new-payment") + public ResponseEntity createPayment(@Valid @RequestBody PaymentRequest payment) { + log.info("Processing new payment: {}", payment); return new ResponseEntity<>( paymentService.createPayment(payment), HttpStatus.CREATED diff --git a/payment/src/main/java/dev/nano/payment/PaymentDTO.java b/payment/src/main/java/dev/nano/payment/PaymentDTO.java index 6ae41be..6781159 100644 --- a/payment/src/main/java/dev/nano/payment/PaymentDTO.java +++ b/payment/src/main/java/dev/nano/payment/PaymentDTO.java @@ -1,13 +1,25 @@ package dev.nano.payment; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import java.time.LocalDateTime; @Data +@Builder +@AllArgsConstructor +@NoArgsConstructor public class PaymentDTO { private Long id; + + @NotNull(message = "Customer ID is required") private Long customerId; + + @NotNull(message = "Order ID is required") private Long orderId; + private LocalDateTime createAt; } diff --git a/payment/src/main/java/dev/nano/payment/PaymentMapper.java b/payment/src/main/java/dev/nano/payment/PaymentMapper.java index eb6db79..eab745d 100644 --- a/payment/src/main/java/dev/nano/payment/PaymentMapper.java +++ b/payment/src/main/java/dev/nano/payment/PaymentMapper.java @@ -1,13 +1,12 @@ package dev.nano.payment; import org.mapstruct.Mapper; - import java.util.List; @Mapper(componentModel = "spring") public interface PaymentMapper { - PaymentEntity toEntity(PaymentDTO payment); - List toListEntity(List paymentDTOList); PaymentDTO toDTO(PaymentEntity entity); - List toListDTO(List paymentEntityList); + PaymentEntity toEntity(PaymentDTO dto); + List toListDTO(List entities); } + diff --git a/payment/src/main/java/dev/nano/payment/PaymentService.java b/payment/src/main/java/dev/nano/payment/PaymentService.java index 2b7027a..cf6222c 100644 --- a/payment/src/main/java/dev/nano/payment/PaymentService.java +++ b/payment/src/main/java/dev/nano/payment/PaymentService.java @@ -5,7 +5,6 @@ import java.util.List; public interface PaymentService { - PaymentDTO getPayment(Long paymentId); List getAllPayments(); PaymentDTO createPayment(PaymentRequest payment); diff --git a/payment/src/main/java/dev/nano/payment/PaymentServiceImpl.java b/payment/src/main/java/dev/nano/payment/PaymentServiceImpl.java index b574640..6b7b5e3 100644 --- a/payment/src/main/java/dev/nano/payment/PaymentServiceImpl.java +++ b/payment/src/main/java/dev/nano/payment/PaymentServiceImpl.java @@ -4,16 +4,22 @@ import dev.nano.clients.notification.NotificationRequest; import dev.nano.clients.order.OrderClient; import dev.nano.clients.payment.PaymentRequest; +import exceptionhandler.business.NotificationException; +import exceptionhandler.business.PaymentException; +import exceptionhandler.core.ResourceNotFoundException; +import feign.FeignException; import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.util.List; -import static dev.nano.payment.PaymentConstant.PAYMENT_NOT_FOUND_EXCEPTION; +import static dev.nano.payment.PaymentConstant.*; @Service @AllArgsConstructor +@Slf4j public class PaymentServiceImpl implements PaymentService { private final PaymentRepository paymentRepository; @@ -23,42 +29,61 @@ public class PaymentServiceImpl implements PaymentService { @Override public PaymentDTO getPayment(Long paymentId) { - PaymentEntity payment = paymentRepository.findById(paymentId).orElseThrow(() -> - new IllegalStateException(PAYMENT_NOT_FOUND_EXCEPTION)); - - return paymentMapper.toDTO(payment); + return paymentRepository.findById(paymentId) + .map(paymentMapper::toDTO) + .orElseThrow(() -> new ResourceNotFoundException( + String.format(PAYMENT_NOT_FOUND, paymentId) + )); } @Override public List getAllPayments() { - List paymentList = paymentRepository.findAll(); - return paymentMapper.toListDTO(paymentList); + List payments = paymentRepository.findAll(); + if (payments.isEmpty()) { + throw new ResourceNotFoundException(NO_PAYMENTS_FOUND); + } + return paymentMapper.toListDTO(payments); } @Override - public PaymentDTO createPayment(PaymentRequest payment) { + public PaymentDTO createPayment(PaymentRequest paymentRequest) { + try { + // Verify order exists + orderClient.getOrder(paymentRequest.getOrderId()); - // find order - orderClient.getOrder(payment.getOrderId()); + PaymentEntity payment = PaymentEntity.builder() + .customerId(paymentRequest.getCustomerId()) + .orderId(paymentRequest.getOrderId()) + .createAt(LocalDateTime.now()) + .build(); - // add new payment - PaymentEntity paymentEntity = paymentRepository.save(PaymentEntity.builder() - .customerId(payment.getCustomerId()) - .orderId(payment.getOrderId()) - .createAt(LocalDateTime.now()).build()); + PaymentEntity savedPayment = paymentRepository.save(payment); + sendPaymentNotification(paymentRequest); - // create notification request - NotificationRequest notificationRequest = NotificationRequest.builder() - .customerId(payment.getCustomerId()) - .customerName(payment.getCustomerName()) - .customerEmail(payment.getCustomerEmail()) - .sender("nanodev") - .message("Your payment passed successfully. Thank you") - .build(); + return paymentMapper.toDTO(savedPayment); + } catch (FeignException e) { + throw new PaymentException(String.format(PAYMENT_CREATE_ERROR, e.getMessage())); + } + } - // publishing notification to rabbitmq - rabbitMQProducer.publish("internal.exchange", "internal.notification.routing-key", notificationRequest); + private void sendPaymentNotification(PaymentRequest payment) { + try { + NotificationRequest notificationRequest = NotificationRequest.builder() + .customerId(payment.getCustomerId()) + .customerName(payment.getCustomerName()) + .customerEmail(payment.getCustomerEmail()) + .sender("nanodev") + .message("Your payment has been processed successfully") + .build(); - return paymentMapper.toDTO(paymentEntity); + rabbitMQProducer.publish( + "internal.exchange", + "internal.notification.routing-key", + notificationRequest + ); + } catch (Exception e) { + log.error("Failed to send payment notification: {}", e.getMessage()); + throw new NotificationException("Failed to send payment notification"); + } } } diff --git a/product/src/main/java/dev/nano/product/ProductConstant.java b/product/src/main/java/dev/nano/product/ProductConstant.java index c8867b3..589e56e 100644 --- a/product/src/main/java/dev/nano/product/ProductConstant.java +++ b/product/src/main/java/dev/nano/product/ProductConstant.java @@ -2,5 +2,9 @@ public class ProductConstant { public static final String PRODUCT_URI_REST_API = "/api/v1/products"; - public static final String PRODUCT_NOT_FOUND_EXCEPTION = "Product not found"; + public static final String PRODUCT_NOT_FOUND = "Product with ID %d not found"; + public static final String PRODUCT_CREATE_ERROR = "Failed to create product: %s"; + public static final String PRODUCT_DELETE_ERROR = "Failed to delete product: %s"; + public static final String NO_PRODUCTS_FOUND = "No products found"; } + diff --git a/product/src/main/java/dev/nano/product/ProductController.java b/product/src/main/java/dev/nano/product/ProductController.java index a69e945..4f69e43 100644 --- a/product/src/main/java/dev/nano/product/ProductController.java +++ b/product/src/main/java/dev/nano/product/ProductController.java @@ -1,9 +1,9 @@ package dev.nano.product; +import jakarta.validation.Valid; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -18,28 +18,42 @@ public class ProductController { private final ProductService productService; - @GetMapping(path = "/{productId}") + @GetMapping("/{productId}") public ResponseEntity getProduct(@PathVariable("productId") Long productId) { - log.info("Retrieving product with id {}", productId); + return ResponseEntity.ok(productService.getProduct(productId)); + } + + @GetMapping("/list") + public ResponseEntity> getAllProducts( + @RequestParam(value="page", defaultValue = "1") int page, + @RequestParam(value="limit", defaultValue = "10") int limit, + @RequestParam(value="search", defaultValue = "") String search) { + log.info("Retrieving all products with page {}, limit {}, search {}", page, limit, search); + return ResponseEntity.ok(productService.getAllProducts(page, limit, search)); + } + + @PostMapping + public ResponseEntity createProduct(@Valid @RequestBody ProductDTO productDTO) { + log.info("Creating new product: {}", productDTO); return new ResponseEntity<>( - productService.getProduct(productId), - HttpStatus.OK + productService.create(productDTO), + HttpStatus.CREATED ); } - @GetMapping( - path = "/list", - produces = {MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE} - ) - public ResponseEntity> getAllProducts ( - @RequestParam(value="page", defaultValue = "1") int page, - @RequestParam(value="limit", defaultValue = "10") int limit , - @RequestParam(value="search", defaultValue = "") String search - ) { + @PutMapping("/{productId}") + public ResponseEntity updateProduct( + @PathVariable Long productId, + @Valid @RequestBody ProductDTO productDTO) { + log.info("Updating product with ID {}: {}", productId, productDTO); + return ResponseEntity.ok(productService.update(productId, productDTO)); + } - log.info("Retrieving all products with page {}, limit {}, search {}", page, limit, search); - List products = productService.getAllProducts(page, limit, search); - return new ResponseEntity<>(products, HttpStatus.OK); + @DeleteMapping("/{productId}") + public ResponseEntity deleteProduct(@PathVariable Long productId) { + log.info("Deleting product with ID: {}", productId); + productService.delete(productId); + return ResponseEntity.noContent().build(); } } diff --git a/product/src/main/java/dev/nano/product/ProductDTO.java b/product/src/main/java/dev/nano/product/ProductDTO.java index d66d42c..d2603db 100644 --- a/product/src/main/java/dev/nano/product/ProductDTO.java +++ b/product/src/main/java/dev/nano/product/ProductDTO.java @@ -1,13 +1,31 @@ package dev.nano.product; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; - -import java.io.Serializable; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.URL; @Data -public class ProductDTO implements Serializable { - private long id; +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ProductDTO { + private Long id; + + @NotBlank(message = "Product name is required") + @Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters") private String name; + + @NotNull(message = "Price is required") + @Min(value = 0, message = "Price must be greater than or equal to 0") + private Integer price; + + @URL(message = "Invalid image URL") private String image; - private int price; } + diff --git a/product/src/main/java/dev/nano/product/ProductMapper.java b/product/src/main/java/dev/nano/product/ProductMapper.java index de44d46..69622c0 100644 --- a/product/src/main/java/dev/nano/product/ProductMapper.java +++ b/product/src/main/java/dev/nano/product/ProductMapper.java @@ -1,15 +1,15 @@ package dev.nano.product; import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; import java.util.List; @Mapper(componentModel = "spring") public interface ProductMapper { - - ProductEntity toEntity(ProductDTO dto); - List toListEntity(List listDTO); ProductDTO toDTO(ProductEntity entity); - List toListDTO(List listEntity); - + ProductEntity toEntity(ProductDTO dto); + List toListDTO(List entities); + void updateProductFromDTO(ProductDTO dto, @MappingTarget ProductEntity entity); } + diff --git a/product/src/main/java/dev/nano/product/ProductServiceImpl.java b/product/src/main/java/dev/nano/product/ProductServiceImpl.java index 786bada..95c7ebc 100644 --- a/product/src/main/java/dev/nano/product/ProductServiceImpl.java +++ b/product/src/main/java/dev/nano/product/ProductServiceImpl.java @@ -1,15 +1,16 @@ package dev.nano.product; +import exceptionhandler.business.ProductException; +import exceptionhandler.core.ResourceNotFoundException; import lombok.AllArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; -import java.util.ArrayList; import java.util.List; -import static dev.nano.product.ProductConstant.PRODUCT_NOT_FOUND_EXCEPTION; +import static dev.nano.product.ProductConstant.*; @Service @AllArgsConstructor @@ -17,46 +18,67 @@ public class ProductServiceImpl implements ProductService { private final ProductRepository productRepository; private final ProductMapper productMapper; + @Override public ProductDTO getProduct(long productId) { - ProductEntity product = productRepository.findById(productId).orElseThrow(() -> - new IllegalStateException(PRODUCT_NOT_FOUND_EXCEPTION)); - - return productMapper.toDTO(product); + return productRepository.findById(productId) + .map(productMapper::toDTO) + .orElseThrow(() -> new ResourceNotFoundException( + String.format(PRODUCT_NOT_FOUND, productId) + )); } @Override public List getAllProducts(int page, int limit, String search) { - if(page > 0) page = page - 1; /* page starts at 1, so we need to subtract 1 because page starts at 0 */ + if(page > 0) page = page - 1; - List productDTOList = new ArrayList<>(); Pageable pageableRequest = PageRequest.of(page, limit); - Page productDTOPage; + Page productPage; if(search == null || search.isEmpty()) { - productDTOPage = productRepository.findAllProducts(pageableRequest); + productPage = productRepository.findAllProducts(pageableRequest); } else { - productDTOPage = productRepository.findAllProductsByCriteria(pageableRequest, search); + productPage = productRepository.findAllProductsByCriteria(pageableRequest, search); } - List products = productDTOPage.getContent(); - productDTOList.addAll(productMapper.toListDTO(products)); + List products = productPage.getContent(); + if (products.isEmpty()) { + throw new ResourceNotFoundException(NO_PRODUCTS_FOUND); + } - return productDTOList; + return productMapper.toListDTO(products); } @Override - public ProductDTO create(ProductDTO product) { - return null; + public ProductDTO create(ProductDTO productDTO) { + try { + ProductEntity product = productMapper.toEntity(productDTO); + return productMapper.toDTO(productRepository.save(product)); + } catch (Exception e) { + throw new ProductException(String.format(PRODUCT_CREATE_ERROR, e.getMessage())); + } } @Override - public ProductDTO update(long id, ProductDTO product) { - return null; + public ProductDTO update(long id, ProductDTO productDTO) { + ProductEntity existingProduct = productRepository.findById(id) + .orElseThrow(() -> new ResourceNotFoundException( + String.format(PRODUCT_NOT_FOUND, id) + )); + + productMapper.updateProductFromDTO(productDTO, existingProduct); + return productMapper.toDTO(productRepository.save(existingProduct)); } @Override public void delete(long id) { - + if (!productRepository.existsById(id)) { + throw new ResourceNotFoundException(String.format(PRODUCT_NOT_FOUND, id)); + } + try { + productRepository.deleteById(id); + } catch (Exception e) { + throw new ProductException(String.format(PRODUCT_DELETE_ERROR, e.getMessage())); + } } } diff --git a/product/src/main/resources/db/data.sql b/product/src/main/resources/db/data.sql index c1561de..25ebdfe 100644 --- a/product/src/main/resources/db/data.sql +++ b/product/src/main/resources/db/data.sql @@ -1,6 +1,7 @@ -INSERT INTO product (name, image, price) -VALUES (nextval('product_sequence'), 'MacBook Pro M3', 'https://picsum.photos/id/1/200/200', 1999), - (nextval('product_sequence'), 'iPhone 15 Pro', 'https://picsum.photos/id/2/200/200', 1299), - (nextval('product_sequence'), 'AirPods Pro', 'https://picsum.photos/id/3/200/200', 249), - (nextval('product_sequence'), 'iPad Air', 'https://picsum.photos/id/4/200/200', 599), - (nextval('product_sequence'), 'Apple Watch Series 9', 'https://picsum.photos/id/5/200/200', 499); +INSERT INTO product (id, name, image, price) +VALUES + (nextval('product_sequence'), 'MacBook Pro M3', 'https://picsum.photos/id/1/200/200', 1999), + (nextval('product_sequence'), 'iPhone 15 Pro', 'https://picsum.photos/id/2/200/200', 1299), + (nextval('product_sequence'), 'AirPods Pro', 'https://picsum.photos/id/3/200/200', 249), + (nextval('product_sequence'), 'iPad Air', 'https://picsum.photos/id/4/200/200', 599), + (nextval('product_sequence'), 'Apple Watch Series 9', 'https://picsum.photos/id/5/200/200', 499);