Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[FEATURE] : 토스페이먼츠 결제 API를 구현합니다. #62

Open
wants to merge 22 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d5bea3d
Refactor : viewCount Long- > long 수정
na0th Oct 16, 2024
853c048
Fix : 페이징 element 카운트 쿼리 수정
na0th Oct 16, 2024
bf0bfca
Feat : payment 도메인 구성
na0th Oct 16, 2024
0431fb4
Feat : 세션 임시 저장 엔드포인트 구성
na0th Oct 16, 2024
e4213bc
Feat : wallet 도메인 구성
na0th Oct 16, 2024
61b0401
Test : 세션에 값 임시 저장 테스트 추가
na0th Oct 16, 2024
42bf230
Rename : PaymentController 소문자 -> 대문자
na0th Oct 16, 2024
50a7367
.
na0th Oct 19, 2024
0efa6cf
Merge pull request #3 from na0th/payment
na0th Oct 19, 2024
6a26c84
feat : User 생성 시 Wallet도 같이 생성
na0th Oct 31, 2024
8a08bec
feat : Wallet 도메인 메서드 추가
na0th Oct 31, 2024
804ef09
fix : 임시 이름 변경
na0th Oct 31, 2024
a89ccbd
fix : PaymentController로 고치기
na0th Oct 31, 2024
1da494a
refactor : payment 도메인 리팩토링
na0th Oct 31, 2024
c4cd219
feat : Payment create 메서드, API 메세지, 예외 메세지 추가
na0th Oct 31, 2024
c7c5e1a
fix : UserDetailResponse 생성자 파라미터 갯수로 인한 빌드 에러
na0th Oct 31, 2024
2a69f5e
refactor : User.create 메서드를 createWithWallet 메서드로 변경
na0th Oct 31, 2024
8921041
test : Wallet 생성을 User 쪽에서 하면서 테스트 코드 변경
na0th Oct 31, 2024
8236348
refactor : 결제 리팩토링
na0th Dec 1, 2024
f1ff1fa
test : 결제 관련 테스트 추가
na0th Dec 1, 2024
812cb55
test : PaymentController 테스트 추가
na0th Dec 2, 2024
bf34aed
refactor : PaymentResponse 형식 변경
na0th Dec 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,7 @@ out/

### VS Code ###
.vscode/


### yml file ###
/src/main/resources/application-payment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.tasksprints.auction.api.payment;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.tasksprints.auction.common.config.PaymentConfig;
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.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.exception.PaymentUserNotFoundException;
import com.tasksprints.auction.domain.payment.model.Payment;
import com.tasksprints.auction.domain.payment.repository.PaymentRepository;
import com.tasksprints.auction.domain.payment.service.PaymentService;
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 com.tasksprints.auction.domain.wallet.service.WalletServiceImpl;
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.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.math.BigDecimal;
import java.net.http.HttpClient;
import java.net.http.HttpResponse;

@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<ApiResult<String>> 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<Object> response = paymentService.sendPaymentRequest(confirmRequest);
//토스페이먼츠로 보낸 결제 승인 요청에 대한 response 리턴
Response<Object> 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");
}
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.tasksprints.auction.api.wallet;

public class WalletController {
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.tasksprints.auction.common.config;

import lombok.Getter;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

import java.util.Base64;

@Configuration
@Getter
public class PaymentConfig {
@Value("${payment.toss.test_client_api_key}")
private String testClientApiKey;

@Value("${payment.toss.test_secret_api_key}")
private String testSecretApiKey;

@Value("${payment.toss.success_url}")
private String successUrl;

@Value("${payment.toss.fail_url}")
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -61,6 +63,18 @@ public ResponseEntity<ApiResult<String>> handleAuctionEndedException(AuctionEnde
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiResult.failure(message));
}

@ExceptionHandler(InvalidSessionException.class)
public ResponseEntity<ApiResult<String>> handleInvalidSessionException(InvalidSessionException ex) {
String message = "Invalid Session Error. ";
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiResult.failure(message));
}

@ExceptionHandler(PaymentDataMismatchException.class)
public ResponseEntity<ApiResult<String>> PaymentDataMismatchException(PaymentDataMismatchException ex) {
String message = "Session Data Mismatch Error. ";
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiResult.failure(message));
}

@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ApiResult<String>> handleIllegalArgumentException(IllegalArgumentException ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiResult.failure(ex.getMessage()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public AuctionInitializer(UserRepository userRepository, AuctionRepository aucti
}

private void createDummyUser() {
User user1 = User.create("name", "[email protected]", "password", "NickName");
User user1 = User.createWithWallet("name", "[email protected]", "password", "NickName");
userRepository.save(user1);
}

Expand All @@ -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 protected]", "password", "NickName"));
User user = userRepository.save(User.createWithWallet("name", "[email protected]", "password", "NickName"));

// 각 제품에 대해 새로운 경매를 생성
for (int i = 0; i < 100; i++) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,7 @@ public class Auction extends BaseEntity {
private List<Bid> 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()
Expand All @@ -88,9 +81,6 @@ public void addUser(User seller) {
}

public void incrementViewCount() {
if (viewCount == null) {
viewCount = 0L;
}
this.viewCount += 1;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,10 @@ public Page<Auction> getAuctionsByFilters(Pageable pageable, AuctionRequest.Sear
List<Auction> 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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.tasksprints.auction.domain.payment.api;

import lombok.Getter;

@Getter
public class Response<T> {
private final int statusCode;
private final T body;

public Response(int statusCode, T body) {
this.statusCode = statusCode;
this.body = body;

}

public static <T> Response<T> success(int statusCode, T body) {
return new Response<>(statusCode, body);
}

public static <T> Response<T> failure(int statusCode, T body) {
return new Response<>(statusCode, body);
}

public boolean isSuccess() {
return statusCode == 200;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.tasksprints.auction.domain.payment.client;

import com.fasterxml.jackson.databind.ObjectMapper;
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;
private final ObjectMapper objectMapper;

@Override
public Response<String> send(HttpRequest request) throws IOException, InterruptedException {
HttpResponse<String> 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());
}
}
Original file line number Diff line number Diff line change
@@ -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<String> send(HttpRequest request) throws IOException, InterruptedException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.tasksprints.auction.domain.payment.client;

import com.fasterxml.jackson.core.JsonProcessingException;
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 java.io.IOException;
import java.net.http.HttpResponse;

public interface PaymentClient {
Response<Object> sendPaymentRequest(PaymentRequest.Confirm confirmRequest) throws IOException, InterruptedException;
Response<Object> cancelPaymentApproval(PaymentRequest.Cancel cancelRequest) throws IOException, InterruptedException;
}
Loading