diff --git a/docker-compose.yml b/docker-compose.yml index c126715..1ff4abe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,6 +23,16 @@ services: ports: - "5432:5432" + rabbitmq: + image: rabbitmq:3-management + container_name: rabbitmq + ports: + - "15672:15672" + - "5672:5672" + environment: + RABBITMQ_DEFAULT_USER: user + RABBITMQ_DEFAULT_PASS: password + loki: image: grafana/loki:3.5.0 container_name: loki diff --git a/pom.xml b/pom.xml index a7ebd93..a4bf087 100644 --- a/pom.xml +++ b/pom.xml @@ -29,11 +29,22 @@ 23 + + + jitpack.io + https://jitpack.io + + org.springframework.boot spring-boot-starter-data-jdbc + + org.springframework.cloud + spring-cloud-starter-stream-rabbit + 4.0.0 + org.springdoc springdoc-openapi-starter-webmvc-ui @@ -82,8 +93,12 @@ spring-boot-starter-test test + + com.github.Podzilla + podzilla-utils-lib + v1.1.11 + - diff --git a/src/main/java/com/podzilla/order/OrderApplication.java b/src/main/java/com/podzilla/order/OrderApplication.java index 7492983..7d4ba92 100644 --- a/src/main/java/com/podzilla/order/OrderApplication.java +++ b/src/main/java/com/podzilla/order/OrderApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; @SpringBootApplication +@ComponentScan(basePackages = {"com.podzilla"}) public class OrderApplication { public static void main(final String[] args) { diff --git a/src/main/java/com/podzilla/order/config/WebClientConfig.java b/src/main/java/com/podzilla/order/config/WebClientConfig.java new file mode 100644 index 0000000..c0e81c6 --- /dev/null +++ b/src/main/java/com/podzilla/order/config/WebClientConfig.java @@ -0,0 +1,14 @@ +package com.podzilla.order.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class WebClientConfig { + + @Bean + public WebClient webClient() { + return WebClient.builder().build(); + } +} diff --git a/src/main/java/com/podzilla/order/controller/OrderController.java b/src/main/java/com/podzilla/order/controller/OrderController.java index 2e21d66..1c18bb6 100644 --- a/src/main/java/com/podzilla/order/controller/OrderController.java +++ b/src/main/java/com/podzilla/order/controller/OrderController.java @@ -1,6 +1,7 @@ package com.podzilla.order.controller; import com.podzilla.order.model.Order; +import com.podzilla.order.model.OrderLocation; import com.podzilla.order.model.OrderStatus; import com.podzilla.order.service.OrderService; import io.swagger.v3.oas.annotations.tags.Tag; @@ -13,6 +14,7 @@ import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.RestController; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -38,6 +40,8 @@ public OrderController(final OrderService orderService) { this.orderService = orderService; } + + @PostMapping @Operation(summary = "Create a new order", description = "Creates a new order and returns the created order") @@ -49,6 +53,8 @@ public ResponseEntity createOrder(@RequestBody final Order order) { return ResponseEntity.ok(createdOrder); } + + @GetMapping @Operation(summary = "Get all orders", description = "Returns a list of all orders") @@ -59,6 +65,8 @@ public ResponseEntity> getAllOrders() { return ResponseEntity.ok(orders); } + + @GetMapping("/{id}") @Operation(summary = "Get order by ID", description = "Returns an order by its ID") @@ -70,7 +78,9 @@ public ResponseEntity getOrderById(@PathVariable final UUID id) { return ResponseEntity.ok(order); } - @PutMapping("/{id}") + + + @PatchMapping("/{id}") @Operation(summary = "Update an order", description = "Updates an existing order and returns the updated " + "order") @@ -83,6 +93,8 @@ public ResponseEntity updateOrder(@PathVariable final UUID id, return ResponseEntity.ok(updatedOrder); } + + @DeleteMapping("/{id}") @Operation(summary = "Delete an order", description = "Deletes an order by its ID") @@ -94,6 +106,8 @@ public ResponseEntity deleteOrder(@PathVariable final UUID id) { return ResponseEntity.noContent().build(); } + + @GetMapping("/user/{userId}") @Operation( summary = "Get order by user ID", @@ -109,6 +123,8 @@ public ResponseEntity> getOrderByUserId( return ResponseEntity.ok(order); } + + @PutMapping("/cancel/{id}") @Operation( summary = "Cancel order", @@ -117,12 +133,15 @@ public ResponseEntity> getOrderByUserId( @ApiResponse( responseCode = "200", description = "Order cancelled" ) - public ResponseEntity cancelOrder(@PathVariable final UUID id) { - Order order = orderService.cancelOrder(id); + public ResponseEntity cancelOrder(@PathVariable final UUID id, + @RequestBody final String reason) { + Order order = orderService.cancelOrder(id, reason); LOGGER.info("Order with ID: {} cancelled", id); return ResponseEntity.ok(order); } + + @PutMapping("/status/{id}") @Operation( summary = "Update order status", @@ -138,4 +157,21 @@ public ResponseEntity updateOrderStatus(@PathVariable final UUID id, LOGGER.info("Order status updated for ID: {}", id); return ResponseEntity.ok(order); } + + + + @GetMapping("/trackOrder/{id}") + @Operation( + summary = "Track order", + description = "Tracks the location of an order based on " + + "the provided order ID" + ) + @ApiResponse( + responseCode = "200", description = "Order location tracked" + ) + public ResponseEntity trackOrder(@PathVariable final UUID id) { + OrderLocation location = orderService.trackOrder(id); + LOGGER.info("Order location tracked for ID: {}", id); + return ResponseEntity.ok(location); + } } diff --git a/src/main/java/com/podzilla/order/dtos/LocationDTO.java b/src/main/java/com/podzilla/order/dtos/LocationDTO.java new file mode 100644 index 0000000..bfa3dfb --- /dev/null +++ b/src/main/java/com/podzilla/order/dtos/LocationDTO.java @@ -0,0 +1,14 @@ +package com.podzilla.order.dtos; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class LocationDTO { + private double first; // Latitude + private double second; // Longitude +} diff --git a/src/main/java/com/podzilla/order/messaging/OrderConsumer.java b/src/main/java/com/podzilla/order/messaging/OrderConsumer.java new file mode 100644 index 0000000..9549673 --- /dev/null +++ b/src/main/java/com/podzilla/order/messaging/OrderConsumer.java @@ -0,0 +1,163 @@ +package com.podzilla.order.messaging; + +import com.podzilla.mq.EventsConstants; +import com.podzilla.mq.events.BaseEvent; +import com.podzilla.mq.events.CartCheckedoutEvent; +import com.podzilla.mq.events.OrderAssignedToCourierEvent; +import com.podzilla.mq.events.OrderDeliveredEvent; +import com.podzilla.mq.events.OrderOutForDeliveryEvent; +import com.podzilla.mq.events.OrderPackagedEvent; +import com.podzilla.mq.events.WarehouseOrderFulfillmentFailedEvent; +import com.podzilla.mq.events.WarehouseStockReservedEvent; +import com.podzilla.order.model.Address; +import com.podzilla.order.model.Order; +import com.podzilla.order.model.OrderProduct; +import com.podzilla.order.model.OrderStatus; + +import com.podzilla.order.service.OrderService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.UUID; + +@Component +@Slf4j +public class OrderConsumer { + + private final OrderService orderService; + + public OrderConsumer(final OrderService orderService) { + this.orderService = orderService; + } + + @RabbitListener(queues = EventsConstants.ORDER_INVENTORY_EVENT_QUEUE) + public void handleStockReserved(final BaseEvent payload) { + if (payload instanceof WarehouseStockReservedEvent) { + WarehouseStockReservedEvent warehouseStockReservedEvent = + (WarehouseStockReservedEvent) payload; + handleWarehouseStockReservedEvent(warehouseStockReservedEvent); + } + if (payload instanceof WarehouseOrderFulfillmentFailedEvent) { + WarehouseOrderFulfillmentFailedEvent warehouseOrderFulfillmentFailedEvent = + (WarehouseOrderFulfillmentFailedEvent) payload; + handleWarehouseOrderFulfillmentFailedEvent( + warehouseOrderFulfillmentFailedEvent); + } + } + + @RabbitListener(queues = EventsConstants.ORDER_ORDER_EVENT_QUEUE) + public void trackOrder(final BaseEvent payload) { + if (payload instanceof OrderPackagedEvent) { + OrderPackagedEvent orderPackagedEvent = (OrderPackagedEvent) payload; + handleOrderPackagedEvent(orderPackagedEvent); + } + if (payload instanceof OrderAssignedToCourierEvent) { + OrderAssignedToCourierEvent orderAssignedToCourierEvent = + (OrderAssignedToCourierEvent) payload; + handleOrderAssignedToCourierEvent(orderAssignedToCourierEvent); + } + if (payload instanceof OrderOutForDeliveryEvent) { + OrderOutForDeliveryEvent orderOutForDeliveryEvent = + (OrderOutForDeliveryEvent) payload; + handleOrderOutForDeliveryEvent(orderOutForDeliveryEvent); + } + if (payload instanceof OrderDeliveredEvent) { + OrderDeliveredEvent orderDeliveredEvent = + (OrderDeliveredEvent) payload; + handleOrderDeliveredEvent(orderDeliveredEvent); + } + if (payload instanceof CartCheckedoutEvent) { + CartCheckedoutEvent cartCheckedoutEvent = + (CartCheckedoutEvent) payload; + handleCartCheckoutEvent(cartCheckedoutEvent); + } + } + + private void handleWarehouseStockReservedEvent( + final WarehouseStockReservedEvent warehouseStockReservedEvent) { + + log.info("✅ Stock reserved for order: {}", + warehouseStockReservedEvent.getOrderId()); + orderService.placeOrder(UUID.fromString(warehouseStockReservedEvent.getOrderId())); + } + + private void handleWarehouseOrderFulfillmentFailedEvent( + final WarehouseOrderFulfillmentFailedEvent + warehouseOrderFulfillmentFailedEvent) { + log.info("❌ Order fulfillment failed for order: {}, reason: {}", + warehouseOrderFulfillmentFailedEvent.getOrderId(), + warehouseOrderFulfillmentFailedEvent.getReason()); + orderService.cancelOrder( + UUID.fromString(warehouseOrderFulfillmentFailedEvent.getOrderId()), + warehouseOrderFulfillmentFailedEvent.getReason()); + } + + private void handleCartCheckoutEvent( + final CartCheckedoutEvent cartCheckedoutEvent) { + log.info("✅ Cart checked out for user with id: {}", + cartCheckedoutEvent.getCustomerId()); + Address address = new Address(); + address.setStreet(cartCheckedoutEvent.getDeliveryAddress().getStreet()); + address.setCity(cartCheckedoutEvent.getDeliveryAddress().getCity()); + address.setState(cartCheckedoutEvent.getDeliveryAddress().getState()); + address.setCountry(cartCheckedoutEvent.getDeliveryAddress().getCountry()); + address.setPostalCode(cartCheckedoutEvent.getDeliveryAddress().getPostalCode()); + List orderProducts = cartCheckedoutEvent.getItems().stream() + .map(orderProduct -> { + OrderProduct product = new OrderProduct(); + product.setProductId(UUID.fromString(orderProduct.getProductId())); + product.setQuantity(orderProduct.getQuantity()); + product.setPricePerUnit(orderProduct.getPricePerUnit()); + return product; + }).toList(); + Order order = new Order(); + order.setUserId(UUID.fromString(cartCheckedoutEvent.getCustomerId())); + order.setTotalAmount(cartCheckedoutEvent.getTotalAmount()); + order.setStatus(OrderStatus.PENDING); + order.setShippingAddress(address); + order.setOrderProducts(orderProducts); + order.setConfirmationType(cartCheckedoutEvent.getConfirmationType()); + order.setSignature(cartCheckedoutEvent.getSignature()); + order.setOrderLatitude(cartCheckedoutEvent.getOrderLatitude()); + order.setOrderLongitude(cartCheckedoutEvent.getOrderLongitude()); + orderService.createOrder(order); + } + + private void handleOrderAssignedToCourierEvent(final OrderAssignedToCourierEvent orderAssignedToCourierEvent) { + log.info("✅ Order assigned to courier for order: {}", + orderAssignedToCourierEvent.getOrderId()); + orderService.updateOrder( + UUID.fromString(orderAssignedToCourierEvent.getOrderId()), + Order.builder() + .courierId(UUID.fromString(orderAssignedToCourierEvent.getCourierId())) + .status(OrderStatus.ORDER_ASSIGNED_TO_COURIER) + .build()); + } + + private void handleOrderDeliveredEvent( + final OrderDeliveredEvent orderDeliveredEvent) { + log.info("✅ Order delivered for order: {}", + orderDeliveredEvent.getOrderId()); + orderService.updateOrderStatus(UUID.fromString(orderDeliveredEvent.getOrderId()), OrderStatus.DELIVERED); + } + + private void handleOrderOutForDeliveryEvent(final OrderOutForDeliveryEvent orderOutForDeliveryEvent) { + log.info("✅ Order out for delivery for order: {}", orderOutForDeliveryEvent.getOrderId()); + orderService.updateOrder( + UUID.fromString(orderOutForDeliveryEvent.getOrderId()), + Order.builder() + .courierId(UUID.fromString(orderOutForDeliveryEvent.getCourierId())) + .status(OrderStatus.OUT_FOR_DELIVERY) + .build()); + } + + private void handleOrderPackagedEvent(final OrderPackagedEvent orderPackagedEvent) { + log.info("✅ Order packaged for order: {}", + orderPackagedEvent.getOrderId()); + orderService.updateOrderStatus( + UUID.fromString(orderPackagedEvent.getOrderId()), + OrderStatus.PACKAGED); + } +} diff --git a/src/main/java/com/podzilla/order/messaging/OrderProducer.java b/src/main/java/com/podzilla/order/messaging/OrderProducer.java new file mode 100644 index 0000000..a34781d --- /dev/null +++ b/src/main/java/com/podzilla/order/messaging/OrderProducer.java @@ -0,0 +1,43 @@ +package com.podzilla.order.messaging; + +import com.podzilla.mq.EventPublisher; +import com.podzilla.mq.EventsConstants; +import com.podzilla.mq.events.OrderCancelledEvent; +import com.podzilla.mq.events.OrderPlacedEvent; +import com.podzilla.mq.events.OrderStockReservationRequestedEvent; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class OrderProducer { + + + private final EventPublisher eventPublisher; + + + public void sendStockReservationRequest( + final OrderStockReservationRequestedEvent orderStockReservationRequestedEvent) { + + eventPublisher.publishEvent( + EventsConstants.ORDER_STOCK_RESERVATION_REQUESTED, + orderStockReservationRequestedEvent + ); + } + + public void sendOrderPlaced( + final OrderPlacedEvent orderPlaced) { + eventPublisher.publishEvent( + EventsConstants.ORDER_PLACED, + orderPlaced + ); + } + + public void sendCancelOrder( + final OrderCancelledEvent orderCancelledEvent) { + eventPublisher.publishEvent( + EventsConstants.ORDER_CANCELLED, + orderCancelledEvent + ); + } +} diff --git a/src/main/java/com/podzilla/order/messaging/RabbitMQConfig.java b/src/main/java/com/podzilla/order/messaging/RabbitMQConfig.java new file mode 100644 index 0000000..2d8892b --- /dev/null +++ b/src/main/java/com/podzilla/order/messaging/RabbitMQConfig.java @@ -0,0 +1,68 @@ +package com.podzilla.order.messaging; + +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.core.TopicExchange; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RabbitMQConfig { + // Exchange + public static final String EXCHANGE = "shared_exchange"; + + // Outbound (sending to warehouse) + public static final String ORDER_ROUTING_KEY = + "order.stock_reservation_requested"; + + public static final String ORDER_PLACED_ROUTING_KEY = + "order.placed"; + + // Inbound (responses from warehouse) + public static final String STOCK_RESERVED_ROUTING_KEY = + "warehouse.stock_reserved"; + public static final String ORDER_FAILED_ROUTING_KEY = + "warehouse.order_fulfillment_failed"; + + // Queues this service will consume from (responses from warehouse) + public static final String STOCK_RESERVED_QUEUE = + "order.stock_reserved.queue"; + public static final String ORDER_FAILED_QUEUE = + "order.fulfillment_failed.queue"; + + @Bean + public Queue stockReservedQueue() { + return new Queue(STOCK_RESERVED_QUEUE); + } + + @Bean + public Queue orderFailedQueue() { + return new Queue(ORDER_FAILED_QUEUE); + } + + @Bean + public TopicExchange exchange() { + return new TopicExchange(EXCHANGE); + } + + @Bean + public Binding warehouseStockReservedBinding( + final Queue stockReservedQueue, + final TopicExchange exchange) { + return BindingBuilder + .bind(stockReservedQueue) + .to(exchange) + .with("warehouse.stock_reserved"); + } + + @Bean + public Binding warehouseFulfillmentFailedBinding( + final Queue orderFailedQueue, + final TopicExchange exchange) { + return BindingBuilder + .bind(orderFailedQueue) + .to(exchange) + .with("warehouse.order_fulfillment_failed"); + } +} diff --git a/src/main/java/com/podzilla/order/model/Order.java b/src/main/java/com/podzilla/order/model/Order.java index fbc14ee..11f857b 100644 --- a/src/main/java/com/podzilla/order/model/Order.java +++ b/src/main/java/com/podzilla/order/model/Order.java @@ -7,16 +7,18 @@ import jakarta.persistence.Id; import jakarta.persistence.OneToMany; import jakarta.persistence.OneToOne; -import jakarta.persistence.Column; import jakarta.persistence.Enumerated; import jakarta.persistence.EnumType; import jakarta.persistence.CascadeType; import jakarta.persistence.GenerationType; +import com.podzilla.mq.events.ConfirmationType; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -28,41 +30,38 @@ @AllArgsConstructor @Getter @Setter +@Builder public class Order { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) + @GeneratedValue(strategy = GenerationType.UUID) private UUID id; private UUID userId; - private double totalAmount; + private UUID courierId; + + private BigDecimal totalAmount; @OneToOne(cascade = CascadeType.ALL) private Address shippingAddress; @Enumerated(EnumType.STRING) - @Column(nullable = false) private OrderStatus status; - @Column(nullable = false) private LocalDateTime createdAt; - @Column(nullable = false) private LocalDateTime updatedAt; - @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = - true) - private List orderItems = new ArrayList<>(); + private ConfirmationType confirmationType; + + private String signature; + + private double orderLatitude; - public Order(final UUID userId, final double totalAmount, - final OrderStatus status, final List orderItems) { - this.userId = userId; - this.totalAmount = totalAmount; - this.status = status; - this.createdAt = LocalDateTime.now(); - this.updatedAt = LocalDateTime.now(); - this.orderItems = orderItems; - } + private double orderLongitude; + @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, + orphanRemoval = true) + private List orderProducts = new ArrayList<>(); } diff --git a/src/main/java/com/podzilla/order/model/OrderLocation.java b/src/main/java/com/podzilla/order/model/OrderLocation.java new file mode 100644 index 0000000..b4fe5cb --- /dev/null +++ b/src/main/java/com/podzilla/order/model/OrderLocation.java @@ -0,0 +1,18 @@ +package com.podzilla.order.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.Data; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Data +public class OrderLocation { + + private double latitude; + private double longitude; +} diff --git a/src/main/java/com/podzilla/order/model/OrderItem.java b/src/main/java/com/podzilla/order/model/OrderProduct.java similarity index 75% rename from src/main/java/com/podzilla/order/model/OrderItem.java rename to src/main/java/com/podzilla/order/model/OrderProduct.java index 0d7a03c..dad2e31 100644 --- a/src/main/java/com/podzilla/order/model/OrderItem.java +++ b/src/main/java/com/podzilla/order/model/OrderProduct.java @@ -4,36 +4,39 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.Table; -import jakarta.persistence.Column; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.Column; import jakarta.persistence.FetchType; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import java.math.BigDecimal; +import java.util.UUID; + @Entity -@Table(name = "order_items") +@Table(name = "order_products") @NoArgsConstructor @AllArgsConstructor @Getter @Setter -public class OrderItem { +public class OrderProduct { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private long id; + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; @Column(nullable = false) - private long productId; + private UUID productId; @Column(nullable = false) private int quantity; @Column(nullable = false) - private double price; + private BigDecimal pricePerUnit; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "order_id", nullable = false) diff --git a/src/main/java/com/podzilla/order/model/OrderStatus.java b/src/main/java/com/podzilla/order/model/OrderStatus.java index 07de71e..5b56052 100644 --- a/src/main/java/com/podzilla/order/model/OrderStatus.java +++ b/src/main/java/com/podzilla/order/model/OrderStatus.java @@ -1,10 +1,12 @@ package com.podzilla.order.model; public enum OrderStatus { + PENDING, PLACED, - CANCELLED, - SHIPPED, + PACKAGED, + ORDER_ASSIGNED_TO_COURIER, + OUT_FOR_DELIVERY, DELIVERED, - FAILED + CANCELLED, } diff --git a/src/main/java/com/podzilla/order/service/OrderService.java b/src/main/java/com/podzilla/order/service/OrderService.java index fab9aba..8bfd0a7 100644 --- a/src/main/java/com/podzilla/order/service/OrderService.java +++ b/src/main/java/com/podzilla/order/service/OrderService.java @@ -1,14 +1,27 @@ package com.podzilla.order.service; +import com.podzilla.mq.events.OrderCancelledEvent; +import com.podzilla.mq.events.OrderItem; +import com.podzilla.mq.events.OrderPlacedEvent; +import com.podzilla.mq.events.OrderStockReservationRequestedEvent; +import com.podzilla.mq.events.DeliveryAddress; +import com.podzilla.order.dtos.LocationDTO; import com.podzilla.order.exception.NotFoundException; +import com.podzilla.order.messaging.OrderProducer; import com.podzilla.order.model.Order; +import com.podzilla.order.model.OrderLocation; +import com.podzilla.order.model.OrderProduct; import com.podzilla.order.model.OrderStatus; import com.podzilla.order.repository.OrderRepository; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; + import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -18,17 +31,59 @@ @Service public class OrderService { private final OrderRepository orderRepository; + private final OrderProducer orderProducer; + private final WebClient webClient; + + @Value("${api.gateway.url}") + private String apiGatewayUrl; @Autowired - public OrderService(final OrderRepository orderRepository) { + public OrderService(final OrderRepository orderRepository, + final OrderProducer orderProducer, + final WebClient webClient) { this.orderRepository = orderRepository; + this.orderProducer = orderProducer; + this.webClient = webClient; } public Order createOrder(final Order order) { log.info("Creating new order: {}", order); order.setCreatedAt(LocalDateTime.now()); order.setUpdatedAt(LocalDateTime.now()); - return orderRepository.save(order); + orderRepository.save(order); + OrderStockReservationRequestedEvent stockReservationRequest = + OrderStockReservationRequestedEvent.builder() + .orderId(order.getId().toString()) + .items(getOrderItems(order)) + .build(); + orderProducer.sendStockReservationRequest(stockReservationRequest); + return order; + } + + public Order placeOrder(final UUID orderId) { + log.info("Placing order with ID: {}", orderId); + Order order = updateOrderStatus(orderId, OrderStatus.PLACED); + DeliveryAddress deliveryAddress = new DeliveryAddress( + order.getShippingAddress().getStreet(), + order.getShippingAddress().getCity(), + order.getShippingAddress().getState(), + order.getShippingAddress().getCountry(), + order.getShippingAddress().getPostalCode() + ); + OrderPlacedEvent orderPlaced = + OrderPlacedEvent.builder() + .orderId(order.getId().toString()) + .customerId(order.getUserId().toString()) + .items(getOrderItems(order)) + .totalAmount(order.getTotalAmount()) + .deliveryAddress(deliveryAddress) + .confirmationType(order.getConfirmationType()) + .signature(order.getSignature()) + .orderLatitude(order.getOrderLatitude()) + .orderLongitude(order.getOrderLongitude()) + .build(); + orderProducer.sendOrderPlaced(orderPlaced); + return order; } public List getAllOrders() { @@ -76,7 +131,7 @@ public Optional getOrderByUserId(final UUID userId) { return orderRepository.findByUserId(userId); } - public Order cancelOrder(final UUID id) { + public Order cancelOrder(final UUID id, final String reason) { log.info("Cancelling order with ID: {}", id); Optional existingOrder = orderRepository.findById(id); @@ -87,7 +142,15 @@ public Order cancelOrder(final UUID id) { Order order = existingOrder.get(); order.setStatus(OrderStatus.CANCELLED); order.setUpdatedAt(LocalDateTime.now()); - return orderRepository.save(order); + orderRepository.save(order); + OrderCancelledEvent orderCancelledEvent = + OrderCancelledEvent.builder() + .orderId(order.getId().toString()) + .customerId(order.getUserId().toString()) + .reason(reason) + .build(); + orderProducer.sendCancelOrder(orderCancelledEvent); + return order; } public Order updateOrderStatus(final UUID id, @@ -105,10 +168,45 @@ public Order updateOrderStatus(final UUID id, return orderRepository.save(order); } + public OrderLocation trackOrder(final UUID id) { + log.info("Tracking order with ID: {}", id); + Optional existingOrder = orderRepository.findById(id); + checkNotFoundException(existingOrder.orElse(null), + "Order not found with id: " + id); + String url = apiGatewayUrl + "/delivery-tasks/" + id + "/location"; + LocationDTO location = webClient + .get() + .uri(url) + .retrieve() + .bodyToMono(LocationDTO.class) + .block(); + + if (location == null) { + throw new RuntimeException("Failed to get location for order " + id); + } + log.info("Order location: {}", location); + return new OrderLocation(location.getFirst(), location.getSecond()); + } + private void checkNotFoundException(final Object value, final String message) { if (value == null) { throw new NotFoundException(message); } } + + private List getOrderItems(final Order order) { + List orderItems = new ArrayList<>(); + List orderProducts = order.getOrderProducts(); + for (OrderProduct product : orderProducts) { + OrderItem orderItem = new OrderItem(); + orderItem.setProductId(product.getId().toString()); + orderItem.setQuantity(product.getQuantity()); + orderItem.setPricePerUnit(product.getPricePerUnit()); + orderItems.add(orderItem); + } + return orderItems; + } + + } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index bcd3ef5..6b013d9 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,2 @@ spring.application.name=order +api.gateway.url=http://localhost:8080 \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..0bc1010 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,8 @@ +spring: + application: + name: User + rabbitmq: + host: rabbitmq # container name + port: 5672 + username: user + password: password diff --git a/src/test/java/com/podzilla/order/OrderApplicationTests.java b/src/test/java/com/podzilla/order/OrderApplicationTests.java index a1d6a75..8556e0a 100644 --- a/src/test/java/com/podzilla/order/OrderApplicationTests.java +++ b/src/test/java/com/podzilla/order/OrderApplicationTests.java @@ -1,13 +1,13 @@ -package com.podzilla.order; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class OrderApplicationTests { - - @Test - void contextLoads() { - } - -} +//package com.podzilla.order; +// +//import org.junit.jupiter.api.Test; +//import org.springframework.boot.test.context.SpringBootTest; +// +//@SpringBootTest +//class OrderApplicationTests { +// +// @Test +// void contextLoads() { +// } +// +//}