diff --git a/build.gradle b/build.gradle index ac20d2f1..769acf2c 100644 --- a/build.gradle +++ b/build.gradle @@ -51,12 +51,15 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-api:0.11.5' implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + //S3 implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + //LiveKit implementation 'io.livekit:livekit-server:0.6.1' - + //Spring Security Test 의존성 + testImplementation 'org.springframework.security:spring-security-test' } test { diff --git a/src/main/java/space/space_spring/controller/PostController.java b/src/main/java/space/space_spring/controller/PostController.java index 05dbfc5c..56f7540b 100644 --- a/src/main/java/space/space_spring/controller/PostController.java +++ b/src/main/java/space/space_spring/controller/PostController.java @@ -89,7 +89,7 @@ public BaseResponse getPost( } // 게시글 수정 - @PostMapping("/board/post/postId}") + @PostMapping("/board/post/{postId}") @CheckUserSpace(required = false) public BaseResponse updatePost( @JwtLoginAuth Long userId, diff --git a/src/main/java/space/space_spring/domain/pay/controller/PayController.java b/src/main/java/space/space_spring/domain/pay/controller/PayController.java index 0a11280f..4d786e33 100644 --- a/src/main/java/space/space_spring/domain/pay/controller/PayController.java +++ b/src/main/java/space/space_spring/domain/pay/controller/PayController.java @@ -6,15 +6,8 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import space.space_spring.argumentResolver.jwtLogin.JwtLoginAuth; -import space.space_spring.argumentResolver.userSpace.CheckUserSpace; -import space.space_spring.domain.pay.model.dto.PayTargetInfoDto; -import space.space_spring.domain.pay.model.dto.PayRequestInfoDto; -import space.space_spring.domain.pay.model.dto.TotalPayInfoDto; -import space.space_spring.domain.pay.model.request.PostPayCompleteRequest; -import space.space_spring.domain.pay.model.request.PostPayCreateRequest; +import space.space_spring.domain.pay.model.request.PayCreateRequest; import space.space_spring.domain.pay.model.response.*; -import space.space_spring.domain.pay.model.entity.PayRequestTarget; -import space.space_spring.domain.user.model.entity.User; import space.space_spring.exception.CustomException; import space.space_spring.response.BaseResponse; import space.space_spring.domain.pay.service.PayService; @@ -22,8 +15,6 @@ import space.space_spring.util.user.UserUtils; import space.space_spring.util.userSpace.UserSpaceUtils; -import java.util.List; - import static space.space_spring.response.status.BaseExceptionResponseStatus.*; import static space.space_spring.util.bindingResult.BindingResultUtils.getErrorMessage; @@ -33,9 +24,9 @@ public class PayController { private final PayService payService; - private final UserSpaceUtils userSpaceUtils; - private final UserUtils userUtils; - private final PayUtils payUtils; +// private final UserSpaceUtils userSpaceUtils; +// private final UserUtils userUtils; +// private final PayUtils payUtils; /** * 정산 홈 view @@ -98,12 +89,24 @@ public BaseResponse showPayHome(@JwtLoginAuth Long userId, // return new BaseResponse<>(payService.getRecentPayRequestBankInfoForUser(userId)); // } // -// /** -// * 정산 생성 -// * response 추가 협의 필요 -> 굳이 PayRequestId를 response 안해도 될꺼같음 -// */ + + /** + * 정산 생성 + * response 추가 협의 필요 -> 굳이 PayRequestId를 response 안해도 될꺼같음 + */ + @PostMapping("/space/{spaceId}/pay") + public BaseResponse createPay(@JwtLoginAuth Long userId, @PathVariable Long spaceId, @Validated @RequestBody PayCreateRequest payCreateRequest, BindingResult bindingResult) { + // request 입력 모델 유효성 검증 + if (bindingResult.hasErrors()) { + throw new CustomException(INVALID_PAY_CREATE, getErrorMessage(bindingResult)); + } + + return new BaseResponse<>(payService.createPay(userId, spaceId, payCreateRequest.toServiceRequest())); + } + + // @PostMapping("/space/{spaceId}/pay") -// public BaseResponse createPay(@JwtLoginAuth Long userId, @PathVariable Long spaceId, @Validated @RequestBody PostPayCreateRequest postPayCreateRequest, BindingResult bindingResult) { +// public BaseResponse createPay(@JwtLoginAuth Long userId, @PathVariable Long spaceId, @Validated @RequestBody PayCreateRequest payCreateRequest, BindingResult bindingResult) { // // // TODO 1. request dto validation // if (bindingResult.hasErrors()) { @@ -117,7 +120,7 @@ public BaseResponse showPayHome(@JwtLoginAuth Long userId, // // 현재 검증 시 에러가 발생하면 그냥 "스페이스에 속하는 유저가 아닙니다" 라는 에러메시지만 나오고, // // 어떤 유저가 스페이스에 속하지 않는지에 대한 정보가 없음 // // => 추후 에러메시지의 수정이 필요할듯?? -// for (PostPayCreateRequest.TargetInfo targetInfo : postPayCreateRequest.getTargetInfoList()) { +// for (PayCreateRequest.TargetInfo targetInfo : payCreateRequest.getTargetInfoList()) { // validateIsUserInSpace(targetInfo.getTargetUserId(), spaceId); // } // @@ -126,10 +129,10 @@ public BaseResponse showPayHome(@JwtLoginAuth Long userId, // // // TODO 5. 정산 요청 금액의 유효성 검사 // // return 값 : 미정산 금액 -// int unRequestedAmount = validatePayAmount(postPayCreateRequest); +// int unRequestedAmount = validatePayAmount(payCreateRequest); // // // TODO 6. 정산 생성 -// payService.createPay(userId, spaceId, postPayCreateRequest, unRequestedAmount); +// payService.createPay(userId, spaceId, payCreateRequest, unRequestedAmount); // // return new BaseResponse<>("정산 생성 성공"); // } diff --git a/src/main/java/space/space_spring/domain/pay/model/EqualSplitPayAmountPolicy.java b/src/main/java/space/space_spring/domain/pay/model/EqualSplitPayAmountPolicy.java new file mode 100644 index 00000000..ff75215a --- /dev/null +++ b/src/main/java/space/space_spring/domain/pay/model/EqualSplitPayAmountPolicy.java @@ -0,0 +1,25 @@ +package space.space_spring.domain.pay.model; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import space.space_spring.exception.CustomException; + +import java.util.List; + +import static space.space_spring.response.status.BaseExceptionResponseStatus.INVALID_EQUAL_SPLIT_AMOUNT; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class EqualSplitPayAmountPolicy implements PayAmountPolicy { + + @Override + public void validatePayAmount(int totalAmount, List targetAmounts) { + int numberOfTarget = targetAmounts.size(); + int expectedAmountPerTarget = totalAmount / numberOfTarget; + + for (Integer targetAmount : targetAmounts) { + if (targetAmount != expectedAmountPerTarget) { + throw new CustomException(INVALID_EQUAL_SPLIT_AMOUNT); + } + } + } +} diff --git a/src/main/java/space/space_spring/domain/pay/model/IndividualPayAmountPolicy.java b/src/main/java/space/space_spring/domain/pay/model/IndividualPayAmountPolicy.java new file mode 100644 index 00000000..5524bbee --- /dev/null +++ b/src/main/java/space/space_spring/domain/pay/model/IndividualPayAmountPolicy.java @@ -0,0 +1,23 @@ +package space.space_spring.domain.pay.model; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import space.space_spring.exception.CustomException; + +import java.util.List; + +import static space.space_spring.response.status.BaseExceptionResponseStatus.INVALID_INDIVIDUAL_AMOUNT; + + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class IndividualPayAmountPolicy implements PayAmountPolicy { + + @Override + public void validatePayAmount(int totalAmount, List targetAmounts) { + int sumOfTargetAmounts = targetAmounts.stream().mapToInt(Integer::intValue).sum(); + + if (sumOfTargetAmounts != totalAmount) { + throw new CustomException(INVALID_INDIVIDUAL_AMOUNT); + } + } +} diff --git a/src/main/java/space/space_spring/domain/pay/model/Money.java b/src/main/java/space/space_spring/domain/pay/model/Money.java new file mode 100644 index 00000000..ae8de676 --- /dev/null +++ b/src/main/java/space/space_spring/domain/pay/model/Money.java @@ -0,0 +1,7 @@ +package space.space_spring.domain.pay.model; + +public class Money { + + + +} \ No newline at end of file diff --git a/src/main/java/space/space_spring/domain/pay/model/PayAmountPolicy.java b/src/main/java/space/space_spring/domain/pay/model/PayAmountPolicy.java new file mode 100644 index 00000000..615044ab --- /dev/null +++ b/src/main/java/space/space_spring/domain/pay/model/PayAmountPolicy.java @@ -0,0 +1,8 @@ +package space.space_spring.domain.pay.model; + +import java.util.List; + +public interface PayAmountPolicy { + + void validatePayAmount(int totalAmount, List targetAmounts); +} diff --git a/src/main/java/space/space_spring/domain/pay/model/PayCreateTargetInfo.java b/src/main/java/space/space_spring/domain/pay/model/PayCreateTargetInfo.java new file mode 100644 index 00000000..957d18cd --- /dev/null +++ b/src/main/java/space/space_spring/domain/pay/model/PayCreateTargetInfo.java @@ -0,0 +1,20 @@ +package space.space_spring.domain.pay.model; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class PayCreateTargetInfo { + + private Long targetUserId; + + private int requestedAmount; + + @Builder + private PayCreateTargetInfo(Long targetUserId, int requestedAmount) { + this.targetUserId = targetUserId; + this.requestedAmount = requestedAmount; + } +} diff --git a/src/main/java/space/space_spring/domain/pay/model/PayCreateValidator.java b/src/main/java/space/space_spring/domain/pay/model/PayCreateValidator.java new file mode 100644 index 00000000..7b50d277 --- /dev/null +++ b/src/main/java/space/space_spring/domain/pay/model/PayCreateValidator.java @@ -0,0 +1,16 @@ +package space.space_spring.domain.pay.model; + +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +@Component +public class PayCreateValidator { + + public void validatePayAmount(PayType payType, int totalAmount, List targetAmounts) { + payType.getPayAmountPolicy().validatePayAmount(totalAmount, targetAmounts); + } + + +} diff --git a/src/main/java/space/space_spring/domain/pay/model/PayType.java b/src/main/java/space/space_spring/domain/pay/model/PayType.java new file mode 100644 index 00000000..e8d4e0c4 --- /dev/null +++ b/src/main/java/space/space_spring/domain/pay/model/PayType.java @@ -0,0 +1,21 @@ +package space.space_spring.domain.pay.model; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public enum PayType { + /** + * INDIVIDUAL : 정산 요청 금액을 직접 입력 + * EQUAL_SPLIT : 정산 요청 금액을 1/n 분배 + */ + + INDIVIDUAL(new IndividualPayAmountPolicy()), + EQUAL_SPLIT(new EqualSplitPayAmountPolicy()); + + private final PayAmountPolicy payAmountPolicy; + + PayType (PayAmountPolicy payAmountPolicy) { + this.payAmountPolicy = payAmountPolicy; + } +} diff --git a/src/main/java/space/space_spring/domain/pay/model/entity/PayRequest.java b/src/main/java/space/space_spring/domain/pay/model/entity/PayRequest.java index d0cdd8a9..bc341759 100644 --- a/src/main/java/space/space_spring/domain/pay/model/entity/PayRequest.java +++ b/src/main/java/space/space_spring/domain/pay/model/entity/PayRequest.java @@ -5,6 +5,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import space.space_spring.domain.pay.model.PayType; import space.space_spring.domain.pay.model.dto.PayRequestInfoDto; import space.space_spring.domain.pay.model.firstCollection.PayRequestTargets; import space.space_spring.domain.user.model.entity.User; @@ -44,6 +45,9 @@ public class PayRequest extends BaseEntity { @Column(name = "receive_amount") private int receiveAmount; // 정산 받은 금액 + @Enumerated(EnumType.STRING) + private PayType payType; + @Column(name = "is_complete") private boolean isComplete; @@ -64,7 +68,7 @@ public void addPayRequestTarget(PayRequestTarget payRequestTarget) { } @Builder - private PayRequest(User payCreateUser, Space space, int totalAmount, String bankName, String bankAccountNum) { + private PayRequest(User payCreateUser, Space space, int totalAmount, String bankName, String bankAccountNum, PayType payType) { this.payCreateUser = payCreateUser; this.space = space; this.totalAmount = totalAmount; @@ -72,15 +76,17 @@ private PayRequest(User payCreateUser, Space space, int totalAmount, String bank this.bankAccountNum = bankAccountNum; this.receiveAmount = 0; this.isComplete = false; + this.payType = payType; } - public static PayRequest create(User payCreateUser, Space space, int totalAmount, String bankName, String bankAccountNum) { + public static PayRequest create(User payCreateUser, Space space, int totalAmount, String bankName, String bankAccountNum, PayType payType) { PayRequest build = PayRequest.builder() .payCreateUser(payCreateUser) .space(space) .totalAmount(totalAmount) .bankName(bankName) .bankAccountNum(bankAccountNum) + .payType(payType) .build(); build.payRequestTargets = PayRequestTargets.create(new ArrayList<>()); diff --git a/src/main/java/space/space_spring/domain/pay/model/request/PayCreateRequest.java b/src/main/java/space/space_spring/domain/pay/model/request/PayCreateRequest.java new file mode 100644 index 00000000..6cc98486 --- /dev/null +++ b/src/main/java/space/space_spring/domain/pay/model/request/PayCreateRequest.java @@ -0,0 +1,61 @@ +package space.space_spring.domain.pay.model.request; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import space.space_spring.domain.pay.model.PayCreateTargetInfo; +import space.space_spring.domain.pay.model.PayType; +import space.space_spring.global.common.EnumValidator; + +import java.util.List; + +@Getter +@NoArgsConstructor +public class PayCreateRequest { + + /** + * PayRequest 엔티티 생성 시 필요한 정보 + */ + + @NotNull(message = "총 정산 요청 금액은 공백일 수 없습니다.") + @Positive(message = "총 정산 요청 금액은 양수이어야 합니다.") + private int totalAmount; + + @NotBlank(message = "은행 이름은 공백일 수 없습니다.") + private String bankName; + + @NotBlank(message = "은행 계좌 번호는 공백일 수 없습니다.") + private String bankAccountNum; + + @Valid + private List targetInfoRequests; + + @EnumValidator(enumClass = PayType.class, message = "payType은 INDIVIDUAL 또는 EQUAL_SPLIT 이어야 합니다.") + private String payType; + + + @Builder + private PayCreateRequest(int totalAmount, String bankName, String bankAccountNum, List targetInfoRequests, String payType) { + this.totalAmount = totalAmount; + this.bankName = bankName; + this.bankAccountNum = bankAccountNum; + this.targetInfoRequests = targetInfoRequests; + this.payType = payType; + } + + public PayCreateServiceRequest toServiceRequest() { + return PayCreateServiceRequest.builder() + .totalAmount(totalAmount) + .bankName(bankName) + .bankAccountNum(bankAccountNum) + .payCreateTargetInfos(targetInfoRequests.stream() + .map(TargetInfoRequest::toPayCreateTargetInfo) + .toList()) + .payType(PayType.valueOf(payType)) + .build(); + } +} diff --git a/src/main/java/space/space_spring/domain/pay/model/request/PayCreateServiceRequest.java b/src/main/java/space/space_spring/domain/pay/model/request/PayCreateServiceRequest.java new file mode 100644 index 00000000..d2bcb72a --- /dev/null +++ b/src/main/java/space/space_spring/domain/pay/model/request/PayCreateServiceRequest.java @@ -0,0 +1,34 @@ +package space.space_spring.domain.pay.model.request; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import space.space_spring.domain.pay.model.PayCreateTargetInfo; +import space.space_spring.domain.pay.model.PayType; + +import java.util.List; + +@Getter +@NoArgsConstructor +public class PayCreateServiceRequest { + + private int totalAmount; + + private String bankName; + + private String bankAccountNum; + + private List payCreateTargetInfos; + + private PayType payType; + + @Builder + private PayCreateServiceRequest(int totalAmount, String bankName, String bankAccountNum, List payCreateTargetInfos, PayType payType) { + this.totalAmount = totalAmount; + this.bankName = bankName; + this.bankAccountNum = bankAccountNum; + this.payCreateTargetInfos = payCreateTargetInfos; + this.payType = payType; + } + +} diff --git a/src/main/java/space/space_spring/domain/pay/model/request/PostPayCreateRequest.java b/src/main/java/space/space_spring/domain/pay/model/request/PostPayCreateRequest.java deleted file mode 100644 index b6ae0411..00000000 --- a/src/main/java/space/space_spring/domain/pay/model/request/PostPayCreateRequest.java +++ /dev/null @@ -1,46 +0,0 @@ -package space.space_spring.domain.pay.model.request; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.util.ArrayList; -import java.util.List; - -@Getter -@NoArgsConstructor -@AllArgsConstructor -public class PostPayCreateRequest { - - /** - * PayRequest 엔티티 생성 시 필요한 정보 - */ - @NotNull(message = "총 정산 요청 금액은 공백일 수 없습니다.") - private int totalAmount; - - @NotBlank(message = "은행 이름은 공백일 수 없습니다.") - private String bankName; - - @NotBlank(message = "은행 계좌 번호는 공백일 수 없습니다.") - private String bankAccountNum; - - /** - * PayRequestTarget 엔티티 생성 시 필요한 정보 - * 쌍 - */ - private List targetInfoList = new ArrayList<>(); - - @Getter - @NoArgsConstructor - @AllArgsConstructor - public static class TargetInfo { - - @NotBlank(message = "정산 요청 타겟 유저의 id값은 공백일 수 없습니다.") - private Long targetUserId; - - @NotBlank(message = "정산 요청 금액은 공백일 수 없습니다.") - private int requestedAmount; - } -} diff --git a/src/main/java/space/space_spring/domain/pay/model/request/TargetInfoRequest.java b/src/main/java/space/space_spring/domain/pay/model/request/TargetInfoRequest.java new file mode 100644 index 00000000..789a6850 --- /dev/null +++ b/src/main/java/space/space_spring/domain/pay/model/request/TargetInfoRequest.java @@ -0,0 +1,40 @@ +package space.space_spring.domain.pay.model.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import space.space_spring.domain.pay.model.PayCreateTargetInfo; + +@Getter +@NoArgsConstructor +public class TargetInfoRequest { + + /** + * PayRequestTarget 엔티티 생성 시 필요한 정보 + * 쌍 + */ + + @NotNull(message = "정산 요청 타겟 유저의 id값은 공백일 수 없습니다.") + private Long targetUserId; + + @NotNull(message = "정산 요청 금액은 공백일 수 없습니다.") + @Positive(message = "정산 요청 금액은 양수이어야 합니다.") + private int requestedAmount; + + + @Builder + private TargetInfoRequest(Long targetUserId, int requestedAmount) { + this.targetUserId = targetUserId; + this.requestedAmount = requestedAmount; + } + + public PayCreateTargetInfo toPayCreateTargetInfo() { + return PayCreateTargetInfo.builder() + .targetUserId(targetUserId) + .requestedAmount(requestedAmount) + .build(); + } +} diff --git a/src/main/java/space/space_spring/domain/pay/repository/PayDao.java b/src/main/java/space/space_spring/domain/pay/repository/PayDao.java index e31b0bd9..3ec4d13f 100644 --- a/src/main/java/space/space_spring/domain/pay/repository/PayDao.java +++ b/src/main/java/space/space_spring/domain/pay/repository/PayDao.java @@ -4,6 +4,7 @@ import jakarta.persistence.PersistenceContext; import jakarta.persistence.TypedQuery; import org.springframework.stereotype.Repository; +import space.space_spring.domain.pay.model.PayType; import space.space_spring.domain.pay.model.dto.RecentPayRequestBankInfoDto; import space.space_spring.domain.pay.model.entity.PayRequest; import space.space_spring.domain.pay.model.entity.PayRequestTarget; @@ -73,7 +74,7 @@ public List findRecentPayRequestBankInfoByUser(User } public PayRequest createPayRequest(User payCreateUser, Space space, int totalAmount, String bankName, String bankAccountNum) { - PayRequest payRequest = PayRequest.create(payCreateUser, space, totalAmount, bankName, bankAccountNum); + PayRequest payRequest = PayRequest.create(payCreateUser, space, totalAmount, bankName, bankAccountNum, PayType.INDIVIDUAL); em.persist(payRequest); return payRequest; diff --git a/src/main/java/space/space_spring/domain/pay/repository/PayRequestRepository.java b/src/main/java/space/space_spring/domain/pay/repository/PayRequestRepository.java index d5ae2025..d5025123 100644 --- a/src/main/java/space/space_spring/domain/pay/repository/PayRequestRepository.java +++ b/src/main/java/space/space_spring/domain/pay/repository/PayRequestRepository.java @@ -16,4 +16,5 @@ public interface PayRequestRepository extends JpaRepository { @Query("select pr from PayRequest pr where pr.payCreateUser = :user and pr.space = :space and pr.isComplete = :isComplete and pr.status = 'ACTIVE'") List findAllByUserAndSpace(User user, Space space, boolean isComplete); + } diff --git a/src/main/java/space/space_spring/domain/pay/service/PayService.java b/src/main/java/space/space_spring/domain/pay/service/PayService.java index a3510111..dd1e0101 100644 --- a/src/main/java/space/space_spring/domain/pay/service/PayService.java +++ b/src/main/java/space/space_spring/domain/pay/service/PayService.java @@ -3,15 +3,17 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import space.space_spring.domain.pay.model.PayCreateValidator; import space.space_spring.domain.pay.model.firstCollection.PayRequestInfos; import space.space_spring.domain.pay.model.firstCollection.PayRequestTargets; import space.space_spring.domain.pay.model.firstCollection.PayRequests; import space.space_spring.domain.pay.model.firstCollection.PayTargetInfos; import space.space_spring.domain.pay.model.mapper.PayMapper; +import space.space_spring.domain.pay.model.request.PayCreateServiceRequest; +import space.space_spring.domain.pay.model.PayCreateTargetInfo; import space.space_spring.domain.userSpace.model.entity.UserSpace; import space.space_spring.domain.userSpace.repository.UserSpaceRepository; import space.space_spring.domain.pay.model.entity.PayRequest; -import space.space_spring.domain.pay.model.dto.*; import space.space_spring.domain.pay.model.entity.PayRequestTarget; import space.space_spring.domain.pay.model.response.PayHomeViewResponse; import space.space_spring.domain.pay.repository.PayRequestRepository; @@ -38,6 +40,7 @@ public class PayService { private final PayRequestRepository payRequestRepository; private final PayRequestTargetRepository payRequestTargetRepository; private final PayMapper payMapper; + private final PayCreateValidator payCreateValidator; private final boolean INCOMPLETE_PAY = false; private final boolean COMPLETE_PAY = true; @@ -76,6 +79,47 @@ private PayTargetInfos getPayTargetInfos(UserSpace userSpace, boolean payRequest return payRequestTargets.getPayTargetInfos(); } + @Transactional + public Long createPay(Long userId, Long spaceId, PayCreateServiceRequest serviceRequest) { + // 유저(= 정산 생성자)가 스페이스에 속하는 지 검증하고, + UserSpace userSpace = validatePayCreatorInSpace(userId, spaceId); + + // 정산 타겟 유저들이 모두 스페이스에 속하는 지 검증하고, + for (PayCreateTargetInfo targetInfo : serviceRequest.getPayCreateTargetInfos()) { + validatePayTargetInSpace(targetInfo.getTargetUserId(), spaceId); + } + + // 정산 요청 금액의 유효성을 검사하고, + List targetAmounts = new ArrayList<>(); + for (PayCreateTargetInfo payCreateTargetInfo : serviceRequest.getPayCreateTargetInfos()) { + targetAmounts.add(payCreateTargetInfo.getRequestedAmount()); + } + + payCreateValidator.validatePayAmount(serviceRequest.getPayType(), serviceRequest.getTotalAmount(), targetAmounts); + + // 정산 관련 엔티티들을 생성 & 저장 + PayRequest payRequest = PayRequest.create(userSpace.getUser(), userSpace.getSpace(), serviceRequest.getTotalAmount(), serviceRequest.getBankName(), serviceRequest.getBankAccountNum(), serviceRequest.getPayType()); + PayRequest save = payRequestRepository.save(payRequest); + + for (PayCreateTargetInfo targetInfo : serviceRequest.getPayCreateTargetInfos()) { + PayRequestTarget payRequestTarget = PayRequestTarget.create(save, targetInfo.getTargetUserId(), targetInfo.getRequestedAmount()); + payRequestTargetRepository.save(payRequestTarget); + } + + return save.getPayRequestId(); + } + + private UserSpace validatePayCreatorInSpace(Long payCreatorUserId, Long spaceId) { + return userSpaceRepository.findUserSpaceByUserAndSpace(payCreatorUserId, spaceId).orElseThrow(() -> new CustomException(PAY_CREATOR_IS_NOT_IN_SPACE)); + + } + + private UserSpace validatePayTargetInSpace(Long targetUserId, Long spaceId) { + return userSpaceRepository.findUserSpaceByUserAndSpace(targetUserId, spaceId).orElseThrow(() -> new CustomException(PAY_TARGET_IS_NOT_IN_SPACE)); + + } + + // @Transactional // public List getPayRequestInfoForUser(Long userId, Long spaceId, boolean isComplete) { diff --git a/src/main/java/space/space_spring/global/common/EnumValidator.java b/src/main/java/space/space_spring/global/common/EnumValidator.java new file mode 100644 index 00000000..01e92b86 --- /dev/null +++ b/src/main/java/space/space_spring/global/common/EnumValidator.java @@ -0,0 +1,25 @@ +package space.space_spring.global.common; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = EnumValidatorConstraint.class) +@Target({ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface EnumValidator { + + /** + * 특정 enum 클래스의 값만 허용하도록 검증하는 커스텀 어노테이션 + */ + + Class> enumClass(); + + String message() default "값이 올바르지 않습니다."; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/space/space_spring/global/common/EnumValidatorConstraint.java b/src/main/java/space/space_spring/global/common/EnumValidatorConstraint.java new file mode 100644 index 00000000..76a6ed42 --- /dev/null +++ b/src/main/java/space/space_spring/global/common/EnumValidatorConstraint.java @@ -0,0 +1,26 @@ +package space.space_spring.global.common; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +import java.util.Arrays; + +public class EnumValidatorConstraint implements ConstraintValidator { + + private Class> enumClass; + + @Override + public void initialize(EnumValidator constraintAnnotation) { + this.enumClass = constraintAnnotation.enumClass(); + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (value == null) { + return true; + } + + return Arrays.stream(enumClass.getEnumConstants()) + .anyMatch(e -> e.name().equals(value)); + } +} diff --git a/src/main/java/space/space_spring/response/status/BaseExceptionResponseStatus.java b/src/main/java/space/space_spring/response/status/BaseExceptionResponseStatus.java index eef91e92..62dc1749 100644 --- a/src/main/java/space/space_spring/response/status/BaseExceptionResponseStatus.java +++ b/src/main/java/space/space_spring/response/status/BaseExceptionResponseStatus.java @@ -123,10 +123,13 @@ public enum BaseExceptionResponseStatus implements ResponseStatus { * 12000 : Pay 오류 */ INVALID_PAY_CREATE(12000, HttpStatus.BAD_REQUEST, "정산 생성 요청에서 잘못된 값이 존재합니다."), - INVALID_PAY_AMOUNT(12001, HttpStatus.BAD_REQUEST, "정산 요청 금액들의 합과 정산 요청 총 금액이 일치하지 않습니다."), - PAY_REQUEST_NOT_FOUND(12002, HttpStatus.NOT_FOUND, "존재하지 않는 정산입니다."), - PAY_REQUEST_TARGET_NOT_FOUND(12003, HttpStatus.NOT_FOUND, "존재하지 않는 정산요청타겟 입니다"), - INVALID_PAY_REQUEST_TARGET_ID(12004, HttpStatus.BAD_REQUEST, "정산 요청 타겟 id의 타겟 유저가 본인과 일치하지 않습니다. 본인의 정산에 대해서만 완료처리를 할 수 있습니다."); + PAY_CREATOR_IS_NOT_IN_SPACE(12001, HttpStatus.BAD_REQUEST, "정산 생성자가 해당 스페이스에 속하지 않는 유저입니다. 정산 생성은 본인이 속한 스페이스 내에서만 가능합니다."), + PAY_TARGET_IS_NOT_IN_SPACE(12002, HttpStatus.BAD_REQUEST, "정산 요청 타겟들 중, 해당 스페이스에 속하지 않는 유저가 존재합니다."), + INVALID_INDIVIDUAL_AMOUNT(12003, HttpStatus.BAD_REQUEST, "정산 요청 금액들의 합과 정산 요청 총 금액이 일치하지 않습니다."), + INVALID_EQUAL_SPLIT_AMOUNT(12004, HttpStatus.BAD_REQUEST, "정산 요청 금액들 중 1/N 정산 정책에 위배되는 값이 있습니다."), + PAY_REQUEST_NOT_FOUND(12005, HttpStatus.NOT_FOUND, "존재하지 않는 정산입니다."), + PAY_REQUEST_TARGET_NOT_FOUND(12006, HttpStatus.NOT_FOUND, "존재하지 않는 정산요청타겟 입니다"), + INVALID_PAY_REQUEST_TARGET_ID(12007, HttpStatus.BAD_REQUEST, "정산 요청 타겟 id의 타겟 유저가 본인과 일치하지 않습니다. 본인의 정산에 대해서만 완료처리를 할 수 있습니다."); private final int code; private final HttpStatus status; diff --git a/src/test/java/space/space_spring/domain/pay/controller/PayControllerTest.java b/src/test/java/space/space_spring/domain/pay/controller/PayControllerTest.java new file mode 100644 index 00000000..5edea02c --- /dev/null +++ b/src/test/java/space/space_spring/domain/pay/controller/PayControllerTest.java @@ -0,0 +1,132 @@ +package space.space_spring.domain.pay.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import space.space_spring.argumentResolver.jwtLogin.JwtLoginAuthHandlerArgumentResolver; +import space.space_spring.argumentResolver.userSpace.UserSpaceAuthHandlerArgumentResolver; +import space.space_spring.argumentResolver.userSpace.UserSpaceIdHandlerArgumentResolver; +import space.space_spring.domain.pay.model.request.PayCreateRequest; +import space.space_spring.domain.pay.model.request.TargetInfoRequest; +import space.space_spring.domain.pay.service.PayService; +import space.space_spring.interceptor.UserSpaceValidationInterceptor; +import space.space_spring.interceptor.jwtLogin.JwtLoginAuthInterceptor; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; + +@WebMvcTest(controllers = PayController.class) +class PayControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private PayService payService; // PayController 의 bean 생성을 위함 + + @MockBean + private JwtLoginAuthInterceptor jwtLoginAuthInterceptor; + + @MockBean + private UserSpaceValidationInterceptor userSpaceValidationInterceptor; + + @MockBean + private JwtLoginAuthHandlerArgumentResolver jwtLoginAuthHandlerArgumentResolver; + + @MockBean + private UserSpaceIdHandlerArgumentResolver userSpaceIdHandlerArgumentResolver; + + @MockBean + private UserSpaceAuthHandlerArgumentResolver userSpaceAuthHandlerArgumentResolver; + + @BeforeEach + void setUp() throws Exception { + // Mock the behavior of interceptors to always return true + given(jwtLoginAuthInterceptor.preHandle(any(), any(), any())).willReturn(true); + given(userSpaceValidationInterceptor.preHandle(any(), any(), any())).willReturn(true); + + // Mock the behavior of argument resolvers + given(jwtLoginAuthHandlerArgumentResolver.supportsParameter(any())).willReturn(true); + given(jwtLoginAuthHandlerArgumentResolver.resolveArgument(any(), any(), any(), any())) + .willReturn(1L); // Mock userId + + given(userSpaceIdHandlerArgumentResolver.supportsParameter(any())).willReturn(true); + given(userSpaceIdHandlerArgumentResolver.resolveArgument(any(), any(), any(), any())) + .willReturn(1L); // Mock userSpaceId + + given(userSpaceAuthHandlerArgumentResolver.supportsParameter(any())).willReturn(true); + given(userSpaceAuthHandlerArgumentResolver.resolveArgument(any(), any(), any(), any())) + .willReturn("SomeAuth"); // Mock auth + } + + + @Test + @DisplayName("정산 생성자에게 받은 정보에 따라서 정산을 생성한다.") + @WithMockUser // 인증된 사용자 요청 가정 + void createPay() throws Exception { + //given + TargetInfoRequest targetInfoRequest1 = TargetInfoRequest.builder() + .targetUserId(1L) + .requestedAmount(10000) + .build(); + TargetInfoRequest targetInfoRequest2 = TargetInfoRequest.builder() + .targetUserId(2L) + .requestedAmount(20000) + .build(); + List targetInfoRequests = List.of(targetInfoRequest1, targetInfoRequest2); + + PayCreateRequest request = PayCreateRequest.builder() + .totalAmount(30000) + .bankName("우리은행") + .bankAccountNum("111-111") + .targetInfoRequests(targetInfoRequests) + .payType("INDIVIDUAL") + .build(); + + //when //then + mockMvc.perform( + post("/space/1/pay") + .content(objectMapper.writeValueAsString(request)) + .contentType(MediaType.APPLICATION_JSON) + .with(csrf()) // csrf 토큰 추가 + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value("1000")) + .andExpect(jsonPath("$.status").value("OK")) + .andExpect(jsonPath("$.message").value("요청에 성공하였습니다.")); + + } + + @Test + @DisplayName("") + void test() throws Exception { + //given + + //when + + //then + } + + +} \ No newline at end of file diff --git a/src/test/java/space/space_spring/domain/pay/model/EqualSplitPayAmountPolicyTest.java b/src/test/java/space/space_spring/domain/pay/model/EqualSplitPayAmountPolicyTest.java new file mode 100644 index 00000000..f2f83be1 --- /dev/null +++ b/src/test/java/space/space_spring/domain/pay/model/EqualSplitPayAmountPolicyTest.java @@ -0,0 +1,68 @@ +package space.space_spring.domain.pay.model; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import space.space_spring.exception.CustomException; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; +import static space.space_spring.response.status.BaseExceptionResponseStatus.INVALID_EQUAL_SPLIT_AMOUNT; + +class EqualSplitPayAmountPolicyTest { + + @Test + @DisplayName("전체 정산 금액을 1/N 한 금액과 정산 타겟들이 각각 요청받은 금액이 일치하는지 검증한다.") + void validatePayAmount1() throws Exception { + //given + int totalAmount = 40000; + int requestedAmount1 = 10000; + int requestedAmount2 = 10000; + int requestedAmount3 = 10000; + int requestedAmount4 = 10000; + List targetAmounts = List.of(requestedAmount1, requestedAmount2, requestedAmount3, requestedAmount4); + + EqualSplitPayAmountPolicy equalSplitPayAmountPolicy = new EqualSplitPayAmountPolicy(); + + //when //then + assertDoesNotThrow(() -> equalSplitPayAmountPolicy.validatePayAmount(totalAmount, targetAmounts)); + } + + @Test + @DisplayName("전체 정산 금액이 나누어 떨어지지 않는 경우, 정수의 나눗셈 계산을 통해 얻은 값을 정산 타겟들에게 요청한 금액으로 간주한다.") + void validatePayAmount2() throws Exception { + //given + int totalAmount = 10000; + int requestedAmount1 = 3333; + int requestedAmount2 = 3333; + int requestedAmount3 = 3333; + List targetAmounts = List.of(requestedAmount1, requestedAmount2, requestedAmount3); + + EqualSplitPayAmountPolicy equalSplitPayAmountPolicy = new EqualSplitPayAmountPolicy(); + + //when //then + assertDoesNotThrow(() -> equalSplitPayAmountPolicy.validatePayAmount(totalAmount, targetAmounts)); + } + + @Test + @DisplayName("전체 정산 금액을 1/N 한 금액과 정산 타겟들이 각각 요청받은 금액이 일치하지 않을 경우, 예외를 발생시킨다.") + void validatePayAmount3() throws Exception { + //given + int totalAmount = 40000; + int requestedAmount1 = 10000; + int requestedAmount2 = 10000; + int requestedAmount3 = 10000; + int requestedAmount4 = 15000; // 10000원이 아님 + List targetAmounts = List.of(requestedAmount1, requestedAmount2, requestedAmount3, requestedAmount4); + + EqualSplitPayAmountPolicy equalSplitPayAmountPolicy = new EqualSplitPayAmountPolicy(); + + //when //then + assertThatThrownBy(() -> equalSplitPayAmountPolicy.validatePayAmount(totalAmount, targetAmounts)) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_EQUAL_SPLIT_AMOUNT.getMessage()); + } + + +} \ No newline at end of file diff --git a/src/test/java/space/space_spring/domain/pay/model/IndividualPayAmountPolicyTest.java b/src/test/java/space/space_spring/domain/pay/model/IndividualPayAmountPolicyTest.java new file mode 100644 index 00000000..636f3e97 --- /dev/null +++ b/src/test/java/space/space_spring/domain/pay/model/IndividualPayAmountPolicyTest.java @@ -0,0 +1,50 @@ +package space.space_spring.domain.pay.model; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import space.space_spring.exception.CustomException; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; +import static space.space_spring.response.status.BaseExceptionResponseStatus.INVALID_INDIVIDUAL_AMOUNT; + +class IndividualPayAmountPolicyTest { + + @Test + @DisplayName("전체 정산 금액과 정산 타겟들이 요청받은 금액들의 합이 일치하는지 검증한다.") + void validatePayAmount1() throws Exception { + //given + int totalAmount = 40000; + int requestedAmount1 = 16000; + int requestedAmount2 = 15000; + int requestedAmount3 = 5000; + int requestedAmount4 = 4000; + List targetAmounts = List.of(requestedAmount1, requestedAmount2, requestedAmount3, requestedAmount4); + + IndividualPayAmountPolicy individualPayAmountPolicy = new IndividualPayAmountPolicy(); + + //when //then + assertDoesNotThrow(() -> individualPayAmountPolicy.validatePayAmount(totalAmount, targetAmounts)); + } + + @Test + @DisplayName("전체 정산 금액과 정산 타겟들이 요청받은 금액들의 합이 일치하지 않을 경우, 예외를 발생시킨다.") + void validatePayAmount2() throws Exception { + //given + int totalAmount = 40000; + int requestedAmount1 = 16000; + int requestedAmount2 = 15000; + int requestedAmount3 = 5000; + int requestedAmount4 = 1000; // 합은 37000 원 + List targetAmounts = List.of(requestedAmount1, requestedAmount2, requestedAmount3, requestedAmount4); + + IndividualPayAmountPolicy individualPayAmountPolicy = new IndividualPayAmountPolicy(); + + //when //then + assertThatThrownBy(() -> individualPayAmountPolicy.validatePayAmount(totalAmount, targetAmounts)) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_INDIVIDUAL_AMOUNT.getMessage()); + } +} \ No newline at end of file diff --git a/src/test/java/space/space_spring/domain/pay/model/PayCreateValidatorTest.java b/src/test/java/space/space_spring/domain/pay/model/PayCreateValidatorTest.java new file mode 100644 index 00000000..a44cbb36 --- /dev/null +++ b/src/test/java/space/space_spring/domain/pay/model/PayCreateValidatorTest.java @@ -0,0 +1,7 @@ +package space.space_spring.domain.pay.model; + +import static org.junit.jupiter.api.Assertions.*; + +class PayCreateValidatorTest { + +} \ No newline at end of file diff --git a/src/test/java/space/space_spring/domain/pay/model/entity/PayRequestTest.java b/src/test/java/space/space_spring/domain/pay/model/entity/PayRequestTest.java index a18576e6..b984bdf6 100644 --- a/src/test/java/space/space_spring/domain/pay/model/entity/PayRequestTest.java +++ b/src/test/java/space/space_spring/domain/pay/model/entity/PayRequestTest.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import space.space_spring.domain.pay.model.PayType; import space.space_spring.domain.pay.model.dto.PayRequestInfoDto; import space.space_spring.domain.space.model.entity.Space; import space.space_spring.domain.user.model.entity.User; @@ -19,7 +20,7 @@ void getPayRequestInfo() throws Exception { Space space = Space.create("space", "img"); - PayRequest payRequest = PayRequest.create(payCreator, space, 30000, "우리은행", "111-111"); + PayRequest payRequest = PayRequest.create(payCreator, space, 30000, "우리은행", "111-111", PayType.INDIVIDUAL); PayRequestTarget target1 = PayRequestTarget.create(payRequest, 1L, 10000); PayRequestTarget target2 = PayRequestTarget.create(payRequest, 2L, 10000); diff --git a/src/test/java/space/space_spring/domain/pay/model/firstCollection/PayRequestTargetsTest.java b/src/test/java/space/space_spring/domain/pay/model/firstCollection/PayRequestTargetsTest.java index 6a4250ac..9f3ad02c 100644 --- a/src/test/java/space/space_spring/domain/pay/model/firstCollection/PayRequestTargetsTest.java +++ b/src/test/java/space/space_spring/domain/pay/model/firstCollection/PayRequestTargetsTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import space.space_spring.domain.pay.model.PayType; import space.space_spring.domain.pay.model.entity.PayRequest; import space.space_spring.domain.pay.model.entity.PayRequestTarget; import space.space_spring.domain.space.model.entity.Space; @@ -36,11 +37,11 @@ void setUp() { @DisplayName("멤버 변수인 PayRequestTarget list 로부터 [payRequestTargetId, 정산 생성자 이름, 요청받은 정산 금액, 송금해야할 은행 이름, 계좌번호] 정보를 찾아준다.") void getPayTargetInfos() throws Exception { //given - PayRequest payRequest1 = PayRequest.create(sangjun, kuit, 40000, "우리은행", "111-111"); + PayRequest payRequest1 = PayRequest.create(sangjun, kuit, 40000, "우리은행", "111-111", PayType.INDIVIDUAL); PayRequestTarget target1 = PayRequestTarget.create(payRequest1, 1L, 20000); PayRequestTarget target2 = PayRequestTarget.create(payRequest1, 2L, 20000); - PayRequest payRequest2 = PayRequest.create(seohyun, kuit, 40000, "신한은행", "222-222"); + PayRequest payRequest2 = PayRequest.create(seohyun, kuit, 40000, "신한은행", "222-222", PayType.INDIVIDUAL); PayRequestTarget target3 = PayRequestTarget.create(payRequest2, 1L, 10000); PayRequestTarget target4 = PayRequestTarget.create(payRequest2, 4L, 30000); @@ -62,11 +63,11 @@ void getPayTargetInfos() throws Exception { @DisplayName("멤버 변수인 PayRequestTarget list 의 전체 크기를 알려준다.") void countTotalTargets() throws Exception { //given - PayRequest payRequest1 = PayRequest.create(sangjun, kuit, 40000, "우리은행", "111-111"); + PayRequest payRequest1 = PayRequest.create(sangjun, kuit, 40000, "우리은행", "111-111", PayType.INDIVIDUAL); PayRequestTarget target1 = PayRequestTarget.create(payRequest1, 1L, 20000); PayRequestTarget target2 = PayRequestTarget.create(payRequest1, 2L, 20000); - PayRequest payRequest2 = PayRequest.create(seohyun, kuit, 40000, "신한은행", "222-222"); + PayRequest payRequest2 = PayRequest.create(seohyun, kuit, 40000, "신한은행", "222-222", PayType.INDIVIDUAL); PayRequestTarget target3 = PayRequestTarget.create(payRequest2, 1L, 10000); PayRequestTarget target4 = PayRequestTarget.create(payRequest2, 4L, 30000); @@ -83,11 +84,11 @@ void countTotalTargets() throws Exception { @DisplayName("멤버 변수인 PayRequestTarget list 중 정산을 완료한 타겟이 몇 명인지 알려준다.") void countCompleteTargets() throws Exception { //given - PayRequest payRequest1 = PayRequest.create(sangjun, kuit, 40000, "우리은행", "111-111"); + PayRequest payRequest1 = PayRequest.create(sangjun, kuit, 40000, "우리은행", "111-111", PayType.INDIVIDUAL); PayRequestTarget target1 = PayRequestTarget.create(payRequest1, 1L, 20000); PayRequestTarget target2 = PayRequestTarget.create(payRequest1, 2L, 20000); - PayRequest payRequest2 = PayRequest.create(seohyun, kuit, 40000, "신한은행", "222-222"); + PayRequest payRequest2 = PayRequest.create(seohyun, kuit, 40000, "신한은행", "222-222", PayType.INDIVIDUAL); PayRequestTarget target3 = PayRequestTarget.create(payRequest2, 1L, 10000); PayRequestTarget target4 = PayRequestTarget.create(payRequest2, 4L, 30000); diff --git a/src/test/java/space/space_spring/domain/pay/model/firstCollection/PayRequestsTest.java b/src/test/java/space/space_spring/domain/pay/model/firstCollection/PayRequestsTest.java index bd509276..8ca52311 100644 --- a/src/test/java/space/space_spring/domain/pay/model/firstCollection/PayRequestsTest.java +++ b/src/test/java/space/space_spring/domain/pay/model/firstCollection/PayRequestsTest.java @@ -3,6 +3,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import space.space_spring.domain.pay.model.PayType; import space.space_spring.domain.pay.model.entity.PayRequest; import space.space_spring.domain.pay.model.entity.PayRequestTarget; import space.space_spring.domain.space.model.entity.Space; @@ -36,11 +37,11 @@ void setUp() { @DisplayName("멤버 변수인 PayRequest list 로부터 [payRequestId, 정산 총 금액, 현재까지 받은 금액, 정산 요청한 총 사람 수, 그 중 돈을 보낸 사람 수] 정보를 찾아준다.") void getPayRequestInfos() throws Exception { //given - PayRequest payRequest1 = PayRequest.create(seongjun, kuit, 40000, "우리은행", "111-111"); + PayRequest payRequest1 = PayRequest.create(seongjun, kuit, 40000, "우리은행", "111-111", PayType.INDIVIDUAL); PayRequestTarget target1 = PayRequestTarget.create(payRequest1, 2L, 20000); PayRequestTarget target2 = PayRequestTarget.create(payRequest1, 3L, 20000); - PayRequest payRequest2 = PayRequest.create(seongjun, kuit, 40000, "신한은행", "222-222"); + PayRequest payRequest2 = PayRequest.create(seongjun, kuit, 40000, "신한은행", "222-222", PayType.INDIVIDUAL); PayRequestTarget target3 = PayRequestTarget.create(payRequest2, 3L, 10000); PayRequestTarget target4 = PayRequestTarget.create(payRequest2, 4L, 30000); diff --git a/src/test/java/space/space_spring/domain/pay/service/PayServiceTest.java b/src/test/java/space/space_spring/domain/pay/service/PayServiceTest.java index c3abe89a..c0a04b2d 100644 --- a/src/test/java/space/space_spring/domain/pay/service/PayServiceTest.java +++ b/src/test/java/space/space_spring/domain/pay/service/PayServiceTest.java @@ -7,7 +7,10 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; +import space.space_spring.domain.pay.model.PayCreateTargetInfo; +import space.space_spring.domain.pay.model.PayType; import space.space_spring.domain.pay.model.dto.PayTargetInfoDto; +import space.space_spring.domain.pay.model.request.PayCreateServiceRequest; import space.space_spring.domain.space.repository.SpaceRepository; import space.space_spring.domain.userSpace.repository.UserSpaceRepository; import space.space_spring.domain.pay.model.dto.PayRequestInfoDto; @@ -27,7 +30,7 @@ import java.util.List; import static org.assertj.core.api.Assertions.*; -import static space.space_spring.response.status.BaseExceptionResponseStatus.USER_IS_NOT_IN_SPACE; +import static space.space_spring.response.status.BaseExceptionResponseStatus.*; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Transactional @@ -79,16 +82,16 @@ void setUp() { void getPayHomeInfos1() throws Exception { //given // seongjun 이 kuit 에서 생성한 3개의 정산 요청 - PayRequest inCompletePay1_seongjun = PayRequest.create(seongjun, kuit, 20000, "bank", "accountNum"); - PayRequest inCompletePay2_seongjun = PayRequest.create(seongjun, kuit, 40000, "bank", "accountNum"); - PayRequest completePay_seongjun = PayRequest.create(seongjun, kuit, 20000, "bank", "accountNum"); + PayRequest inCompletePay1_seongjun = PayRequest.create(seongjun, kuit, 20000, "bank", "accountNum", PayType.INDIVIDUAL); + PayRequest inCompletePay2_seongjun = PayRequest.create(seongjun, kuit, 40000, "bank", "accountNum", PayType.INDIVIDUAL); + PayRequest completePay_seongjun = PayRequest.create(seongjun, kuit, 20000, "bank", "accountNum", PayType.INDIVIDUAL); completePay_seongjun.changeCompleteStatus(true); // seohyun 이 kuit 에서 생성한 1개의 정산 요청 - PayRequest inCompletePay_seohyun = PayRequest.create(seohyun, kuit, 40000, "국민은행", "111-111"); + PayRequest inCompletePay_seohyun = PayRequest.create(seohyun, kuit, 40000, "국민은행", "111-111", PayType.INDIVIDUAL); // Kyeongmin 이 kuit 에서 생성한 1개의 정산 요청 - PayRequest completePay_kyeongmin = PayRequest.create(kyeongmin, kuit, 20000, "우리은행", "222-222"); + PayRequest completePay_kyeongmin = PayRequest.create(kyeongmin, kuit, 20000, "우리은행", "222-222", PayType.INDIVIDUAL); completePay_kyeongmin.changeCompleteStatus(true); // 각 정산 요청의 타겟들 @@ -150,7 +153,7 @@ void getPayHomeInfos1() throws Exception { @DisplayName("유저가 요청한 정산들 중, 완료되지 않은 정산이 없으면 빈 ArrayList 를 return 한다.") void getPayHomeInfos2() throws Exception { //given - PayRequest completePay = PayRequest.create(seongjun, kuit, 20000, "bank", "accountNum"); + PayRequest completePay = PayRequest.create(seongjun, kuit, 20000, "bank", "accountNum", PayType.INDIVIDUAL); completePay.changeCompleteStatus(true); PayRequestTarget completeTarget1 = PayRequestTarget.create(completePay, sangjun.getUserId(), 10000); @@ -173,10 +176,10 @@ void getPayHomeInfos2() throws Exception { } @Test - @DisplayName("유저가 요청받은 정산들 중, 완료되지 않은 정산이 없으면 빈 ArrayList를 return 한다.") + @DisplayName("유저가 요청받은 정산들 중, 완료되지 않은 정산이 없으면 빈 ArrayList 를 return 한다.") void getPayHomeInfos3() throws Exception { //given - PayRequest completePay = PayRequest.create(seongjun, kuit, 20000, "bank", "accountNum"); + PayRequest completePay = PayRequest.create(seongjun, kuit, 20000, "bank", "accountNum", PayType.INDIVIDUAL); completePay.changeCompleteStatus(true); PayRequestTarget completeTarget1 = PayRequestTarget.create(completePay, seohyun.getUserId(), 10000); @@ -199,12 +202,12 @@ void getPayHomeInfos3() throws Exception { } @Test - @DisplayName("user는 자신이 속한 space의 정산 홈 view만을 볼 수 있다.") + @DisplayName("유저는 자신이 속한 스페이스의 정산 홈 view 만을 볼 수 있다.") void getPayHomeInfos4() throws Exception { //given Space alcon = spaceRepository.save(Space.create("space", "profileImg")); - PayRequest payRequest = PayRequest.create(seongjun, kuit, 20000, "bank", "accountNum"); + PayRequest payRequest = PayRequest.create(seongjun, kuit, 20000, "bank", "accountNum", PayType.INDIVIDUAL); PayRequestTarget target1 = PayRequestTarget.create(payRequest, sangjun.getUserId(), 10000); PayRequestTarget target2 = PayRequestTarget.create(payRequest, seohyun.getUserId(), 10000); @@ -220,7 +223,164 @@ void getPayHomeInfos4() throws Exception { .hasMessage(USER_IS_NOT_IN_SPACE.getMessage()); } + @Test + @DisplayName("유저는 자신과 동일한 스페이스에 속한 유저들에게 [금액 직접 입력] 정책으로 정산을 생성할 수 있다.") + void createPay1() throws Exception { + //given + PayCreateTargetInfo targetIsSeongjun = createPayCreateTargetInfo(seongjun, 12000); + PayCreateTargetInfo targetIsSangjun = createPayCreateTargetInfo(sangjun, 15000); + PayCreateTargetInfo targetIsSeohyun = createPayCreateTargetInfo(seohyun, 8000); + PayCreateTargetInfo targetIsKyeongmin = createPayCreateTargetInfo(kyeongmin, 5000); + List targetInfos = List.of(targetIsSeongjun, targetIsSangjun, targetIsSeohyun, targetIsKyeongmin); + + PayCreateServiceRequest serviceRequest = createServiceRequest(40000, targetInfos, PayType.INDIVIDUAL); + + //when + Long savedPayRequestId = payService.createPay(seongjun.getUserId(), kuit.getSpaceId(), serviceRequest); + + //then + PayRequest savedPayRequest = payRequestRepository.findById(savedPayRequestId) + .orElseThrow(() -> new AssertionError("PayRequest 가 제대로 생성되지 않았습니다.")); + + assertThat(savedPayRequest).extracting("payCreateUser", "space", "totalAmount", "bankName", "bankAccountNum", "payType") + .contains(seongjun, kuit, 40000, "우리은행", "111-111", PayType.INDIVIDUAL); + + List allByPayRequest = payRequestTargetRepository.findAllByPayRequest(savedPayRequest); + + assertThat(allByPayRequest).hasSize(4) + .extracting("targetUserId", "requestedAmount") + .containsExactlyInAnyOrder( + tuple(seongjun.getUserId(), 12000), + tuple(sangjun.getUserId(), 15000), + tuple(seohyun.getUserId(), 8000), + tuple(kyeongmin.getUserId(), 5000) + ); + } + + @Test + @DisplayName("유저는 자신과 동일한 스페이스에 속한 유저들에게 [1/N 정산하기] 정책으로 정산을 생성할 수 있다.") + void createPay2() throws Exception { + PayCreateTargetInfo targetIsSeongjun = createPayCreateTargetInfo(seongjun, 3333); + PayCreateTargetInfo targetIsSangjun = createPayCreateTargetInfo(sangjun, 3333); + PayCreateTargetInfo targetIsSeohyun = createPayCreateTargetInfo(seohyun, 3333); + List targetInfos = List.of(targetIsSeongjun, targetIsSangjun, targetIsSeohyun); + PayCreateServiceRequest serviceRequest = createServiceRequest(10000, targetInfos, PayType.EQUAL_SPLIT); + + //when + Long savedPayRequestId = payService.createPay(seongjun.getUserId(), kuit.getSpaceId(), serviceRequest); + + //then + PayRequest savedPayRequest = payRequestRepository.findById(savedPayRequestId) + .orElseThrow(() -> new AssertionError("PayRequest 가 제대로 생성되지 않았습니다.")); + + assertThat(savedPayRequest).extracting("payCreateUser", "space", "totalAmount", "bankName", "bankAccountNum", "payType") + .contains(seongjun, kuit, 10000, "우리은행", "111-111", PayType.EQUAL_SPLIT); + + List allByPayRequest = payRequestTargetRepository.findAllByPayRequest(savedPayRequest); + + assertThat(allByPayRequest).hasSize(3) + .extracting("targetUserId", "requestedAmount") + .containsExactlyInAnyOrder( + tuple(seongjun.getUserId(), 3333), + tuple(sangjun.getUserId(), 3333), + tuple(seohyun.getUserId(), 3333) + ); + } + + @Test + @DisplayName("유저는 본인이 속한 스페이스에서만 정산을 생성할 수 있다.") + void createPay3() throws Exception { + //given + PayCreateTargetInfo targetIsSeohyun = createPayCreateTargetInfo(seohyun, 8000); + PayCreateTargetInfo targetIsKyeongmin = createPayCreateTargetInfo(kyeongmin, 5000); + List targetInfos = List.of(targetIsSeohyun, targetIsKyeongmin); + + // alcon 동아리에 속해있는 양석준씨 + User seokjun = userRepository.save(User.create("email1", "password", "양석준", UserSignupType.LOCAL)); + Space alcon = spaceRepository.save(Space.create("space", "profileImg")); + userSpaceRepository.save(UserSpace.create(seokjun, alcon, UserSpaceAuth.NORMAL)); + PayCreateServiceRequest serviceRequest = createServiceRequest(13000, targetInfos, PayType.INDIVIDUAL); + //when //then + assertThatThrownBy(() -> payService.createPay(seokjun.getUserId(), kuit.getSpaceId(), serviceRequest)) + .isInstanceOf(CustomException.class) + .hasMessage(PAY_CREATOR_IS_NOT_IN_SPACE.getMessage()); + } + + @Test + @DisplayName("자신과 다른 스페이스에 속한 유저에게는 정산을 생성할 수 없다.") + void createPay4() throws Exception { + //given + // alcon 동아리에 속해있는 양석준씨 + User seokjun = userRepository.save(User.create("email1", "password", "양석준", UserSignupType.LOCAL)); + Space alcon = spaceRepository.save(Space.create("space", "profileImg")); + userSpaceRepository.save(UserSpace.create(seokjun, alcon, UserSpaceAuth.NORMAL)); + + PayCreateTargetInfo targetIsSeokjun = createPayCreateTargetInfo(seokjun, 12000); + PayCreateTargetInfo targetIsKyeongmin = createPayCreateTargetInfo(kyeongmin, 5000); + List targetInfos = List.of(targetIsSeokjun, targetIsKyeongmin); + + PayCreateServiceRequest serviceRequest = createServiceRequest(17000, targetInfos, PayType.INDIVIDUAL); + + //when //then + assertThatThrownBy(() -> payService.createPay(seongjun.getUserId(), kuit.getSpaceId(), serviceRequest)) + .isInstanceOf(CustomException.class) + .hasMessage(PAY_TARGET_IS_NOT_IN_SPACE.getMessage()); + } + + @Test + @DisplayName("[금액 직접 입력] 정책을 따르는 정산에서 정산 타겟들에게 요청한 금액들의 합이 totalAmount 와 다를 경우, 예외가 발생한다.") + void createPay5() throws Exception { + //given + // 타겟들에게 요청한 금액들의 합은 40000원 + PayCreateTargetInfo targetIsSeongjun = createPayCreateTargetInfo(seongjun, 12000); + PayCreateTargetInfo targetIsSangjun = createPayCreateTargetInfo(sangjun, 15000); + PayCreateTargetInfo targetIsSeohyun = createPayCreateTargetInfo(seohyun, 8000); + PayCreateTargetInfo targetIsKyeongmin = createPayCreateTargetInfo(kyeongmin, 5000); + List targetInfos = List.of(targetIsSeongjun, targetIsSangjun, targetIsSeohyun, targetIsKyeongmin); + + PayCreateServiceRequest serviceRequest = createServiceRequest(30000, targetInfos, PayType.INDIVIDUAL); + + //when //then + assertThatThrownBy(() -> payService.createPay(seongjun.getUserId(), kuit.getSpaceId(), serviceRequest)) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_INDIVIDUAL_AMOUNT.getMessage()); + } + + @Test + @DisplayName("[1/N 정산하기] 정책을 따르는 정산에서 정산 타겟들에게 요청한 금액이 totalAmount/N 과 다를 경우, 예외가 발생한다.") + void createPay6() throws Exception { + //given + // 타겟들에게 요청한 금액들은 3333원 이어야 함 + PayCreateTargetInfo targetIsSeongjun = createPayCreateTargetInfo(seongjun, 3333); + PayCreateTargetInfo targetIsSangjun = createPayCreateTargetInfo(sangjun, 3333); + PayCreateTargetInfo targetIsSeohyun = createPayCreateTargetInfo(seohyun, 3334); // != 3333 + List targetInfos = List.of(targetIsSeongjun, targetIsSangjun, targetIsSeohyun); + + PayCreateServiceRequest serviceRequest = createServiceRequest(10000, targetInfos, PayType.EQUAL_SPLIT); + + //when //then + assertThatThrownBy(() -> payService.createPay(seongjun.getUserId(), kuit.getSpaceId(), serviceRequest)) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_EQUAL_SPLIT_AMOUNT.getMessage()); + } + + private PayCreateTargetInfo createPayCreateTargetInfo(User user, int requestedAmount) { + return PayCreateTargetInfo.builder() + .targetUserId(user.getUserId()) + .requestedAmount(requestedAmount) + .build(); + } + + private PayCreateServiceRequest createServiceRequest(int totalAmount, List targetInfos, PayType payType) { + return PayCreateServiceRequest.builder() + .totalAmount(totalAmount) + .bankName("우리은행") + .bankAccountNum("111-111") + .payCreateTargetInfos(targetInfos) + .payType(payType) + .build(); + } } \ No newline at end of file