From d81a783de0a466343805a6f1df5f1745947e0012 Mon Sep 17 00:00:00 2001 From: nguyenvanhadncntt Date: Mon, 16 Sep 2024 10:55:09 +0700 Subject: [PATCH] #1009 create list promotion page --- backoffice/asset/data/sidebar.tsx | 25 ++- backoffice/common/components/Layout.tsx | 34 +++- backoffice/constants/Common.ts | 1 + backoffice/modules/promotion/models/Brand.ts | 6 + .../modules/promotion/models/Category.ts | 6 + .../modules/promotion/models/Product.ts | 11 ++ .../modules/promotion/models/Promotion.ts | 39 +++++ .../promotion/services/PromotionService.ts | 17 ++ .../promotion/manager-promotion/index.tsx | 149 ++++++++++++++++++ .../product/controller/BrandController.java | 14 +- .../controller/CategoryController.java | 18 ++- .../product/controller/ProductController.java | 5 + .../product/repository/BrandRepository.java | 3 + .../repository/CategoryRepository.java | 3 + .../com/yas/product/service/BrandService.java | 4 + .../yas/product/service/CategoryService.java | 14 +- .../yas/product/service/ProductService.java | 4 + .../controller/BrandControllerTest.java | 2 +- .../controller/CategoryControllerTest.java | 2 +- .../product/service/CategoryServiceTest.java | 15 +- .../promotion/config/RestClientConfig.java | 18 +++ .../controller/PromotionController.java | 11 +- .../com/yas/promotion/model/Promotion.java | 6 +- .../repository/PromotionRepository.java | 10 +- .../AbstractCircuitBreakFallbackHandler.java | 23 +++ .../yas/promotion/service/ProductService.java | 92 +++++++++++ .../promotion/service/PromotionService.java | 33 +++- .../promotion/utils/AuthenticationUtils.java | 31 ++++ .../com/yas/promotion/utils/Constants.java | 1 + .../com/yas/promotion/viewmodel/BrandVm.java | 4 + .../promotion/viewmodel/CategoryGetVm.java | 7 + .../yas/promotion/viewmodel/ProductVm.java | 15 ++ .../viewmodel/PromotionDetailVm.java | 35 +++- .../promotion/viewmodel/PromotionPostVm.java | 6 +- .../promotion/viewmodel/PromotionPutVm.java | 5 +- .../yas/promotion/viewmodel/PromotionVm.java | 5 +- .../resources/messages/messages.properties | 1 + .../controller/PromotionControllerTest.java | 36 ++--- .../service/PromotionServiceTest.java | 50 ++++-- 39 files changed, 667 insertions(+), 94 deletions(-) create mode 100644 backoffice/modules/promotion/models/Brand.ts create mode 100644 backoffice/modules/promotion/models/Category.ts create mode 100644 backoffice/modules/promotion/models/Product.ts create mode 100644 backoffice/modules/promotion/models/Promotion.ts create mode 100644 backoffice/modules/promotion/services/PromotionService.ts create mode 100644 backoffice/pages/promotion/manager-promotion/index.tsx create mode 100644 promotion/src/main/java/com/yas/promotion/config/RestClientConfig.java create mode 100644 promotion/src/main/java/com/yas/promotion/service/AbstractCircuitBreakFallbackHandler.java create mode 100644 promotion/src/main/java/com/yas/promotion/service/ProductService.java create mode 100644 promotion/src/main/java/com/yas/promotion/utils/AuthenticationUtils.java create mode 100644 promotion/src/main/java/com/yas/promotion/viewmodel/BrandVm.java create mode 100644 promotion/src/main/java/com/yas/promotion/viewmodel/CategoryGetVm.java create mode 100644 promotion/src/main/java/com/yas/promotion/viewmodel/ProductVm.java diff --git a/backoffice/asset/data/sidebar.tsx b/backoffice/asset/data/sidebar.tsx index 2afd437db1..207ff25868 100644 --- a/backoffice/asset/data/sidebar.tsx +++ b/backoffice/asset/data/sidebar.tsx @@ -2,18 +2,19 @@ import { COUNTRY_URL, INVENTORY_WAREHOUSE_PRODUCTS_URL, INVENTORY_WAREHOUSE_STOCKS_URL, - STATE_OR_PROVINCE_URL, - TAX_CLASS_URL, - TAX_RATE_URL, - WAREHOUSE_URL, + MANAGER_PROMOTIONS_URL, + SALES_GIFT_CARDS_URL, SALES_ORDERS_URL, - SALES_SHIPMENTS_URL, - SALES_RETURN_REQUESTS_URL, SALES_RECURRING_PAYMENTS_URL, - SALES_GIFT_CARDS_URL, + SALES_RETURN_REQUESTS_URL, + SALES_SHIPMENTS_URL, SALES_SHOPPING_CARTS_AND_WISHLISTS_URL, + STATE_OR_PROVINCE_URL, SYSTEM_PAYMENT_PROVIDERS, SYSTEM_SETTINGS, + TAX_CLASS_URL, + TAX_RATE_URL, + WAREHOUSE_URL, WEBHOOKS_URL, } from '@constants/Common'; @@ -139,7 +140,7 @@ export const menu_sale_item_data = [ link: SALES_GIFT_CARDS_URL, }, { - id: 5, + id: 6, name: 'Shopping carts and wishlists', link: SALES_SHOPPING_CARTS_AND_WISHLISTS_URL, }, @@ -162,3 +163,11 @@ export const menu_system_item_data = [ link: WEBHOOKS_URL, }, ]; + +export const menu_promotion_item_data = [ + { + id: 1, + name: 'Manager Promotions', + link: MANAGER_PROMOTIONS_URL, + }, +]; diff --git a/backoffice/common/components/Layout.tsx b/backoffice/common/components/Layout.tsx index d49ff6ffdf..eea108ef6e 100644 --- a/backoffice/common/components/Layout.tsx +++ b/backoffice/common/components/Layout.tsx @@ -1,19 +1,20 @@ import Head from 'next/head'; +import Link from 'next/link'; +import { MouseEventHandler, useState } from 'react'; import { Navbar } from 'react-bootstrap'; import { ToastContainer } from 'react-toastify'; -import styles from '../../styles/Layout.module.css'; -import AuthenticationInfo from './AuthenticationInfo'; -import { MouseEventHandler, useState } from 'react'; -import Link from 'next/link'; import { menu_catalog_item_data, menu_customer_item_data, menu_inventory_item_data, menu_location_item_data, - menu_tax_item_data, + menu_promotion_item_data, menu_sale_item_data, menu_system_item_data, + menu_tax_item_data, } from '../../asset/data/sidebar'; +import styles from '../../styles/Layout.module.css'; +import AuthenticationInfo from './AuthenticationInfo'; interface DataProps { id: number; @@ -175,7 +176,7 @@ const Sidebar = (menu: MenuProps) => { data-target="#salesSubmenu" data-bs-toggle="collapse" aria-controls="salesSubmenu" - aria-expanded="false" + aria-expanded="true" className="dropdown-toggle" > Sales @@ -235,6 +236,27 @@ const Sidebar = (menu: MenuProps) => { +
  • + changeMenu('promotion')} + > + Promotion + + +
  • +
  • changeMenu('tax')}> { + const [isLoading, setIsLoading] = useState(false); + const [promotionPage, setPromotionPage] = useState(); + const [couponCode, setCouponCode] = useState(''); + const [promotionName, setPromotionName] = useState(''); + const [pageNo, setPageNo] = useState(DEFAULT_PAGE_NUMBER); + const [totalPage, setTotalPage] = useState(1); + + useEffect(() => { + setIsLoading(true); + getPromotionList(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pageNo]); + + const getPromotionList = () => { + getPromotions(createRequestParams()).then((data) => { + setIsLoading(false); + setTotalPage(data.totalPage); + setPromotionPage(data); + }); + }; + + const createRequestParams = (): PromotionListRequest => { + return { + couponCode: couponCode, + pageNo: pageNo, + pageSize: DEFAULT_PAGE_SIZE, + promotionName: promotionName, + }; + }; + + const changePage = ({ selected }: any) => { + setPageNo(selected); + }; + + const convertToStringDate = (date: Date) => { + if (typeof date === 'string') { + date = new Date(date); + } + const month = date.getMonth() + 1; + return `${date.getFullYear()}-${month > 9 ? month : '0' + month}-${date.getDate()}`; + }; + + return ( + <> +
    +
    +

    Promotions

    +
    +
    + + + +
    +
    +
    +
    + setCouponCode(e.target.value)} + /> +
    +
    + setPromotionName(e.target.value)} + /> +
    +
    + +
    +
    + {!isLoading && ( + + + + + + + + + + + + + + + + + + + {promotionPage?.promotionDetailVmList?.map((promotion) => ( + + + + + + + + + + + + + + + ))} + +
    NameCoupon codeSlugDescriptionIs activeDiscount TypeUsage countUsage limitApply ToStart dateEnd dateAction
    {promotion.name}{promotion.couponCode}{promotion.slug}{promotion.description}{promotion.isActive}{promotion.discountType}{promotion.usageCount}{promotion.usageLimit}{promotion.applyTo}{convertToStringDate(promotion.startDate)}{convertToStringDate(promotion.endDate)} + + + +
    + )} + {totalPage > 1 && ( + + )} + + ); +}; +export default PromotionList; diff --git a/product/src/main/java/com/yas/product/controller/BrandController.java b/product/src/main/java/com/yas/product/controller/BrandController.java index dd0194a9aa..79580f3fb5 100644 --- a/product/src/main/java/com/yas/product/controller/BrandController.java +++ b/product/src/main/java/com/yas/product/controller/BrandController.java @@ -42,9 +42,9 @@ public BrandController(BrandRepository brandRepository, BrandService brandServic } @GetMapping({"/backoffice/brands", "/storefront/brands"}) - public ResponseEntity> listBrands() { + public ResponseEntity> listBrands(@RequestParam(required = false) String brandName) { log.info("[Test logging with trace] Got a request"); - List brandVms = brandRepository.findAll().stream() + List brandVms = brandRepository.findByNameContainingIgnoreCase(brandName).stream() .map(BrandVm::fromModel) .toList(); return ResponseEntity.ok(brandVms); @@ -118,4 +118,14 @@ public ResponseEntity deleteBrand(@PathVariable long id) { brandRepository.deleteById(id); return ResponseEntity.noContent().build(); } + + @GetMapping("/backoffice/brands/by-ids") + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "No content", content = @Content()), + @ApiResponse(responseCode = "404", description = "Not found", + content = @Content(schema = @Schema(implementation = ErrorVm.class)))}) + public ResponseEntity> getBrandsByIds(@RequestParam List ids) { + return ResponseEntity.ok(brandService.getBrandsByIds(ids)); + } + } diff --git a/product/src/main/java/com/yas/product/controller/CategoryController.java b/product/src/main/java/com/yas/product/controller/CategoryController.java index b27fe3c12c..52315511ae 100644 --- a/product/src/main/java/com/yas/product/controller/CategoryController.java +++ b/product/src/main/java/com/yas/product/controller/CategoryController.java @@ -23,6 +23,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.UriComponentsBuilder; @@ -37,8 +38,8 @@ public CategoryController(CategoryRepository categoryRepository, CategoryService } @GetMapping({"/backoffice/categories", "/storefront/categories"}) - public ResponseEntity> listCategories() { - return ResponseEntity.ok(categoryService.getCategories()); + public ResponseEntity> listCategories(@RequestParam(required = false) String categoryName) { + return ResponseEntity.ok(categoryService.getCategories(categoryName)); } @GetMapping("/backoffice/categories/{id}") @@ -106,4 +107,15 @@ public ResponseEntity deleteCategory(@PathVariable Long id) { categoryRepository.deleteById(id); return ResponseEntity.noContent().build(); } -} \ No newline at end of file + + @GetMapping("/backoffice/categories/by-ids") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Ok", + content = @Content(schema = @Schema(implementation = CategoryGetDetailVm.class))), + @ApiResponse(responseCode = "404", description = "Not found", + content = @Content(schema = @Schema(implementation = ErrorVm.class))) + }) + public ResponseEntity> getCategoriesByIds(@RequestParam List ids) { + return ResponseEntity.ok(categoryService.getCategoryByIds(ids)); + } +} diff --git a/product/src/main/java/com/yas/product/controller/ProductController.java b/product/src/main/java/com/yas/product/controller/ProductController.java index 220854162f..3692cca246 100644 --- a/product/src/main/java/com/yas/product/controller/ProductController.java +++ b/product/src/main/java/com/yas/product/controller/ProductController.java @@ -248,4 +248,9 @@ public ResponseEntity subtractProductQuantity( productService.subtractStockQuantity(productQuantityPutVm); return ResponseEntity.noContent().build(); } + + @GetMapping("/backoffice/products/by-ids") + public ResponseEntity> getProductByIds(@RequestParam List ids) { + return ResponseEntity.ok(productService.getProductByIds(ids)); + } } diff --git a/product/src/main/java/com/yas/product/repository/BrandRepository.java b/product/src/main/java/com/yas/product/repository/BrandRepository.java index 9dc1862753..1952dc596b 100644 --- a/product/src/main/java/com/yas/product/repository/BrandRepository.java +++ b/product/src/main/java/com/yas/product/repository/BrandRepository.java @@ -1,6 +1,7 @@ package com.yas.product.repository; import com.yas.product.model.Brand; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -12,4 +13,6 @@ public interface BrandRepository extends JpaRepository { @Query("select e from Brand e where e.name = ?1 and (?2 is null or e.id != ?2)") Brand findExistedName(String name, Long id); + + List findByNameContainingIgnoreCase(String name); } diff --git a/product/src/main/java/com/yas/product/repository/CategoryRepository.java b/product/src/main/java/com/yas/product/repository/CategoryRepository.java index 70b2ef1322..212a7732cd 100644 --- a/product/src/main/java/com/yas/product/repository/CategoryRepository.java +++ b/product/src/main/java/com/yas/product/repository/CategoryRepository.java @@ -1,6 +1,7 @@ package com.yas.product.repository; import com.yas.product.model.Category; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -12,4 +13,6 @@ public interface CategoryRepository extends JpaRepository { @Query("select e from Category e where e.name = ?1 and (?2 is null or e.id != ?2)") Category findExistedName(String name, Long id); + + List findByNameContainingIgnoreCase(String name); } diff --git a/product/src/main/java/com/yas/product/service/BrandService.java b/product/src/main/java/com/yas/product/service/BrandService.java index bfdfa1f16e..bfa77f8918 100644 --- a/product/src/main/java/com/yas/product/service/BrandService.java +++ b/product/src/main/java/com/yas/product/service/BrandService.java @@ -72,4 +72,8 @@ private void validateExistedName(String name, Long id) { private boolean checkExistedName(String name, Long id) { return brandRepository.findExistedName(name, id) != null; } + + public List getBrandsByIds(List ids) { + return brandRepository.findAllById(ids).stream().map(BrandVm::fromModel).toList(); + } } diff --git a/product/src/main/java/com/yas/product/service/CategoryService.java b/product/src/main/java/com/yas/product/service/CategoryService.java index c13e339c1c..3f579a7efe 100644 --- a/product/src/main/java/com/yas/product/service/CategoryService.java +++ b/product/src/main/java/com/yas/product/service/CategoryService.java @@ -124,15 +124,13 @@ public CategoryGetDetailVm getCategoryById(Long id) { ); } - public List getCategories() { - List category = categoryRepository.findAll(); + public List getCategories(String categoryName) { + List category = categoryRepository.findByNameContainingIgnoreCase(categoryName); List categoryGetVms = new ArrayList<>(); - category.stream().forEach(cate -> { + category.forEach(cate -> { ImageVm categoryImage = null; if (cate.getImageId() != null) { - if (cate.getImageId() != null) { - categoryImage = new ImageVm(cate.getImageId(), mediaService.getMedia(cate.getImageId()).url()); - } + categoryImage = new ImageVm(cate.getImageId(), mediaService.getMedia(cate.getImageId()).url()); } Category parent = cate.getParent(); long parentId = parent == null ? -1 : parent.getId(); @@ -168,4 +166,8 @@ private boolean checkParent(Long id, Category category) { return true; } } + + public List getCategoryByIds(List ids) { + return categoryRepository.findAllById(ids).stream().map(CategoryGetVm::fromModel).toList(); + } } diff --git a/product/src/main/java/com/yas/product/service/ProductService.java b/product/src/main/java/com/yas/product/service/ProductService.java index 3ab28ec93e..69cf8d2ffe 100644 --- a/product/src/main/java/com/yas/product/service/ProductService.java +++ b/product/src/main/java/com/yas/product/service/ProductService.java @@ -1066,6 +1066,10 @@ public void subtractStockQuantity(List productQuantityItem .forEach(it -> partitionUpdateStockQuantityByCalculation(it, this.subtractStockQuantity())); } + public List getProductByIds(List productIds) { + return this.productRepository.findAllByIdIn(productIds).stream().map(ProductListVm::fromModel).toList(); + } + private BiFunction subtractStockQuantity() { return (totalQuantity, amount) -> { long result = totalQuantity - amount; diff --git a/product/src/test/java/com/yas/product/controller/BrandControllerTest.java b/product/src/test/java/com/yas/product/controller/BrandControllerTest.java index 90bf87318a..51295de4f3 100644 --- a/product/src/test/java/com/yas/product/controller/BrandControllerTest.java +++ b/product/src/test/java/com/yas/product/controller/BrandControllerTest.java @@ -51,7 +51,7 @@ class BrandControllerTest { @Test void testListBrands() throws Exception { - when(brandRepository.findAll()).thenReturn(Arrays.asList( + when(brandRepository.findByNameContainingIgnoreCase(any())).thenReturn(Arrays.asList( createBrand(1L, "Brand 1"), createBrand(2L, "Brand 2") )); diff --git a/product/src/test/java/com/yas/product/controller/CategoryControllerTest.java b/product/src/test/java/com/yas/product/controller/CategoryControllerTest.java index 4181701b23..41ebf080b6 100644 --- a/product/src/test/java/com/yas/product/controller/CategoryControllerTest.java +++ b/product/src/test/java/com/yas/product/controller/CategoryControllerTest.java @@ -58,7 +58,7 @@ void testListCategories() throws Exception { CategoryGetVm category2 = new CategoryGetVm(3L, "Category 2", "category-2", 1L, new ImageVm(3L, "")); - when(categoryService.getCategories()).thenReturn(Arrays.asList(category1, category2)); + when(categoryService.getCategories(any())).thenReturn(Arrays.asList(category1, category2)); mockMvc.perform(get("/backoffice/categories")) .andExpect(status().isOk()) diff --git a/product/src/test/java/com/yas/product/service/CategoryServiceTest.java b/product/src/test/java/com/yas/product/service/CategoryServiceTest.java index 634b784b06..0fcbf77b6d 100644 --- a/product/src/test/java/com/yas/product/service/CategoryServiceTest.java +++ b/product/src/test/java/com/yas/product/service/CategoryServiceTest.java @@ -1,5 +1,10 @@ package com.yas.product.service; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + import com.yas.product.ProductApplication; import com.yas.product.model.Category; import com.yas.product.repository.CategoryRepository; @@ -15,10 +20,6 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.Mockito.when; - @SpringBootTest(classes = ProductApplication.class) class CategoryServiceTest { @Autowired @@ -65,9 +66,9 @@ void getCategoryById_Success() { @Test void getCategories_Success() { - when(mediaService.getMedia(category.getImageId())).thenReturn(noFileMediaVm); - Assertions.assertEquals(1, categoryService.getCategories().size()); - CategoryGetVm categoryGetVm = categoryService.getCategories().get(0); + when(mediaService.getMedia(any())).thenReturn(noFileMediaVm); + Assertions.assertEquals(1, categoryService.getCategories("name").size()); + CategoryGetVm categoryGetVm = categoryService.getCategories("name").getFirst(); assertEquals("name", categoryGetVm.name()); } } diff --git a/promotion/src/main/java/com/yas/promotion/config/RestClientConfig.java b/promotion/src/main/java/com/yas/promotion/config/RestClientConfig.java new file mode 100644 index 0000000000..8043aa3adc --- /dev/null +++ b/promotion/src/main/java/com/yas/promotion/config/RestClientConfig.java @@ -0,0 +1,18 @@ +package com.yas.promotion.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.web.client.RestClient; + +@Configuration +public class RestClientConfig { + + @Bean + public RestClient getRestClient(RestClient.Builder restClientBuilder) { + return restClientBuilder + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .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 887a142036..8004540364 100644 --- a/promotion/src/main/java/com/yas/promotion/controller/PromotionController.java +++ b/promotion/src/main/java/com/yas/promotion/controller/PromotionController.java @@ -11,7 +11,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; -import java.time.ZonedDateTime; +import java.time.Instant; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -37,11 +37,12 @@ public ResponseEntity listPromotions( @RequestParam(defaultValue = "") String couponCode, @RequestParam( defaultValue = - "#{T(java.time.ZonedDateTime).of(1970, 1, 1, 0, 0, 0, 0, T(java.time.ZoneId).systemDefault())}" + "#{T(java.time.Instant).ofEpochSecond(0)}" + ) - ZonedDateTime startDate, - @RequestParam(defaultValue = "#{T(java.time.ZonedDateTime).now(T(java.time.ZoneId).systemDefault())}") - ZonedDateTime endDate + Instant startDate, + @RequestParam(defaultValue = "#{T(java.time.Instant).now()}") + Instant endDate ) { return ResponseEntity.ok(promotionService.getPromotions( pageNo, pageSize, promotionName, couponCode, startDate, endDate)); diff --git a/promotion/src/main/java/com/yas/promotion/model/Promotion.java b/promotion/src/main/java/com/yas/promotion/model/Promotion.java index d81cfbe562..d97186dae3 100644 --- a/promotion/src/main/java/com/yas/promotion/model/Promotion.java +++ b/promotion/src/main/java/com/yas/promotion/model/Promotion.java @@ -13,7 +13,7 @@ import jakarta.persistence.Id; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; -import java.time.ZonedDateTime; +import java.time.Instant; import java.util.ArrayList; import java.util.List; import lombok.AccessLevel; @@ -65,9 +65,9 @@ public class Promotion extends AbstractAuditEntity { private Boolean isActive; - private ZonedDateTime startDate; + private Instant startDate; - private ZonedDateTime endDate; + private Instant endDate; @OneToMany(mappedBy = "promotion", cascade = CascadeType.ALL, orphanRemoval = true) List promotionApplies; diff --git a/promotion/src/main/java/com/yas/promotion/repository/PromotionRepository.java b/promotion/src/main/java/com/yas/promotion/repository/PromotionRepository.java index 6ddbbe92d0..0be1b29abe 100644 --- a/promotion/src/main/java/com/yas/promotion/repository/PromotionRepository.java +++ b/promotion/src/main/java/com/yas/promotion/repository/PromotionRepository.java @@ -1,7 +1,7 @@ package com.yas.promotion.repository; import com.yas.promotion.model.Promotion; -import java.time.ZonedDateTime; +import java.time.Instant; import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -17,12 +17,12 @@ public interface PromotionRepository extends JpaRepository { @Query("SELECT p FROM Promotion p " + "WHERE LOWER(p.name) LIKE LOWER(CONCAT('%',:name,'%')) " + "AND LOWER(p.couponCode) LIKE LOWER(CONCAT('%',:couponCode,'%')) " - + "AND p.startDate >= :startDate " - + "AND p.endDate <= :endDate") + + "AND p.startDate BETWEEN :startDate AND :endDate " + + "AND p.endDate BETWEEN :startDate AND :endDate") Page findPromotions(@Param("name") String name, @Param("couponCode") String couponCode, - @Param("startDate") ZonedDateTime startDate, - @Param("endDate") ZonedDateTime endDate, + @Param("startDate") Instant startDate, + @Param("endDate") Instant endDate, Pageable pageable); } diff --git a/promotion/src/main/java/com/yas/promotion/service/AbstractCircuitBreakFallbackHandler.java b/promotion/src/main/java/com/yas/promotion/service/AbstractCircuitBreakFallbackHandler.java new file mode 100644 index 0000000000..f92433a849 --- /dev/null +++ b/promotion/src/main/java/com/yas/promotion/service/AbstractCircuitBreakFallbackHandler.java @@ -0,0 +1,23 @@ +package com.yas.promotion.service; + +import java.util.List; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +abstract class AbstractCircuitBreakFallbackHandler { + + protected void handleBodilessFallback(List ids, Throwable throwable) throws Throwable { + handleError(ids, throwable); + } + + protected Object handleFallback(List ids, Throwable throwable) throws Throwable { + handleError(ids, throwable); + return null; + } + + + private void handleError(List ids, Throwable throwable) throws Throwable { + log.error("Circuit breaker records an error. Detail {} with ids {}", throwable.getMessage(), ids); + throw throwable; + } +} diff --git a/promotion/src/main/java/com/yas/promotion/service/ProductService.java b/promotion/src/main/java/com/yas/promotion/service/ProductService.java new file mode 100644 index 0000000000..098bfa0b91 --- /dev/null +++ b/promotion/src/main/java/com/yas/promotion/service/ProductService.java @@ -0,0 +1,92 @@ +package com.yas.promotion.service; + +import com.yas.promotion.config.ServiceUrlConfig; +import com.yas.promotion.utils.AuthenticationUtils; +import com.yas.promotion.viewmodel.BrandVm; +import com.yas.promotion.viewmodel.CategoryGetVm; +import com.yas.promotion.viewmodel.ProductVm; +import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; +import io.github.resilience4j.retry.annotation.Retry; +import java.net.URI; +import java.util.List; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClient; +import org.springframework.web.util.UriComponentsBuilder; + +@Service +@Transactional +@RequiredArgsConstructor +public class ProductService extends AbstractCircuitBreakFallbackHandler { + private final RestClient restClient; + private final ServiceUrlConfig serviceUrlConfig; + + @Retry(name = "restApi") + @CircuitBreaker(name = "restCircuitBreaker", fallbackMethod = "handleFallback") + public List getProductByIds(List ids) { + String jwt = AuthenticationUtils.extractJwt(); + final URI url = UriComponentsBuilder + .fromHttpUrl(serviceUrlConfig.product()) + .path("/backoffice/products/by-ids") + .queryParams(createIdParams(ids)) + .build() + .toUri(); + + return restClient.get() + .uri(url) + .headers(h -> h.setBearerAuth(jwt)) + .retrieve() + .toEntity(new ParameterizedTypeReference>() { + }) + .getBody(); + } + + @Retry(name = "restApi") + @CircuitBreaker(name = "restCircuitBreaker", fallbackMethod = "handleFallback") + public List getCategoryByIds(List ids) { + String jwt = AuthenticationUtils.extractJwt(); + final URI url = UriComponentsBuilder + .fromHttpUrl(serviceUrlConfig.product()) + .path("/backoffice/categories/by-ids") + .queryParams(createIdParams(ids)) + .build() + .toUri(); + return restClient.get() + .uri(url) + .headers(h -> h.setBearerAuth(jwt)) + .retrieve() + .toEntity(new ParameterizedTypeReference>() { + }) + .getBody(); + } + + @Retry(name = "restApi") + @CircuitBreaker(name = "restCircuitBreaker", fallbackMethod = "handleFallback") + public List getBrandByIds(List ids) { + String jwt = AuthenticationUtils.extractJwt(); + final URI url = UriComponentsBuilder + .fromHttpUrl(serviceUrlConfig.product()) + .path("/backoffice/brands/by-ids") + .queryParams(createIdParams(ids)) + .build() + .toUri(); + return restClient.get() + .uri(url) + .headers(h -> h.setBearerAuth(jwt)) + .retrieve() + .toEntity(new ParameterizedTypeReference>() { + }) + .getBody(); + } + + private static MultiValueMap createIdParams(List ids) { + MultiValueMap params = new LinkedMultiValueMap<>(); + ids.stream().map(Objects::toString).forEach(id -> params.add("ids", id)); + return params; + } +} 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 c69088aca2..5370b47638 100644 --- a/promotion/src/main/java/com/yas/promotion/service/PromotionService.java +++ b/promotion/src/main/java/com/yas/promotion/service/PromotionService.java @@ -8,14 +8,16 @@ import com.yas.promotion.repository.PromotionRepository; import com.yas.promotion.repository.PromotionUsageRepository; import com.yas.promotion.utils.Constants; +import com.yas.promotion.viewmodel.BrandVm; +import com.yas.promotion.viewmodel.CategoryGetVm; +import com.yas.promotion.viewmodel.ProductVm; 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 java.time.ZonedDateTime; +import java.time.Instant; import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -29,6 +31,7 @@ public class PromotionService { private final PromotionRepository promotionRepository; private final PromotionUsageRepository promotionUsageRepository; + private final ProductService productService; public PromotionDetailVm createPromotion(PromotionPostVm promotionPostVm) { validateIfPromotionExistedSlug(promotionPostVm.getSlug()); @@ -93,8 +96,8 @@ public PromotionListVm getPromotions( int pageSize, String promotionName, String couponCode, - ZonedDateTime startDate, - ZonedDateTime endDate + Instant startDate, + Instant endDate ) { Pageable pageable = PageRequest.of(pageNo, pageSize); @@ -111,8 +114,8 @@ public PromotionListVm getPromotions( List promotionDetailVmList = promotionPage .getContent() .stream() - .map(PromotionDetailVm::fromModel) - .collect(Collectors.toList()); + .map(this::toPromotionDetail) + .toList(); return PromotionListVm.builder() .promotionDetailVmList(promotionDetailVmList) @@ -123,13 +126,29 @@ public PromotionListVm getPromotions( .build(); } + private PromotionDetailVm toPromotionDetail(Promotion promotion) { + List brandVms = null; + List categoryGetVms = null; + List productVms = null; + List promotionApplies = promotion.getPromotionApplies(); + switch (promotion.getApplyTo()) { + case CATEGORY -> + categoryGetVms = productService.getCategoryByIds(promotionApplies.stream().map(PromotionApply::getCategoryId).toList()); + case BRAND -> + brandVms = productService.getBrandByIds(promotionApplies.stream().map(PromotionApply::getBrandId).toList()); + case PRODUCT -> + productVms = productService.getProductByIds(promotionApplies.stream().map(PromotionApply::getProductId).toList()); + } + return PromotionDetailVm.fromModel(promotion, brandVms, categoryGetVms, productVms); + } + private void validateIfPromotionExistedSlug(String slug) { if (promotionRepository.findBySlugAndIsActiveTrue(slug).isPresent()) { throw new DuplicatedException(String.format(Constants.ErrorCode.SLUG_ALREADY_EXITED, slug)); } } - private void validateIfPromotionEndDateIsBeforeStartDate(ZonedDateTime startDate, ZonedDateTime endDate) { + private void validateIfPromotionEndDateIsBeforeStartDate(Instant startDate, Instant endDate) { if (endDate != null && startDate != null && endDate.isBefore(startDate)) { throw new BadRequestException(String.format(Constants.ErrorCode.DATE_RANGE_INVALID)); } diff --git a/promotion/src/main/java/com/yas/promotion/utils/AuthenticationUtils.java b/promotion/src/main/java/com/yas/promotion/utils/AuthenticationUtils.java new file mode 100644 index 0000000000..c2bd5232e9 --- /dev/null +++ b/promotion/src/main/java/com/yas/promotion/utils/AuthenticationUtils.java @@ -0,0 +1,31 @@ +package com.yas.promotion.utils; + + +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +public class AuthenticationUtils { + + private AuthenticationUtils() { + } + + public static String extractUserId() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication instanceof AnonymousAuthenticationToken) { + throw new AccessDeniedException(Constants.ErrorCode.ACCESS_DENIED); + } + + JwtAuthenticationToken contextHolder = (JwtAuthenticationToken) authentication; + + return contextHolder.getToken().getSubject(); + } + + public static String extractJwt() { + return ((Jwt) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getTokenValue(); + } +} diff --git a/promotion/src/main/java/com/yas/promotion/utils/Constants.java b/promotion/src/main/java/com/yas/promotion/utils/Constants.java index 241ddcd26a..02eba591a5 100644 --- a/promotion/src/main/java/com/yas/promotion/utils/Constants.java +++ b/promotion/src/main/java/com/yas/promotion/utils/Constants.java @@ -10,6 +10,7 @@ public final class ErrorCode { public static final String DATE_RANGE_INVALID = "End date cannot be before start date"; public static final String PROMOTION_IN_USE_ERROR_MESSAGE = "PROMOTION_IN_USE"; public static final String PROMOTION_NOT_FOUND_ERROR_MESSAGE = "PROMOTION_NOT_FOUND"; + public static final String ACCESS_DENIED = "ACCESS_DENIED"; } public final class Pageable { diff --git a/promotion/src/main/java/com/yas/promotion/viewmodel/BrandVm.java b/promotion/src/main/java/com/yas/promotion/viewmodel/BrandVm.java new file mode 100644 index 0000000000..37007a2f5d --- /dev/null +++ b/promotion/src/main/java/com/yas/promotion/viewmodel/BrandVm.java @@ -0,0 +1,4 @@ +package com.yas.promotion.viewmodel; + +public record BrandVm(Long id, String name, String slug, Boolean isPublish) { +} diff --git a/promotion/src/main/java/com/yas/promotion/viewmodel/CategoryGetVm.java b/promotion/src/main/java/com/yas/promotion/viewmodel/CategoryGetVm.java new file mode 100644 index 0000000000..8e9b2b1e7e --- /dev/null +++ b/promotion/src/main/java/com/yas/promotion/viewmodel/CategoryGetVm.java @@ -0,0 +1,7 @@ +package com.yas.promotion.viewmodel; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record CategoryGetVm(long id, String name, String slug, long parentId) { +} diff --git a/promotion/src/main/java/com/yas/promotion/viewmodel/ProductVm.java b/promotion/src/main/java/com/yas/promotion/viewmodel/ProductVm.java new file mode 100644 index 0000000000..98358eeae3 --- /dev/null +++ b/promotion/src/main/java/com/yas/promotion/viewmodel/ProductVm.java @@ -0,0 +1,15 @@ +package com.yas.promotion.viewmodel; + + +import java.time.ZonedDateTime; + +public record ProductVm(Long id, + String name, + String slug, + Boolean isAllowedToOrder, + Boolean isPublished, + Boolean isFeatured, + Boolean isVisibleIndividually, + ZonedDateTime createdOn, + Long taxClassId) { +} diff --git a/promotion/src/main/java/com/yas/promotion/viewmodel/PromotionDetailVm.java b/promotion/src/main/java/com/yas/promotion/viewmodel/PromotionDetailVm.java index ddb1fbfcaa..4e7da541ab 100644 --- a/promotion/src/main/java/com/yas/promotion/viewmodel/PromotionDetailVm.java +++ b/promotion/src/main/java/com/yas/promotion/viewmodel/PromotionDetailVm.java @@ -4,10 +4,12 @@ import com.yas.promotion.model.enumeration.ApplyTo; import com.yas.promotion.model.enumeration.DiscountType; import com.yas.promotion.model.enumeration.UsageType; +import java.time.Instant; import java.time.ZonedDateTime; +import java.util.List; import lombok.Builder; -@Builder +@Builder(toBuilder = true) public record PromotionDetailVm(Long id, String name, String slug, @@ -21,8 +23,11 @@ public record PromotionDetailVm(Long id, Long discountPercentage, Long discountAmount, Boolean isActive, - ZonedDateTime startDate, - ZonedDateTime endDate + Instant startDate, + Instant endDate, + List brands, + List categories, + List products ) { public static PromotionDetailVm fromModel(Promotion promotion) { return PromotionDetailVm.builder() @@ -43,4 +48,28 @@ public static PromotionDetailVm fromModel(Promotion promotion) { .endDate(promotion.getEndDate()) .build(); } + + public static PromotionDetailVm fromModel( + Promotion promotion, List brands, List categories, List products) { + return PromotionDetailVm.builder() + .id(promotion.getId()) + .name(promotion.getName()) + .slug(promotion.getSlug()) + .couponCode(promotion.getCouponCode()) + .usageLimit(promotion.getUsageLimit()) + .usageCount(promotion.getUsageCount()) + .discountType(promotion.getDiscountType()) + .applyTo(promotion.getApplyTo()) + .usageType(promotion.getUsageType()) + .description(promotion.getDescription()) + .discountPercentage(promotion.getDiscountPercentage()) + .discountAmount(promotion.getDiscountAmount()) + .isActive(promotion.getIsActive()) + .startDate(promotion.getStartDate()) + .endDate(promotion.getEndDate()) + .brands(brands) + .categories(categories) + .products(products) + .build(); + } } diff --git a/promotion/src/main/java/com/yas/promotion/viewmodel/PromotionPostVm.java b/promotion/src/main/java/com/yas/promotion/viewmodel/PromotionPostVm.java index 15d725e9c2..9213f7fd37 100644 --- a/promotion/src/main/java/com/yas/promotion/viewmodel/PromotionPostVm.java +++ b/promotion/src/main/java/com/yas/promotion/viewmodel/PromotionPostVm.java @@ -6,6 +6,7 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; +import java.time.Instant; import java.time.ZonedDateTime; import java.util.List; import lombok.AllArgsConstructor; @@ -30,8 +31,9 @@ public class PromotionPostVm extends PromotionDto { private String couponCode; Long minimumOrderPurchaseAmount; boolean isActive; - @NotNull ZonedDateTime startDate; - @NotNull ZonedDateTime endDate; + @NotNull + Instant startDate; + @NotNull Instant endDate; public static List createPromotionApplies(PromotionPostVm promotionPostVm, Promotion promotion) { return switch (promotion.getApplyTo()) { diff --git a/promotion/src/main/java/com/yas/promotion/viewmodel/PromotionPutVm.java b/promotion/src/main/java/com/yas/promotion/viewmodel/PromotionPutVm.java index 8f1642daf4..4857b364b1 100644 --- a/promotion/src/main/java/com/yas/promotion/viewmodel/PromotionPutVm.java +++ b/promotion/src/main/java/com/yas/promotion/viewmodel/PromotionPutVm.java @@ -6,6 +6,7 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; +import java.time.Instant; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.List; @@ -34,9 +35,9 @@ public class PromotionPutVm extends PromotionDto { private Long minimumOrderPurchaseAmount; private boolean isActive; @NotNull - private ZonedDateTime startDate; + private Instant startDate; @NotNull - private ZonedDateTime endDate; + private Instant endDate; public static List createPromotionApplies(PromotionPutVm promotionPutVm, Promotion promotion) { return switch (promotion.getApplyTo()) { diff --git a/promotion/src/main/java/com/yas/promotion/viewmodel/PromotionVm.java b/promotion/src/main/java/com/yas/promotion/viewmodel/PromotionVm.java index 64081319f5..3e1fbddfbd 100644 --- a/promotion/src/main/java/com/yas/promotion/viewmodel/PromotionVm.java +++ b/promotion/src/main/java/com/yas/promotion/viewmodel/PromotionVm.java @@ -1,6 +1,7 @@ package com.yas.promotion.viewmodel; import com.yas.promotion.model.Promotion; +import java.time.Instant; import java.time.ZonedDateTime; import lombok.Builder; @@ -12,8 +13,8 @@ public record PromotionVm(Long id, Long discountPercentage, Long discountAmount, Boolean isActive, - ZonedDateTime startDate, - ZonedDateTime endDate + Instant startDate, + Instant endDate ) { public static PromotionVm fromModel(Promotion promotion) { return PromotionVm.builder() diff --git a/promotion/src/main/resources/messages/messages.properties b/promotion/src/main/resources/messages/messages.properties index 438cdd840f..de1946d9c2 100644 --- a/promotion/src/main/resources/messages/messages.properties +++ b/promotion/src/main/resources/messages/messages.properties @@ -1,2 +1,3 @@ PROMOTION_NOT_FOUND=Promotion {} is not found PROMOTION_IN_USE=Can't delete promotion {} because it is in use +ACCESS_DENIED=Access denied diff --git a/promotion/src/test/java/com/yas/promotion/controller/PromotionControllerTest.java b/promotion/src/test/java/com/yas/promotion/controller/PromotionControllerTest.java index f47ba5e4ee..a5e9564596 100644 --- a/promotion/src/test/java/com/yas/promotion/controller/PromotionControllerTest.java +++ b/promotion/src/test/java/com/yas/promotion/controller/PromotionControllerTest.java @@ -1,5 +1,8 @@ package com.yas.promotion.controller; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; import com.yas.promotion.PromotionApplication; @@ -8,6 +11,9 @@ import com.yas.promotion.model.enumeration.UsageType; import com.yas.promotion.service.PromotionService; import com.yas.promotion.viewmodel.PromotionPostVm; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -20,12 +26,6 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; -import java.time.ZonedDateTime; -import java.util.List; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - @ExtendWith(SpringExtension.class) @WebMvcTest(controllers = PromotionController.class) @ContextConfiguration(classes = PromotionApplication.class) @@ -60,8 +60,8 @@ void testCreatePromotion_whenRequestIsValid_thenReturnOk() throws Exception { .productIds(List.of(1L,2L,3L)) .isActive(true) .usageLimit(0) - .startDate(ZonedDateTime.now()) - .endDate(ZonedDateTime.now().plusDays(30)) + .startDate(Instant.now()) + .endDate(Instant.now().plus(30, ChronoUnit.DAYS)) .build(); String request = objectWriter.writeValueAsString(promotionPostVm); @@ -85,8 +85,8 @@ void testCreatePromotion_whenNameIsOverMaxLength_thenReturnBadRequest() throws E .productIds(List.of(1L,2L,3L)) .isActive(true) .usageLimit(0) - .startDate(ZonedDateTime.now()) - .endDate(ZonedDateTime.now().plusDays(30)) + .startDate(Instant.now()) + .endDate(Instant.now().plus(30, ChronoUnit.DAYS)) .build(); String request = objectWriter.writeValueAsString(promotionPostVm); @@ -110,8 +110,8 @@ void testCreatePromotion_whenNameIsBlank_thenReturnBadRequest() throws Exception .productIds(List.of(1L,2L,3L)) .isActive(true) .usageLimit(0) - .startDate(ZonedDateTime.now()) - .endDate(ZonedDateTime.now().plusDays(30)) + .startDate(Instant.now()) + .endDate(Instant.now().plus(30, ChronoUnit.DAYS)) .build(); String request = objectWriter.writeValueAsString(promotionPostVm); @@ -135,8 +135,8 @@ void testCreatePromotion_whenDiscountAmountIsSmallerThanZero_thenReturnBadReques .productIds(List.of(1L,2L,3L)) .isActive(true) .usageLimit(0) - .startDate(ZonedDateTime.now()) - .endDate(ZonedDateTime.now().plusDays(30)) + .startDate(Instant.now()) + .endDate(Instant.now().plus(30, ChronoUnit.DAYS)) .build(); String request = objectWriter.writeValueAsString(promotionPostVm); @@ -160,8 +160,8 @@ void testCreatePromotion_whenDiscountPercentageIsSmallerThanZero_thenReturnBadRe .productIds(List.of(1L,2L,3L)) .isActive(true) .usageLimit(0) - .startDate(ZonedDateTime.now()) - .endDate(ZonedDateTime.now().plusDays(30)) + .startDate(Instant.now()) + .endDate(Instant.now().plus(30, ChronoUnit.DAYS)) .build(); String request = objectWriter.writeValueAsString(promotionPostVm); @@ -185,8 +185,8 @@ void testCreatePromotion_whenDiscountPercentageIsGreaterThanHundred_thenReturnBa .productIds(List.of(1L,2L,3L)) .isActive(true) .usageLimit(0) - .startDate(ZonedDateTime.now()) - .endDate(ZonedDateTime.now().plusDays(30)) + .startDate(Instant.now()) + .endDate(Instant.now().plus(30, ChronoUnit.DAYS)) .build(); String request = objectWriter.writeValueAsString(promotionPostVm); diff --git a/promotion/src/test/java/com/yas/promotion/service/PromotionServiceTest.java b/promotion/src/test/java/com/yas/promotion/service/PromotionServiceTest.java index 3dbfa5da11..75578389c2 100644 --- a/promotion/src/test/java/com/yas/promotion/service/PromotionServiceTest.java +++ b/promotion/src/test/java/com/yas/promotion/service/PromotionServiceTest.java @@ -7,25 +7,30 @@ import com.yas.promotion.exception.BadRequestException; import com.yas.promotion.exception.DuplicatedException; import com.yas.promotion.model.Promotion; +import com.yas.promotion.model.PromotionApply; import com.yas.promotion.model.enumeration.ApplyTo; +import com.yas.promotion.model.enumeration.DiscountType; import com.yas.promotion.repository.PromotionRepository; import com.yas.promotion.utils.Constants; import com.yas.promotion.viewmodel.PromotionDetailVm; import com.yas.promotion.viewmodel.PromotionListVm; import com.yas.promotion.viewmodel.PromotionPostVm; -import java.time.ZonedDateTime; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.List; - import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; @SpringBootTest(classes = PromotionApplication.class) class PromotionServiceTest { @Autowired private PromotionRepository promotionRepository; + @MockBean + private ProductService productService; @Autowired private PromotionService promotionService; @@ -43,8 +48,11 @@ void setUp() { .discountAmount(100L) .discountPercentage(10L) .isActive(true) - .startDate(ZonedDateTime.now()) - .endDate(ZonedDateTime.now().plusDays(30)) + .startDate(Instant.now()) + .endDate(Instant.now().plus(30, ChronoUnit.DAYS)) + .applyTo(ApplyTo.BRAND) + .promotionApplies( + List.of(PromotionApply.builder().brandId(1L).build())) .build(); promotion1 = promotionRepository.save(promotion1); @@ -56,8 +64,11 @@ void setUp() { .discountAmount(200L) .discountPercentage(20L) .isActive(false) - .startDate(ZonedDateTime.now().minusDays(30)) - .endDate(ZonedDateTime.now().plusDays(60)) + .startDate(Instant.now().plus(30, ChronoUnit.DAYS)) + .endDate(Instant.now().plus(60, ChronoUnit.DAYS)) + .applyTo(ApplyTo.PRODUCT) + .promotionApplies( + List.of(PromotionApply.builder().productId(1L).build())) .build(); promotionRepository.save(promotion2); @@ -68,9 +79,10 @@ void setUp() { .couponCode("codeWrong") .discountAmount(200L) .discountPercentage(20L) + .applyTo(ApplyTo.PRODUCT) .isActive(false) - .startDate(ZonedDateTime.now().minusDays(30)) - .endDate(ZonedDateTime.now().minusDays(60)) + .startDate(Instant.now().plus(30, ChronoUnit.DAYS)) + .endDate(Instant.now().plus(60, ChronoUnit.DAYS)) .build(); wrongRangeDatePromotion = promotionRepository.save(wrongRangeDatePromotion); } @@ -87,11 +99,12 @@ void createPromotion_ThenSuccess() { .slug("promotion-3") .description("Description 3") .couponCode("code3") + .discountType(DiscountType.FIXED) .discountAmount(300L) .discountPercentage(30L) .isActive(true) - .startDate(ZonedDateTime.now().plusDays(60)) - .endDate(ZonedDateTime.now().plusDays(90)) + .startDate(Instant.now().plus(60, ChronoUnit.DAYS)) + .endDate(Instant.now().plus(90, ChronoUnit.DAYS)) .applyTo(ApplyTo.PRODUCT) .productIds(List.of(1L, 2L, 3L)) .build(); @@ -106,6 +119,13 @@ void createPromotion_ThenSuccess() { void createPromotion_WhenExistedSlug_ThenDuplicatedExceptionThrown() { promotionPostVm = PromotionPostVm.builder() .slug(promotion1.getSlug()) + .applyTo(ApplyTo.PRODUCT) + .name("12345") + .couponCode("cp-12345") + .productIds(List.of(1L, 2L, 3L)) + .discountType(DiscountType.FIXED) + .discountAmount(300L) + .discountPercentage(30L) .build(); assertThrows(DuplicatedException.class, () -> promotionService.createPromotion(promotionPostVm), String.format(Constants.ErrorCode.SLUG_ALREADY_EXITED, promotionPostVm.getSlug())); @@ -114,8 +134,12 @@ void createPromotion_WhenExistedSlug_ThenDuplicatedExceptionThrown() { @Test void createPromotion_WhenEndDateBeforeStartDate_ThenDateRangeExceptionThrown() { promotionPostVm = PromotionPostVm.builder() - .endDate(wrongRangeDatePromotion.getEndDate()) - .startDate(wrongRangeDatePromotion.getStartDate()) + .applyTo(ApplyTo.PRODUCT) + .name("12345") + .couponCode("cp-12345") + .productIds(List.of(1L, 2L, 3L)) + .endDate(Instant.now().minus(2, ChronoUnit.DAYS)) + .startDate(Instant.now()) .build(); BadRequestException exception = assertThrows(BadRequestException.class, () -> @@ -128,7 +152,7 @@ void createPromotion_WhenEndDateBeforeStartDate_ThenDateRangeExceptionThrown() { void getPromotionList_ThenSuccess() { PromotionListVm result = promotionService.getPromotions(0, 5, "Promotion", "code", - ZonedDateTime.now().minusDays(120), ZonedDateTime.now().plusDays(120)); + Instant.now().minus(120, ChronoUnit.DAYS), Instant.now().plus(120, ChronoUnit.DAYS)); assertEquals(2, result.promotionDetailVmList().size()); PromotionDetailVm promotionDetailVm = result.promotionDetailVmList().getFirst(); assertEquals("promotion-1", promotionDetailVm.slug());