Skip to content

Commit

Permalink
Merge pull request #44 from 9oormthon-univ/32-feature-integrate-kakao…
Browse files Browse the repository at this point in the history
…-pay-open-api-for-point-payment-feature

32 feature integrate kakao pay open api for point payment feature
  • Loading branch information
hyeneung authored Nov 28, 2024
2 parents 85bab56 + b61cdfd commit a63d482
Show file tree
Hide file tree
Showing 11 changed files with 329 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.example.mymoo.domain.payment.controller;

import com.example.mymoo.domain.payment.dto.api.KakaoPayReadyResponse;
import com.example.mymoo.domain.payment.dto.request.PayRequestDTO;
import com.example.mymoo.domain.payment.dto.response.PayResponseDTO;
import com.example.mymoo.domain.payment.service.PaymentService;
import com.example.mymoo.global.security.CustomUserDetails;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("api/v1/payment")
@RequiredArgsConstructor
public class PaymentController {

private final PaymentService paymentService;

@PostMapping("ready")
public ResponseEntity<KakaoPayReadyResponse> payReady(
@AuthenticationPrincipal CustomUserDetails userDetails,
@RequestBody PayRequestDTO req
){
KakaoPayReadyResponse res = paymentService.payReady(req.getName(), req.getTotalPrice(), userDetails.getAccountId());
return ResponseEntity.status(HttpStatus.ACCEPTED).body(res);
}

@GetMapping("approve")
public ResponseEntity<PayResponseDTO> approve(
@AuthenticationPrincipal CustomUserDetails userDetails,
@RequestParam("pg_token") String pgToken,
@RequestParam("tid") String tid
) {
//승인 처리 - 이 부분은 프론트에서 1차적으로 리다이렉트
// 프론트에서 받은 결제 정보(pg_token)를 해당 api에 넘겨주면 서버에 반영됨
return ResponseEntity.status(HttpStatus.ACCEPTED)
.body(paymentService.approve(pgToken, tid, userDetails.getAccountId()));
}

@GetMapping("cancel")
public String cancel() {
// 주문건이 진짜 취소되었는지 확인 후 취소 처리
// 결제내역조회(/v1/payment/status) api에서 status를 확인한다.
// To prevent the unwanted request cancellation caused by attack,
// the “show payment status” API is called and then check if the status is QUIT_PAYMENT before suspending the payment
return "cancel";
}

@GetMapping("fail")
public String fail() {
// 주문건이 진짜 실패되었는지 확인 후 실패 처리
// 결제내역조회(/v1/payment/status) api에서 status를 확인한다.
// To prevent the unwanted request cancellation caused by attack,
// the “show payment status” API is called and then check if the status is FAIL_PAYMENT before suspending the payment
return "fail";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.example.mymoo.domain.payment.dto.api;

import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.Builder;
import lombok.Data;

@Data
@Builder
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
public class KakaoPayApproveRequest {
private String tid;
private String cid;
private String partnerOrderId;
private String partnerUserId;
private String pgToken;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.example.mymoo.domain.payment.dto.api;

import lombok.Data;

@Data
public class KakaoPayApproveResponse {
private String aid; // 요청 고유 번호
private String tid; // 결제 고유 번호
private String cid; // 가맹점 코드
private String sid; // 정기결제용 ID
private String partner_order_id; // 가맹점 주문 번호
private String partner_user_id; // 가맹점 회원 id
private String payment_method_type; // 결제 수단
private Amount amount; // 결제 금액 정보
private String item_name; // 상품명
private String item_code; // 상품 코드
private int quantity; // 상품 수량
private String created_at; // 결제 요청 시간
private String approved_at; // 결제 승인 시간
private String payload; // 결제 승인 요청에 대해 저장 값, 요청 시 전달 내용

@Data
public class Amount{
private int total; // 총 결제 금액
private int tax_free; // 비과세 금액
private int tax; // 부가세 금액
private int point; // 사용한 포인트
private int discount; // 할인금액
private int green_deposit; // 컵 보증금
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.example.mymoo.domain.payment.dto.api;

import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.Builder;
import lombok.Data;

@Data @Builder
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
public class KakaoPayReadyRequest {
private String cid;
private String partnerOrderId;
private String partnerUserId;
private String itemName;
private Integer quantity;
private Integer totalAmount;
private Integer taxFreeAmount;
private Integer vatAmount;
private String approvalUrl;
private String cancelUrl;
private String failUrl;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.example.mymoo.domain.payment.dto.api;

import lombok.Data;
import lombok.ToString;

@Data
@ToString
public class KakaoPayReadyResponse {
private String tid;
private Boolean tms_result;
private String created_at;
private String next_redirect_pc_url;
private String next_redirect_mobile_url;
private String next_redirect_app_url;
private String android_app_scheme;
private String ios_app_scheme;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.mymoo.domain.payment.dto.request;

import lombok.Data;

@Data
public class PayRequestDTO {
private String name;
private Integer totalPrice;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.mymoo.domain.payment.dto.response;

import lombok.Builder;
import lombok.Data;

@Data @Builder
public class PayResponseDTO {
private String item_name;
private String account_name;
private int total;
private String created_at; // 결제 요청 시간
private String approved_at; // 결제 승인 시간
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.mymoo.domain.payment.exception;

import com.example.mymoo.domain.store.exception.StoreExceptionDetails;
import com.example.mymoo.global.exception.CustomException;

public class PaymentException extends CustomException {
public PaymentException(PaymentExceptionDetails paymentExceptionDetails){
super(paymentExceptionDetails);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.example.mymoo.domain.payment.exception;

import com.example.mymoo.global.exception.ExceptionDetails;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;

@Getter
@RequiredArgsConstructor
public enum PaymentExceptionDetails implements ExceptionDetails {
// 가게 id가 store 테이블에 존재하지 않을 때
APPROVE_FAILED(HttpStatus.BAD_REQUEST, "승인 요청이 실패했습니다."),
;

private final HttpStatus status;
private final String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package com.example.mymoo.domain.payment.service.Impl;

import com.example.mymoo.domain.account.entity.Account;
import com.example.mymoo.domain.account.exception.AccountException;
import com.example.mymoo.domain.account.exception.AccountExceptionDetails;
import com.example.mymoo.domain.account.repository.AccountRepository;
import com.example.mymoo.domain.payment.dto.api.KakaoPayApproveRequest;
import com.example.mymoo.domain.payment.dto.api.KakaoPayApproveResponse;
import com.example.mymoo.domain.payment.dto.api.KakaoPayReadyRequest;
import com.example.mymoo.domain.payment.dto.api.KakaoPayReadyResponse;
import com.example.mymoo.domain.payment.dto.response.PayResponseDTO;
import com.example.mymoo.domain.payment.exception.PaymentException;
import com.example.mymoo.domain.payment.exception.PaymentExceptionDetails;
import com.example.mymoo.domain.payment.service.PaymentService;
import com.example.mymoo.global.aop.LogExecutionTime;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.function.client.WebClient;


@Service @LogExecutionTime
@RequiredArgsConstructor
public class PaymentServiceImpl implements PaymentService {

private final AccountRepository accountRepository;

@Value("${kakao.pay.secret-key}")
private String secretKey;
@Value("${kakao.pay.uri}")
private String uri;
@Value("${kakao.pay.cid}")
private String cid;
@Value("${kakao.pay.approve-Url}")
private String approvalUrl;
@Value("${kakao.pay.partner-order-id}")
private String partnerOrderId;

private String tid;

public KakaoPayReadyResponse payReady(String name, Integer totalPrice, Long accountId){
// Request header
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "DEV_SECRET_KEY " + secretKey);
headers.setContentType(MediaType.APPLICATION_JSON);

// Request param
KakaoPayReadyRequest readyRequest = KakaoPayReadyRequest.builder()
.cid(cid)
.partnerOrderId(partnerOrderId)
.partnerUserId(String.valueOf(accountId))
.itemName(name)
.quantity(1)
.totalAmount(totalPrice)
.taxFreeAmount(0)
.vatAmount(100)
.approvalUrl(approvalUrl)
.cancelUrl("http://localhost:8080/payment/cancel")
.failUrl("http://localhost:8080/payment/fail")
.build();

// Send reqeust
HttpEntity<KakaoPayReadyRequest> entityMap = new HttpEntity<>(readyRequest, headers);
ResponseEntity<KakaoPayReadyResponse> response = new RestTemplate().postForEntity(
uri,
entityMap,
KakaoPayReadyResponse.class
);
this.tid = response.getBody().getTid();
// 주문번호와 TID를 매핑해서 저장해놓는다.
// Mapping TID with partner_order_id then save it to use for approval request.
return response.getBody();
}

@Transactional
public PayResponseDTO approve(String pgToken, String tida, Long accountId){
// ready할 때 저장해놓은 TID로 승인 요청
// Call “Execute approved payment” API by pg_token, TID mapping to the current payment transaction and other parameters.
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "SECRET_KEY " + secretKey);
headers.setContentType(MediaType.APPLICATION_JSON);
// Request param
KakaoPayApproveRequest approveRequest = KakaoPayApproveRequest.builder()
.cid(cid)
.tid(tid)
.partnerOrderId(partnerOrderId)
.partnerUserId(String.valueOf(accountId))
.pgToken(pgToken)
.build();

// Send Request
HttpEntity<KakaoPayApproveRequest> entityMap = new HttpEntity<>(approveRequest, headers);
try {
ResponseEntity<KakaoPayApproveResponse> response = new RestTemplate().postForEntity(
"https://open-api.kakaopay.com/online/v1/payment/approve",
entityMap,
KakaoPayApproveResponse.class
);

// 승인 결과를 저장한다.
// save the result of approval
KakaoPayApproveResponse res = response.getBody();
// account 계정에 결제금액 만큼 포인트 충전
Account foundAccount = accountRepository.findById(Long.valueOf(res.getPartner_user_id()))
.orElseThrow(() -> new AccountException(AccountExceptionDetails.ACCOUNT_NOT_FOUND));
System.out.println(Long.valueOf(res.getAmount().getTotal()));
foundAccount.chargePoint(Long.valueOf(res.getAmount().getTotal()));

return PayResponseDTO.builder()
.item_name(res.getItem_name())
.account_name(foundAccount.getNickname())
.total(res.getAmount().getTotal())
.created_at(res.getCreated_at())
.approved_at(res.getApproved_at())
.build();

} catch (HttpStatusCodeException ex) {
throw new PaymentException(PaymentExceptionDetails.APPROVE_FAILED);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.mymoo.domain.payment.service;

import com.example.mymoo.domain.payment.dto.api.KakaoPayReadyResponse;
import com.example.mymoo.domain.payment.dto.response.PayResponseDTO;

public interface PaymentService {
KakaoPayReadyResponse payReady(String name, Integer totalPrice, Long accountId);
PayResponseDTO approve(String pgToken, String tid, Long accountId);
}

0 comments on commit a63d482

Please sign in to comment.