diff --git a/pom.xml b/pom.xml index 937a7fd..a16c4fb 100644 --- a/pom.xml +++ b/pom.xml @@ -1,113 +1,124 @@ - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.4.5 - - - com.podzilla - courier - 0.0.1-SNAPSHOT - courier - Courier Service - - - - - - - - - - - - - - - 21 - - - - org.springframework.boot - spring-boot-starter-amqp - - - org.springframework.boot - spring-boot-starter-data-mongodb - - - org.springframework.boot - spring-boot-starter-data-rest - - - org.springframework.boot - spring-boot-starter-web - + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.4.5 + + + com.podzilla + courier + 0.0.1-SNAPSHOT + courier + Courier Service + + + + + + + + + + + + + + + 21 + + + + jitpack.io + https://jitpack.io + + + + + org.springframework.boot + spring-boot-starter-amqp + + + org.springframework.boot + spring-boot-starter-data-mongodb + + + org.springframework.boot + spring-boot-starter-data-rest + + + org.springframework.boot + spring-boot-starter-web + - - org.springframework.boot - spring-boot-devtools - runtime - true - - - org.projectlombok - lombok - true - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.amqp - spring-rabbit-test - test - - - jakarta.validation - jakarta.validation-api - 3.0.2 - - - org.springdoc - springdoc-openapi-starter-webmvc-ui - 2.8.3 - - + + org.springframework.boot + spring-boot-devtools + runtime + true + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.amqp + spring-rabbit-test + test + + + jakarta.validation + jakarta.validation-api + 3.0.2 + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.8.3 + + + com.github.Podzilla + podzilla-utils-lib + v1.1.6 + + - - - - org.apache.maven.plugins - maven-compiler-plugin - - - - org.projectlombok - lombok - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - - - - + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + \ No newline at end of file diff --git a/src/main/java/com/podzilla/courier/controllers/CourierController.java b/src/main/java/com/podzilla/courier/controllers/CourierController.java index 5ebfa56..5a15093 100644 --- a/src/main/java/com/podzilla/courier/controllers/CourierController.java +++ b/src/main/java/com/podzilla/courier/controllers/CourierController.java @@ -3,7 +3,7 @@ import com.podzilla.courier.dtos.couriers.CourierResponseDto; import com.podzilla.courier.dtos.couriers.CreateCourierRequestDto; import com.podzilla.courier.dtos.couriers.UpdateCourierRequestDto; -import com.podzilla.courier.services.CourierService; +import com.podzilla.courier.services.courier.CourierService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponse; diff --git a/src/main/java/com/podzilla/courier/controllers/DeliveryTaskController.java b/src/main/java/com/podzilla/courier/controllers/DeliveryTaskController.java index 27997b8..f8e6851 100644 --- a/src/main/java/com/podzilla/courier/controllers/DeliveryTaskController.java +++ b/src/main/java/com/podzilla/courier/controllers/DeliveryTaskController.java @@ -9,7 +9,7 @@ import com.podzilla.courier.dtos.delivery_tasks.DeliveryTaskResponseDto; import com.podzilla.courier.dtos.delivery_tasks.UpdateDeliveryStatusRequestDto; import com.podzilla.courier.models.DeliveryStatus; -import com.podzilla.courier.services.DeliveryTaskService; +import com.podzilla.courier.services.delivery_task.DeliveryTaskService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.parameters.RequestBody; @@ -174,30 +174,18 @@ public ResponseEntity deleteDeliveryTask( return ResponseEntity.ok(deliveryTaskService.deleteDeliveryTask(id)); } - @PutMapping("/{id}/otp") - @Operation(summary = "Update OTP", description = "Updates the One-Time Password for a delivery task") - @ApiResponse(responseCode = "200", description = "OTP updated successfully") - @ApiResponse(responseCode = "404", description = "Task not found") - public ResponseEntity updateOtp( - @Parameter(description = "ID of the delivery task") - @PathVariable final String id, - @RequestBody(description = "New OTP value") - @org.springframework.web.bind.annotation.RequestBody final String otp) { - return deliveryTaskService.updateOtp(id, otp) - .map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build()); - } - - @PutMapping("/{id}/otp-confirmation") - @Operation(summary = "Confirm OTP", description = "Validates the One-Time Password for a delivery task") - @ApiResponse(responseCode = "200", description = "OTP confirmed successfully") - @ApiResponse(responseCode = "404", description = "Invalid OTP or task not found") - public ResponseEntity confirmOTP( + @PutMapping("/{id}/confirmation") + @Operation(summary = "Confirm delivery", + description = "Validates the delivery confirmation input (e.g., OTP, QR code, or signature)") + @ApiResponse(responseCode = "200", description = "Delivery confirmed successfully") + @ApiResponse(responseCode = "404", description = "Invalid confirmation input or task not found") + public ResponseEntity confirmDelivery( @Parameter(description = "ID of the delivery task") @PathVariable final String id, - @RequestBody(description = "OTP to validate") - @org.springframework.web.bind.annotation.RequestBody final String otp) { - return deliveryTaskService.confirmOTP(id, otp) + @RequestBody(description = "Confirmation input (e.g., OTP, QR code, or signature)") + @org.springframework.web.bind.annotation.RequestBody final String confirmationInput) { + LOGGER.info("Received request to confirm delivery for task ID: {}", id); + return deliveryTaskService.confirmDelivery(id, confirmationInput) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } diff --git a/src/main/java/com/podzilla/courier/dtos/events/OrderDeliveredEvent.java b/src/main/java/com/podzilla/courier/dtos/events/OrderDeliveredEvent.java deleted file mode 100644 index c11abc8..0000000 --- a/src/main/java/com/podzilla/courier/dtos/events/OrderDeliveredEvent.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.podzilla.courier.dtos.events; - -import java.math.BigDecimal; -import java.time.Instant; - -public record OrderDeliveredEvent(String orderId, String courierId, Instant timestamp, BigDecimal courierRating) { -} diff --git a/src/main/java/com/podzilla/courier/dtos/events/OrderFailedEvent.java b/src/main/java/com/podzilla/courier/dtos/events/OrderFailedEvent.java deleted file mode 100644 index a4bd019..0000000 --- a/src/main/java/com/podzilla/courier/dtos/events/OrderFailedEvent.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.podzilla.courier.dtos.events; - -import java.time.Instant; - -public record OrderFailedEvent(String orderId, String courierId, String reason, Instant timestamp) { -} diff --git a/src/main/java/com/podzilla/courier/dtos/events/OrderShippedEvent.java b/src/main/java/com/podzilla/courier/dtos/events/OrderShippedEvent.java deleted file mode 100644 index ee14987..0000000 --- a/src/main/java/com/podzilla/courier/dtos/events/OrderShippedEvent.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.podzilla.courier.dtos.events; - -import java.time.Instant; - -public record OrderShippedEvent(String orderId, String courierId, Instant timestamp) { -} diff --git a/src/main/java/com/podzilla/courier/events/EventPublisher.java b/src/main/java/com/podzilla/courier/events/EventPublisher.java deleted file mode 100644 index 600cb64..0000000 --- a/src/main/java/com/podzilla/courier/events/EventPublisher.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.podzilla.courier.events; - -import com.podzilla.courier.dtos.events.OrderDeliveredEvent; -import com.podzilla.courier.dtos.events.OrderFailedEvent; -import com.podzilla.courier.dtos.events.OrderShippedEvent; - -public interface EventPublisher { - void publishOrderShipped(OrderShippedEvent event); - void publishOrderDelivered(OrderDeliveredEvent event); - void publishOrderFailed(OrderFailedEvent event); -} \ No newline at end of file diff --git a/src/main/java/com/podzilla/courier/events/RabbitMQEventPublisher.java b/src/main/java/com/podzilla/courier/events/RabbitMQEventPublisher.java deleted file mode 100644 index b8f9eb7..0000000 --- a/src/main/java/com/podzilla/courier/events/RabbitMQEventPublisher.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.podzilla.courier.events; - -import com.podzilla.courier.config.RabbitMQConfig; -import com.podzilla.courier.dtos.events.OrderDeliveredEvent; -import com.podzilla.courier.dtos.events.OrderFailedEvent; -import com.podzilla.courier.dtos.events.OrderShippedEvent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.amqp.rabbit.core.RabbitTemplate; -import org.springframework.stereotype.Service; - -@Service -public class RabbitMQEventPublisher implements EventPublisher { - private static final Logger logger = LoggerFactory.getLogger(RabbitMQEventPublisher.class); - private final RabbitTemplate rabbitTemplate; - - public RabbitMQEventPublisher(RabbitTemplate rabbitTemplate) { - this.rabbitTemplate = rabbitTemplate; - } - - @Override - public void publishOrderShipped(OrderShippedEvent event) { - try { - rabbitTemplate.convertAndSend(RabbitMQConfig.ORDER_EXCHANGE, RabbitMQConfig.ORDER_SHIPPED_KEY, event, message -> { - message.getMessageProperties().setCorrelationId(java.util.UUID.randomUUID().toString()); - return message; - }); - logger.info("Published order.shipped event for order ID: {}", event.orderId()); - } catch (Exception e) { - logger.error("Failed to publish order.shipped event for order ID: {}", event.orderId(), e); - } - } - - @Override - public void publishOrderDelivered(OrderDeliveredEvent event) { - try { - rabbitTemplate.convertAndSend(RabbitMQConfig.ORDER_EXCHANGE, RabbitMQConfig.ORDER_DELIVERED_KEY, event, message -> { - message.getMessageProperties().setCorrelationId(java.util.UUID.randomUUID().toString()); - return message; - }); - logger.info("Published order.delivered event for order ID: {}", event.orderId()); - } catch (Exception e) { - logger.error("Failed to publish order.delivered event for order ID: {}", event.orderId(), e); - } - } - - @Override - public void publishOrderFailed(OrderFailedEvent event) { - try { - rabbitTemplate.convertAndSend(RabbitMQConfig.ORDER_EXCHANGE, RabbitMQConfig.ORDER_FAILED_KEY, event, message -> { - message.getMessageProperties().setCorrelationId(java.util.UUID.randomUUID().toString()); - return message; - }); - logger.info("Published order.failed event for order ID: {}", event.orderId()); - } catch (Exception e) { - logger.error("Failed to publish order.failed event for order ID: {}", event.orderId(), e); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/podzilla/courier/models/ConfirmationType.java b/src/main/java/com/podzilla/courier/models/ConfirmationType.java new file mode 100644 index 0000000..1fe3be3 --- /dev/null +++ b/src/main/java/com/podzilla/courier/models/ConfirmationType.java @@ -0,0 +1,7 @@ +package com.podzilla.courier.models; + +public enum ConfirmationType { + OTP, + QR_CODE, + SIGNATURE +} diff --git a/src/main/java/com/podzilla/courier/models/DeliveryTask.java b/src/main/java/com/podzilla/courier/models/DeliveryTask.java index 688d402..a4a6835 100644 --- a/src/main/java/com/podzilla/courier/models/DeliveryTask.java +++ b/src/main/java/com/podzilla/courier/models/DeliveryTask.java @@ -20,11 +20,14 @@ public class DeliveryTask { private Double courierLatitude; private Double courierLongitude; private String otp; + private String qrCode; + private String signature; private String cancellationReason; private Double courierRating; private LocalDateTime ratingTimestamp; private LocalDateTime createdAt; private LocalDateTime updatedAt; + private ConfirmationType confirmationType; public DeliveryTask() { this.status = DeliveryStatus.ASSIGNED; @@ -33,4 +36,4 @@ public DeliveryTask() { this.courierLatitude = 0.0; // warehouse lat this.courierLongitude = 0.0; // warehouse long } -} \ No newline at end of file +} diff --git a/src/main/java/com/podzilla/courier/services/CourierService.java b/src/main/java/com/podzilla/courier/services/courier/CourierService.java similarity index 50% rename from src/main/java/com/podzilla/courier/services/CourierService.java rename to src/main/java/com/podzilla/courier/services/courier/CourierService.java index e856005..45ef08d 100644 --- a/src/main/java/com/podzilla/courier/services/CourierService.java +++ b/src/main/java/com/podzilla/courier/services/courier/CourierService.java @@ -1,6 +1,8 @@ -package com.podzilla.courier.services; +package com.podzilla.courier.services.courier; -import com.podzilla.courier.dtos.couriers.*; +import com.podzilla.courier.dtos.couriers.CourierResponseDto; +import com.podzilla.courier.dtos.couriers.CreateCourierRequestDto; +import com.podzilla.courier.dtos.couriers.UpdateCourierRequestDto; import com.podzilla.courier.mappers.CourierMapper; import com.podzilla.courier.models.Courier; import com.podzilla.courier.repositories.courier.CourierRepository; @@ -16,64 +18,71 @@ public class CourierService { private final CourierRepository courierRepository; - private static final Logger logger = LoggerFactory.getLogger(CourierService.class); + private static final Logger LOGGER = LoggerFactory.getLogger(CourierService.class); - public CourierService(CourierRepository courierRepository) { + public CourierService(final CourierRepository courierRepository) { this.courierRepository = courierRepository; } public List getAllCouriers() { - logger.info("Fetching all couriers"); + LOGGER.info("Fetching all couriers"); List couriers = courierRepository.findAll().stream() .map(CourierMapper::toCreateResponseDto).collect(Collectors.toList()); - logger.info("Couriers Fetched: {}", couriers); + LOGGER.info("Couriers Fetched: {}", couriers); return couriers; } - public Optional getCourierById(String id) { - logger.info("Fetching courier with ID: {}", id); + public Optional getCourierById(final String id) { + LOGGER.info("Fetching courier with ID: {}", id); Optional courier = courierRepository.findById(id); if (courier.isPresent()) { - logger.debug("Courier found with ID: {}", courier.get().getId()); + LOGGER.debug("Courier found with ID: {}", courier.get().getId()); } else { - logger.debug("Courier not found with ID: {}", id); + LOGGER.debug("Courier not found with ID: {}", id); } return courier.map(CourierMapper::toCreateResponseDto); } - public CourierResponseDto createCourier(CreateCourierRequestDto courier) { - logger.info("Creating new courier"); + public CourierResponseDto createCourier(final CreateCourierRequestDto courier) { + LOGGER.info("Creating new courier"); Courier newCourier = CourierMapper.toEntity(courier); Courier savedCourier = courierRepository.save(newCourier); - logger.info("Created courier with ID: {}", savedCourier.getId()); + LOGGER.info("Created courier with ID: {}", savedCourier.getId()); return CourierMapper.toCreateResponseDto(savedCourier); } - public Optional updateCourier(String id, UpdateCourierRequestDto courierDto) { - logger.info("Updating courier with ID: {}", id); + public Optional updateCourier(final String id, final UpdateCourierRequestDto courierDto) { + LOGGER.info("Updating courier with ID: {}", id); Optional existingCourier = courierRepository.findById(id); if (existingCourier.isEmpty()) { - logger.debug("Courier not found with ID: {}", id); + LOGGER.debug("Courier not found with ID: {}", id); return Optional.empty(); } Courier courier = existingCourier.get(); - if (courierDto.getName() != null) courier.setName(courierDto.getName()); - if (courierDto.getMobileNo() != null) courier.setMobileNo(courierDto.getMobileNo()); - if (courierDto.getStatus() != null) courier.setStatus(courierDto.getStatus()); + if (courierDto.getName() != null) { + courier.setName(courierDto.getName()); + } + if (courierDto.getMobileNo() != null) { + courier.setMobileNo(courierDto.getMobileNo()); + } + if (courierDto.getStatus() != null) { + courier.setStatus(courierDto.getStatus()); + } Courier savedCourier = courierRepository.save(courier); - logger.info("Updated courier with ID: {}", savedCourier.getId()); + LOGGER.info("Updated courier with ID: {}", savedCourier.getId()); return Optional.of(CourierMapper.toCreateResponseDto(savedCourier)); } - public Optional deleteCourier(String id) { - logger.info("Deleting courier with ID: {}", id); + public Optional deleteCourier(final String id) { + LOGGER.info("Deleting courier with ID: {}", id); Optional courier = courierRepository.findById(id); if (courier.isPresent()) { courierRepository.deleteById(id); - logger.info("Deleted courier with ID: {}", id); + LOGGER.info("Deleted courier with ID: {}", id); return courier.map(CourierMapper::toCreateResponseDto); } - logger.debug("Courier not found with ID: {}", id); + LOGGER.debug("Courier not found with ID: {}", id); return Optional.empty(); } } + diff --git a/src/main/java/com/podzilla/courier/services/DeliveryTaskService.java b/src/main/java/com/podzilla/courier/services/delivery_task/DeliveryTaskService.java similarity index 52% rename from src/main/java/com/podzilla/courier/services/DeliveryTaskService.java rename to src/main/java/com/podzilla/courier/services/delivery_task/DeliveryTaskService.java index 847cc0d..7be93de 100644 --- a/src/main/java/com/podzilla/courier/services/DeliveryTaskService.java +++ b/src/main/java/com/podzilla/courier/services/delivery_task/DeliveryTaskService.java @@ -1,229 +1,242 @@ -package com.podzilla.courier.services; +package com.podzilla.courier.services.delivery_task; -import com.podzilla.courier.config.RabbitMQConfig; import com.podzilla.courier.dtos.delivery_tasks.CancelDeliveryTaskResponseDto; import com.podzilla.courier.dtos.delivery_tasks.CreateDeliveryTaskRequestDto; import com.podzilla.courier.dtos.delivery_tasks.DeliveryTaskResponseDto; import com.podzilla.courier.dtos.delivery_tasks.SubmitCourierRatingResponseDto; -import com.podzilla.courier.dtos.events.OrderDeliveredEvent; -import com.podzilla.courier.dtos.events.OrderFailedEvent; -import com.podzilla.courier.dtos.events.OrderShippedEvent; -import com.podzilla.courier.events.EventPublisher; import com.podzilla.courier.mappers.DeliveryTaskMapper; +import com.podzilla.courier.models.ConfirmationType; import com.podzilla.courier.models.DeliveryStatus; import com.podzilla.courier.models.DeliveryTask; import com.podzilla.courier.repositories.delivery_task.IDeliveryTaskRepository; +import com.podzilla.courier.services.delivery_task.confirmation_strategy.DeliveryConfirmationStrategy; +import com.podzilla.courier.services.delivery_task.poll_command.Command; +import com.podzilla.courier.services.delivery_task.poll_command.StopPollingCommand; +import com.podzilla.courier.services.delivery_task.poll_command.StartPollingCommand; +import com.podzilla.mq.EventPublisher; +import com.podzilla.mq.events.OrderCancelledEvent; +import com.podzilla.mq.events.OrderOutForDeliveryEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Value; import org.springframework.data.util.Pair; import org.springframework.stereotype.Service; -import java.math.BigDecimal; -import java.time.Instant; import java.time.LocalDateTime; import java.util.List; +import java.util.Map; import java.util.Optional; -import java.util.UUID; import java.util.stream.Collectors; +import java.util.stream.IntStream; @Service public class DeliveryTaskService { private final IDeliveryTaskRepository deliveryTaskRepository; private final EventPublisher eventPublisher; - private static final Logger logger = LoggerFactory.getLogger(DeliveryTaskService.class); - - - public DeliveryTaskService(IDeliveryTaskRepository deliveryTaskRepository, EventPublisher eventPublisher) { + private final Map confirmationStrategies; + private static final Logger LOGGER = LoggerFactory.getLogger(DeliveryTaskService.class); + @Value("${otp.length}") + private int otpLength; + + public DeliveryTaskService(final IDeliveryTaskRepository deliveryTaskRepository, + final EventPublisher eventPublisher, + final Map confirmationStrategies) { this.deliveryTaskRepository = deliveryTaskRepository; this.eventPublisher = eventPublisher; + this.confirmationStrategies = confirmationStrategies; } - public DeliveryTaskResponseDto createDeliveryTask(CreateDeliveryTaskRequestDto deliveryTaskRequestDto) { - logger.info("Creating delivery task for order ID: {}", deliveryTaskRequestDto.getOrderId()); + public DeliveryTaskResponseDto createDeliveryTask(final CreateDeliveryTaskRequestDto deliveryTaskRequestDto) { + LOGGER.info("Creating delivery task for order ID: {}", deliveryTaskRequestDto.getOrderId()); DeliveryTask deliveryTask = DeliveryTaskMapper.toEntity(deliveryTaskRequestDto); DeliveryTask savedTask = deliveryTaskRepository.save(deliveryTask); - logger.debug("Delivery task created with ID: {}", savedTask.getId()); + LOGGER.debug("Delivery task created with ID: {}", savedTask.getId()); return DeliveryTaskMapper.toCreateResponseDto(savedTask); } public List getAllDeliveryTasks() { - logger.info("Fetching all delivery tasks"); + LOGGER.info("Fetching all delivery tasks"); List deliveryTasks = deliveryTaskRepository.findAll() .stream() .map(DeliveryTaskMapper::toCreateResponseDto) .collect(Collectors.toList()); - logger.debug("Delivery tasks fetched: {}", deliveryTasks); + LOGGER.debug("Delivery tasks fetched: {}", deliveryTasks); return deliveryTasks; } - public Optional getDeliveryTaskById(String id) { - logger.info("Fetching delivery task with ID: {}", id); + public Optional getDeliveryTaskById(final String id) { + LOGGER.info("Fetching delivery task with ID: {}", id); Optional deliveryTask = deliveryTaskRepository.findById(id); if (deliveryTask.isPresent()) { - logger.debug("Delivery task found with ID: {}", deliveryTask.get().getId()); + LOGGER.debug("Delivery task found with ID: {}", deliveryTask.get().getId()); } else { - logger.debug("Delivery task not found with ID: {}", id); + LOGGER.debug("Delivery task not found with ID: {}", id); } return deliveryTask.map(DeliveryTaskMapper::toCreateResponseDto); } - public List getDeliveryTasksByCourierId(String courierId) { - logger.info("Fetching delivery tasks by courier ID: {}", courierId); + public List getDeliveryTasksByCourierId(final String courierId) { + LOGGER.info("Fetching delivery tasks by courier ID: {}", courierId); List deliveryTasks = deliveryTaskRepository.findByCourierId(courierId) .stream() .map(DeliveryTaskMapper::toCreateResponseDto) .collect(Collectors.toList()); - logger.debug("Retrieved {} delivery tasks for courier ID: {}", deliveryTasks.size(), courierId); + LOGGER.debug("Retrieved {} delivery tasks for courier ID: {}", deliveryTasks.size(), courierId); return deliveryTasks; } - public List getDeliveryTasksByStatus(DeliveryStatus status) { - logger.info("Fetching delivery tasks by status: {}", status); + public List getDeliveryTasksByStatus(final DeliveryStatus status) { + LOGGER.info("Fetching delivery tasks by status: {}", status); List deliveryTasks = deliveryTaskRepository.findByStatus(status) .stream() .map(DeliveryTaskMapper::toCreateResponseDto) .collect(Collectors.toList()); - logger.debug("Retrieved {} delivery tasks for status: {}", deliveryTasks.size(), status); + LOGGER.debug("Retrieved {} delivery tasks for status: {}", deliveryTasks.size(), status); return deliveryTasks; } - public List getDeliveryTasksByOrderId(String orderId) { - logger.info("Fetching delivery tasks by order ID: {}", orderId); + public List getDeliveryTasksByOrderId(final String orderId) { + LOGGER.info("Fetching delivery tasks by order ID: {}", orderId); List deliveryTasks = deliveryTaskRepository.findByOrderId(orderId) .stream() .map(DeliveryTaskMapper::toCreateResponseDto) .collect(Collectors.toList()); - logger.debug("Retrieved {} delivery tasks for order ID: {}", deliveryTasks.size(), orderId); + LOGGER.debug("Retrieved {} delivery tasks for order ID: {}", deliveryTasks.size(), orderId); return deliveryTasks; } - public Optional updateDeliveryTaskStatus(String id, DeliveryStatus status) { - logger.info("Updating delivery task with ID: {} to {}", id, status); + public Optional updateDeliveryTaskStatus(final String id, final DeliveryStatus status) { + LOGGER.info("Updating delivery task with ID: {} to {}", id, status); Optional updatedDeliveryTask = deliveryTaskRepository.findById(id); if (updatedDeliveryTask.isPresent()) { DeliveryTask task = updatedDeliveryTask.get(); task.setStatus(status); deliveryTaskRepository.save(task); - logger.debug("Delivery task ID: {} updated to status: {}", id, status); + LOGGER.debug("Delivery task ID: {} updated to status: {}", id, status); // publish order.shipped event if status is OUT_FOR_DELIVERY if (status == DeliveryStatus.OUT_FOR_DELIVERY) { - OrderShippedEvent event = new OrderShippedEvent(task.getOrderId(), task.getCourierId(), Instant.now()); - eventPublisher.publishOrderShipped(event); + OrderOutForDeliveryEvent event = new OrderOutForDeliveryEvent(task.getOrderId(), + task.getCourierId()); + Command startPollingCommand = new StartPollingCommand( + eventPublisher, + event + ); + startPollingCommand.execute(); + + if (task.getConfirmationType() == ConfirmationType.OTP) { + String otp = IntStream.range(0, otpLength) + .mapToObj(i -> String.valueOf(task.getId().charAt(i))) + .collect(Collectors.joining()); + task.setOtp(otp); + } else if (task.getConfirmationType() == ConfirmationType.QR_CODE) { + String qrContent = "QR-" + task.getId(); + task.setQrCode(qrContent); + } } return Optional.of(DeliveryTaskMapper.toCreateResponseDto(updatedDeliveryTask.get())); } - logger.warn("Delivery task not found with ID: {} for status update", id); + LOGGER.warn("Delivery task not found with ID: {} for status update", id); return Optional.empty(); } - public Pair getDeliveryTaskLocation(String id) { - logger.info("Fetching location for delivery task with ID: {}", id); + public Pair getDeliveryTaskLocation(final String id) { + LOGGER.info("Fetching location for delivery task with ID: {}", id); Optional deliveryTask = deliveryTaskRepository.findById(id); if (deliveryTask.isPresent()) { Double latitude = deliveryTask.get().getCourierLatitude(); Double longitude = deliveryTask.get().getCourierLongitude(); - logger.debug("Location for delivery task ID: {} is ({}, {})", id, latitude, longitude); + LOGGER.debug("Location for delivery task ID: {} is ({}, {})", id, latitude, longitude); return Pair.of(latitude, longitude); } - logger.warn("Delivery task not found with ID for location: {}", id); + LOGGER.warn("Delivery task not found with ID for location: {}", id); return Pair.of(0.0, 0.0); } - public DeliveryTaskResponseDto updateDeliveryTaskLocation(String id, Double latitude, Double longitude) { - logger.info("Updating location for delivery task with ID: {} to ({}, {})", id, latitude, longitude); + public DeliveryTaskResponseDto updateDeliveryTaskLocation(final String id, final Double latitude, + final Double longitude) { + LOGGER.info("Updating location for delivery task with ID: {} to ({}, {})", id, latitude, longitude); Optional updatedDeliveryTask = deliveryTaskRepository.findById(id); if (updatedDeliveryTask.isPresent()) { DeliveryTask deliveryTask = updatedDeliveryTask.get(); deliveryTask.setCourierLongitude(latitude); deliveryTask.setCourierLongitude(longitude); deliveryTaskRepository.save(deliveryTask); - logger.debug("Location updated for delivery task ID: {}", id); + LOGGER.debug("Location updated for delivery task ID: {}", id); return DeliveryTaskMapper.toCreateResponseDto(deliveryTask); } - logger.warn("Delivery task not found with ID: {} for location update", id); + LOGGER.warn("Delivery task not found with ID: {} for location update", id); return null; } - public CancelDeliveryTaskResponseDto cancelDeliveryTask(String id, String cancellationReason) { - logger.info("Cancelling delivery task with ID: {}", id); + public CancelDeliveryTaskResponseDto cancelDeliveryTask(final String id, final String cancellationReason) { + LOGGER.info("Cancelling delivery task with ID: {}", id); Optional deliveryTask = deliveryTaskRepository.findById(id); if (deliveryTask.isPresent()) { DeliveryTask deliveryTaskToCancel = deliveryTask.get(); deliveryTaskToCancel.setStatus(DeliveryStatus.CANCELLED); deliveryTaskToCancel.setCancellationReason(cancellationReason); deliveryTaskRepository.save(deliveryTaskToCancel); - logger.debug("Delivery task cancelled for delivery task ID: {}", id); + LOGGER.debug("Delivery task cancelled for delivery task ID: {}", id); // publish order.failed event - OrderFailedEvent event = new OrderFailedEvent( + OrderCancelledEvent event = new OrderCancelledEvent( deliveryTaskToCancel.getOrderId(), deliveryTaskToCancel.getCourierId(), - cancellationReason, - Instant.now() + cancellationReason ); - eventPublisher.publishOrderFailed(event); + Command stopPollingCommand = new StopPollingCommand( + eventPublisher, + event + ); + stopPollingCommand.execute(); + return DeliveryTaskMapper.toCancelResponseDto(deliveryTaskToCancel); } - logger.warn("Delivery task not found with ID: {}", id); + LOGGER.warn("Delivery task not found with ID: {}", id); return null; } - public DeliveryTaskResponseDto deleteDeliveryTask(String id) { - logger.info("Deleting delivery task with ID: {}", id); + public DeliveryTaskResponseDto deleteDeliveryTask(final String id) { + LOGGER.info("Deleting delivery task with ID: {}", id); Optional deliveryTask = deliveryTaskRepository.findById(id); if (deliveryTask.isPresent()) { deliveryTaskRepository.delete(deliveryTask.get()); - logger.debug("Delivery task with ID: {} deleted", id); + LOGGER.debug("Delivery task with ID: {} deleted", id); return DeliveryTaskMapper.toCreateResponseDto(deliveryTask.get()); } - logger.warn("Delivery task not found with ID: {} for deletion", id); + LOGGER.warn("Delivery task not found with ID: {} for deletion", id); return null; } - public Optional updateOtp(String id, String otp) { - logger.info("Updating otp for delivery task with ID: {}", id); + public Optional confirmDelivery(final String id, final String confirmationInput) { + LOGGER.info("Confirming delivery for task ID: {}", id); DeliveryTask task = deliveryTaskRepository.findById(id).orElse(null); - if (task == null) + if (task == null) { + LOGGER.warn("Delivery task not found with ID: {} for customer delivery confirmation", id); return Optional.empty(); - task.setOtp(otp); - deliveryTaskRepository.save(task); - logger.debug("OTP updated for delivery task ID: {}", id); - return Optional.of("Updated OTP"); - } + } - public Optional confirmOTP(String id, String otp) { - logger.info("Confirming otp for delivery task with ID: {}", id); - DeliveryTask task = deliveryTaskRepository.findById(id).orElse(null); - if (task == null) - return Optional.empty(); - String message = "Wrong OTP"; - if (task.getOtp().equals(otp)) { - task.setStatus(DeliveryStatus.DELIVERED); - message = "OTP confirmed"; - logger.debug("OTP confirmed for delivery task ID: {}", id); - // publish order.delivered event - OrderDeliveredEvent event = new OrderDeliveredEvent( - task.getOrderId(), - task.getCourierId(), - Instant.now(), - task.getCourierRating() != null ? BigDecimal.valueOf(task.getCourierRating()) : null - ); - eventPublisher.publishOrderDelivered(event); - } else { - logger.debug("OTP not confirmed for delivery task ID: {}", id); + ConfirmationType confirmationType = task.getConfirmationType(); + DeliveryConfirmationStrategy strategy = confirmationStrategies.get(confirmationType); + if (strategy == null) { + LOGGER.error("No confirmation strategy found for type: {}", confirmationType); + return Optional.of("Invalid confirmation type"); + } + + Optional result = strategy.confirmDelivery(task, confirmationInput); + if (result.isPresent() && result.get().contains("confirmed")) { + deliveryTaskRepository.save(task); } - deliveryTaskRepository.save(task); - return Optional.of(message); + return result; } - public SubmitCourierRatingResponseDto submitCourierRating(String id, Double rating) { - logger.info("Submitting courier rating for delivery task with ID: {}", id); + public SubmitCourierRatingResponseDto submitCourierRating(final String id, final Double rating) { + LOGGER.info("Submitting courier rating for delivery task with ID: {}", id); Optional deliveryTask = deliveryTaskRepository.findById(id); if (deliveryTask.isPresent()) { DeliveryTask deliveryTaskToSubmit = deliveryTask.get(); - if(!deliveryTaskToSubmit.getStatus().equals(DeliveryStatus.DELIVERED)) { - logger.error("Delivery task status is not DELIVERED"); + if (!deliveryTaskToSubmit.getStatus().equals(DeliveryStatus.DELIVERED)) { + LOGGER.error("Delivery task status is not DELIVERED"); throw new IllegalStateException("Task must be delivered to submit a rating"); } deliveryTaskToSubmit.setCourierRating(rating); @@ -232,7 +245,7 @@ public SubmitCourierRatingResponseDto submitCourierRating(String id, Double rati deliveryTaskRepository.save(deliveryTaskToSubmit); return DeliveryTaskMapper.toSubmitCourierRatingResponseDto(deliveryTaskToSubmit); } - logger.warn("Delivery task not found with ID: {} for courier rating", id); + LOGGER.warn("Delivery task not found with ID: {} for courier rating", id); return null; } -} \ No newline at end of file +} diff --git a/src/main/java/com/podzilla/courier/services/delivery_task/confirmation_strategy/DeliveryConfirmationStrategy.java b/src/main/java/com/podzilla/courier/services/delivery_task/confirmation_strategy/DeliveryConfirmationStrategy.java new file mode 100644 index 0000000..266e5b4 --- /dev/null +++ b/src/main/java/com/podzilla/courier/services/delivery_task/confirmation_strategy/DeliveryConfirmationStrategy.java @@ -0,0 +1,9 @@ +package com.podzilla.courier.services.delivery_task.confirmation_strategy; + +import com.podzilla.courier.models.DeliveryTask; + +import java.util.Optional; + +public interface DeliveryConfirmationStrategy { + Optional confirmDelivery(DeliveryTask task, String confirmationInput); +} diff --git a/src/main/java/com/podzilla/courier/services/delivery_task/confirmation_strategy/OtpConfirmationStrategy.java b/src/main/java/com/podzilla/courier/services/delivery_task/confirmation_strategy/OtpConfirmationStrategy.java new file mode 100644 index 0000000..5c23693 --- /dev/null +++ b/src/main/java/com/podzilla/courier/services/delivery_task/confirmation_strategy/OtpConfirmationStrategy.java @@ -0,0 +1,45 @@ +package com.podzilla.courier.services.delivery_task.confirmation_strategy; + +import com.podzilla.courier.models.DeliveryStatus; +import com.podzilla.courier.models.DeliveryTask; +import com.podzilla.courier.services.delivery_task.poll_command.StopPollingCommand; +import com.podzilla.mq.EventPublisher; +import com.podzilla.mq.events.OrderDeliveredEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.util.Optional; + +@Component +public class OtpConfirmationStrategy implements DeliveryConfirmationStrategy { + private static final Logger LOGGER = LoggerFactory.getLogger(OtpConfirmationStrategy.class); + private final EventPublisher eventPublisher; + + public OtpConfirmationStrategy(final EventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + + @Override + public Optional confirmDelivery(final DeliveryTask task, final String confirmationInput) { + LOGGER.info("Confirming delivery with OTP for task ID: {}", task.getId()); + if (task.getOtp() == null || !task.getOtp().equals(confirmationInput)) { + LOGGER.debug("Invalid OTP for task ID: {}", task.getId()); + return Optional.of("Wrong OTP"); + } + + task.setStatus(DeliveryStatus.DELIVERED); + LOGGER.debug("OTP confirmed for task ID: {}", task.getId()); + + OrderDeliveredEvent event = new OrderDeliveredEvent( + task.getOrderId(), + task.getCourierId(), + task.getCourierRating() != null ? BigDecimal.valueOf(task.getCourierRating()) : null + ); + StopPollingCommand stopPollingCommand = new StopPollingCommand(eventPublisher, event); + stopPollingCommand.execute(); + + return Optional.of("OTP confirmed"); + } +} diff --git a/src/main/java/com/podzilla/courier/services/delivery_task/confirmation_strategy/QrCodeConfirmationStrategy.java b/src/main/java/com/podzilla/courier/services/delivery_task/confirmation_strategy/QrCodeConfirmationStrategy.java new file mode 100644 index 0000000..8b3a135 --- /dev/null +++ b/src/main/java/com/podzilla/courier/services/delivery_task/confirmation_strategy/QrCodeConfirmationStrategy.java @@ -0,0 +1,48 @@ +package com.podzilla.courier.services.delivery_task.confirmation_strategy; + +import com.podzilla.courier.models.DeliveryStatus; +import com.podzilla.courier.models.DeliveryTask; +import com.podzilla.courier.services.delivery_task.poll_command.StopPollingCommand; +import com.podzilla.mq.EventPublisher; +import com.podzilla.mq.events.OrderDeliveredEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.util.Optional; + +@Component +public class QrCodeConfirmationStrategy implements DeliveryConfirmationStrategy { + private static final Logger LOGGER = LoggerFactory.getLogger(QrCodeConfirmationStrategy.class); + private final EventPublisher eventPublisher; + + public QrCodeConfirmationStrategy(final EventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + + @Override + public Optional confirmDelivery(final DeliveryTask task, final String confirmationInput) { + LOGGER.info("Confirming delivery with QR code for task ID: {}", task.getId()); + // assume QR code is valid if it matches a predefined value or logic + String expectedQrCode = task.getQrCode(); + if (expectedQrCode == null || !expectedQrCode.equals(confirmationInput)) { + LOGGER.debug("Invalid QR code for task ID: {}", task.getId()); + return Optional.of("Invalid QR code"); + } + + task.setStatus(DeliveryStatus.DELIVERED); + LOGGER.debug("QR code confirmed for task ID: {}", task.getId()); + + // publish order.delivered event + OrderDeliveredEvent event = new OrderDeliveredEvent( + task.getOrderId(), + task.getCourierId(), + task.getCourierRating() != null ? BigDecimal.valueOf(task.getCourierRating()) : null + ); + StopPollingCommand stopPollingCommand = new StopPollingCommand(eventPublisher, event); + stopPollingCommand.execute(); + + return Optional.of("QR code confirmed"); + } +} diff --git a/src/main/java/com/podzilla/courier/services/delivery_task/confirmation_strategy/SignatureConfirmationStrategy.java b/src/main/java/com/podzilla/courier/services/delivery_task/confirmation_strategy/SignatureConfirmationStrategy.java new file mode 100644 index 0000000..330fcdc --- /dev/null +++ b/src/main/java/com/podzilla/courier/services/delivery_task/confirmation_strategy/SignatureConfirmationStrategy.java @@ -0,0 +1,48 @@ +package com.podzilla.courier.services.delivery_task.confirmation_strategy; + +import com.podzilla.courier.models.DeliveryStatus; +import com.podzilla.courier.models.DeliveryTask; +import com.podzilla.courier.services.delivery_task.poll_command.StopPollingCommand; +import com.podzilla.mq.EventPublisher; +import com.podzilla.mq.events.OrderDeliveredEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.util.Optional; + +@Component +public class SignatureConfirmationStrategy implements DeliveryConfirmationStrategy { + private static final Logger LOGGER = LoggerFactory.getLogger(SignatureConfirmationStrategy.class); + private final EventPublisher eventPublisher; + + public SignatureConfirmationStrategy(final EventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + + @Override + public Optional confirmDelivery(final DeliveryTask task, final String confirmationInput) { + LOGGER.info("Confirming delivery with signature for task ID: {}", task.getId()); + // assume signature is valid if input is non-empty + if (confirmationInput == null || confirmationInput.isEmpty() + || !task.getSignature().equals(confirmationInput)) { + LOGGER.debug("Invalid signature for task ID: {}", task.getId()); + return Optional.of("Invalid signature"); + } + + task.setStatus(DeliveryStatus.DELIVERED); + LOGGER.debug("Signature confirmed for task ID: {}", task.getId()); + + // publish order.delivered event + OrderDeliveredEvent event = new OrderDeliveredEvent( + task.getOrderId(), + task.getCourierId(), + task.getCourierRating() != null ? BigDecimal.valueOf(task.getCourierRating()) : null + ); + StopPollingCommand stopPollingCommand = new StopPollingCommand(eventPublisher, event); + stopPollingCommand.execute(); + + return Optional.of("Signature confirmed"); + } +} diff --git a/src/main/java/com/podzilla/courier/services/delivery_task/poll_command/Command.java b/src/main/java/com/podzilla/courier/services/delivery_task/poll_command/Command.java new file mode 100644 index 0000000..a79be50 --- /dev/null +++ b/src/main/java/com/podzilla/courier/services/delivery_task/poll_command/Command.java @@ -0,0 +1,5 @@ +package com.podzilla.courier.services.delivery_task.poll_command; + +public interface Command { + void execute(); +} diff --git a/src/main/java/com/podzilla/courier/services/delivery_task/poll_command/StartPollingCommand.java b/src/main/java/com/podzilla/courier/services/delivery_task/poll_command/StartPollingCommand.java new file mode 100644 index 0000000..a3835c0 --- /dev/null +++ b/src/main/java/com/podzilla/courier/services/delivery_task/poll_command/StartPollingCommand.java @@ -0,0 +1,22 @@ +package com.podzilla.courier.services.delivery_task.poll_command; + +import com.podzilla.mq.EventPublisher; +import com.podzilla.mq.EventsConstants; +import com.podzilla.mq.events.OrderOutForDeliveryEvent; + +public class StartPollingCommand implements Command { + + private final EventPublisher eventPublisher; + private final OrderOutForDeliveryEvent event; + + public StartPollingCommand(final EventPublisher eventPublisher, final OrderOutForDeliveryEvent event) { + this.eventPublisher = eventPublisher; + this.event = event; + } + + @Override + public void execute() { + // publish out_for_delivery event so that the order service start tracking courier location + eventPublisher.publishEvent(EventsConstants.ORDER_OUT_FOR_DELIVERY, event); + } +} diff --git a/src/main/java/com/podzilla/courier/services/delivery_task/poll_command/StopPollingCommand.java b/src/main/java/com/podzilla/courier/services/delivery_task/poll_command/StopPollingCommand.java new file mode 100644 index 0000000..fe65603 --- /dev/null +++ b/src/main/java/com/podzilla/courier/services/delivery_task/poll_command/StopPollingCommand.java @@ -0,0 +1,30 @@ +package com.podzilla.courier.services.delivery_task.poll_command; + +import com.podzilla.mq.EventPublisher; +import com.podzilla.mq.EventsConstants; +import com.podzilla.mq.events.OrderCancelledEvent; +import com.podzilla.mq.events.OrderDeliveredEvent; + +public class StopPollingCommand implements Command { + private final EventPublisher eventPublisher; + private final Object event; + + public StopPollingCommand(final EventPublisher eventPublisher, final Object event) { + this.eventPublisher = eventPublisher; + this.event = event; + } + + + @Override + public void execute() { + if (event instanceof OrderCancelledEvent) { + OrderCancelledEvent cancelledEvent = (OrderCancelledEvent) event; + // publish order_cancelled event so that the order service stops tracking courier location + eventPublisher.publishEvent(EventsConstants.ORDER_CANCELLED, cancelledEvent); + } else if (event instanceof OrderDeliveredEvent) { + OrderDeliveredEvent deliveredEvent = (OrderDeliveredEvent) event; + // publish order_delivered event so that the order service stops tracking courier location + eventPublisher.publishEvent(EventsConstants.ORDER_DELIVERED, deliveredEvent); + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 71de7b7..84a2b2f 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -3,7 +3,6 @@ spring.application.name=courier spring.data.mongodb.uri=mongodb://courier:courier-password@mongodb:27017/courier-db?authSource=admin springdoc.swagger-ui.path=/swagger-ui.html springdoc.api-docs.path=/v3/api-docs -springdoc.show-spring-data-rest-sort-parameter=false logging.level.org.springdoc=DEBUG logging.level.org.springframework=INFO @@ -14,3 +13,5 @@ spring.rabbitmq.port=5672 spring.rabbitmq.username=guest spring.rabbitmq.password=guest +otp.length=4 + diff --git a/src/test/java/com/podzilla/courier/CourierApplicationTests.java b/src/test/java/com/podzilla/courier/CourierApplicationTests.java index 500e026..e7df220 100644 --- a/src/test/java/com/podzilla/courier/CourierApplicationTests.java +++ b/src/test/java/com/podzilla/courier/CourierApplicationTests.java @@ -6,7 +6,7 @@ import com.podzilla.courier.dtos.couriers.UpdateCourierRequestDto; import com.podzilla.courier.dtos.couriers.CourierResponseDto; import com.podzilla.courier.models.CourierStatus; -import com.podzilla.courier.services.CourierService; +import com.podzilla.courier.services.courier.CourierService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test;