본문 바로가기
Team project/[파이널] 개발자 매칭 서비스 - Perfectfolio

[10~15일차]토스페이먼츠 자동결제, 결제 취소 api

by 개발자공부 2024. 9. 13.
1. 토스페이먼츠 개발자 센터 가입

https://developers.tosspayments.com/

 

토스페이먼츠 개발자센터

토스페이먼츠 결제 연동 문서, API, 키, 테스트 내역, 웹훅 등록 등 개발에 필요한 정보와 기능을 확인해 보세요. 결제 연동에 필요한 모든 개발자 도구를 제공해 드립니다.

developers.tosspayments.com

 

https://docs.tosspayments.com/reference/using-api/api-keys

 

API 키 | 토스페이먼츠 개발자센터

토스페이먼츠 클라이언트 키 및 시크릿 키를 발급받고 사용하는 방법을 알아봅니다. 클라이언트 키는 SDK를 초기화할 때 사용하고 시크릿 키는 API를 호출할 때 사용합니다.

docs.tosspayments.com

내 개발정보에서 API 키를 발급받을 수 있습니다. 테스트 결제 내역도 확인할 수 있습니다.
가이드와 API & SDK 래퍼런스를 참고합니다.

 

2. JSP header에 토스페이먼츠를 호출하는 js 추가
<!-- tosspayments js -->
<script src="https://js.tosspayments.com/v1"></script>

 

3. 자동 결제창 띄우기

필수 파라미터 값을 확인합니다.
successUrl과 failUrl을 입력해야 합니다. redirect url 개념입니다.

// 베이직 결제창 호출
function basicPay(){
    	const clientKey = "test_ck_LlDJaYngro1K6KqdMdnG3ezGdRpX"; // 서버에서 전달받은 클라이언트 키
        const tossPayments = TossPayments(clientKey);
        const customerKey = Math.random().toString(36).substring(2, 12); // 고객 고유키를 서버로부터 받아옵니다.

        tossPayments.requestBillingAuth("카드", {
            customerKey : customerKey, // 서버에서 전달받은 고객 키
            successUrl: "http://localhost:8080/pay/success", // 성공 시 리디렉션 URL
            failUrl: "http://localhost:8080/pay/fail" // 실패 시 리디렉션 URL
        })
        .catch(function (error) {
if (error.code === "USER_CANCEL") {
  // 결제 고객이 결제창을 닫았을 때 에러 처리
} else if (error.code === "INVALID_CARD_COMPANY") {
  // 유효하지 않은 카드 코드에 대한 에러 처리
}
});
};

 

 

4. 카드 정보 전달이 성공했을 시 빌링키 발급 요청과 결제 진행

 

authKey로 카드 빌링키 발급

헤더와 바디를 구성하여 요청합니다.

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.tosspayments.com/v1/billing/authorizations/issue"))
    .header("Authorization", "Basic dGVzdF9za19lcVJHZ1lPMXI1S0FFQjlheUs2MjNRbk4yRXlhOg==")
    .header("Content-Type", "application/json")
    .method("POST", HttpRequest.BodyPublishers.ofString("{\"authKey\":\"e_826EDB0730790E96F116FFF3799A65DE\",\"customerKey\":\"aENcQAtPdYbTjGhtQnNVj\"}"))
    .build();
HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());

 

발급 받은 빌링키로 카드 자동결제 승인

헤더에 시크릿키 입력도 필수입니다!

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.tosspayments.com/v1/billing/Z_t5vOvQxrj4499PeiJcjen28-V2RyqgYTwN44Rdzk0="))
    .header("Authorization", "Basic dGVzdF9za19lcVJHZ1lPMXI1S0FFQjlheUs2MjNRbk4yRXlhOg==")
    .header("Content-Type", "application/json")
    .method("POST", HttpRequest.BodyPublishers.ofString("{\"customerKey\":\"aENcQAtPdYbTjGhtQnNVj\",\"amount\":4900,\"orderId\":\"a4CWyWY5m89PNh7xJwhk1\",\"orderName\":\"토스 프라임 구독\",\"customerEmail\":\"customer@email.com\",\"customerName\":\"박토스\",\"taxFreeAmount\":0,\"taxExemptionAmount\":0}"))
    .build();
HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());

+) header 부분에 Basic 다음에는 한 칸 띄운 후 발급받은 시크릿 키를 Base64 방식으로 인코딩하여 입력합니다!

https://docs.tosspayments.com/resources/glossary/base64 (<참고)

 

바디에 필수 파라미터 값입니다.
예시 응답입니다.

 

Controller

@Controller
@RequestMapping("/pay")
public class PaymentController {
/**
	 * 베이직 결제 로직 처리
	 *
	 * @param authKey
	 * @param customerKey
	 * @param model
	 * @return
	 */
	@GetMapping("/success")
	public String success(@RequestParam("authKey") String authKey, @RequestParam("customerKey") String customerKey,
			Model model) {

		// 권한 확인
		if (session.getAttribute("principal") == null) {
			throw new DataDeliveryException(Define.NOT_AN_AUTHENTICATED_USER, HttpStatus.BAD_REQUEST);
		}
		// 구독 진행중인지 확인
		PrincipalDTO principalDTO = (PrincipalDTO) session.getAttribute("principal");
		int userPk = principalDTO.getId();
		int duplication = paymentService.checkDuplication(userPk);

		if (duplication == 0) {
			try {
				// 주문 ID 생성
				String orderId = UUID.randomUUID().toString();
				String orderName = "basic";
				int amount = 5900;

				// 빌링키 발급과 자동 결제 실행
				String response = paymentService.authorizeBillingAndAutoPayment(authKey, customerKey, orderId,
						orderName, amount, userPk); // 금액은 실제 금액으로 대체

				return "payment/success";

			} catch (Exception e) {
				model.addAttribute("message", e.getMessage());
				throw new DataDeliveryException(Define.FAILED_PAYMENT, HttpStatus.BAD_REQUEST);
//				return "redirect:/pay/fail";
			}

		} else {
			throw new DataDeliveryException(Define.FAILED_SUBSCRIBE, HttpStatus.BAD_REQUEST);
		}
	}
    
 }

 

Service

@Service
@RequiredArgsConstructor
public class PaymentService {

	@Value("${payment.toss.test-secret-api-key}")
	private String secretKey;
	@Autowired
	private PaymentRepository paymentRepository;
	private final ObjectMapper objectMapper = new ObjectMapper();

	/**
	 * 결제 요청 로직
	 * @param authKey
	 * @param customerKey
	 * @param orderId
	 * @param orderName
	 * @param amount
	 * @param userPk
	 * @return
	 * @throws Exception
	 */
	@Transactional
	public String authorizeBillingAndAutoPayment(String authKey, String customerKey, String orderId, String orderName,
			Integer amount, Integer userPk) throws Exception {
		String encodedAuthHeader = Base64.getEncoder().encodeToString((secretKey + ":").getBytes());

		// 빌링키 발급과 동시에 자동 결제 수행
		HttpRequest billingRequest = HttpRequest.newBuilder()
				.uri(URI.create("https://api.tosspayments.com/v1/billing/authorizations/issue"))
				.header("Authorization", "Basic " + encodedAuthHeader).header("Content-Type", "application/json")
				.method("POST",
						HttpRequest.BodyPublishers
								.ofString("{\"authKey\":\"" + authKey + "\",\"customerKey\":\"" + customerKey + "\"}"))
				.build();

		HttpResponse<String> billingResponse = HttpClient.newHttpClient().send(billingRequest,
				HttpResponse.BodyHandlers.ofString());

		if (billingResponse.statusCode() == 200) {
			JsonNode billingJson = objectMapper.readTree(billingResponse.body());
			String billingKey = billingJson.get("billingKey").asText();

			// 자동 결제 요청
			HttpRequest paymentRequest = HttpRequest.newBuilder()
					.uri(URI.create("https://api.tosspayments.com/v1/billing/" + billingKey))
					.header("Authorization", "Basic " + encodedAuthHeader).header("Content-Type", "application/json")
					.method("POST",
							HttpRequest.BodyPublishers.ofString(
									"{\"customerKey\":\"" + customerKey + "\"," + "\"orderId\":\"" + orderId + "\","
											+ "\"orderName\":\"" + orderName + "\"," + "\"amount\":" + amount + "}"))
					.build();

			HttpResponse<String> paymentResponse = HttpClient.newHttpClient().send(paymentRequest,
					HttpResponse.BodyHandlers.ofString());

			if (paymentResponse.statusCode() == 200) {
				 JsonNode paymentJson = objectMapper.readTree(paymentResponse.body());

				 // 다음 결제일 계산
				 String dateFormatType = "yyyy-MM-dd";
				 Date toDay = new Date();
				 SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormatType);
				 Calendar cal = Calendar.getInstance();
				 cal.setTime(toDay);
				 cal.add(Calendar.MONTH, +1);
				 String nextDate = simpleDateFormat.format(cal.getTime());
				 
				 // DTO 변환
				 PaymentDTO paymentDTO = PaymentDTO.builder()
						.userId(userPk)
						.lastTransactionKey(paymentJson.get("lastTransactionKey").asText())
						.paymentKey(paymentJson.get("paymentKey").asText())
						.orderId(paymentJson.get("orderId").asText())
						.orderName2(paymentJson.get("orderName").asText())
						.billingKey(billingKey)
						.customerKey(customerKey)
						.amount(amount)
						.totalAmount(paymentJson.get("totalAmount").asText())
						.requestedAt(paymentJson.get("requestedAt").asText())
						.approvedAt(paymentJson.get("approvedAt").asText())
						.cancel("N")
						.nextPay(nextDate)
						.build();
				paymentRepository.insert(paymentDTO.toPayment());
				paymentRepository.insertSubscribing(paymentDTO.toSubscribing());

				return paymentJson.toPrettyString();
			} else {

				// DTO 변환
				PaymentDTO errorPaymentDTO = PaymentDTO.builder()
						.userId(userPk).customerKey(customerKey)
						.billingKey(billingKey)
						.amount(amount)
						.orderId(orderId)
						.orderName(orderName)
						.billingErrorCode(billingResponse.statusCode())
						.payErrorCode(paymentResponse.statusCode())
						.build();
				paymentRepository.insertOrder(errorPaymentDTO.toOrder());

				throw new RuntimeException(Define.FAILED_PROCESS_PAYMENT + paymentResponse.body());
			}
		} else {
			throw new RuntimeException(Define.FAILED_ISSUE_BILLINGKEY + billingResponse.body());
		}
	}
    
    }

 


환불 구현하기

5. 결제 취소 api

결제 취소는 시크릿키, payment key, cancelReason만 있으면 됩니다.

 

 

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.tosspayments.com/v1/payments/5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1/cancel"))
    .header("Authorization", "Basic dGVzdF9za19lcVJHZ1lPMXI1S0FFQjlheUs2MjNRbk4yRXlhOg==")
    .header("Content-Type", "application/json")
    .method("POST", HttpRequest.BodyPublishers.ofString("{\"cancelReason\":\"고객 변심\"}"))
    .build();
HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());

+) header 부분에 Basic 다음에는 한 칸 띄운 후 발급받은 시크릿 키를 Base64 방식으로 인코딩!


 

기타 코드
더보기
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class PaymentDTO{
	// 결제 시도시 필요한 값
	private Integer userId;
	private String customerKey;
	private String billingKey;
	private Integer amount;
	private String orderName;

	// 결제 완료 후 받아오는 값
	private String lastTransactionKey;
	private String paymentKey;
	private String orderId;
	private String orderName2;
	private String requestedAt;
	private String approvedAt;
	private String totalAmount;
	private String cancel;
	private Card card;

	// 결체 취소시 입력되는 값
	private String cancelReason;
	private String cancelAmount;
	private Integer adminId;

	// 다음 정기결제일 계산
	private String nextPay;
	
	// 에러 메시지
	private Integer billingErrorCode;
	private Integer payErrorCode;
	private String billingErrorMsg;
	private String payErrorMsg;

	// 보류
	@Data
	public class Card {
		String number;
		String installmentPlanMonths;
		String cardType;
		String ownerType;
		String amount;
	}

	// Order 객체 반환
	public Order toOrder() {
		return Order.builder()
				.userId(userId)
				.customerKey(customerKey)
				.billingKey(billingKey)
				.amount(amount)
				.orderId(orderId)
				.orderName(orderName)
				.billingErrorCode(billingErrorCode)
				.payErrorCode(payErrorCode)
				.build();

	}

	// Payment 객체 반환
	public Payment toPayment() {
		return Payment.builder()
				.userId(userId)
				.lastTransactionKey(lastTransactionKey)
				.paymentKey(paymentKey)
				.orderId(orderId)
				.orderName(orderName2)
				.billingKey(billingKey)
				.customerKey(customerKey)
				.amount(amount)
				.totalAmount(totalAmount)
				.requestedAt(requestedAt)
				.approvedAt(approvedAt)
				.cancel(cancel)
				.build();
	}

	// Refund 객체 변환
	public Refund toRefund() {
		return Refund.builder()
				.lastTransactionKey(lastTransactionKey)
				.paymentKey(paymentKey)
				.cancelReason(cancelReason)
				.requestedAt(requestedAt)
				.approvedAt(approvedAt)
				.cancelAmount(cancelAmount)
				.adminId(adminId)
				.build();
	}

	// subscribing 객체 반환
	public Subscribing toSubscribing() {
		return Subscribing.builder()
				.subscribing("Y")
				.userId(userId)
				.orderName(orderName2)
				.billingKey(billingKey)
				.customerKey(customerKey)
				.amount(amount)
				.nextPay(nextPay)
				.build();
	}
	
}

 

Order Model (결제 실패시)

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class Order {
	
	private Integer id;
	private Integer userId;
	private String customerKey;
	private String billingKey;
	private Integer amount;
	private String orderId;
	private String orderName;
	private Integer billingErrorCode;
	private Integer payErrorCode;
	private String created_at;
	
}

 

Payment Model (결제 성공시)

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class Payment{

	private Integer id;
	private Integer userId;
	private String lastTransactionKey;
	private String paymentKey;
	private String orderId;
	private String orderName;
	private String billingKey;
	private String customerKey;
	private Integer amount;
	private String totalAmount;
	private String requestedAt;
	private String approvedAt;
	private String cancel;
	
}

 

Refund Model (환불시)

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
public class Refund{
	
	private Integer id;
	private String lastTransactionKey;
	private String paymentKey;
	private String cancelReason;
	private String requestedAt;
	private String approvedAt;
	private String cancelAmount;
	private Integer adminId;
	
}