Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Seller Coupon Management REST API #54

Merged
merged 35 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
2db826b
feat: Add review listing by seller with pagination
krkarma777 Mar 24, 2024
16a6a60
Refactor ReviewAPIControllerTest for Enhanced Clarity and Functionality
krkarma777 Mar 24, 2024
8e80179
Enhance findReviewsBySeller response with pagination details
krkarma777 Mar 24, 2024
2b3e36f
Remove unnecessary comments
krkarma777 Mar 24, 2024
bde9815
Refactor findReviewsBySeller to fetch all reviews for products posted…
krkarma777 Mar 24, 2024
92d0b95
Update tests for findReviewsBySeller to reflect API changes, includin…
krkarma777 Mar 24, 2024
cf02a6f
Refactor SellerReviewManageController and related view for dynamic co…
krkarma777 Mar 24, 2024
8d5c124
feat: Add endpoint to retrieve coupons by seller
krkarma777 Mar 25, 2024
fcb8330
Refactor coupon list sorting and streamline code
krkarma777 Mar 25, 2024
9767119
Enhance Coupon API Tests with New Features and Improvements
krkarma777 Mar 25, 2024
b2c2673
refactor: Modify CouponResponseDTO to format date and display coupon …
krkarma777 Mar 25, 2024
2a16bf9
refactor: Modify CouponAPIController to return DTOs instead of entities
krkarma777 Mar 25, 2024
a5afd3f
Refactor SellerCouponManageController for Cleaner Routing and Logic S…
krkarma777 Mar 25, 2024
144f2bf
Added currentPage to the response in findListBySeller method to enhan…
krkarma777 Mar 25, 2024
e2e06d2
Enhanced dynamic pagination in coupon list with currentPage tracking.
krkarma777 Mar 25, 2024
d77ae4e
Removed z-index from navbar to prevent modal window from being obscured.
krkarma777 Mar 26, 2024
52399db
Refactor coupon management code for clarity and maintainability. Simp…
krkarma777 Mar 26, 2024
1f90ae0
Refactor coupon deletion logic to check for user authorization before…
krkarma777 Mar 26, 2024
8d69fe3
Optimize the coupon management modal interaction and enhance UI consi…
krkarma777 Mar 26, 2024
df4b3c2
feat: Add product search API endpoint for sellers
krkarma777 Mar 26, 2024
3776ad0
refactor: Streamline coupon selection and review management in Seller…
krkarma777 Mar 26, 2024
e01bc5a
Refactor seller and coupon management for RESTful API integration and…
krkarma777 Mar 26, 2024
d7bfe62
Refactored Coupon Management: Moved applicable product management fro…
krkarma777 Mar 26, 2024
5dbb161
Refactor seller coupon management page to use RESTful API for asynchr…
krkarma777 Mar 26, 2024
3620a71
feat: Enhance coupon-applicable products configuration
krkarma777 Mar 26, 2024
9ecff34
feat: Validate product ownership in coupon application
krkarma777 Mar 26, 2024
ad4dd76
Add unit test for selecting products applicable to a coupon
krkarma777 Mar 26, 2024
5447cd6
fix: Correct Product instantiation in CouponApplicableProductAPIContr…
krkarma777 Mar 26, 2024
2fdd3e9
refactor: Delete SellerCouponManageController
krkarma777 Mar 26, 2024
44b1213
Fix NullPointerException in getCurrentUser by adding null check for P…
krkarma777 Mar 26, 2024
4c7fcc3
feat: Added global exception handler
krkarma777 Mar 26, 2024
8926462
feat: Added constructor with parameters
krkarma777 Mar 26, 2024
0046df8
Refactor CouponApplicableProductAPIControllerTest to handle various e…
krkarma777 Mar 26, 2024
645b25c
Enhanced test coverage for CouponApplicableProductAPIController
krkarma777 Mar 26, 2024
4063e86
Enhance coupon product selection feature with AJAX submission
krkarma777 Mar 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/main/java/com/bulkpurchase/config/GlobalExceptionHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.bulkpurchase.config;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.server.ResponseStatusException;

import java.util.Map;
import java.util.Objects;

@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(ResponseStatusException.class)
public ResponseEntity<Object> handleResponseStatusException(ResponseStatusException ex, WebRequest request) {
return ResponseEntity.status(ex.getStatusCode()).body(Map.of("message", Objects.requireNonNull(ex.getReason())));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.bulkpurchase.domain.dto.coupon;

import jakarta.validation.constraints.NotEmpty;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import java.util.List;

@Getter
@Setter
@ToString
public class CouponApplicableProductRequestDTO {

@NotEmpty
private Long couponID;

private List<Long> productIDs;
}
Original file line number Diff line number Diff line change
@@ -1,35 +1,35 @@
package com.bulkpurchase.domain.dto.coupon;

import com.bulkpurchase.domain.entity.coupon.Coupon;
import com.bulkpurchase.domain.enums.CouponType;
import lombok.Getter;
import lombok.Setter;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Getter
@Setter
public class CouponResponseDTO {

private Long couponID;
private String code;
private CouponType type;
private String type;
private Double discount;
private LocalDateTime validFrom;
private LocalDateTime validUntil;
private String validFrom;
private String validUntil;
private Double minimumOrderAmount;
private Integer quantity;
private String name;
private String description;
private Double maxDiscountAmount;

public CouponResponseDTO(Coupon coupon) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm");
this.couponID = coupon.getCouponID();
this.code = coupon.getCode();
this.type = coupon.getType();
this.type = coupon.getType().getDescription();
this.discount = coupon.getDiscount();
this.validFrom = coupon.getValidFrom();
this.validUntil = coupon.getValidUntil();
this.validFrom = coupon.getValidFrom().format(formatter);
this.validUntil = coupon.getValidUntil().format(formatter);
this.minimumOrderAmount = coupon.getMinimumOrderAmount();
this.quantity = coupon.getQuantity();
this.name = coupon.getName();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ public ProductForCouponDTO(Product product) {
public ProductForCouponDTO() {
}

public ProductForCouponDTO(Long productID, String productName) {
this.productID = productID;
this.productName = productName;
}

@Override
public String toString() {
return "ProductForCouponDTO{" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ public class Coupon {
public Coupon() {
}

public Coupon(Long couponID, User createdBy) {
this.couponID = couponID;
this.createdBy = createdBy;
}

public Coupon(CouponCreateRequestDTO couponCreateRequestDTO) {
this.type = couponCreateRequestDTO.getType();
this.discount = couponCreateRequestDTO.getDiscount();
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/com/bulkpurchase/domain/entity/product/Product.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ public class Product {
@OneToMany(mappedBy = "product")
private List<FavoriteProduct> favoritedByUsers;

public Product(Long productID, String productName, Category category) {
this.productID = productID;
this.productName = productName;
this.category = category;
}

public ProductStatus getOppositeStatus() {
return this.status == ProductStatus.ACTIVE ? ProductStatus.INACTIVE : ProductStatus.ACTIVE;
}
Expand All @@ -92,6 +98,11 @@ public Product(ProductRequestDTO productRequestDTO, User user) {
public Product() {
}

public Product(Long productID, User user) {
this.productID = productID;
this.user = user;
}

public void update(ProductRequestDTO productRequestDTO) {
this.productName = productRequestDTO.getProductName();
this.description = productRequestDTO.getDescription();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.bulkpurchase.domain.entity.coupon.Coupon;
import com.bulkpurchase.domain.entity.user.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;
Expand All @@ -11,6 +13,7 @@ public interface CouponRepository extends JpaRepository<Coupon, Long> {


List<Coupon> findByCreatedBy(User user);
Page<Coupon> findByCreatedBy(User user, Pageable pageable);

Coupon findByCode(String couponCode);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public interface ReviewRepository extends JpaRepository<Review, Long> {
"(SELECT COUNT(f) FROM ReviewFeedback f WHERE f.review = r AND f.feedbackType = com.bulkpurchase.domain.enums.FeedbackType.DISLIKE) AS dislikeCount " +
"FROM Review r WHERE r.product.user.userID = :userID " +
"ORDER BY r.creationDate DESC")
List<Object[]> findAllReviewDetailsWithFeedbackCountsByUserId(@Param("userID") Long userID);
List<Object[]> findAllReviewDetailsWithFeedbackCountsByUserId(@Param("userID") Long userID, Pageable page);

@Query("SELECT r, " +
"(SELECT COUNT(f) FROM ReviewFeedback f WHERE f.review = r AND f.feedbackType = com.bulkpurchase.domain.enums.FeedbackType.LIKE) AS likeCount, " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import com.bulkpurchase.domain.entity.user.User;
import com.bulkpurchase.domain.repository.coupon.CouponRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;

Expand All @@ -23,6 +25,9 @@ public Coupon save(Coupon coupon) {
public List<Coupon> findByUser(User user) {
return couponRepository.findByCreatedBy(user);
}
public Page<Coupon> findByCreatedBy(User user, Pageable pageable) {
return couponRepository.findByCreatedBy(user, pageable);
}

public Optional<Coupon> findById(Long couponID) {
return couponRepository.findById(couponID);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,14 @@ public Double findAverageRatingByProductID(Long productID) {
return reviewRepository.findAverageRatingByProductID(productID);
}

public List<ReviewDetailDTO> findAllReviewDetailsWithFeedbackCountsBySeller(Long userID) {
List<Object[]> results = reviewRepository.findAllReviewDetailsWithFeedbackCountsByUserId(userID);
return results.stream().map(result -> new ReviewDetailDTO(
public Page<ReviewDetailDTO> findAllReviewDetailsWithFeedbackCountsBySeller(Long userID, Pageable page) {
List<Object[]> results = reviewRepository.findAllReviewDetailsWithFeedbackCountsByUserId(userID,page);
List<ReviewDetailDTO> dtos = results.stream().map(result -> new ReviewDetailDTO(
(Review) result[0],
(Long) result[1],
(Long) result[2]
)).collect(Collectors.toList());
return new PageImpl<>(dtos, page, dtos.size());
}

public Page<ReviewDetailDTO> findByUser(User user, Pageable page) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.bulkpurchase.domain.validator;

import com.bulkpurchase.domain.entity.coupon.Coupon;
import com.bulkpurchase.domain.entity.coupon.CouponApplicableProduct;
import com.bulkpurchase.domain.entity.product.Product;
import com.bulkpurchase.domain.entity.user.User;
import com.bulkpurchase.domain.service.coupon.CouponApplicableProductService;
import com.bulkpurchase.domain.service.product.ProductService;
import com.bulkpurchase.domain.validator.user.UserAuthValidator;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ResponseStatusException;

import java.security.Principal;
import java.util.List;
import java.util.stream.Collectors;

@Component
@RequiredArgsConstructor
public class CouponApplicableProductValidator {

private final UserAuthValidator userAuthValidator;
private final CouponApplicableProductService couponApplicableProductService;
private final ProductService productService;

public void validateCouponOwner(Principal principal, Coupon coupon) {
User user = userAuthValidator.getCurrentUser(principal);
if (!user.equals(coupon.getCreatedBy())) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "권한이 없습니다.");
}
}

public void validateAndApplyProductsToCoupon(List<Long> productIDs, Coupon coupon, Principal principal) {
User user = userAuthValidator.getCurrentUser(principal);
List<Product> products = productIDs.stream()
.map(this::validateAndGetProduct)
.filter(product -> product.getUser().equals(user)) // 상품이 현재 사용자에 의해 게시되었는지 확인
.toList();

if (products.size() != productIDs.size()) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "하나 이상의 상품이 사용자에 의해 게시되지 않았습니다.");
}

couponApplicableProductService.deleteByCoupon(coupon);
products.forEach(product -> couponApplicableProductService.save(new CouponApplicableProduct(coupon, product.getProductID())));
}

private Product validateAndGetProduct(Long productID) {
return productService.findById(productID)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "잘못된 상품 정보입니다."));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ public Optional<User> authenticate(Principal principal) {
}

public User getCurrentUser(Principal principal) {
if (principal == null) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "인증된 사용자 정보가 없습니다.");
}
return userService.findByUsername(principal.getName())
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
package com.bulkpurchase.web.controller.api;

import com.bulkpurchase.domain.dto.coupon.*;
import com.bulkpurchase.domain.dto.product.ProductForCouponDTO;
import com.bulkpurchase.domain.entity.coupon.Coupon;
import com.bulkpurchase.domain.entity.coupon.CouponApplicableProduct;
import com.bulkpurchase.domain.entity.coupon.UserCoupon;
import com.bulkpurchase.domain.entity.user.User;
import com.bulkpurchase.domain.service.coupon.CouponApplicableProductService;
import com.bulkpurchase.domain.enums.UserRole;
import com.bulkpurchase.domain.service.coupon.CouponService;
import com.bulkpurchase.domain.service.coupon.UserCouponService;
import com.bulkpurchase.domain.service.product.ProductService;
import com.bulkpurchase.domain.validator.coupon.CouponValidatorImpl;
import com.bulkpurchase.domain.validator.user.UserAuthValidator;
import com.bulkpurchase.web.service.coupon.ApplyCouponService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
Expand All @@ -24,7 +24,6 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

@RestController
@RequiredArgsConstructor
Expand All @@ -34,8 +33,6 @@ public class CouponAPIController {
private final CouponService couponService;
private final UserAuthValidator userAuthValidator;
private final UserCouponService userCouponService;
private final CouponApplicableProductService couponApplicableProductService;
private final ProductService productService;
private final ApplyCouponService applyCouponService;
private final CouponValidatorImpl couponValidator;

Expand Down Expand Up @@ -69,7 +66,11 @@ public ResponseEntity<?> delete(@PathVariable("couponID") Long couponID, Princip
if (couponOpt.isEmpty()) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Map.of("message", "쿠폰이 존재하지 않습니다."));
}
couponService.delete(couponOpt.get());
Coupon coupon = couponOpt.get();
if (!coupon.getCreatedBy().equals(user)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "쿠폰을 삭제할 권한이 없습니다."));
}
couponService.delete(coupon);
return ResponseEntity.ok(Map.of("message", "쿠폰이 정상적으로 삭제되었습니다."));
}

Expand All @@ -85,16 +86,30 @@ public ResponseEntity<?> one(@PathVariable("couponID") Long couponID) {

@GetMapping("/list")
public ResponseEntity<List<CouponResponseDTO>> listPage() {
Sort sort = Sort.by(Sort.Direction.DESC, "id");
Sort sort = Sort.by(Sort.Direction.DESC, "couponID");
List<Coupon> couponList = couponService.findAll(sort);

List<CouponResponseDTO> dtoList = couponList.stream()
.map(CouponResponseDTO::new)
.collect(Collectors.toList());
.toList();

return ResponseEntity.ok(dtoList);
}

@GetMapping("/seller/{sellerID}")
public ResponseEntity<?> findListBySeller(@PathVariable("sellerID") Long sellerID,
@RequestParam(value = "page",defaultValue = "1") Integer page) {
User user = userAuthValidator.getCurrentUserByUserID(sellerID);
if (user.getRole() != UserRole.ROLE_판매자) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("message", "잘못된 요청입니다."));
}
Sort sort = Sort.by(Sort.Direction.DESC, "couponID");
Pageable pageable = PageRequest.of(page - 1, 10, sort);
Page<Coupon> couponPage = couponService.findByCreatedBy(user, pageable);
List<CouponResponseDTO> coupons = couponPage.getContent().stream().map(CouponResponseDTO::new).toList();
return ResponseEntity.ok(Map.of("coupons", coupons, "totalPages", couponPage.getTotalPages(), "currentPage", page));
}


@PostMapping("/redeem")
public Map<String, Object> userCouponRedeem(Principal principal, @RequestParam("code") String code) {
Expand All @@ -117,20 +132,6 @@ public Map<String, Object> userCouponRedeem(Principal principal, @RequestParam("
return response;
}

@GetMapping("/{couponID}/products")
public ResponseEntity<?> findApplicableProductsForCoupon(@PathVariable("couponID") Long couponID) {
List<CouponApplicableProduct> couponApplicableProducts = couponApplicableProductService.findByCouponCouponID(couponID);

List<ProductForCouponDTO> productForCouponDTOList = couponApplicableProducts.stream()
.map(CouponApplicableProduct::getProductId)
.map(productService::findById)
.filter(Optional::isPresent)
.map(Optional::get)
.map(ProductForCouponDTO::new)
.collect(Collectors.toList());

return ResponseEntity.ok(productForCouponDTOList);
}

@GetMapping("/user")
public ResponseEntity<?> userCoupons(Principal principal) {
Expand Down
Loading