diff --git a/docker-compose.yml b/docker-compose.yml index c0cd372b70..5c01a8488a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -276,7 +276,7 @@ services: environment: - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/payment - SERVER_SERVLET_CONTEXT_PATH=/payment - - YAS_PUBLIC_URL=http://storefront/complete-payment + - YAS_PUBLIC_URL=${YAS_PUBLIC_API_URL}/payment - YAS_SERVICES_ORDER - SERVER_PORT - LOGGING_CONFIG @@ -300,6 +300,35 @@ services: - ./deployment/app-config:/app-config networks: - yas-network + payment-paypal: + build: ./payment-paypal + image: ghcr.io/nashtech-garage/yas-payment-paypal:latest + environment: + - SERVER_SERVLET_CONTEXT_PATH=/payment-paypal + - YAS_PUBLIC_URL=http://storefront/complete-payment + - YAS_SERVICES_PAYMENT + - SERVER_PORT + - LOGGING_CONFIG + - JAVA_TOOL_OPTIONS + - OTEL_EXPORTER_OTLP_ENDPOINT + - OTEL_EXPORTER_OTLP_PROTOCOL + - OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE + - OTEL_RESOURCE_ATTRIBUTES + - OTEL_SERVICE_NAME=payment-paypal-service + - OTEL_LOGS_EXPORTER + - OTEL_TRACES_EXPORTER + - OTEL_METRICS_EXPORTER + - OTEL_INSTRUMENTATION_LOGBACK-MDC_ADD-BAGGAGE + - OTEL_JAVAAGENT_LOGGING + - OTEL_JAVAAGENT_ENABLED + - OTEL_JAVAAGENT_DEBUG + - YAS_CURRENCY_UNIT + - YAS_PRICE_INCLUDES_TAX + volumes: + - ./docker/libs/opentelemetry-javaagent.jar:/opentelemetry-javaagent.jar + - ./deployment/app-config:/app-config + networks: + - yas-network location: build: ./location image: ghcr.io/nashtech-garage/yas-location:latest diff --git a/nginx/templates/default.conf.template b/nginx/templates/default.conf.template index 58206a72fe..71fca5ce3e 100644 --- a/nginx/templates/default.conf.template +++ b/nginx/templates/default.conf.template @@ -45,6 +45,9 @@ server { location /payment/ { proxy_pass http://payment; } + location /payment-paypal/ { + proxy_pass http://payment-paypal; + } location /webhook/ { proxy_pass http://webhook; } diff --git a/order/src/it/java/com/yas/order/service/OrderServiceIT.java b/order/src/it/java/com/yas/order/service/OrderServiceIT.java index ccf84409b2..8cd7446d9b 100644 --- a/order/src/it/java/com/yas/order/service/OrderServiceIT.java +++ b/order/src/it/java/com/yas/order/service/OrderServiceIT.java @@ -43,6 +43,9 @@ class OrderServiceIT { @MockBean private ProductService productService; + @MockBean + private PromotionService promotionService; + @MockBean private CartService cartService; diff --git a/order/src/main/java/com/yas/order/config/ServiceUrlConfig.java b/order/src/main/java/com/yas/order/config/ServiceUrlConfig.java index 691ef97f26..9c5c794291 100644 --- a/order/src/main/java/com/yas/order/config/ServiceUrlConfig.java +++ b/order/src/main/java/com/yas/order/config/ServiceUrlConfig.java @@ -4,5 +4,5 @@ @ConfigurationProperties(prefix = "yas.services") public record ServiceUrlConfig( - String cart, String customer, String product, String tax) { + String cart, String customer, String product, String tax, String promotion) { } diff --git a/order/src/main/java/com/yas/order/mapper/CheckoutMapper.java b/order/src/main/java/com/yas/order/mapper/CheckoutMapper.java index 76908564b1..92f26c967f 100644 --- a/order/src/main/java/com/yas/order/mapper/CheckoutMapper.java +++ b/order/src/main/java/com/yas/order/mapper/CheckoutMapper.java @@ -10,6 +10,8 @@ import org.mapstruct.Mapping; import org.springframework.stereotype.Component; +import java.math.BigDecimal; + @Mapper(componentModel = "spring") @Component public interface CheckoutMapper { @@ -19,10 +21,16 @@ public interface CheckoutMapper { @Mapping(target = "id", ignore = true) @Mapping(target = "checkoutState", ignore = true) + @Mapping(target = "totalAmount", source = "totalAmount") // Ánh xạ tường minh cho totalAmount + @Mapping(target = "totalDiscountAmount", source = "totalDiscountAmount") Checkout toModel(CheckoutPostVm checkoutPostVm); CheckoutItemVm toVm(CheckoutItem checkoutItem); @Mapping(target = "checkoutItemVms", ignore = true) CheckoutVm toVm(Checkout checkout); + + default BigDecimal map(BigDecimal value) { + return value != null ? value : BigDecimal.ZERO; + } } diff --git a/order/src/main/java/com/yas/order/service/OrderService.java b/order/src/main/java/com/yas/order/service/OrderService.java index 638821dfe6..8f3a1cd42e 100644 --- a/order/src/main/java/com/yas/order/service/OrderService.java +++ b/order/src/main/java/com/yas/order/service/OrderService.java @@ -1,7 +1,5 @@ package com.yas.order.service; -import static com.yas.order.utils.Constants.ErrorCode.ORDER_NOT_FOUND; - import com.yas.commonlibrary.csv.BaseCsv; import com.yas.commonlibrary.csv.CsvExporter; import com.yas.commonlibrary.exception.NotFoundException; @@ -27,6 +25,16 @@ import com.yas.order.viewmodel.order.PaymentOrderStatusVm; import com.yas.order.viewmodel.orderaddress.OrderAddressPostVm; import com.yas.order.viewmodel.product.ProductVariationVm; +import com.yas.order.viewmodel.promotion.PromotionUsageVm; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + import java.io.IOException; import java.time.ZonedDateTime; import java.util.ArrayList; @@ -36,14 +44,8 @@ import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.CollectionUtils; + +import static com.yas.order.utils.Constants.ErrorCode.ORDER_NOT_FOUND; @Slf4j @Service @@ -55,6 +57,7 @@ public class OrderService { private final ProductService productService; private final CartService cartService; private final OrderMapper orderMapper; + private final PromotionService promotionService; public OrderVm createOrder(OrderPostVm orderPostVm) { @@ -128,6 +131,18 @@ public OrderVm createOrder(OrderPostVm orderPostVm) { productService.subtractProductStockQuantity(orderVm); cartService.deleteCartItems(orderVm); acceptOrder(orderVm.id()); + + // update promotion + List promotionUsageVms = new ArrayList<>(); + orderItems.forEach(item -> { + PromotionUsageVm promotionUsageVm = PromotionUsageVm.builder() + .productId(item.getProductId()) + .orderId(order.getId()) + .promotionCode(order.getCouponCode()) + .build(); + promotionUsageVms.add(promotionUsageVm); + }); + promotionService.updateUsagePromotion(promotionUsageVms); return orderVm; } @@ -179,7 +194,7 @@ public List getLatestOrders(int count) { } Pageable pageable = PageRequest.of(0, count); - List orders = orderRepository.getLatestOrders(pageable); + List orders = orderRepository.getLatestOrders(pageable); if (CollectionUtils.isEmpty(orders)) { return List.of(); @@ -272,14 +287,14 @@ public byte[] exportCsv(OrderRequest orderRequest) throws IOException { int pageSize = orderRequest.getPageSize(); OrderListVm orderListVm = getAllOrder(createdFrom, createdTo, - warehouse, productName, - orderStatus, billingCountry, billingPhoneNumber, email, pageNo, pageSize); + warehouse, productName, + orderStatus, billingCountry, billingPhoneNumber, email, pageNo, pageSize); if (Objects.isNull(orderListVm.orderList())) { return CsvExporter.exportToCsv(List.of(), OrderItemCsv.class); } List orders = orderListVm.orderList().stream().map(orderMapper::toCsv).collect( - Collectors.toUnmodifiableList()); + Collectors.toUnmodifiableList()); return CsvExporter.exportToCsv(orders, OrderItemCsv.class); } } diff --git a/order/src/main/java/com/yas/order/service/PromotionService.java b/order/src/main/java/com/yas/order/service/PromotionService.java new file mode 100644 index 0000000000..e904d0ddcb --- /dev/null +++ b/order/src/main/java/com/yas/order/service/PromotionService.java @@ -0,0 +1,41 @@ +package com.yas.order.service; + +import com.yas.order.config.ServiceUrlConfig; +import com.yas.order.viewmodel.promotion.PromotionUsageVm; +import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; +import io.github.resilience4j.retry.annotation.Retry; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClient; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; +import java.util.List; + + +@Service +@RequiredArgsConstructor +public class PromotionService extends AbstractCircuitBreakFallbackHandler { + private final RestClient restClient; + private final ServiceUrlConfig serviceUrlConfig; + + @Retry(name = "restApi") + @CircuitBreaker(name = "restCircuitBreaker", fallbackMethod = "handleBodilessFallback") + public void updateUsagePromotion(List promotionUsageVms) { + final String jwt = ((Jwt) SecurityContextHolder.getContext().getAuthentication().getPrincipal()) + .getTokenValue(); + final URI url = UriComponentsBuilder + .fromHttpUrl(serviceUrlConfig.promotion()) + .path("/backoffice/promotions/updateUsage") + .buildAndExpand() + .toUri(); + + restClient.post() + .uri(url) + .headers(h -> h.setBearerAuth(jwt)) + .body(promotionUsageVms) + .retrieve(); + } +} diff --git a/order/src/main/java/com/yas/order/viewmodel/checkout/CheckoutPostVm.java b/order/src/main/java/com/yas/order/viewmodel/checkout/CheckoutPostVm.java index 499086218e..e8462413c6 100644 --- a/order/src/main/java/com/yas/order/viewmodel/checkout/CheckoutPostVm.java +++ b/order/src/main/java/com/yas/order/viewmodel/checkout/CheckoutPostVm.java @@ -1,12 +1,16 @@ package com.yas.order.viewmodel.checkout; import jakarta.validation.constraints.NotNull; + +import java.math.BigDecimal; import java.util.List; public record CheckoutPostVm( String email, String note, String couponCode, + BigDecimal totalAmount, + BigDecimal totalDiscountAmount, @NotNull List checkoutItemPostVms ) { diff --git a/order/src/main/java/com/yas/order/viewmodel/checkout/CheckoutVm.java b/order/src/main/java/com/yas/order/viewmodel/checkout/CheckoutVm.java index 02f424a5cc..815200c8b6 100644 --- a/order/src/main/java/com/yas/order/viewmodel/checkout/CheckoutVm.java +++ b/order/src/main/java/com/yas/order/viewmodel/checkout/CheckoutVm.java @@ -1,5 +1,6 @@ package com.yas.order.viewmodel.checkout; +import java.math.BigDecimal; import java.util.Set; import lombok.Builder; @@ -9,6 +10,8 @@ public record CheckoutVm( String email, String note, String couponCode, + BigDecimal totalAmount, + BigDecimal totalDiscountAmount, Set checkoutItemVms ) { } \ No newline at end of file diff --git a/order/src/main/java/com/yas/order/viewmodel/order/OrderItemGetVm.java b/order/src/main/java/com/yas/order/viewmodel/order/OrderItemGetVm.java index 66e95c8a90..e53561712e 100644 --- a/order/src/main/java/com/yas/order/viewmodel/order/OrderItemGetVm.java +++ b/order/src/main/java/com/yas/order/viewmodel/order/OrderItemGetVm.java @@ -12,7 +12,9 @@ public record OrderItemGetVm( Long productId, String productName, Integer quantity, - BigDecimal productPrice + BigDecimal productPrice, + BigDecimal discountAmount, + BigDecimal taxAmount ) { public static OrderItemGetVm fromModel(OrderItem orderItem) { return new OrderItemGetVm( @@ -20,7 +22,9 @@ public static OrderItemGetVm fromModel(OrderItem orderItem) { orderItem.getProductId(), orderItem.getProductName(), orderItem.getQuantity(), - orderItem.getProductPrice()); + orderItem.getProductPrice(), + orderItem.getDiscountAmount(), + orderItem.getTaxAmount()); } public static List fromModels(Collection orderItems) { diff --git a/order/src/main/java/com/yas/order/viewmodel/promotion/PromotionUsageVm.java b/order/src/main/java/com/yas/order/viewmodel/promotion/PromotionUsageVm.java new file mode 100644 index 0000000000..1663e1a463 --- /dev/null +++ b/order/src/main/java/com/yas/order/viewmodel/promotion/PromotionUsageVm.java @@ -0,0 +1,12 @@ +package com.yas.order.viewmodel.promotion; + +import lombok.Builder; + +@Builder +public record PromotionUsageVm( + String promotionCode, + Long productId, + String userId, + Long orderId +) { +} diff --git a/order/src/main/resources/application.properties b/order/src/main/resources/application.properties index 544d47da37..31e9b86291 100644 --- a/order/src/main/resources/application.properties +++ b/order/src/main/resources/application.properties @@ -15,6 +15,7 @@ yas.services.cart=http://api.yas.local/cart yas.services.customer=http://api.yas.local/customer yas.services.product=http://api.yas.local/product yas.services.tax=http://api.yas.local/tax +yas.services.promotion=http://api.yas.local/promotion spring.datasource.driver-class-name=org.postgresql.Driver spring.datasource.url=jdbc:postgresql://localhost:5432/order diff --git a/order/src/test/java/com/yas/order/controller/CheckoutControllerTest.java b/order/src/test/java/com/yas/order/controller/CheckoutControllerTest.java index aea2ace062..7e6e38a702 100644 --- a/order/src/test/java/com/yas/order/controller/CheckoutControllerTest.java +++ b/order/src/test/java/com/yas/order/controller/CheckoutControllerTest.java @@ -11,12 +11,19 @@ import com.fasterxml.jackson.databind.ObjectWriter; import com.yas.order.OrderApplication; import com.yas.order.service.CheckoutService; +import com.yas.order.viewmodel.checkout.CheckoutItemPostVm; +import com.yas.order.viewmodel.checkout.CheckoutItemVm; +import com.yas.order.viewmodel.checkout.CheckoutPostVm; +import com.yas.order.viewmodel.checkout.CheckoutStatusPutVm; +import com.yas.order.viewmodel.checkout.CheckoutVm; + import com.yas.order.viewmodel.checkout.*; import java.math.BigDecimal; import java.util.HashSet; import java.util.List; import java.util.Set; + import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -57,18 +64,20 @@ void testCreateCheckout_whenRequestIsValid_thenReturnCheckoutVm() throws Excepti when(checkoutService.createCheckout(any(CheckoutPostVm.class))).thenReturn(response); List items = getCheckoutItemPostVms(); - CheckoutPostVm request = new CheckoutPostVm( - "customer@example.com", - "Please deliver before noon.", - "SUMMER2024", - items + CheckoutPostVm request = new CheckoutPostVm( + "customer@example.com", + "Please deliver before noon.", + "SUMMER2024", + BigDecimal.valueOf(900), + BigDecimal.valueOf(9), + items ); mockMvc.perform(post("/storefront/checkouts") - .contentType(MediaType.APPLICATION_JSON) - .content(objectWriter.writeValueAsString(request))) - .andExpect(status().isOk()) - .andExpect(MockMvcResultMatchers.content().json(objectWriter.writeValueAsString(response))); + .contentType(MediaType.APPLICATION_JSON) + .content(objectWriter.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(MockMvcResultMatchers.content().json(objectWriter.writeValueAsString(response))); } @@ -80,10 +89,10 @@ void testUpdateCheckoutStatus_whenRequestIsValid_thenReturnLong() throws Excepti when(checkoutService.updateCheckoutStatus(any(CheckoutStatusPutVm.class))).thenReturn(response); mockMvc.perform(put("/storefront/checkouts/status") - .contentType(MediaType.APPLICATION_JSON) - .content(objectWriter.writeValueAsString(request))) - .andExpect(status().isOk()) - .andExpect(MockMvcResultMatchers.content().json(objectWriter.writeValueAsString(response))); + .contentType(MediaType.APPLICATION_JSON) + .content(objectWriter.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(MockMvcResultMatchers.content().json(objectWriter.writeValueAsString(response))); } @Test @@ -94,9 +103,9 @@ void testGetOrderWithItemsById_whenRequestIsValid_thenReturnCheckoutVm() throws when(checkoutService.getCheckoutPendingStateWithItemsById(id)).thenReturn(response); mockMvc.perform(get("/storefront/checkouts/{id}", id) - .accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(MockMvcResultMatchers.content().json(objectWriter.writeValueAsString(response))); + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(MockMvcResultMatchers.content().json(objectWriter.writeValueAsString(response))); } @Test @@ -116,29 +125,29 @@ void testUpdatePaymentMethod_whenRequestIsValid_thenReturnNoContent() throws Exc private static @NotNull List getCheckoutItemPostVms() { CheckoutItemPostVm item1 = new CheckoutItemPostVm( - 101L, - "Product One", - 3, - new BigDecimal("29.99"), - "First item note", - new BigDecimal("5.00"), - new BigDecimal("2.50"), - new BigDecimal("8.5"), - new BigDecimal("8.5"), - new BigDecimal("8.5") + 101L, + "Product One", + 3, + new BigDecimal("29.99"), + "First item note", + new BigDecimal("5.00"), + new BigDecimal("2.50"), + new BigDecimal("8.5"), + new BigDecimal("8.5"), + new BigDecimal("8.5") ); CheckoutItemPostVm item2 = new CheckoutItemPostVm( - 102L, - "Product Two", - 1, - new BigDecimal("49.99"), - "Second item note", - new BigDecimal("10.00"), - new BigDecimal("5.00"), - new BigDecimal("10.0"), - new BigDecimal("8.5"), - new BigDecimal("8.5") + 102L, + "Product Two", + 1, + new BigDecimal("49.99"), + "Second item note", + new BigDecimal("10.00"), + new BigDecimal("5.00"), + new BigDecimal("10.0"), + new BigDecimal("8.5"), + new BigDecimal("8.5") ); return List.of(item1, item2); @@ -146,41 +155,43 @@ void testUpdatePaymentMethod_whenRequestIsValid_thenReturnNoContent() throws Exc private CheckoutVm getCheckoutVm() { CheckoutItemVm item1 = CheckoutItemVm.builder() - .id(1L) - .productId(101L) - .productName("Product 1") - .quantity(2) - .productPrice(new BigDecimal("19.99")) - .note("First item note") - .discountAmount(new BigDecimal("2.00")) - .taxAmount(new BigDecimal("1.50")) - .taxPercent(new BigDecimal("5.0")) - .checkoutId("checkout123") - .build(); + .id(1L) + .productId(101L) + .productName("Product 1") + .quantity(2) + .productPrice(new BigDecimal("19.99")) + .note("First item note") + .discountAmount(new BigDecimal("2.00")) + .taxAmount(new BigDecimal("1.50")) + .taxPercent(new BigDecimal("5.0")) + .checkoutId("checkout123") + .build(); CheckoutItemVm item2 = CheckoutItemVm.builder() - .id(2L) - .productId(102L) - .productName("Product 2") - .quantity(1) - .productPrice(new BigDecimal("9.99")) - .note("Second item note") - .discountAmount(new BigDecimal("1.00")) - .taxAmount(new BigDecimal("0.75")) - .taxPercent(new BigDecimal("5.0")) - .checkoutId("checkout123") - .build(); + .id(2L) + .productId(102L) + .productName("Product 2") + .quantity(1) + .productPrice(new BigDecimal("9.99")) + .note("Second item note") + .discountAmount(new BigDecimal("1.00")) + .taxAmount(new BigDecimal("0.75")) + .taxPercent(new BigDecimal("5.0")) + .checkoutId("checkout123") + .build(); Set checkoutItemVms = new HashSet<>(); checkoutItemVms.add(item1); checkoutItemVms.add(item2); return new CheckoutVm( - "checkout123", - "user@example.com", - "Please deliver after 5 PM", - "DISCOUNT20", - checkoutItemVms + "checkout123", + "user@example.com", + "Please deliver after 5 PM", + "DISCOUNT20", + BigDecimal.valueOf(900), + BigDecimal.valueOf(9), + checkoutItemVms ); } } \ No newline at end of file diff --git a/order/src/test/java/com/yas/order/service/PromotionServiceTest.java b/order/src/test/java/com/yas/order/service/PromotionServiceTest.java new file mode 100644 index 0000000000..b02a4538f9 --- /dev/null +++ b/order/src/test/java/com/yas/order/service/PromotionServiceTest.java @@ -0,0 +1,74 @@ +package com.yas.order.service; + +import com.yas.order.config.ServiceUrlConfig; +import com.yas.order.viewmodel.promotion.PromotionUsageVm; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.web.client.RestClient; + +import java.net.URI; +import java.util.List; + +import static com.yas.order.utils.SecurityContextUtils.setUpSecurityContext; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class PromotionServiceTest { + + private RestClient restClient; + + private ServiceUrlConfig serviceUrlConfig; + + private PromotionService promotionService; + + private RestClient.ResponseSpec responseSpec; + + private static final String PROMOTION_URL = "http://api.yas.local/promotion"; + + @BeforeEach + void setUp() { + restClient = mock(RestClient.class); + serviceUrlConfig = mock(ServiceUrlConfig.class); + promotionService = new PromotionService(restClient, serviceUrlConfig); + responseSpec = Mockito.mock(RestClient.ResponseSpec.class); + setUpSecurityContext("test"); + when(serviceUrlConfig.promotion()).thenReturn(PROMOTION_URL); + } + + @Test + void testUpdateUsagePromotion_ifNormalCase_shouldNoException() { + + List promotionUsageVms = getPromotionUsageVms(); + RestClient.RequestBodyUriSpec requestBodyUriSpec = mock(RestClient.RequestBodyUriSpec.class); + + when(restClient.post()).thenReturn(requestBodyUriSpec); + when(requestBodyUriSpec.uri(any(URI.class))).thenReturn(requestBodyUriSpec); + when(requestBodyUriSpec.headers(any())).thenReturn(requestBodyUriSpec); + when(requestBodyUriSpec.body(any(Object.class))).thenReturn(requestBodyUriSpec); + when(requestBodyUriSpec.retrieve()).thenReturn(responseSpec); + + assertDoesNotThrow(() -> promotionService.updateUsagePromotion(promotionUsageVms)); + } + + private static @NotNull List getPromotionUsageVms() { + return List.of( + PromotionUsageVm.builder() + .promotionCode("123") + .userId("user123") + .orderId(1001L) + .productId(5001L) + .build(), + + PromotionUsageVm.builder() + .promotionCode("1234") + .userId("user456") + .orderId(1002L) + .productId(5002L) + .build() + ); + } +} diff --git a/promotion/src/main/java/com/yas/promotion/controller/PromotionController.java b/promotion/src/main/java/com/yas/promotion/controller/PromotionController.java index de6d2f890f..99d67a9671 100644 --- a/promotion/src/main/java/com/yas/promotion/controller/PromotionController.java +++ b/promotion/src/main/java/com/yas/promotion/controller/PromotionController.java @@ -1,12 +1,7 @@ package com.yas.promotion.controller; import com.yas.promotion.service.PromotionService; -import com.yas.promotion.viewmodel.PromotionDetailVm; -import com.yas.promotion.viewmodel.PromotionListVm; -import com.yas.promotion.viewmodel.PromotionPostVm; -import com.yas.promotion.viewmodel.PromotionPutVm; -import com.yas.promotion.viewmodel.PromotionVerifyResultDto; -import com.yas.promotion.viewmodel.PromotionVerifyVm; +import com.yas.promotion.viewmodel.*; import com.yas.promotion.viewmodel.error.ErrorVm; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -14,6 +9,8 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; import java.time.Instant; +import java.util.List; + import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -111,4 +108,11 @@ public ResponseEntity verifyPromotion( @RequestBody PromotionVerifyVm promotionVerifyInfo) { return ResponseEntity.ok(promotionService.verifyPromotion(promotionVerifyInfo)); } + + @PostMapping({"/storefront/promotions/updateUsage", "/backoffice/promotions/updateUsage"}) + public ResponseEntity updateUsagePromotion( + @RequestBody List promotionUsageVms) { + promotionService.updateUsagePromotion(promotionUsageVms); + return ResponseEntity.ok().build(); + } } diff --git a/promotion/src/main/java/com/yas/promotion/service/PromotionService.java b/promotion/src/main/java/com/yas/promotion/service/PromotionService.java index 08e7cb6197..4c51d0f542 100644 --- a/promotion/src/main/java/com/yas/promotion/service/PromotionService.java +++ b/promotion/src/main/java/com/yas/promotion/service/PromotionService.java @@ -5,10 +5,12 @@ import com.yas.commonlibrary.exception.NotFoundException; import com.yas.promotion.model.Promotion; import com.yas.promotion.model.PromotionApply; +import com.yas.promotion.model.PromotionUsage; import com.yas.promotion.model.enumeration.DiscountType; import com.yas.promotion.model.enumeration.UsageType; import com.yas.promotion.repository.PromotionRepository; import com.yas.promotion.repository.PromotionUsageRepository; +import com.yas.promotion.utils.AuthenticationUtils; import com.yas.promotion.utils.Constants; import com.yas.promotion.viewmodel.BrandVm; import com.yas.promotion.viewmodel.CategoryGetVm; @@ -17,14 +19,9 @@ import com.yas.promotion.viewmodel.PromotionListVm; import com.yas.promotion.viewmodel.PromotionPostVm; import com.yas.promotion.viewmodel.PromotionPutVm; +import com.yas.promotion.viewmodel.PromotionUsageVm; import com.yas.promotion.viewmodel.PromotionVerifyResultDto; import com.yas.promotion.viewmodel.PromotionVerifyVm; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Optional; import lombok.RequiredArgsConstructor; import org.apache.commons.collections4.CollectionUtils; import org.springframework.data.domain.Page; @@ -33,6 +30,13 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; + @Service @Transactional @RequiredArgsConstructor @@ -72,8 +76,8 @@ private void validateNewPromotion(PromotionPostVm promotionPostVm) { validateIfPromotionExistedSlug(promotionPostVm.getSlug()); validateIfCouponCodeIsExisted(promotionPostVm.getCouponCode()); validateIfPromotionEndDateIsBeforeStartDate( - promotionPostVm.getStartDate().toInstant(), - promotionPostVm.getEndDate().toInstant()); + promotionPostVm.getStartDate().toInstant(), + promotionPostVm.getEndDate().toInstant()); } public PromotionDetailVm updatePromotion(PromotionPutVm promotionPutVm) { @@ -107,23 +111,23 @@ public PromotionDetailVm updatePromotion(PromotionPutVm promotionPutVm) { } public PromotionListVm getPromotions( - int pageNo, - int pageSize, - String promotionName, - String couponCode, - Instant startDate, - Instant endDate + int pageNo, + int pageSize, + String promotionName, + String couponCode, + Instant startDate, + Instant endDate ) { Pageable pageable = PageRequest.of(pageNo, pageSize); Page promotionPage; promotionPage = promotionRepository.findPromotions( - promotionName.trim(), - couponCode.trim(), - startDate, - endDate, - pageable + promotionName.trim(), + couponCode.trim(), + startDate, + endDate, + pageable ); List promotionDetailVmList = promotionPage @@ -147,14 +151,11 @@ private PromotionDetailVm toPromotionDetail(Promotion promotion) { List productVms = null; List promotionApplies = promotion.getPromotionApplies(); switch (promotion.getApplyTo()) { - case CATEGORY -> - categoryGetVms = productService.getCategoryByIds(promotionApplies.stream() + case CATEGORY -> categoryGetVms = productService.getCategoryByIds(promotionApplies.stream() .map(PromotionApply::getCategoryId).toList()); - case BRAND -> - brandVms = productService.getBrandByIds(promotionApplies.stream() + case BRAND -> brandVms = productService.getBrandByIds(promotionApplies.stream() .map(PromotionApply::getBrandId).toList()); - case PRODUCT -> - productVms = productService.getProductByIds(promotionApplies.stream() + case PRODUCT -> productVms = productService.getProductByIds(promotionApplies.stream() .map(PromotionApply::getProductId).toList()); default -> { break; @@ -198,10 +199,10 @@ public PromotionDetailVm getPromotion(Long promotionId) { public PromotionVerifyResultDto verifyPromotion(PromotionVerifyVm promotionVerifyData) { Optional promotionOp = - promotionRepository.findByCouponCodeAndIsActiveTrue(promotionVerifyData.couponCode()); + promotionRepository.findByCouponCodeAndIsActiveTrue(promotionVerifyData.couponCode()); if (promotionOp.isEmpty()) { throw new NotFoundException(Constants.ErrorCode.PROMOTION_NOT_FOUND_ERROR_MESSAGE, - promotionVerifyData.couponCode()); + promotionVerifyData.couponCode()); } Promotion promotion = promotionOp.get(); @@ -227,44 +228,44 @@ public PromotionVerifyResultDto verifyPromotion(PromotionVerifyVm promotionVerif } List productsCanApply = products.stream() - .filter(product -> commonProductIds.contains(product.id())) + .filter(product -> commonProductIds.contains(product.id())) .sorted(Comparator.comparing(ProductVm::price)).toList(); return new PromotionVerifyResultDto(true, productsCanApply.getFirst().id(), - promotion.getCouponCode(), - promotion.getDiscountType(), - DiscountType.FIXED.equals(promotion.getDiscountType()) - ? promotion.getDiscountAmount() : promotion.getDiscountPercentage()); + promotion.getCouponCode(), + promotion.getDiscountType(), + DiscountType.FIXED.equals(promotion.getDiscountType()) + ? promotion.getDiscountAmount() : promotion.getDiscountPercentage()); } private boolean isInvalidOrderPrice(PromotionVerifyVm promotionVerifyData, Promotion promotion) { return promotionVerifyData.orderPrice() <= 0 - || promotionVerifyData.orderPrice() < promotion.getMinimumOrderPurchaseAmount(); + || promotionVerifyData.orderPrice() < promotion.getMinimumOrderPurchaseAmount(); } private boolean isExhaustedUsageQuantity(Promotion promotion) { return UsageType.LIMITED.equals(promotion.getUsageType()) - && promotion.getUsageLimit() <= promotion.getUsageCount(); + && promotion.getUsageLimit() <= promotion.getUsageCount(); } private List getProductsCanApplyPromotion(Promotion promotion) { switch (promotion.getApplyTo()) { case CATEGORY -> { List categoryIds = promotion.getPromotionApplies().stream() - .map(PromotionApply::getCategoryId) - .toList(); + .map(PromotionApply::getCategoryId) + .toList(); return productService.getProductByCategoryIds(categoryIds); } case BRAND -> { List brandIds = promotion.getPromotionApplies().stream() - .map(PromotionApply::getBrandId) - .toList(); + .map(PromotionApply::getBrandId) + .toList(); return productService.getProductByBrandIds(brandIds); } case PRODUCT -> { List productIds = promotion.getPromotionApplies().stream() - .map(PromotionApply::getProductId) - .toList(); + .map(PromotionApply::getProductId) + .toList(); return productService.getProductByIds(productIds); } default -> { @@ -272,4 +273,30 @@ private List getProductsCanApplyPromotion(Promotion promotion) { } } } + + public void updateUsagePromotion(List promotionUsageVms) { + for (PromotionUsageVm promotionUsageVm : promotionUsageVms) { + Optional promotion = + promotionRepository.findByCouponCodeAndIsActiveTrue(promotionUsageVm.promotionCode()); + + if (!promotion.isPresent()) { + throw new NotFoundException(Constants.ErrorCode.PROMOTION_NOT_FOUND_ERROR_MESSAGE, + promotionUsageVm.promotionCode()); + } + + PromotionUsage promotionUsage = PromotionUsage.builder() + .promotion(promotion.get()) + .userId(AuthenticationUtils.extractUserId()) + .productId(promotionUsageVm.productId()) + .orderId(promotionUsageVm.orderId()) + .build(); + + promotionUsageRepository.save(promotionUsage); + + Promotion existingPromotion = promotion.get(); + existingPromotion.setUsageCount(existingPromotion.getUsageCount() + 1); + promotionRepository.save(existingPromotion); + } + } + } \ No newline at end of file diff --git a/promotion/src/main/java/com/yas/promotion/viewmodel/PromotionUsageVm.java b/promotion/src/main/java/com/yas/promotion/viewmodel/PromotionUsageVm.java new file mode 100644 index 0000000000..50b4311109 --- /dev/null +++ b/promotion/src/main/java/com/yas/promotion/viewmodel/PromotionUsageVm.java @@ -0,0 +1,9 @@ +package com.yas.promotion.viewmodel; + +public record PromotionUsageVm( + String promotionCode, + Long productId, + String userId, + Long orderId +) { +} diff --git a/promotion/src/main/resources/db/changelog/ddl/changelog-0003.sql b/promotion/src/main/resources/db/changelog/ddl/changelog-0003.sql new file mode 100644 index 0000000000..2692d5457d --- /dev/null +++ b/promotion/src/main/resources/db/changelog/ddl/changelog-0003.sql @@ -0,0 +1,4 @@ +--liquibase formatted sql +--changeset loannguyent5:issue-1174 +ALTER TABLE promotion_usage ALTER COLUMN created_on SET DEFAULT CURRENT_TIMESTAMP; +ALTER TABLE promotion_usage ALTER COLUMN created_by SET DEFAULT 'system'; \ No newline at end of file diff --git a/storefront/modules/order/components/CheckOutDetail.tsx b/storefront/modules/order/components/CheckOutDetail.tsx index af608ca67c..e1eae4066e 100644 --- a/storefront/modules/order/components/CheckOutDetail.tsx +++ b/storefront/modules/order/components/CheckOutDetail.tsx @@ -3,7 +3,6 @@ import { formatPrice } from '@/utils/formatPrice'; import { useEffect, useState } from 'react'; import { getEnabledPaymentProviders } from '../../payment/services/PaymentProviderService'; import { OrderItem } from '../models/OrderItem'; - type Props = { orderItems: OrderItem[]; disablePaymentProcess: boolean; @@ -12,6 +11,8 @@ type Props = { const CheckOutDetail = ({ orderItems, disablePaymentProcess, setPaymentMethod }: Props) => { const [totalPrice, setTotalPrice] = useState(0); + const [discountAmount, setDiscountAmount] = useState(0); + const [taxAmount, setTaxAmount] = useState(0); const [disableCheckout, setDisableCheckout] = useState(true); const [paymentProviders, setPaymentProviders] = useState([]); const [selectedPayment, setSelectedPayment] = useState(null); @@ -46,10 +47,21 @@ const CheckOutDetail = ({ orderItems, disablePaymentProcess, setPaymentMethod }: .map((item) => calculateProductPrice(item)) .reduce((accumulator, currentValue) => accumulator + currentValue, 0); setTotalPrice(totalPrice); + const totalDiscountAmount = orderItems + .map((item) => item.discountAmount ?? 0) + .reduce((accumulator, discount) => accumulator + discount, 0); + setDiscountAmount(totalDiscountAmount); + + const totalTaxAmount = orderItems + .map((item) => item.taxAmount ?? 0) + .reduce((accumulator, discount) => accumulator + discount, 0); + setTaxAmount(totalTaxAmount); }, [orderItems]); const calculateProductPrice = (item: OrderItem) => { - return item.productPrice * item.quantity; + console.log('item'); + console.log(item); + return item.productPrice * item.quantity - (item.discountAmount ?? 0); }; const handleAgreeTerms = (e: any) => { @@ -79,10 +91,10 @@ const CheckOutDetail = ({ orderItems, disablePaymentProcess, setPaymentMethod }: ))}
- Delivery Fee $750.99 + Discount {formatPrice(discountAmount)}
- Tax $750.99 + Tax {formatPrice(taxAmount)}
Total {formatPrice(totalPrice)} diff --git a/storefront/modules/order/models/Checkout.ts b/storefront/modules/order/models/Checkout.ts index dcb82de348..12f1e1497d 100644 --- a/storefront/modules/order/models/Checkout.ts +++ b/storefront/modules/order/models/Checkout.ts @@ -5,5 +5,7 @@ export type Checkout = { email: string; note?: string; couponCode?: string; + totalAmount: number; + totalDiscountAmount: number; checkoutItemPostVms: CheckoutItem[]; }; diff --git a/storefront/modules/order/models/OrderItemGetVm.ts b/storefront/modules/order/models/OrderItemGetVm.ts index 989e955d5c..7b71acbcf0 100644 --- a/storefront/modules/order/models/OrderItemGetVm.ts +++ b/storefront/modules/order/models/OrderItemGetVm.ts @@ -5,4 +5,5 @@ export type OrderItemGetVm = { quantity: number; productPrice: number; mediaUrl: string; + discountAmount: number; }; diff --git a/storefront/pages/cart/index.tsx b/storefront/pages/cart/index.tsx index 2b0145b83c..53479d2612 100644 --- a/storefront/pages/cart/index.tsx +++ b/storefront/pages/cart/index.tsx @@ -202,10 +202,11 @@ const Cart = () => { let checkout: Checkout = { email: email, note: '', - couponCode: '', + couponCode: couponCode, + totalAmount: totalPrice, + totalDiscountAmount: discountMoney, checkoutItemPostVms: checkoutItems, }; - createCheckout(checkout) .then((res) => { router.push(`/checkout/${res?.id}`); //NOSONAR @@ -221,6 +222,7 @@ const Cart = () => { productName: item.productName, quantity: item.quantity, productPrice: item.price, + discountAmount: discountMoney, }; }; diff --git a/storefront/pages/checkout/[id].tsx b/storefront/pages/checkout/[id].tsx index 70f053cb79..27980ec441 100644 --- a/storefront/pages/checkout/[id].tsx +++ b/storefront/pages/checkout/[id].tsx @@ -99,8 +99,6 @@ const Checkout = () => { const fetchCheckout = async () => { await getCheckoutById(id as string) .then((res) => { - console.log(res); - setCheckout(res); const newItems: OrderItem[] = []; res.checkoutItemVms.forEach((result: CheckoutItem) => { @@ -109,6 +107,8 @@ const Checkout = () => { quantity: result.quantity, productName: result.productName, productPrice: result.productPrice!, + discountAmount: result.discountAmount, + taxAmount: result.taxAmount, }); }); setOrderItems(newItems); @@ -189,14 +189,14 @@ const Checkout = () => { order.email = checkout?.email!; order.note = data.note; order.tax = 0; - order.discount = 0; + order.discount = checkout?.totalDiscountAmount; order.numberItem = orderItems.reduce((result, item) => result + item.quantity, 0); order.totalPrice = orderItems.reduce( - (result, item) => result + item.quantity * item.productPrice, + (result, item) => result + item.quantity * item.productPrice - (item.discountAmount ?? 0), 0 ); order.deliveryFee = 0; - order.couponCode = ''; + order.couponCode = checkout?.couponCode; order.deliveryMethod = 'YAS_EXPRESS'; order.paymentStatus = 'PENDING'; order.orderItemPostVms = orderItems;