diff --git a/src/main/java/NextLevel/demo/exception/CustomExceptionHandler.java b/src/main/java/NextLevel/demo/exception/CustomExceptionHandler.java index 3c5e9ed..40ed9c0 100644 --- a/src/main/java/NextLevel/demo/exception/CustomExceptionHandler.java +++ b/src/main/java/NextLevel/demo/exception/CustomExceptionHandler.java @@ -2,6 +2,8 @@ import java.util.HashMap; import java.util.Map; + +import jakarta.validation.ConstraintViolationException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.HttpRequestMethodNotSupportedException; @@ -26,6 +28,13 @@ public ResponseEntity handleValidationException(MethodArgumentNotValidExcepti map.put("message", ErrorCode.INPUT_REQUIRED_PARAMETER.errorMessage); return ResponseEntity.status(ErrorCode.INPUT_REQUIRED_PARAMETER.statusCode).body(map); } + @ExceptionHandler(ConstraintViolationException.class) + public ResponseEntity handleValidationException(ConstraintViolationException e) { + Map map = new HashMap<>(); + map.put("code", ErrorCode.INPUT_REQUIRED_PARAMETER.CustomErrorCode); + map.put("message", ErrorCode.INPUT_REQUIRED_PARAMETER.errorMessage); + return ResponseEntity.status(ErrorCode.INPUT_REQUIRED_PARAMETER.statusCode).body(map); + } @ExceptionHandler(HttpRequestMethodNotSupportedException.class) public ResponseEntity handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) { return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED) diff --git a/src/main/java/NextLevel/demo/exception/ErrorCode.java b/src/main/java/NextLevel/demo/exception/ErrorCode.java index 3a5521a..aeb3ba9 100644 --- a/src/main/java/NextLevel/demo/exception/ErrorCode.java +++ b/src/main/java/NextLevel/demo/exception/ErrorCode.java @@ -34,6 +34,7 @@ public enum ErrorCode { // funding NOT_ENOUGH_POINT(HttpStatus.BAD_REQUEST, "05001","not enough point left:%s, need:%s"), + ALREADY_USED_COUPON(HttpStatus.BAD_REQUEST, "05002","already used coupon"), // option diff --git a/src/main/java/NextLevel/demo/funding/controller/CouponController.java b/src/main/java/NextLevel/demo/funding/controller/CouponController.java index adacf27..4d6b16b 100644 --- a/src/main/java/NextLevel/demo/funding/controller/CouponController.java +++ b/src/main/java/NextLevel/demo/funding/controller/CouponController.java @@ -5,6 +5,7 @@ import NextLevel.demo.funding.dto.response.ResponseCouponDto; import NextLevel.demo.funding.service.CouponService; import NextLevel.demo.util.jwt.JWTUtil; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -21,7 +22,7 @@ public class CouponController { private final CouponService couponService; @PostMapping("/admin/coupon/add") - public ResponseEntity addCoupon(@RequestBody RequestAddCouponDto dto) { + public ResponseEntity addCoupon(@RequestBody @Valid RequestAddCouponDto dto) { if(dto.getUserId()==null) dto.setUserId(JWTUtil.getUserIdFromSecurityContext()); couponService.addCoupon(dto); diff --git a/src/main/java/NextLevel/demo/funding/controller/FundingController.java b/src/main/java/NextLevel/demo/funding/controller/FundingController.java index b75fb9f..a547a72 100644 --- a/src/main/java/NextLevel/demo/funding/controller/FundingController.java +++ b/src/main/java/NextLevel/demo/funding/controller/FundingController.java @@ -29,10 +29,10 @@ public class FundingController { public ResponseEntity funding(@RequestBody @Valid RequestFundingDto dto) { dto.setUserId(JWTUtil.getUserIdFromSecurityContext()); - if(dto.getFree() != null) - fundingService.freeFunding(dto.getFree()); if(dto.getOption() != null) fundingService.optionFunding(dto.getOption()); + if(dto.getFree() != null) + fundingService.freeFunding(dto.getFree()); return ResponseEntity.ok().body(new SuccessResponse("success", null)); } diff --git a/src/main/java/NextLevel/demo/funding/dto/request/RequestFreeFundingDto.java b/src/main/java/NextLevel/demo/funding/dto/request/RequestFreeFundingDto.java index b3b0f6d..4874f7b 100644 --- a/src/main/java/NextLevel/demo/funding/dto/request/RequestFreeFundingDto.java +++ b/src/main/java/NextLevel/demo/funding/dto/request/RequestFreeFundingDto.java @@ -3,6 +3,9 @@ import NextLevel.demo.funding.entity.FreeFundingEntity; import NextLevel.demo.project.project.entity.ProjectEntity; import NextLevel.demo.user.entity.UserEntity; +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -12,7 +15,10 @@ @NoArgsConstructor public class RequestFreeFundingDto { + @JsonAlias("price") + @NotNull private Long freePrice; + @NotNull private Long projectId; private Long userId; diff --git a/src/main/java/NextLevel/demo/funding/entity/FreeFundingEntity.java b/src/main/java/NextLevel/demo/funding/entity/FreeFundingEntity.java index bdf732f..e1b1237 100644 --- a/src/main/java/NextLevel/demo/funding/entity/FreeFundingEntity.java +++ b/src/main/java/NextLevel/demo/funding/entity/FreeFundingEntity.java @@ -28,4 +28,6 @@ public class FreeFundingEntity { @JoinColumn(name = "project_id") private ProjectEntity project; + public void updatePrice(long price) {this.price += price;} + } diff --git a/src/main/java/NextLevel/demo/funding/entity/OptionFundingEntity.java b/src/main/java/NextLevel/demo/funding/entity/OptionFundingEntity.java index 5021135..3268480 100644 --- a/src/main/java/NextLevel/demo/funding/entity/OptionFundingEntity.java +++ b/src/main/java/NextLevel/demo/funding/entity/OptionFundingEntity.java @@ -36,7 +36,7 @@ public class OptionFundingEntity extends BasedEntity { @Column private long count; - public void updateCount(int count) { + public void updateCount(long count) { this.count += count; } diff --git a/src/main/java/NextLevel/demo/funding/repository/FreeFundingRepository.java b/src/main/java/NextLevel/demo/funding/repository/FreeFundingRepository.java index 0aea7f9..095105b 100644 --- a/src/main/java/NextLevel/demo/funding/repository/FreeFundingRepository.java +++ b/src/main/java/NextLevel/demo/funding/repository/FreeFundingRepository.java @@ -5,6 +5,9 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.util.List; +import java.util.Optional; + public interface FreeFundingRepository extends JpaRepository { @Query("select sum(f.price) from FreeFundingEntity f where f.project.id = :projectId") @@ -13,4 +16,14 @@ public interface FreeFundingRepository extends JpaRepository findByProjectIdAndUserId(@Param("projectId") long projectId, @Param("userId") long userId); + + //for rollback funding + @Query("select ff from FreeFundingEntity ff left join fetch ff.user where ff.project.id = :projectId") + List findAllWithUserByProject(@Param("projectId") Long projectId); + + @Query("select ff from FreeFundingEntity ff where ff.user.id = :userId") + List findAllByUser(@Param("userId") Long userId); + } diff --git a/src/main/java/NextLevel/demo/funding/repository/OptionFundingRepository.java b/src/main/java/NextLevel/demo/funding/repository/OptionFundingRepository.java index a48f902..59ffc1e 100644 --- a/src/main/java/NextLevel/demo/funding/repository/OptionFundingRepository.java +++ b/src/main/java/NextLevel/demo/funding/repository/OptionFundingRepository.java @@ -6,6 +6,9 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.List; +import java.util.Optional; + @Repository public interface OptionFundingRepository extends JpaRepository { @@ -20,4 +23,16 @@ public interface OptionFundingRepository extends JpaRepository findByOptionIdAndUserId(@Param("optionId") long optionId, @Param("userId") long userId); + + // for rollback funding + @Query("select of from OptionFundingEntity of left join fetch of.user where of.option.id = :optionId") + List findAllWithUserByOption(@Param("optionId") Long optionId); + + @Query("select of from OptionFundingEntity of left join fetch of.user where of.option.project.id = :projectId") + List findAllWithUserByProject(@Param("projectId") Long projectId); + + @Query("select of from OptionFundingEntity of where of.user.id = :userId") + List findAllByUser(@Param("userId") Long userId); } diff --git a/src/main/java/NextLevel/demo/funding/service/CouponService.java b/src/main/java/NextLevel/demo/funding/service/CouponService.java index 0b85dc6..f9b7abe 100644 --- a/src/main/java/NextLevel/demo/funding/service/CouponService.java +++ b/src/main/java/NextLevel/demo/funding/service/CouponService.java @@ -40,6 +40,9 @@ public long useCoupon(long userId, long couponId, OptionFundingEntity optionFund if(!coupon.getUser().getId().equals(userId)) throw new CustomException(ErrorCode.NOT_AUTHOR); + if(coupon.getOptionFunding() != null) + throw new CustomException(ErrorCode.ALREADY_USED_COUPON); + price -= coupon.getPrice(); coupon.updateProjectFundingEntity(optionFunding); @@ -47,11 +50,4 @@ public long useCoupon(long userId, long couponId, OptionFundingEntity optionFund return price>0?price:0; } - public long rollBackUseCoupon(CouponEntity coupon, long price) { - price -= coupon.getPrice(); - coupon.rollBackUseCoupon(); - couponRepository.save(coupon); - return price; - } - } diff --git a/src/main/java/NextLevel/demo/funding/service/FundingRollbackService.java b/src/main/java/NextLevel/demo/funding/service/FundingRollbackService.java new file mode 100644 index 0000000..f5f5f79 --- /dev/null +++ b/src/main/java/NextLevel/demo/funding/service/FundingRollbackService.java @@ -0,0 +1,74 @@ +package NextLevel.demo.funding.service; + +import NextLevel.demo.funding.entity.CouponEntity; +import NextLevel.demo.funding.entity.FreeFundingEntity; +import NextLevel.demo.funding.entity.OptionFundingEntity; +import NextLevel.demo.funding.repository.CouponRepository; +import NextLevel.demo.funding.repository.FreeFundingRepository; +import NextLevel.demo.funding.repository.OptionFundingRepository; +import NextLevel.demo.option.OptionEntity; +import NextLevel.demo.project.project.entity.ProjectEntity; +import NextLevel.demo.user.entity.UserEntity; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class FundingRollbackService { + + private final CouponRepository couponRepository; + private final OptionFundingRepository optionFundingRepository; + private final FreeFundingRepository freeFundingRepository; + + private long rollbackCoupon(CouponEntity coupon, long price) { + price -= coupon.getPrice(); + coupon.rollBackUseCoupon(); + couponRepository.save(coupon); // transaction내부에서는 필요 없음? (transaction에 영속성 밖이라 적용이 안될려나?) + return price>0?price:0; + } + + public void rollbackOptionFunding(UserEntity user, OptionFundingEntity optionFunding) { + long price = optionFunding.getCount() * optionFunding.getOption().getPrice(); + + if(optionFunding.getCoupon() != null){ + price = rollbackCoupon(optionFunding.getCoupon(), price); + } + + optionFundingRepository.deleteById(optionFunding.getId()); + user.updatePoint(+price); + } + + public void rollbackFreeFunding(UserEntity user, FreeFundingEntity freeFunding) { + long price = freeFunding.getPrice(); + freeFundingRepository.deleteById(freeFunding.getId()); + user.updatePoint(+price); + } + + // option 삭제 / 변경 시 사용 (option의 funding들만 rollback 시켜줌 + public void rollbackByOption(OptionEntity option) { + // option의 funding의 모든 user에게 + List optionFundingList = optionFundingRepository.findAllWithUserByOption(option.getId()); + optionFundingList.forEach((funding)-> rollbackOptionFunding(funding.getUser(), funding)); + } + + // project 삭제시 사용 + public void rollbackByProject(ProjectEntity project) { + List optionFundingList = optionFundingRepository.findAllWithUserByProject(project.getId()); + List freeFundingList = freeFundingRepository.findAllWithUserByProject(project.getId()); + + optionFundingList.forEach(optionFunding -> rollbackOptionFunding(optionFunding.getUser(), optionFunding)); + freeFundingList.forEach(freeFunding -> rollbackFreeFunding(freeFunding.getUser(), freeFunding)); + } + + // user의 모든 funding을 rollback 시킴 (user delete시 data 무결성 때문에 rollback기능은 필요함!) + public void rollbackByUser(UserEntity user) { + List optionFundingList = optionFundingRepository.findAllByUser(user.getId()); + List freeFundingList = freeFundingRepository.findAllByUser(user.getId()); + + optionFundingList.forEach(optionFunding -> rollbackOptionFunding(user, optionFunding)); + freeFundingList.forEach(freeFunding -> rollbackFreeFunding(user, freeFunding)); + } + +} diff --git a/src/main/java/NextLevel/demo/funding/service/FundingService.java b/src/main/java/NextLevel/demo/funding/service/FundingService.java index c1977c4..700356f 100644 --- a/src/main/java/NextLevel/demo/funding/service/FundingService.java +++ b/src/main/java/NextLevel/demo/funding/service/FundingService.java @@ -5,7 +5,6 @@ import NextLevel.demo.funding.dto.request.RequestCancelFundingDto; import NextLevel.demo.funding.dto.request.RequestFreeFundingDto; import NextLevel.demo.funding.dto.request.RequestOptionFundingDto; -import NextLevel.demo.funding.entity.CouponEntity; import NextLevel.demo.funding.entity.FreeFundingEntity; import NextLevel.demo.option.OptionEntity; import NextLevel.demo.funding.entity.OptionFundingEntity; @@ -16,14 +15,19 @@ import NextLevel.demo.project.project.service.ProjectValidateService; import NextLevel.demo.user.entity.UserEntity; import NextLevel.demo.user.service.UserValidateService; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.Optional; @Service @RequiredArgsConstructor @Slf4j +@Validated public class FundingService { private final UserValidateService userValidateService; @@ -33,6 +37,8 @@ public class FundingService { private final OptionFundingRepository optionFundingRepository; private final FreeFundingRepository freeFundingRepository; + private final FundingRollbackService fundingRollbackService; + private final CouponService couponService; @Transactional @@ -43,7 +49,8 @@ public void cancelFreeFunding(RequestCancelFundingDto dto) { ); if(!user.getId().equals(funding.getUser().getId())) throw new CustomException(ErrorCode.NOT_AUTHOR); - freeFundingRepository.deleteById(dto.getId()); + + fundingRollbackService.rollbackFreeFunding(user, funding); } @Transactional @@ -52,21 +59,15 @@ public void cancelOptionFunding(RequestCancelFundingDto dto) { OptionFundingEntity funding = optionFundingRepository.findById(dto.getId()).orElseThrow( ()->{return new CustomException(ErrorCode.NOT_FOUND, "optionFunding");} ); - long price = funding.getCount() * funding.getOption().getPrice(); if(!user.getId().equals(funding.getUser().getId())) throw new CustomException(ErrorCode.NOT_AUTHOR); - if(funding.getCoupon() != null){ - price = couponService.rollBackUseCoupon(funding.getCoupon(), price); - } - - optionFundingRepository.deleteById(dto.getId()); - user.updatePoint(+price); + fundingRollbackService.rollbackOptionFunding(user, funding); } @Transactional - public void optionFunding(RequestOptionFundingDto dto) { + public void optionFunding(@Valid RequestOptionFundingDto dto) { UserEntity user = userValidateService.getUserInfo(dto.getUserId()); OptionEntity option = optionValidateService.getOption(dto.getOptionId()); @@ -80,19 +81,31 @@ public void optionFunding(RequestOptionFundingDto dto) { if(totalPrice > user.getPoint()) throw new CustomException(ErrorCode.NOT_ENOUGH_POINT, String.valueOf(user.getPoint()), String.valueOf(totalPrice)); - optionFundingRepository.save(entity); + Optional oldOptionFundingOpt = optionFundingRepository.findByOptionIdAndUserId(dto.getOptionId(), dto.getUserId()); + + if(oldOptionFundingOpt.isPresent()) + oldOptionFundingOpt.get().updateCount(dto.getCount()); + else + optionFundingRepository.save(entity); + user.updatePoint(-totalPrice); } @Transactional - public void freeFunding(RequestFreeFundingDto dto) { + public void freeFunding(@Valid RequestFreeFundingDto dto) { UserEntity user = userValidateService.getUserInfo(dto.getUserId()); ProjectEntity project = projectValidateService.getProjectEntity(dto.getProjectId()); if(dto.getFreePrice() > user.getPoint()) throw new CustomException(ErrorCode.NOT_ENOUGH_POINT, String.valueOf(user.getPoint()), String.valueOf(dto.getFreePrice())); - freeFundingRepository.save(dto.toEntity(user, project)); + Optional oldFreeFundingOpt = freeFundingRepository.findByProjectIdAndUserId(project.getId(), dto.getUserId()); + + if(oldFreeFundingOpt.isPresent()) + oldFreeFundingOpt.get().updatePrice(dto.getFreePrice()); + else + freeFundingRepository.save(dto.toEntity(user, project)); + user.updatePoint(-dto.getFreePrice()); } diff --git a/src/main/java/NextLevel/demo/option/OptionService.java b/src/main/java/NextLevel/demo/option/OptionService.java index 3c6caa2..ecf629b 100644 --- a/src/main/java/NextLevel/demo/option/OptionService.java +++ b/src/main/java/NextLevel/demo/option/OptionService.java @@ -2,6 +2,7 @@ import NextLevel.demo.exception.CustomException; import NextLevel.demo.exception.ErrorCode; +import NextLevel.demo.funding.service.FundingRollbackService; import NextLevel.demo.project.project.entity.ProjectEntity; import java.util.List; @@ -19,6 +20,7 @@ public class OptionService { private final OptionRepository optionRepository; private final ProjectValidateService projectValidateService; + private final FundingRollbackService fundingRollbackService; public void add(SaveOptionRequestDto dto){ ProjectEntity project = projectValidateService.validateAuthor(dto.getProjectId(), dto.getUserId()); @@ -32,6 +34,7 @@ public void update(SaveOptionRequestDto dto){ ()->{return new CustomException(ErrorCode.NOT_FOUND, "option");} ); projectValidateService.validateAuthor(option.getProject().getId(), dto.getUserId()); + fundingRollbackService.rollbackByOption(option); option.update(dto); } @@ -41,6 +44,7 @@ public void delete(Long optionId, Long userId){ ()->{return new CustomException(ErrorCode.NOT_FOUND, "option");} ); projectValidateService.validateAuthor(option.getProject().getId(), userId); + fundingRollbackService.rollbackByOption(option); optionRepository.deleteById(option.getId()); } diff --git a/src/main/java/NextLevel/demo/project/project/service/ProjectService.java b/src/main/java/NextLevel/demo/project/project/service/ProjectService.java index b309fcc..a62c185 100644 --- a/src/main/java/NextLevel/demo/project/project/service/ProjectService.java +++ b/src/main/java/NextLevel/demo/project/project/service/ProjectService.java @@ -2,6 +2,7 @@ import NextLevel.demo.exception.CustomException; import NextLevel.demo.exception.ErrorCode; +import NextLevel.demo.funding.service.FundingRollbackService; import NextLevel.demo.funding.service.FundingValidateService; import NextLevel.demo.img.entity.ImgEntity; import NextLevel.demo.img.service.ImgServiceImpl; @@ -44,6 +45,7 @@ public class ProjectService { private final FundingValidateService fundingValidateService; private final ProjectDslRepository projectDslRepository; + private final FundingRollbackService fundingRollbackService; private final ProjectValidateService projectValidateService; // 추가 @@ -98,17 +100,21 @@ public void update(CreateProjectDto dto, ArrayList imgPaths) { } // 삭제 - public void deleteProject(Long id) { + @Transactional + @ImgTransaction + public void deleteProject(Long id, ArrayList imgPaths) { Optional oldProjectOptional = projectRepository.findById(id); if(oldProjectOptional.isEmpty()) throw new CustomException(ErrorCode.NOT_FOUND, "project"); ProjectEntity oldProject = oldProjectOptional.get(); // 펀딩 금액이 남아있다면 모두 환불 처리하기 + fundingRollbackService.rollbackByProject(oldProject); // 다른 soft적 처리 필요한 부분 처리하기 // img 처리 + projectStoryService.updateProjectStory(oldProject, new ArrayList<>(), imgPaths); return; // 아직 구현하지 않음 + soft delete 처리 고민중 ..... }