diff --git a/.gitignore b/.gitignore index 8daee5e0..426d2d6d 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,5 @@ out/ .vscode/ ### yml file ### -/src/main/resources/application-jwt.yml +/src/main/resources/application-payment.yml +/src/main/resources/application-jwt.yml \ No newline at end of file diff --git a/src/main/java/com/tasksprints/auction/AuctionApplication.java b/src/main/java/com/tasksprints/auction/AuctionApplication.java index 69bb978d..f8d5bf32 100644 --- a/src/main/java/com/tasksprints/auction/AuctionApplication.java +++ b/src/main/java/com/tasksprints/auction/AuctionApplication.java @@ -4,10 +4,11 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import java.util.Arrays; + @EnableJpaAuditing @SpringBootApplication public class AuctionApplication { - public static void main(String[] args) { SpringApplication.run(AuctionApplication.class, args); } diff --git a/src/main/java/com/tasksprints/auction/api/payment/PaymentController.java b/src/main/java/com/tasksprints/auction/api/payment/PaymentController.java new file mode 100644 index 00000000..82dbd29f --- /dev/null +++ b/src/main/java/com/tasksprints/auction/api/payment/PaymentController.java @@ -0,0 +1,76 @@ +package com.tasksprints.auction.api.payment; + +import com.tasksprints.auction.common.constant.ApiResponseMessages; +import com.tasksprints.auction.common.response.ApiResult; +import com.tasksprints.auction.domain.payment.api.Response; +import com.tasksprints.auction.domain.payment.dto.request.PaymentRequest; +import com.tasksprints.auction.domain.payment.dto.response.PaymentResponse; +import com.tasksprints.auction.domain.payment.exception.InvalidSessionException; +import com.tasksprints.auction.domain.payment.exception.PaymentDataMismatchException; +import com.tasksprints.auction.domain.payment.service.PaymentService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import jakarta.servlet.http.HttpSession; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.math.BigDecimal; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/payment") +public class PaymentController { + private final PaymentService paymentService; + + @PostMapping("/prepare") + @Operation(summary = "Temporarily stores the payment element", description = "Save orderID and amount in session") + @ApiResponse(responseCode = "200", description = "Payment prepared successfully") + public ResponseEntity> preparePayment(HttpSession session, @RequestBody PaymentRequest.Prepare prepareRequest) { + paymentService.prepare(session, prepareRequest); + return ResponseEntity.ok(ApiResult.success(ApiResponseMessages.PAYMENT_PREPARED_SUCCESS)); + } + + @PostMapping("/confirm") + public ResponseEntity confirmPayment(HttpSession session, @RequestBody PaymentRequest.Confirm confirmRequest, @RequestParam Long userId) throws IOException, InterruptedException { + validateSession(session); + validatePaymentConfirmRequest(confirmRequest, session); + + Response response = paymentService.sendPaymentRequest(confirmRequest); + //토스페이먼츠로 보낸 결제 승인 요청에 대한 response 리턴 + Response objectResponse = paymentService.handleTossPaymentResponse(userId, confirmRequest, response); + + if (objectResponse.isSuccess()) { + PaymentResponse paymentResponse = (PaymentResponse) objectResponse.getBody(); + return ResponseEntity.ok(ApiResult.success("결제가 성공적으로 처리되었습니다.", paymentResponse)); + } + return ResponseEntity.status(response.getStatusCode()).body(response.getBody()); + } + + + private void validatePaymentConfirmRequest(PaymentRequest.Confirm confirmRequest, HttpSession session) { + String savedOrderId = (String) session.getAttribute("orderId"); + BigDecimal savedAmount = (BigDecimal) session.getAttribute("amount"); + + if (!confirmRequest.getOrderId().equals(savedOrderId) || !confirmRequest.getAmount().equals(savedAmount)) { + throw new PaymentDataMismatchException("Payment data mismatch"); + } + } + + private void validateSession(HttpSession session) { + if (session == null) { + throw new InvalidSessionException("Invalid session"); + } + + String savedOrderId = (String) session.getAttribute("orderId"); + BigDecimal savedAmount = (BigDecimal) session.getAttribute("amount"); + + if (savedOrderId == null || savedAmount == null) { + throw new InvalidSessionException("Invalid session"); + } + } + + +} diff --git a/src/main/java/com/tasksprints/auction/api/wallet/WalletController.java b/src/main/java/com/tasksprints/auction/api/wallet/WalletController.java new file mode 100644 index 00000000..77757598 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/api/wallet/WalletController.java @@ -0,0 +1,4 @@ +package com.tasksprints.auction.api.wallet; + +public class WalletController { +} diff --git a/src/main/java/com/tasksprints/auction/common/config/HttpClientConfig.java b/src/main/java/com/tasksprints/auction/common/config/HttpClientConfig.java new file mode 100644 index 00000000..325d8285 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/common/config/HttpClientConfig.java @@ -0,0 +1,14 @@ +package com.tasksprints.auction.common.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.net.http.HttpClient; + +@Configuration +public class HttpClientConfig { + @Bean + public HttpClient httpClient() { + return HttpClient.newHttpClient(); + } +} diff --git a/src/main/java/com/tasksprints/auction/common/constant/ApiResponseMessages.java b/src/main/java/com/tasksprints/auction/common/constant/ApiResponseMessages.java index a8963f05..9804ce3d 100644 --- a/src/main/java/com/tasksprints/auction/common/constant/ApiResponseMessages.java +++ b/src/main/java/com/tasksprints/auction/common/constant/ApiResponseMessages.java @@ -31,4 +31,8 @@ public class ApiResponseMessages { public static final String REVIEW_RETRIEVED = "Review successfully retrieved"; // Additional messages can be defined as needed + + // PAYMENT + public static final String PAYMENT_PREPARED_SUCCESS = "Payment prepared successfully"; + public static final String PAYMENT_SUCCESS = "Payment completed and wallet charged successfully"; } diff --git a/src/main/java/com/tasksprints/auction/common/handler/GlobalExceptionHandler.java b/src/main/java/com/tasksprints/auction/common/handler/GlobalExceptionHandler.java index 4b8a90aa..670739c3 100644 --- a/src/main/java/com/tasksprints/auction/common/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/tasksprints/auction/common/handler/GlobalExceptionHandler.java @@ -8,6 +8,8 @@ import com.tasksprints.auction.domain.auction.exception.InvalidAuctionTimeException; import com.tasksprints.auction.domain.bid.exception.BidNotFoundException; import com.tasksprints.auction.domain.bid.exception.InvalidBidAmountException; +import com.tasksprints.auction.domain.payment.exception.InvalidSessionException; +import com.tasksprints.auction.domain.payment.exception.PaymentDataMismatchException; import com.tasksprints.auction.domain.product.exception.ProductNotFoundException; import com.tasksprints.auction.domain.user.exception.UserNotFoundException; import org.springframework.http.HttpStatus; @@ -61,6 +63,18 @@ public ResponseEntity> handleAuctionEndedException(AuctionEnde return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiResult.failure(message)); } + @ExceptionHandler(InvalidSessionException.class) + public ResponseEntity> handleInvalidSessionException(InvalidSessionException ex) { + String message = "Invalid Session Error. "; + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiResult.failure(message)); + } + + @ExceptionHandler(PaymentDataMismatchException.class) + public ResponseEntity> PaymentDataMismatchException(PaymentDataMismatchException ex) { + String message = "Session Data Mismatch Error. "; + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiResult.failure(message)); + } + @ExceptionHandler(IllegalArgumentException.class) public ResponseEntity> handleIllegalArgumentException(IllegalArgumentException ex) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiResult.failure(ex.getMessage())); diff --git a/src/main/java/com/tasksprints/auction/common/initializer/AuctionInitializer.java b/src/main/java/com/tasksprints/auction/common/initializer/AuctionInitializer.java index c9ea2a40..e77b2094 100644 --- a/src/main/java/com/tasksprints/auction/common/initializer/AuctionInitializer.java +++ b/src/main/java/com/tasksprints/auction/common/initializer/AuctionInitializer.java @@ -37,7 +37,7 @@ public AuctionInitializer(UserRepository userRepository, AuctionRepository aucti } private void createDummyUser() { - User user1 = User.create("name", "email@email.com", "password", "NickName"); + User user1 = User.createWithWallet("name", "email@email.com", "password", "NickName"); userRepository.save(user1); } @@ -58,7 +58,7 @@ private void createDummyProduct(User user, Auction auction) { @Override @Transactional public void run(ApplicationArguments args) throws Exception { - User user = userRepository.save(User.create("name", "email@email.com", "password", "NickName")); + User user = userRepository.save(User.createWithWallet("name", "email@email.com", "password", "NickName")); // 각 제품에 대해 새로운 경매를 생성 for (int i = 0; i < 100; i++) { diff --git a/src/main/java/com/tasksprints/auction/common/properties/PaymentProperties.java b/src/main/java/com/tasksprints/auction/common/properties/PaymentProperties.java new file mode 100644 index 00000000..ca6a1650 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/common/properties/PaymentProperties.java @@ -0,0 +1,25 @@ +package com.tasksprints.auction.common.properties; + +import lombok.Getter; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.Base64; + +@Component +@ConfigurationProperties(prefix = "payment.toss") +@Getter +public class PaymentProperties { + private String testClientApiKey; + private String testSecretApiKey; + private String successUrl; + private String failUrl; + + public static final String CONFIRM_URL = "https://api.tosspayments.com/v1/payments/confirm"; + + public String getAuthorizations() { + String encodedKey = Base64.getEncoder().encodeToString((testSecretApiKey + ":").getBytes()); + return "Basic " + encodedKey; + } +} diff --git a/src/main/java/com/tasksprints/auction/domain/auction/model/Auction.java b/src/main/java/com/tasksprints/auction/domain/auction/model/Auction.java index c141b035..93607251 100644 --- a/src/main/java/com/tasksprints/auction/domain/auction/model/Auction.java +++ b/src/main/java/com/tasksprints/auction/domain/auction/model/Auction.java @@ -56,14 +56,7 @@ public class Auction extends BaseEntity { private List bids = new ArrayList<>(); @Column(nullable = false) - private Long viewCount; - - @PrePersist - protected void onCreate() { - if (viewCount == null) { - viewCount = 0L; // 기본값 설정 - } - } + private long viewCount = 0L; public static Auction create(LocalDateTime startTime, LocalDateTime endTime, BigDecimal startingBid, AuctionCategory auctionCategory, AuctionStatus auctionStatus, User seller) { Auction newAuction = Auction.builder() @@ -88,9 +81,6 @@ public void addUser(User seller) { } public void incrementViewCount() { - if (viewCount == null) { - viewCount = 0L; - } this.viewCount += 1; } diff --git a/src/main/java/com/tasksprints/auction/domain/auction/repository/support/AuctionCriteriaRepositoryImpl.java b/src/main/java/com/tasksprints/auction/domain/auction/repository/support/AuctionCriteriaRepositoryImpl.java index 18e085be..4fa75f16 100644 --- a/src/main/java/com/tasksprints/auction/domain/auction/repository/support/AuctionCriteriaRepositoryImpl.java +++ b/src/main/java/com/tasksprints/auction/domain/auction/repository/support/AuctionCriteriaRepositoryImpl.java @@ -29,12 +29,10 @@ public Page getAuctionsByFilters(Pageable pageable, AuctionRequest.Sear List result = buildQueryWithPaginationAndSorting(builder, pageable, sortOrder); // int 오버플로 주의 -// int total = queryFactory -// .selectFrom(auction) -// .where(builder) -// .fetch().size(); - - long total = result.size(); + int total = queryFactory + .selectFrom(auction) + .where(builder) + .fetch().size(); return new PageImpl<>(result, pageable, total); } diff --git a/src/main/java/com/tasksprints/auction/domain/payment/api/Response.java b/src/main/java/com/tasksprints/auction/domain/payment/api/Response.java new file mode 100644 index 00000000..41482d29 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/payment/api/Response.java @@ -0,0 +1,28 @@ +package com.tasksprints.auction.domain.payment.api; + +import lombok.Getter; + +@Getter +public class Response { + private final int statusCode; + private final T body; + + public Response(int statusCode, T body) { + this.statusCode = statusCode; + this.body = body; + + } + + public static Response success(int statusCode, T body) { + return new Response<>(statusCode, body); + } + + public static Response failure(int statusCode, T body) { + return new Response<>(statusCode, body); + } + + public boolean isSuccess() { + return statusCode == 200; + } + +} diff --git a/src/main/java/com/tasksprints/auction/domain/payment/client/ClientWrapper.java b/src/main/java/com/tasksprints/auction/domain/payment/client/ClientWrapper.java new file mode 100644 index 00000000..fafee92d --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/payment/client/ClientWrapper.java @@ -0,0 +1,26 @@ +package com.tasksprints.auction.domain.payment.client; + +import com.tasksprints.auction.domain.payment.api.Response; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +@Component +@RequiredArgsConstructor +public class ClientWrapper implements HttpClientWrapper{ + private final HttpClient httpClient; + + @Override + public Response send(HttpRequest request) throws IOException, InterruptedException { + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() == 200) { + return Response.success(response.statusCode(), response.body()); + } + return Response.failure(response.statusCode(), response.body()); + } +} diff --git a/src/main/java/com/tasksprints/auction/domain/payment/client/HttpClientWrapper.java b/src/main/java/com/tasksprints/auction/domain/payment/client/HttpClientWrapper.java new file mode 100644 index 00000000..9471b6f3 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/payment/client/HttpClientWrapper.java @@ -0,0 +1,10 @@ +package com.tasksprints.auction.domain.payment.client; + +import com.tasksprints.auction.domain.payment.api.Response; + +import java.io.IOException; +import java.net.http.HttpRequest; + +public interface HttpClientWrapper { + Response send(HttpRequest request) throws IOException, InterruptedException; +} diff --git a/src/main/java/com/tasksprints/auction/domain/payment/client/PaymentApiSerializer.java b/src/main/java/com/tasksprints/auction/domain/payment/client/PaymentApiSerializer.java new file mode 100644 index 00000000..79cd5aff --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/payment/client/PaymentApiSerializer.java @@ -0,0 +1,11 @@ +package com.tasksprints.auction.domain.payment.client; + +import com.tasksprints.auction.domain.payment.api.Response; +import com.tasksprints.auction.domain.payment.dto.request.PaymentRequest; + +import java.io.IOException; + +public interface PaymentApiSerializer { + Response sendPaymentRequest(PaymentRequest.Confirm confirmRequest) throws IOException, InterruptedException; + Response cancelPaymentApproval(PaymentRequest.Cancel cancelRequest) throws IOException, InterruptedException; +} diff --git a/src/main/java/com/tasksprints/auction/domain/payment/client/TossPaymentImpl.java b/src/main/java/com/tasksprints/auction/domain/payment/client/TossPaymentImpl.java new file mode 100644 index 00000000..d2d74101 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/payment/client/TossPaymentImpl.java @@ -0,0 +1,94 @@ +package com.tasksprints.auction.domain.payment.client; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.tasksprints.auction.common.properties.PaymentProperties; +import com.tasksprints.auction.domain.payment.api.Response; +import com.tasksprints.auction.domain.payment.dto.request.PaymentRequest; +import com.tasksprints.auction.domain.payment.dto.response.PaymentErrorResponse; +import com.tasksprints.auction.domain.payment.dto.response.PaymentResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpRequest; + +@Component +@RequiredArgsConstructor +public class TossPaymentImpl implements PaymentApiSerializer { + + private final PaymentProperties paymentProperties; + private final ObjectMapper objectMapper; + private final HttpClientWrapper httpClientWrapper; + + @Override + public Response sendPaymentRequest(PaymentRequest.Confirm confirmRequest) throws IOException, InterruptedException { + String requestBody = buildDtoToRequestBody(confirmRequest); + HttpRequest request = createHttpRequest(requestBody); + Response response = paymentRequestToTossPay(request); + + if (response.isSuccess()) { + PaymentResponse paymentResponse = objectMapper.readValue(response.getBody(), PaymentResponse.class); + return Response.success(response.getStatusCode(), paymentResponse); + } + PaymentErrorResponse errorResponse = objectMapper.readValue(response.getBody(), PaymentErrorResponse.class); + return Response.failure(response.getStatusCode(), errorResponse); + } + + private String buildDtoToRequestBody(PaymentRequest.Confirm confirmRequest) throws JsonProcessingException { + return objectMapper.writeValueAsString(confirmRequest); + } + + private HttpRequest createHttpRequest(String requestBody) { + return HttpRequest.newBuilder() + .uri(URI.create(PaymentProperties.CONFIRM_URL)) + .header("Authorization", paymentProperties.getAuthorizations()) + .header("Content-Type", "application/json") + .method("POST", HttpRequest.BodyPublishers.ofString(requestBody)) + .build(); + } + + private Response paymentRequestToTossPay(HttpRequest request) throws IOException, InterruptedException { + return requestToTossPay(request); + } + + @Override + public Response cancelPaymentApproval(PaymentRequest.Cancel cancelRequest) throws IOException, InterruptedException { + String requestBody = createCancelRequestBody(); + HttpRequest request = buildCancelHttpRequest(cancelRequest, requestBody); + Response response = requestToTossPay(request); + + if (response.isSuccess()) { + PaymentResponse paymentResponse = objectMapper.readValue(response.getBody(), PaymentResponse.class); + return Response.success(response.getStatusCode(), paymentResponse); + } + PaymentErrorResponse errorResponse = objectMapper.readValue(response.getBody(), PaymentErrorResponse.class); + return Response.failure(response.getStatusCode(), errorResponse); + + } + + private String createCancelRequestBody() throws JsonProcessingException { + ObjectNode object = objectMapper.createObjectNode(); + object.put("cancelReason", "결제 도중 오류 발생"); + return objectMapper.writeValueAsString(object); + } + + private HttpRequest buildCancelHttpRequest(PaymentRequest.Cancel cancelRequest, String requestBody) { + String insertPaymentKeyIntoUrl = String.format("https://api.tosspayments.com/v1/payments/%s/cancel", cancelRequest.getPaymentKey()); + return HttpRequest.newBuilder() + .uri(URI.create(insertPaymentKeyIntoUrl)) + .header("Authorization", paymentProperties.getAuthorizations()) + .header("Content-Type", "application/json") + // .header("Idempotency-key", "멱등키") + .method("POST", HttpRequest.BodyPublishers.ofString(requestBody)) + .build(); + } + + private Response requestToTossPay(HttpRequest request) throws IOException, InterruptedException { + return httpClientWrapper.send(request); + } + +} + diff --git a/src/main/java/com/tasksprints/auction/domain/payment/dto/request/PaymentRequest.java b/src/main/java/com/tasksprints/auction/domain/payment/dto/request/PaymentRequest.java new file mode 100644 index 00000000..8fee6d93 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/payment/dto/request/PaymentRequest.java @@ -0,0 +1,53 @@ +package com.tasksprints.auction.domain.payment.dto.request; + +import com.tasksprints.auction.domain.payment.model.PayType; +import com.tasksprints.auction.domain.payment.model.Payment; +import lombok.*; + +import java.math.BigDecimal; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class PaymentRequest { + private BigDecimal amount; + private String orderId; + private String orderName; + private PayType payType; + + @Getter + @AllArgsConstructor + @NoArgsConstructor(access = AccessLevel.PRIVATE) + public static class Prepare { + private String orderId; + private BigDecimal amount; + } + + @Getter + @AllArgsConstructor + @NoArgsConstructor(access = AccessLevel.PRIVATE) + public static class Confirm { + private String orderId; + private BigDecimal amount; + private String paymentKey; + } + + @Getter + @AllArgsConstructor + @NoArgsConstructor(access = AccessLevel.PRIVATE) + public static class Cancel { + private String paymentKey; + } + + public Payment toEntity() { + return Payment.builder() + .amount(amount) + .tossOrderId(orderId) + .orderName(orderName) + .payType(payType) + .build(); + } + + +} diff --git a/src/main/java/com/tasksprints/auction/domain/payment/dto/response/PaymentErrorResponse.java b/src/main/java/com/tasksprints/auction/domain/payment/dto/response/PaymentErrorResponse.java new file mode 100644 index 00000000..c24ac52d --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/payment/dto/response/PaymentErrorResponse.java @@ -0,0 +1,23 @@ +package com.tasksprints.auction.domain.payment.dto.response; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class PaymentErrorResponse { + private String version; + private String traceId; + @JsonAlias({"code", "error.code"}) + private String code; + @JsonAlias({"message", "error.message"}) + private String message; + +} diff --git a/src/main/java/com/tasksprints/auction/domain/payment/dto/response/PaymentResponse.java b/src/main/java/com/tasksprints/auction/domain/payment/dto/response/PaymentResponse.java new file mode 100644 index 00000000..7aaab351 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/payment/dto/response/PaymentResponse.java @@ -0,0 +1,27 @@ +package com.tasksprints.auction.domain.payment.dto.response; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +@Getter +@Builder +@JsonIgnoreProperties(ignoreUnknown = true) +@NoArgsConstructor +@AllArgsConstructor +public class PaymentResponse { + @JsonProperty("method") + private String payType; + private String paymentKey; + @JsonProperty("totalAmount") + private BigDecimal amount; + private String orderName; + private String orderId; + private String status; + +} diff --git a/src/main/java/com/tasksprints/auction/domain/payment/exception/InvalidSessionException.java b/src/main/java/com/tasksprints/auction/domain/payment/exception/InvalidSessionException.java new file mode 100644 index 00000000..4342867a --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/payment/exception/InvalidSessionException.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.domain.payment.exception; + +public class InvalidSessionException extends RuntimeException { + public InvalidSessionException(String message) { + super(message); + } +} diff --git a/src/main/java/com/tasksprints/auction/domain/payment/exception/PaymentDataMismatchException.java b/src/main/java/com/tasksprints/auction/domain/payment/exception/PaymentDataMismatchException.java new file mode 100644 index 00000000..de29bb44 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/payment/exception/PaymentDataMismatchException.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.domain.payment.exception; + +public class PaymentDataMismatchException extends RuntimeException { + public PaymentDataMismatchException(String message) { + super(message); + } +} diff --git a/src/main/java/com/tasksprints/auction/domain/payment/exception/PaymentUserNotFoundException.java b/src/main/java/com/tasksprints/auction/domain/payment/exception/PaymentUserNotFoundException.java new file mode 100644 index 00000000..d840b871 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/payment/exception/PaymentUserNotFoundException.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.domain.payment.exception; + +public class PaymentUserNotFoundException extends RuntimeException { + public PaymentUserNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/tasksprints/auction/domain/payment/model/PayStatus.java b/src/main/java/com/tasksprints/auction/domain/payment/model/PayStatus.java new file mode 100644 index 00000000..e3de7c55 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/payment/model/PayStatus.java @@ -0,0 +1,29 @@ +package com.tasksprints.auction.domain.payment.model; + +import lombok.Getter; + +@Getter +public enum PayStatus { + READY("결제 생성 초기상태 - 결제 진행중"), + IN_PROGRESS("결제 승인 대기중"), + WAITING_FOR_DEPOSIT("가상 계좌 입금 대기중"), + PARTIAL_CANCELED("승인된 결제 부분 취소"), + EXPIRED("결제 유효 시간 초과"), + DONE("결제 완료"), + ABORTED("결제 승인 실패"), + CANCELED("결제 취소"), + FAILED("결제 실패"); + + private final String displayName; + + PayStatus(String displayName) {this.displayName = displayName;} + + public static PayStatus fromString(String value) { + for (PayStatus payStatus : PayStatus.values()) { + if (payStatus.name().equalsIgnoreCase(value)) { + return payStatus; + } + } + throw new IllegalArgumentException("Unknown PayStatus: " + value); + } +} diff --git a/src/main/java/com/tasksprints/auction/domain/payment/model/PayType.java b/src/main/java/com/tasksprints/auction/domain/payment/model/PayType.java new file mode 100644 index 00000000..ae4a3d3f --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/payment/model/PayType.java @@ -0,0 +1,32 @@ +package com.tasksprints.auction.domain.payment.model; + +import lombok.Getter; + +@Getter +public enum PayType { + CARD("카드 결제"), + VIRTUAL_ACCOUNT("가상계좌"), + SIMPLE_PAYMENT("간편결제"), + MOBILE_PAYMENT("휴대폰"), + BANK_TRANSFER("계좌 이체"), + CULTURE_GIFT_CERTIFICATE("문화상품권"), + BOOK_CULTURE_CERTIFICATE("도서문화상품권"), + GAME_CULTURE_CERTIFICATE("게임문화상품권"); + + + private final String displayName; + + PayType(String displayName) { + this.displayName = displayName; + } + + + public static PayType fromString(String value) { + for (PayType type : PayType.values()) { + if (type.displayName.equals(value)) { + return type; + } + } + throw new IllegalArgumentException("Unknown PayType: " + value); + } +} diff --git a/src/main/java/com/tasksprints/auction/domain/payment/model/Payment.java b/src/main/java/com/tasksprints/auction/domain/payment/model/Payment.java new file mode 100644 index 00000000..4669e460 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/payment/model/Payment.java @@ -0,0 +1,71 @@ +package com.tasksprints.auction.domain.payment.model; + +import com.tasksprints.auction.common.entity.BaseEntityWithUpdate; +import com.tasksprints.auction.domain.payment.dto.response.PaymentResponse; +import com.tasksprints.auction.domain.wallet.model.Wallet; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + + +@Entity +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Payment extends BaseEntityWithUpdate { + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false) + private Long paymentId; + + @Column(nullable = false, unique = true) + private String tossPaymentKey; + + @Column(nullable = false) + private String tossOrderId; + + @Column(nullable = false) + private String orderName; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "wallet_id") + private Wallet wallet; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private PayType payType; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private PayStatus payStatus; + + @Column(nullable = false) + private BigDecimal amount; + + @Column(nullable = true) + private String failReason; + + @Column(nullable = true) + private String cancelReason; + + public static Payment create(PaymentResponse paymentResponse) { + return Payment.builder() + .tossOrderId(paymentResponse.getOrderId()) + .tossPaymentKey(paymentResponse.getPaymentKey()) + .orderName(paymentResponse.getOrderName()) + .payType(PayType.fromString(paymentResponse.getPayType())) + .payStatus(PayStatus.fromString(paymentResponse.getStatus())) + .amount(paymentResponse.getAmount()) + .build(); + } + + public void addWallet(Wallet wallet) { + this.wallet = wallet; + wallet.addPayment(this); + } +} diff --git a/src/main/java/com/tasksprints/auction/domain/payment/repository/PaymentRepository.java b/src/main/java/com/tasksprints/auction/domain/payment/repository/PaymentRepository.java new file mode 100644 index 00000000..cbc9456c --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/payment/repository/PaymentRepository.java @@ -0,0 +1,8 @@ +package com.tasksprints.auction.domain.payment.repository; + +import com.tasksprints.auction.domain.payment.model.Payment; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PaymentRepository extends JpaRepository { + +} diff --git a/src/main/java/com/tasksprints/auction/domain/payment/service/PaymentService.java b/src/main/java/com/tasksprints/auction/domain/payment/service/PaymentService.java new file mode 100644 index 00000000..fc9f65ca --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/payment/service/PaymentService.java @@ -0,0 +1,15 @@ +package com.tasksprints.auction.domain.payment.service; + + +import com.tasksprints.auction.domain.payment.api.Response; +import com.tasksprints.auction.domain.payment.dto.request.PaymentRequest; +import jakarta.servlet.http.HttpSession; + +import java.io.IOException; + +public interface PaymentService { + + public void prepare(HttpSession session, PaymentRequest.Prepare prepareRequest); + public Response sendPaymentRequest(PaymentRequest.Confirm confirmRequest) throws IOException, InterruptedException; + public Response handleTossPaymentResponse(Long userId, PaymentRequest.Confirm confirmRequest, Response response) throws IOException, InterruptedException ; +} diff --git a/src/main/java/com/tasksprints/auction/domain/payment/service/PaymentServiceImpl.java b/src/main/java/com/tasksprints/auction/domain/payment/service/PaymentServiceImpl.java new file mode 100644 index 00000000..6940efe2 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/payment/service/PaymentServiceImpl.java @@ -0,0 +1,73 @@ +package com.tasksprints.auction.domain.payment.service; + +import com.tasksprints.auction.domain.payment.api.Response; +import com.tasksprints.auction.domain.payment.client.PaymentApiSerializer; +import com.tasksprints.auction.domain.payment.dto.request.PaymentRequest; +import com.tasksprints.auction.domain.payment.dto.response.PaymentResponse; +import com.tasksprints.auction.domain.payment.model.Payment; +import com.tasksprints.auction.domain.payment.repository.PaymentRepository; +import com.tasksprints.auction.domain.wallet.model.Wallet; +import com.tasksprints.auction.domain.wallet.service.WalletService; +import jakarta.servlet.http.HttpSession; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.IOException; + + +@Service +@RequiredArgsConstructor +@Slf4j +public class PaymentServiceImpl implements PaymentService { + private final PaymentRepository paymentRepository; + private final WalletService walletService; + private final PaymentApiSerializer paymentApiSerializer; + + @Override + public void prepare(HttpSession session, PaymentRequest.Prepare prepareRequest) { + session.setAttribute("orderId", prepareRequest.getOrderId()); + session.setAttribute("amount", prepareRequest.getAmount()); + } + @Override + public Response sendPaymentRequest(PaymentRequest.Confirm confirmRequest) throws IOException, InterruptedException{ + return paymentApiSerializer.sendPaymentRequest(confirmRequest); + } + + @Transactional(rollbackFor = Exception.class) + @Override + public Response handleTossPaymentResponse(Long userId, PaymentRequest.Confirm confirmRequest, Response response) throws IOException, InterruptedException { + if (response.getStatusCode() == 200) { + return handlePaymentSuccess(userId, confirmRequest, response); + } + return handlePaymentFailure(response); + } + + private Response handlePaymentSuccess(Long userId, PaymentRequest.Confirm confirmRequest, Response response) throws IOException, InterruptedException { + //결제에 성공했더라도 결제 정보 저장에 실패하면 트랜잭션 롤백한다 + try { + PaymentResponse paymentResponseFromToss = (PaymentResponse) response.getBody(); + Payment payment = Payment.create(paymentResponseFromToss); + + Wallet paidUserWallet = walletService.getWalletByUserId(userId); + payment.addWallet(paidUserWallet); + + paymentRepository.save(payment); + walletService.chargeMoney(paidUserWallet, payment.getAmount()); + return response; + } catch (Exception e) { + log.error("결제 처리 중 예외 발생: {}", e.getMessage(), e); + String paymentKey = confirmRequest.getPaymentKey(); + PaymentRequest.Cancel cancelRequest = new PaymentRequest.Cancel(paymentKey); + + paymentApiSerializer.cancelPaymentApproval(cancelRequest); + throw e; + } + } + + Response handlePaymentFailure(Response response) { + log.info("결제 실패"); + return response; + } +} diff --git a/src/main/java/com/tasksprints/auction/domain/user/dto/response/UserDetailResponse.java b/src/main/java/com/tasksprints/auction/domain/user/dto/response/UserDetailResponse.java index 58f2ec21..e27e6fe8 100644 --- a/src/main/java/com/tasksprints/auction/domain/user/dto/response/UserDetailResponse.java +++ b/src/main/java/com/tasksprints/auction/domain/user/dto/response/UserDetailResponse.java @@ -12,6 +12,7 @@ public class UserDetailResponse { private String email; private String password; private String nickName; + private String walletId; private UserDetailResponse(User user) { this.id = user.getId(); @@ -19,6 +20,7 @@ private UserDetailResponse(User user) { this.email = user.getEmail(); this.password = user.getPassword(); this.nickName = user.getNickName(); + this.walletId = String.valueOf(user.getWallet().getId()); } public static UserDetailResponse of(User user) { diff --git a/src/main/java/com/tasksprints/auction/domain/user/model/User.java b/src/main/java/com/tasksprints/auction/domain/user/model/User.java index 4db7e595..a440bb98 100644 --- a/src/main/java/com/tasksprints/auction/domain/user/model/User.java +++ b/src/main/java/com/tasksprints/auction/domain/user/model/User.java @@ -2,6 +2,7 @@ import com.tasksprints.auction.common.entity.BaseEntityWithUpdate; import com.tasksprints.auction.domain.auction.model.Auction; +import com.tasksprints.auction.domain.wallet.model.Wallet; import jakarta.persistence.*; import lombok.*; import org.hibernate.annotations.SQLRestriction; @@ -40,22 +41,43 @@ public class User extends BaseEntityWithUpdate { @OneToMany(mappedBy = "seller", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) @Builder.Default private List auctions = new ArrayList<>(); + + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + @JoinColumn(name = "wallet_id") + private Wallet wallet; + // 추후 추가 // @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) // @Builder.Default // private List bids = new ArrayList<>(); + public static User create(String name, String email, String password, String nickName) { + return User.builder() + .name(name) + .email(email) + .password(password) + .nickName(nickName) + .build(); + } /** * @descripton static factory pattern을 적용하여, 구현 */ - public static User create(String name, String email, String password, String nickName) { - return User.builder() + public static User createWithWallet(String name, String email, String password, String nickName) { + User user = User.builder() .name(name) .email(email) .password(password) .nickName(nickName) .build(); + Wallet wallet = Wallet.create(user); + user.addWallet(wallet); + return user; } +// public Wallet createWalletForUser(User user) { +// Wallet wallet = new Wallet(); +// wallet.setUser(user); +// return wallet; +// } public void setAuctions(List auctions) { this.auctions = auctions; @@ -79,4 +101,9 @@ public void delete() { public void addAuction(Auction auction) { this.auctions.add(auction); } + + public void addWallet(Wallet wallet) { + this.wallet = wallet; + wallet.addUser(this); + } } diff --git a/src/main/java/com/tasksprints/auction/domain/user/service/UserService.java b/src/main/java/com/tasksprints/auction/domain/user/service/UserService.java index 2724c622..9ac79052 100644 --- a/src/main/java/com/tasksprints/auction/domain/user/service/UserService.java +++ b/src/main/java/com/tasksprints/auction/domain/user/service/UserService.java @@ -3,6 +3,7 @@ import com.tasksprints.auction.domain.user.dto.request.UserRequest; import com.tasksprints.auction.domain.user.dto.response.UserDetailResponse; import com.tasksprints.auction.domain.user.dto.response.UserSummaryResponse; +import com.tasksprints.auction.domain.user.model.User; import com.tasksprints.auction.domain.user.model.User; import java.util.List; diff --git a/src/main/java/com/tasksprints/auction/domain/user/service/UserServiceImpl.java b/src/main/java/com/tasksprints/auction/domain/user/service/UserServiceImpl.java index 867c3375..4f96751b 100644 --- a/src/main/java/com/tasksprints/auction/domain/user/service/UserServiceImpl.java +++ b/src/main/java/com/tasksprints/auction/domain/user/service/UserServiceImpl.java @@ -6,10 +6,15 @@ import com.tasksprints.auction.domain.user.exception.UserNotFoundException; import com.tasksprints.auction.domain.user.model.User; import com.tasksprints.auction.domain.user.repository.UserRepository; +import com.tasksprints.auction.domain.wallet.exception.WalletCreationException; +import com.tasksprints.auction.domain.wallet.model.Wallet; +import com.tasksprints.auction.domain.wallet.repository.WalletRepository; +import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; @Service diff --git a/src/main/java/com/tasksprints/auction/domain/wallet/exception/InSufficientBalanceException.java b/src/main/java/com/tasksprints/auction/domain/wallet/exception/InSufficientBalanceException.java new file mode 100644 index 00000000..2af6c8f5 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/wallet/exception/InSufficientBalanceException.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.domain.wallet.exception; + +public class InSufficientBalanceException extends RuntimeException { + public InSufficientBalanceException(String message) { + super(message); + } +} diff --git a/src/main/java/com/tasksprints/auction/domain/wallet/exception/WalletCreationException.java b/src/main/java/com/tasksprints/auction/domain/wallet/exception/WalletCreationException.java new file mode 100644 index 00000000..be09e573 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/wallet/exception/WalletCreationException.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.domain.wallet.exception; + +public class WalletCreationException extends RuntimeException { + public WalletCreationException(String message) { + super(message); + } +} diff --git a/src/main/java/com/tasksprints/auction/domain/wallet/exception/WalletNotFoundException.java b/src/main/java/com/tasksprints/auction/domain/wallet/exception/WalletNotFoundException.java new file mode 100644 index 00000000..76461711 --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/wallet/exception/WalletNotFoundException.java @@ -0,0 +1,7 @@ +package com.tasksprints.auction.domain.wallet.exception; + +public class WalletNotFoundException extends RuntimeException { + public WalletNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/tasksprints/auction/domain/wallet/model/Wallet.java b/src/main/java/com/tasksprints/auction/domain/wallet/model/Wallet.java new file mode 100644 index 00000000..1a14a7ee --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/wallet/model/Wallet.java @@ -0,0 +1,62 @@ +package com.tasksprints.auction.domain.wallet.model; + +import com.tasksprints.auction.common.entity.BaseEntityWithUpdate; +import com.tasksprints.auction.domain.payment.model.Payment; +import com.tasksprints.auction.domain.user.model.User; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Wallet extends BaseEntityWithUpdate { + /** + 지갑 식별키를 uuid로 변경 고려 + @GeneratedValue(strategy = GenerationType.AUTO) + private UUID id = UUID.randomUUID(); + */ + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private BigDecimal balance; + + @Column(nullable = false) + private String userName; + + @OneToMany(mappedBy = "wallet", fetch = FetchType.LAZY) + @Builder.Default + private List payments = new ArrayList<>(); + + @OneToOne(mappedBy = "wallet", fetch = FetchType.LAZY) + private User user; + + public static Wallet create(User user) { + return Wallet + .builder() + .balance(BigDecimal.ZERO) + .userName(user.getName()) + .user(user) + .build(); + } + public void addPayment(Payment payment) { + this.payments.add(payment); + } + + public void addUser(User user) { + this.user = user; + } + + public void chargeBalance(BigDecimal amount) { + this.balance = this.balance.add(amount); + } +} diff --git a/src/main/java/com/tasksprints/auction/domain/wallet/repository/WalletRepository.java b/src/main/java/com/tasksprints/auction/domain/wallet/repository/WalletRepository.java new file mode 100644 index 00000000..cfe4e83c --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/wallet/repository/WalletRepository.java @@ -0,0 +1,11 @@ +package com.tasksprints.auction.domain.wallet.repository; + +import com.tasksprints.auction.domain.wallet.model.Wallet; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface WalletRepository extends JpaRepository { + @Query("SELECT u.wallet FROM users u WHERE u.id = :userId") + Wallet getWalletByUserId(@Param("userId") Long userId); +} diff --git a/src/main/java/com/tasksprints/auction/domain/wallet/service/WalletService.java b/src/main/java/com/tasksprints/auction/domain/wallet/service/WalletService.java new file mode 100644 index 00000000..0383a46b --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/wallet/service/WalletService.java @@ -0,0 +1,14 @@ +package com.tasksprints.auction.domain.wallet.service; + +import com.tasksprints.auction.domain.wallet.model.Wallet; + +import java.math.BigDecimal; + +public interface WalletService { + + void chargeMoney(Wallet wallet, BigDecimal amount); + boolean isSufficientMoney(); + + Wallet getWalletByUserId(Long userId); + +} diff --git a/src/main/java/com/tasksprints/auction/domain/wallet/service/WalletServiceImpl.java b/src/main/java/com/tasksprints/auction/domain/wallet/service/WalletServiceImpl.java new file mode 100644 index 00000000..7325c3ab --- /dev/null +++ b/src/main/java/com/tasksprints/auction/domain/wallet/service/WalletServiceImpl.java @@ -0,0 +1,33 @@ +package com.tasksprints.auction.domain.wallet.service; + +import com.tasksprints.auction.domain.user.model.User; +import com.tasksprints.auction.domain.user.service.UserService; +import com.tasksprints.auction.domain.wallet.model.Wallet; +import com.tasksprints.auction.domain.wallet.repository.WalletRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; + +@Service +@RequiredArgsConstructor +public class WalletServiceImpl implements WalletService { + private final WalletRepository walletRepository; + + @Override + public void chargeMoney(Wallet wallet, BigDecimal amount) { + wallet.chargeBalance(amount); + walletRepository.save(wallet); + } + + @Override + public boolean isSufficientMoney() { + return false; + } + + @Override + public Wallet getWalletByUserId(Long userId) { + return walletRepository.getWalletByUserId(userId); + } + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 99934123..ff1c0f23 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -20,7 +20,11 @@ spring: jpa: hibernate: ddl-auto: create-drop - show-sql: true + properties: + hibernate: + format_sql: true +# default_batch_fetch_size: 100 + database-platform: org.hibernate.dialect.H2Dialect thymeleaf: enabled: true @@ -32,6 +36,7 @@ spring: profiles: include: -jwt + -payment springdoc: api-docs: @@ -49,3 +54,4 @@ logging: server: port: 8080 + diff --git a/src/main/resources/templates/test.html b/src/main/resources/templates/test.html new file mode 100644 index 00000000..9bade732 --- /dev/null +++ b/src/main/resources/templates/test.html @@ -0,0 +1,89 @@ + + + + + + + + +
+ + +
+ +
+ +
+ + + + + + diff --git a/src/test/java/com/tasksprints/auction/api/PaymentControllerTest.java b/src/test/java/com/tasksprints/auction/api/PaymentControllerTest.java new file mode 100644 index 00000000..5baef70b --- /dev/null +++ b/src/test/java/com/tasksprints/auction/api/PaymentControllerTest.java @@ -0,0 +1,204 @@ +package com.tasksprints.auction.api; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.tasksprints.auction.api.payment.PaymentController; +import com.tasksprints.auction.common.constant.ApiResponseMessages; +import com.tasksprints.auction.domain.payment.api.Response; +import com.tasksprints.auction.domain.payment.dto.request.PaymentRequest; +import com.tasksprints.auction.domain.payment.dto.response.PaymentErrorResponse; +import com.tasksprints.auction.domain.payment.dto.response.PaymentResponse; +import com.tasksprints.auction.domain.payment.exception.InvalidSessionException; +import com.tasksprints.auction.domain.payment.exception.PaymentDataMismatchException; +import com.tasksprints.auction.domain.payment.service.PaymentService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.test.web.servlet.MockMvc; + +import java.math.BigDecimal; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; +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; + +@WebMvcTest(PaymentController.class) +@MockBean(JpaMetamodelMappingContext.class) + +public class PaymentControllerTest { + @Autowired + private MockMvc mockMvc; + + @MockBean + private PaymentService paymentService; + + @MockBean + MockHttpSession session; + + @BeforeEach + void setup() { + session = new MockHttpSession(); + } + + @Test + @DisplayName("결제 전 임시 값 저장") + public void 결제_전_임시_값_저장() throws Exception { + String jsonRequest = """ + { + "orderId": "test1", + "amount": 1000.00 + } + """; + + mockMvc.perform(post("/api/v1/payment/prepare") + .session(session) + .contentType(MediaType.APPLICATION_JSON) + .content(jsonRequest)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value(ApiResponseMessages.PAYMENT_PREPARED_SUCCESS)); + + } + + @Nested + class sessionTest { + + + @Test + void 결제_전_세션_값이_null인_경우_예외가_발생한다() throws Exception { + // Given + String jsonRequest = """ + { + "orderId": "12345", + "amount": 10000 + } + """; + + // When & Then + mockMvc.perform(post("/api/v1/payment/confirm") + .session(session) + .param("userId", "1") + .contentType(MediaType.APPLICATION_JSON) + .content(jsonRequest)) + .andExpect(status().isBadRequest()) + .andExpect(result -> { + Exception resolvedException = result.getResolvedException(); + assertNotNull(resolvedException); + assertInstanceOf(InvalidSessionException.class, resolvedException); + }); + } + + @Test + void 결제_전_세션_OrderId와_Request의_OrderId가_다른_경우_예외가_발생한다() throws Exception { + // Given + String jsonRequest = """ + { + "orderId": "12345", + "amount": 10000 + } + """; + MockHttpSession session = new MockHttpSession(); + session.setAttribute("orderId", "changed-OrderId"); + session.setAttribute("amount", BigDecimal.valueOf(10000)); // + + // When & Then + mockMvc.perform(post("/api/v1/payment/confirm") + .session(session) + .param("userId", "1") + .contentType(MediaType.APPLICATION_JSON) + .content(jsonRequest)) + .andExpect(status().isBadRequest()) + .andExpect(result -> { + Exception resolvedException = result.getResolvedException(); + assertNotNull(resolvedException); + assertInstanceOf(PaymentDataMismatchException.class, resolvedException); + assertEquals("Payment data mismatch", resolvedException.getMessage()); + + }); + } + } + + @Test + @DisplayName("결제 승인 성공 시 HTTP 200 응답을 반환한다") + void 결제_승인_성공_시_응답() throws Exception { + // Given + String jsonRequest = """ + { + "orderId": "12345", + "amount": 10000 + } + """; + + MockHttpSession session = new MockHttpSession(); + session.setAttribute("orderId", "12345"); + session.setAttribute("amount", BigDecimal.valueOf(10000)); + + PaymentResponse successPaymentResponse = new PaymentResponse("CARD", "paymentKey", BigDecimal.valueOf(10000), "Test Order", "12345", "DONE"); + Response mockResponse = Response.success(200, successPaymentResponse); + + when(paymentService.sendPaymentRequest(any())).thenReturn(mockResponse); + when(paymentService.handleTossPaymentResponse(anyLong(), any(), any())) + .thenReturn(mockResponse); + + // When / Then + mockMvc.perform(post("/api/v1/payment/confirm") + .session(session) + .param("userId", "1") + .contentType(MediaType.APPLICATION_JSON) + .content(jsonRequest)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.message").value("결제가 성공적으로 처리되었습니다.")) + .andExpect(jsonPath("$.data.orderId").value("12345")) + .andExpect(jsonPath("$.data.totalAmount").value(10000)); + } + + @Test + @DisplayName("결제 승인 성공 시 HTTP 400 응답을 반환한다") + void 결제_승인_실패_시_응답() throws Exception { + // Given + String jsonRequest = """ + { + "orderId": "12345", + "amount": 10000 + } + """; + + MockHttpSession session = new MockHttpSession(); + session.setAttribute("orderId", "12345"); + session.setAttribute("amount", BigDecimal.valueOf(10000)); + + PaymentErrorResponse failurePaymentResponse = PaymentErrorResponse.builder() + .version("2022-11-16") + .traceId("{traceId}") + .code("{CODE}") + .message("{MESSAGE}") + .build(); + Response mockResponse = Response.failure(400, failurePaymentResponse); + + when(paymentService.sendPaymentRequest(any())).thenReturn(mockResponse); + when(paymentService.handleTossPaymentResponse(anyLong(), any(), any())) + .thenReturn(mockResponse); + + // When / Then + mockMvc.perform(post("/api/v1/payment/confirm") + .session(session) + .param("userId", "1") + .contentType(MediaType.APPLICATION_JSON) + .content(jsonRequest)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("{CODE}")) + .andExpect(jsonPath("$.message").value("{MESSAGE}")); + } + +} diff --git a/src/test/java/com/tasksprints/auction/api/UserControllerTest.java b/src/test/java/com/tasksprints/auction/api/UserControllerTest.java index 091985ab..3b3dfac5 100644 --- a/src/test/java/com/tasksprints/auction/api/UserControllerTest.java +++ b/src/test/java/com/tasksprints/auction/api/UserControllerTest.java @@ -48,7 +48,7 @@ class SuccessfulTests { @Test @DisplayName("POST /api/v1/user - 성공") void registerUser() throws Exception { - UserDetailResponse userDetailResponse = new UserDetailResponse(1L, "John", "john@example.com", "password", "john123"); + UserDetailResponse userDetailResponse = new UserDetailResponse(1L, "John", "john@example.com", "password", "john123", "1L"); Mockito.when(userService.createUser(any(UserRequest.Register.class))).thenReturn(userDetailResponse); @@ -67,7 +67,7 @@ void registerUser() throws Exception { @Test @DisplayName("GET /api/v1/user/{id} - 성공") void getUserById() throws Exception { - UserDetailResponse userDetailResponse = new UserDetailResponse(1L, "John", "john@example.com", "password", "john123"); + UserDetailResponse userDetailResponse = new UserDetailResponse(1L, "John", "john@example.com", "password", "john123", "1L"); Mockito.when(userService.getUserDetailsById(anyLong())).thenReturn(userDetailResponse); @@ -97,7 +97,7 @@ void getAllUsers() throws Exception { @Test @DisplayName("PUT /api/v1/user - 성공") void updateUser() throws Exception { - UserDetailResponse userDetailResponse = new UserDetailResponse(1L, "John Updated", "john@example.com", "newpassword", "john123updated"); + UserDetailResponse userDetailResponse = new UserDetailResponse(1L, "John Updated", "john@example.com", "newpassword", "john123updated", "1L"); Mockito.when(userService.updateUser(anyLong(), any(UserRequest.Update.class))).thenReturn(userDetailResponse); diff --git a/src/test/java/com/tasksprints/auction/domain/payment/client/ClientWrapperTest.java b/src/test/java/com/tasksprints/auction/domain/payment/client/ClientWrapperTest.java new file mode 100644 index 00000000..8ea3231b --- /dev/null +++ b/src/test/java/com/tasksprints/auction/domain/payment/client/ClientWrapperTest.java @@ -0,0 +1,55 @@ +package com.tasksprints.auction.domain.payment.client; + +import com.tasksprints.auction.domain.payment.api.Response; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ClientWrapperTest { + @Mock + private HttpClient httpClient; + @Mock + private HttpResponse mockHttpResponse; + @InjectMocks + private ClientWrapper clientWrapper; + + @BeforeEach + void setUp() { + + } + + @Test + void send가_성공적으로_응답을_받음() throws IOException, InterruptedException { + //given + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("https://example.com")) + .build(); + + when(mockHttpResponse.statusCode()).thenReturn(200); + when(mockHttpResponse.body()).thenReturn("test Body"); + + when(httpClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) + .thenReturn(mockHttpResponse); + + //when + Response result = clientWrapper.send(request); + + //then + assertEquals(200, result.getStatusCode()); + assertEquals("test Body", result.getBody()); + } +} diff --git a/src/test/java/com/tasksprints/auction/domain/payment/client/TossPaymentImplTest.java b/src/test/java/com/tasksprints/auction/domain/payment/client/TossPaymentImplTest.java new file mode 100644 index 00000000..90d4ddae --- /dev/null +++ b/src/test/java/com/tasksprints/auction/domain/payment/client/TossPaymentImplTest.java @@ -0,0 +1,123 @@ +package com.tasksprints.auction.domain.payment.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.tasksprints.auction.common.properties.PaymentProperties; +import com.tasksprints.auction.domain.payment.api.Response; +import com.tasksprints.auction.domain.payment.dto.request.PaymentRequest; +import com.tasksprints.auction.domain.payment.dto.response.PaymentErrorResponse; +import com.tasksprints.auction.domain.payment.dto.response.PaymentResponse; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.IOException; +import java.math.BigDecimal; +import java.net.http.HttpRequest; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class TossPaymentImplTest { + + @Mock + private HttpClientWrapper httpClientWrapper; + private PaymentProperties paymentProperties; + private ObjectMapper objectMapper; + private PaymentApiSerializer tossPaymentApiSerializer; + + @BeforeEach + void setUp() { + paymentProperties = new PaymentProperties() { + @Override + public String getAuthorizations() { + return "https://api.test.com/payments"; + } + }; + objectMapper = new ObjectMapper(); + tossPaymentApiSerializer = new TossPaymentImpl(paymentProperties, objectMapper, httpClientWrapper); + + } + + @Nested + @DisplayName("결제 요청 테스트") + class payment_request_test { + @Test + void payment_request의_응답이_200번_코드라면_PaymentResponse_타입을_리턴받는다() throws IOException, InterruptedException { + //given + PaymentRequest.Confirm confirmRequest = new PaymentRequest.Confirm("orderId", BigDecimal.TEN, "paymentKey"); + String responseBody = "{ \"status\": \"COMPLETED\", \"paymentKey\": \"abc123\",\"orderName\": \"Sample Order\", \"amount\": 5000 }"; + Response mockResponse = Response.success(200, responseBody); + + when(httpClientWrapper.send(any(HttpRequest.class))).thenReturn(mockResponse); + //when + Response response = tossPaymentApiSerializer.sendPaymentRequest(confirmRequest); + //then + Assertions.assertThat(response.getStatusCode()).isEqualTo(200); + Assertions.assertThat(response.getBody()).isInstanceOf(PaymentResponse.class); + + } + + @Test + void payment_request의_응답이_200번_코드가_아니면_PaymentErrorResponse_타입을_리턴받는다() throws IOException, InterruptedException { + //given + PaymentRequest.Confirm confirmRequest = new PaymentRequest.Confirm("orderId", BigDecimal.TEN, "paymentKey"); + String responseBody = "{\"version\": \"2022-11-16\", \"traceId\": \"{traceId}\", " + + "\"error\": {\"code\": \"{CODE}\", \"message\": \"{MESSAGE}\"}}"; + + Response mockResponse = Response.failure(404, responseBody); + + when(httpClientWrapper.send(any(HttpRequest.class))).thenReturn(mockResponse); + //when + Response response = tossPaymentApiSerializer.sendPaymentRequest(confirmRequest); + //then + Assertions.assertThat(response.getStatusCode()).isEqualTo(404); + Assertions.assertThat(response.getBody()).isInstanceOf(PaymentErrorResponse.class); + + } + } + + @Nested + @DisplayName("결제 취소 요청 테스트") + class payment_cancel_test { + @Test + void cancel_request의_응답이_200번_코드라면_PaymentResponse_타입을_리턴받는다() throws IOException, InterruptedException { + //given + PaymentRequest.Cancel cancelRequest = new PaymentRequest.Cancel("paymentKey"); + String responseBody = "{ \"status\": \"CANCELED\", \"paymentKey\": \"abc123\",\"orderName\": \"Sample Order\", \"amount\": 5000 }"; + Response mockResponse = Response.success(200, responseBody); + + when(httpClientWrapper.send(any(HttpRequest.class))).thenReturn(mockResponse); + //when + Response response = tossPaymentApiSerializer.cancelPaymentApproval(cancelRequest); + //then + Assertions.assertThat(response.getStatusCode()).isEqualTo(200); + Assertions.assertThat(response.getBody()).isInstanceOf(PaymentResponse.class); + + } + @Test + void cancel_request의_응답이_200번이_아니라면_PaymentErrorResponse_타입을_리턴받는다() throws IOException, InterruptedException { + //given + PaymentRequest.Cancel cancelRequest = new PaymentRequest.Cancel("paymentKey"); + String responseBody = "{\"version\": \"2022-11-16\", \"traceId\": \"{traceId}\", " + + "\"error\": {\"code\": \"{CODE}\", \"message\": \"{MESSAGE}\"}}"; + Response mockResponse = Response.success(400, responseBody); + + when(httpClientWrapper.send(any(HttpRequest.class))).thenReturn(mockResponse); + //when + Response response = tossPaymentApiSerializer.cancelPaymentApproval(cancelRequest); + //then + Assertions.assertThat(response.getStatusCode()).isEqualTo(400); + Assertions.assertThat(response.getBody()).isInstanceOf(PaymentErrorResponse.class); + + } + + } + + +} diff --git a/src/test/java/com/tasksprints/auction/domain/payment/repository/PaymentRepositoryTest.java b/src/test/java/com/tasksprints/auction/domain/payment/repository/PaymentRepositoryTest.java new file mode 100644 index 00000000..4aecc0b7 --- /dev/null +++ b/src/test/java/com/tasksprints/auction/domain/payment/repository/PaymentRepositoryTest.java @@ -0,0 +1,68 @@ +package com.tasksprints.auction.domain.payment.repository; + +import com.tasksprints.auction.common.config.QueryDslConfig; +import com.tasksprints.auction.domain.payment.dto.response.PaymentResponse; +import com.tasksprints.auction.domain.payment.model.PayStatus; +import com.tasksprints.auction.domain.payment.model.PayType; +import com.tasksprints.auction.domain.payment.model.Payment; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import java.math.BigDecimal; + + +@DataJpaTest +@Import(QueryDslConfig.class) +public class PaymentRepositoryTest { + + @Autowired + private PaymentRepository paymentRepository; + + @Nested + class 결제_저장_테스트 { + @Test + void 결제_응답이_올바르게_저장되면_성공한다() { + //given + PaymentResponse paymentResponseFromToss = PaymentResponse.builder() + .payType("간편결제") + .paymentKey("testPaymentKey") + .amount(BigDecimal.ZERO) + .orderName("testOrderName") + .orderId("testOrderId") + .status("DONE") + .build(); + + Payment payment = Payment.create(paymentResponseFromToss); + //when + Payment savedPayment = paymentRepository.save(payment); + //then + Assertions.assertThat(savedPayment.getPaymentId()).isNotNull(); + Assertions.assertThat(savedPayment.getPayType()).isEqualTo(PayType.SIMPLE_PAYMENT); + Assertions.assertThat(savedPayment.getTossPaymentKey()).isEqualTo("testPaymentKey"); + Assertions.assertThat(savedPayment.getAmount()).isEqualTo(BigDecimal.ZERO); + Assertions.assertThat(savedPayment.getPayStatus()).isEqualTo(PayStatus.DONE); + + } + + @Test + void 결제_응답을_저장_시_create_메서드_처리_도중_ENUM에_없는_값이_들어오면_예외가_발생한다() { + PaymentResponse paymentResponseFromToss = PaymentResponse.builder() + .payType("잘못된 결제수단 입력") + .paymentKey("testPaymentKey") + .amount(BigDecimal.ZERO) + .orderName("testOrderName") + .orderId("testOrderId") + .status("잘못된 결제 상태 입력") + .build(); + + Assertions.assertThatThrownBy(() -> Payment.create(paymentResponseFromToss)) + .isInstanceOf(IllegalArgumentException.class); + + } + } + +} diff --git a/src/test/java/com/tasksprints/auction/domain/payment/service/PaymentServiceImplTest.java b/src/test/java/com/tasksprints/auction/domain/payment/service/PaymentServiceImplTest.java new file mode 100644 index 00000000..d936f11e --- /dev/null +++ b/src/test/java/com/tasksprints/auction/domain/payment/service/PaymentServiceImplTest.java @@ -0,0 +1,172 @@ +package com.tasksprints.auction.domain.payment.service; + +import com.tasksprints.auction.domain.payment.api.Response; +import com.tasksprints.auction.domain.payment.client.PaymentApiSerializer; +import com.tasksprints.auction.domain.payment.dto.request.PaymentRequest; +import com.tasksprints.auction.domain.payment.dto.response.PaymentErrorResponse; +import com.tasksprints.auction.domain.payment.dto.response.PaymentResponse; +import com.tasksprints.auction.domain.payment.model.Payment; +import com.tasksprints.auction.domain.payment.repository.PaymentRepository; +import com.tasksprints.auction.domain.user.model.User; +import com.tasksprints.auction.domain.wallet.model.Wallet; +import com.tasksprints.auction.domain.wallet.service.WalletService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockHttpSession; + +import java.io.IOException; +import java.math.BigDecimal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + + +@ExtendWith(MockitoExtension.class) +public class PaymentServiceImplTest { + @Spy + @InjectMocks + private PaymentServiceImpl paymentService; + @Mock + private WalletService walletService; + @Mock + private PaymentApiSerializer paymentApiSerializer; + @Mock + private PaymentRepository paymentRepository; + + private MockHttpSession session; + + private User user; + private Wallet wallet; + + @BeforeEach + void setUp() { + session = new MockHttpSession(); + + wallet = Wallet.builder() + .id(1L) + .balance(BigDecimal.ZERO) + .userName("testUser") + .build(); + + user = User.builder() + .id(1L) + .name("testUser") + .nickName("test") + .password("password") + .email("test@naver.com") + .wallet(wallet) + .build(); + + + } + + @Nested + @DisplayName("결제 전 세션 임시 저장 테스트") + class 임시_저장_테스트 { + @Test + void 결제_요청을_받았을_때_세션에_값이_저장되면_성공한다() { + //given + String orderId = "testOrderId"; + BigDecimal amount = BigDecimal.valueOf(1000.00); + PaymentRequest.Prepare prepareRequest = new PaymentRequest.Prepare(orderId, amount); + + //when + paymentService.prepare(session, prepareRequest); + //then + assertThat(session.getAttribute("orderId")).isEqualTo(orderId); + assertThat(session.getAttribute("amount")).isEqualTo(amount); + } + + } + + @Nested + @DisplayName("토스_페이_응답_처리") + class handleTossPayResponse { + @Test + void 결제_성공_시_지갑에_돈을_충전한다() throws IOException, InterruptedException { + //given + PaymentRequest.Confirm confirmRequest = new PaymentRequest.Confirm("orderId", BigDecimal.valueOf(50000), "paymentKey"); + PaymentResponse paymentResponse = PaymentResponse.builder() + .payType("카드 결제") + .paymentKey("paymentKey") + .amount(BigDecimal.valueOf(50000)) + .orderName("Sample Order") + .orderId("orderId") + .status("DONE") + .build(); + + Response successResponse = Response.success(200, paymentResponse); + + when(walletService.getWalletByUserId(user.getId())).thenReturn(wallet); + doNothing().when(walletService).chargeMoney(eq(wallet), eq(BigDecimal.valueOf(50000))); + + //when + paymentService.handleTossPaymentResponse(1L, confirmRequest, successResponse); + + //then + verify(walletService).chargeMoney(eq(wallet), eq(BigDecimal.valueOf(50000))); + verify(paymentRepository).save(any(Payment.class)); + } + + @Test + void 결제_성공_후_결제_정보_저장을_실패하면_결제_취소_요청을_보낸다() throws IOException, InterruptedException { + // given + PaymentRequest.Confirm confirmRequest = new PaymentRequest.Confirm("orderId", BigDecimal.valueOf(50000), "paymentKey"); + PaymentResponse paymentResponse = PaymentResponse.builder() + .payType("카드 결제") + .paymentKey("paymentKey") + .amount(BigDecimal.valueOf(50000)) + .orderName("Sample Order") + .orderId("orderId") + .status("DONE") + .build(); + + Response successResponse = Response.success(200, paymentResponse); + + when(walletService.getWalletByUserId(user.getId())).thenReturn(wallet); + when(paymentRepository.save(any(Payment.class))) + .thenThrow(new RuntimeException("결제 정보 저장 실패")); + + // when + assertThrows(RuntimeException.class, () -> + paymentService.handleTossPaymentResponse(1L, confirmRequest, successResponse) + ); + + // then + verify(paymentApiSerializer).cancelPaymentApproval(any(PaymentRequest.Cancel.class)); + //예외 발생으로 chargeMoney전에 throw 됐을 것 + verify(walletService, times(0)).chargeMoney(any(Wallet.class), any(BigDecimal.class)); + } + + @Test + void 결제_실패_시_handlePaymentFailure_를_실행한다() throws IOException, InterruptedException { + //given + PaymentRequest.Confirm confirmRequest = new PaymentRequest.Confirm("orderId", BigDecimal.valueOf(50000), "paymentKey"); + PaymentErrorResponse errorResponse = PaymentErrorResponse.builder() + .version("2022-11-16") + .traceId("{traceId}") + .code("{CODE}") + .message("{MESSAGE}") + .build(); + Response failureResponse = Response.failure(404, errorResponse); + + //when + paymentService.handleTossPaymentResponse(1L, confirmRequest, failureResponse); + + //then + verify(paymentService).handlePaymentFailure(failureResponse); + } + + } + + +} diff --git a/src/test/java/com/tasksprints/auction/domain/user/UserServiceImplTest.java b/src/test/java/com/tasksprints/auction/domain/user/UserServiceImplTest.java index 95c78348..37db4e07 100644 --- a/src/test/java/com/tasksprints/auction/domain/user/UserServiceImplTest.java +++ b/src/test/java/com/tasksprints/auction/domain/user/UserServiceImplTest.java @@ -6,11 +6,13 @@ import com.tasksprints.auction.domain.user.model.User; import com.tasksprints.auction.domain.user.repository.UserRepository; import com.tasksprints.auction.domain.user.service.UserServiceImpl; +import com.tasksprints.auction.domain.wallet.model.Wallet; import org.junit.jupiter.api.*; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.math.BigDecimal; import java.util.Optional; import static org.mockito.Mockito.*; @@ -19,11 +21,10 @@ public class UserServiceImplTest { @Mock private UserRepository userRepository; - @InjectMocks private UserServiceImpl userService; - private User existingUser; + private Wallet existingWallet; @BeforeEach void setUp() { @@ -34,7 +35,17 @@ void setUp() { .nickName("testNick") .password("testPassword") .email("test@example.com") + .wallet(existingWallet) .build(); + + existingWallet = Wallet.builder() + .id(1L) + .balance(BigDecimal.ZERO) + .userName(existingUser.getName()) + .user(existingUser) + .build(); + + existingUser.addWallet(existingWallet); } @Nested @@ -47,7 +58,6 @@ void shouldCreateNewUser() { // Arrange UserRequest.Register request = new UserRequest.Register("testUser", "test@example.com", "testPassword", "testNick"); when(userRepository.save(any(User.class))).thenReturn(existingUser); - // Act UserDetailResponse createdUser = userService.createUser(request); diff --git a/src/test/java/com/tasksprints/auction/domain/wallet/repository/WalletRepositoryTest.java b/src/test/java/com/tasksprints/auction/domain/wallet/repository/WalletRepositoryTest.java new file mode 100644 index 00000000..bf358704 --- /dev/null +++ b/src/test/java/com/tasksprints/auction/domain/wallet/repository/WalletRepositoryTest.java @@ -0,0 +1,48 @@ +package com.tasksprints.auction.domain.wallet.repository; + +import com.tasksprints.auction.common.config.QueryDslConfig; +import com.tasksprints.auction.domain.user.model.User; +import com.tasksprints.auction.domain.user.repository.UserRepository; +import com.tasksprints.auction.domain.wallet.model.Wallet; +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.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import static org.junit.jupiter.api.Assertions.*; +@DataJpaTest +@Import(QueryDslConfig.class) +class WalletRepositoryTest { + @Autowired + WalletRepository walletRepository; + @Autowired + UserRepository userRepository; + + private User user; + + @BeforeEach + void setUp() { + user = User.createWithWallet( + "testName", + "testEmail", + "testPassword", + "testNickName" + ); + user = userRepository.save(user); + } + + @Test + @DisplayName("getWalletByUserId 테스트 : JPQL로 UserId를 통해 조회했을 시 Wallet이 리턴되면 성공한다") + void JPQL로_UserId를_통해_조회했을_시_Wallet이_리턴되면_성공한다() { + //given + Long userId = user.getId(); + //when + Wallet foundWallet = walletRepository.getWalletByUserId(userId); + //then + assertNotNull(foundWallet); + assertEquals(user.getWallet().getId(), foundWallet.getId()); + } + +} diff --git a/src/test/java/com/tasksprints/auction/domain/wallet/service/WalletServiceImplTest.java b/src/test/java/com/tasksprints/auction/domain/wallet/service/WalletServiceImplTest.java new file mode 100644 index 00000000..2618fb93 --- /dev/null +++ b/src/test/java/com/tasksprints/auction/domain/wallet/service/WalletServiceImplTest.java @@ -0,0 +1,62 @@ +package com.tasksprints.auction.domain.wallet.service; + +import com.tasksprints.auction.domain.wallet.model.Wallet; +import com.tasksprints.auction.domain.wallet.repository.WalletRepository; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.math.BigDecimal; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class WalletServiceImplTest { + + @InjectMocks + private WalletServiceImpl walletService; + + @Mock + private WalletRepository walletRepository; + + private Wallet wallet; + + @BeforeEach + void setUp() { + wallet = Wallet.builder() + .id(1L) + .balance(BigDecimal.ZERO) + .userName("testUser") + .build(); + } + + @Test + void 충전_메서드_실행_시_지갑의_balance가_증가하면_성공한다() { + //given + BigDecimal amount = BigDecimal.valueOf(1000); + //when + walletService.chargeMoney(wallet, amount); + //then + assertEquals(amount, wallet.getBalance()); + verify(walletRepository, times(1)).save(wallet); + } + + @Test + @DisplayName("getWalletByUserId 테스트 ") + void UserId로_Wallet이_조회되면_성공한다() { + //given + Long walletId = 1L; + when(walletRepository.getWalletByUserId(walletId)).thenReturn(wallet); + //when + Wallet wallet = walletService.getWalletByUserId(1L); + //then + assertNotNull(wallet); + assertEquals(walletId, wallet.getId()); + } +}