Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 src/main/java/com/kt/common/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ public enum ErrorCode {
//payment
NOT_FOUND_PAYMENT(HttpStatus.BAD_REQUEST, "해당 결제 정보를 찾을 수 없습니다"),
NOT_FOUND_PAYMENT_TYPE(HttpStatus.BAD_REQUEST, "결제수단을 찾을 수 없습니다"),
FAIL_PAYMENT(HttpStatus.BAD_REQUEST, "결제에 실패했습니다."),
ALREADY_PAID_ORDER(HttpStatus.BAD_REQUEST, "이미 결제가 완료된 주문입니다."),
CANNOT_CANCEL_PAYMENT_AMOUNT_MISMATCH(HttpStatus.BAD_REQUEST, "결제 취소 금액이 결제 금액과 일치하지 않습니다."),
PAYMENT_CANCEL_FAILED(HttpStatus.BAD_REQUEST, "결제 취소에 실패했습니다."),

//rate limit
GLOBAL_EXCEED_REQUEST_LIMIT(HttpStatus.TOO_MANY_REQUESTS, "요청 횟수 제한을 초과했습니다."),
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/com/kt/config/RestTemplateConfiguration.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.kt.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfiguration {

@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}

}
4 changes: 2 additions & 2 deletions src/main/java/com/kt/config/SecurityConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
public class SecurityConfiguration {


private static final String[] GET_PERMIT_ALL = {"/api/health/**", "/swagger-ui.html","/swagger-ui/**", "/v3/api-docs/**", "/actuator/**"};
private static final String[] POST_PERMIT_ALL = {"/api/users/auth/signup", "/api/users/auth/login","/api/admin/users/auth/signup", "/api/admin/users/auth/login","/api/users/reissue"};
private static final String[] GET_PERMIT_ALL = {"/api/health/**", "/swagger-ui.html","/swagger-ui/**", "/v3/api-docs/**", "/actuator/**", "/payment-*.html", "/api/payments/client-key", "/*.css"};
private static final String[] POST_PERMIT_ALL = {"/api/users/auth/signup", "/api/users/auth/login","/api/admin/users/auth/signup", "/api/admin/users/auth/login","/api/users/reissue", "/api/payments/confirm"};
private static final String[] PUT_PERMIT_ALL = {"/api/v1/public/**"};
private static final String[] PATCH_PERMIT_ALL = {"/api/v1/public/**"};
private static final String[] DELETE_PERMIT_ALL = {"/api/v1/public/**"};
Expand Down
161 changes: 160 additions & 1 deletion src/main/java/com/kt/controller/payment/PaymentController.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
package com.kt.controller.payment;

import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

import org.springframework.data.domain.Page;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
Expand All @@ -10,14 +18,26 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;

import com.kt.common.exception.CustomException;
import com.kt.common.exception.ErrorCode;
import com.kt.common.request.Paging;
import com.kt.common.response.ApiResult;
import com.kt.common.support.SwaggerAssistance;
import com.kt.domain.order.Order;
import com.kt.domain.order.OrderStatus;
import com.kt.dto.payment.PaymentCreateRequest;
import com.kt.dto.payment.PaymentDetailResponse;
import com.kt.dto.payment.PaymentListResponse;
import com.kt.dto.payment.PaymentOrderInfoResponse;
import com.kt.dto.payment.PaymentTossCancelRequest;
import com.kt.dto.payment.PaymentTossCancelResponse;
import com.kt.dto.payment.PaymentTossConfirmRequest;
import com.kt.properties.TossPaymentsProperties;
import com.kt.security.CustomUserDetails;
import com.kt.service.order.OrderService;
import com.kt.service.payment.PaymentService;

import io.swagger.v3.oas.annotations.Operation;
Expand All @@ -32,6 +52,9 @@
public class PaymentController extends SwaggerAssistance {

private final PaymentService paymentService;
private final OrderService orderService;
private final TossPaymentsProperties tossPaymentsProperties;
private final RestTemplate restTemplate = new RestTemplate();

@GetMapping
@ResponseStatus(HttpStatus.OK)
Expand All @@ -53,11 +76,147 @@ public ApiResult<PaymentDetailResponse> getPayment(
return ApiResult.ok(paymentService.getPayment(currentUser.getId(), paymentId));
}

@PostMapping("")
/*결제기록 등록 -> toss 결제 이후 성공 시 등록 변경*/
/*@PostMapping("")
@Operation(summary = "결제 기록 등록")
public ApiResult<Void> create(@Valid @RequestBody PaymentCreateRequest request, @AuthenticationPrincipal CustomUserDetails currentUser) {
paymentService.create(request, currentUser.getId());
return ApiResult.ok();
}*/

@PostMapping("/confirm")
@Operation(summary = "Toss 결제 승인 요청")
public ResponseEntity<?> confirmPayment(@RequestBody PaymentTossConfirmRequest request,
@AuthenticationPrincipal CustomUserDetails currentUser
) {
try {
String url = tossPaymentsProperties.getApiUrl() + "/confirm";
String auth = tossPaymentsProperties.getSecretKey() + ":";
String encodedAuth = java.util.Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));

String orderId = request.orderId().split("-")[0];
Long orderIdLong = Long.parseLong(orderId);
var order = orderService.getOrderDetail(orderIdLong, currentUser.getId());
if (order.orderStatus() != OrderStatus.ORDERED) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(Map.of(
"code", ErrorCode.ALREADY_PAID_ORDER.getStatus(),
"message", ErrorCode.ALREADY_PAID_ORDER.getMessage()
));
}

HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Basic " + encodedAuth);
headers.setContentType(MediaType.APPLICATION_JSON);
System.out.println(request.toString());
Map<String, String> body = Map.of(
"paymentKey", request.paymentKey(),
"orderId", request.orderId(),
"amount", request.amount().toString()
);

HttpEntity<Map<String, String>> entity = new HttpEntity<>(body, headers);
ResponseEntity<Map> response = restTemplate.postForEntity(url, entity, Map.class);

if (response.getStatusCode() == HttpStatus.OK) {
Map<String, Object> tossPayment = response.getBody();
Long paymentId = paymentService.create(tossPayment, currentUser.getId());

Map<String, Object> responseBody = new HashMap<>(tossPayment);
responseBody.put("paymentId", paymentId);

return ResponseEntity.ok(responseBody);
}

return ResponseEntity.ok(response.getBody());

} catch (HttpClientErrorException e) {
// Toss API에서 반환한 에러 응답을 그대로 전달
return ResponseEntity
.status(e.getStatusCode())
.body(e.getResponseBodyAsString());
} catch (Exception e) {
e.printStackTrace();
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of(
"code", "INTERNAL_SERVER_ERROR",
"message", "결제 승인 처리 중 오류가 발생했습니다: " + e.getMessage()
));
}
}

@GetMapping("/order/{orderId}")
@Operation(summary = "주문 ID로 결제 정보 조회")
public ApiResult<PaymentDetailResponse> getPaymentByOrderId(
@PathVariable Long orderId,
@AuthenticationPrincipal CustomUserDetails currentUser
) {
return ApiResult.ok(paymentService.getPaymentByOrderId(currentUser.getId(), orderId));
}

@PostMapping("/{paymentId}/cancel")
@Operation(summary = "결제 취소")
public ResponseEntity<?> cancelPayment(
@PathVariable Long paymentId,
@RequestBody PaymentTossCancelRequest request,
@AuthenticationPrincipal CustomUserDetails currentUser) {
try {
String paymentKey = paymentService.getPaymentKey(paymentId, currentUser.getId());


String url = tossPaymentsProperties.getApiUrl() + "/" + paymentKey + "/cancel";
String auth = tossPaymentsProperties.getSecretKey() + ":";
String encodedAuth = java.util.Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));

HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Basic " + encodedAuth);
headers.setContentType(MediaType.APPLICATION_JSON);

Map<String, Object> body = new HashMap<>();
body.put("cancelReason", request.cancelReason());
if (request.cancelAmount() != null) {
body.put("cancelAmount", request.cancelAmount());
}

HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);
ResponseEntity<Map> tossResponse = restTemplate.postForEntity(url, entity, Map.class);

if (tossResponse.getStatusCode() == HttpStatus.OK) {
PaymentTossCancelResponse response = paymentService.cancelPayment(request, currentUser.getId(), paymentId);
return ResponseEntity.ok(ApiResult.ok(response));
}

throw new CustomException(ErrorCode.PAYMENT_CANCEL_FAILED);

} catch (HttpClientErrorException e) {
// Toss API에서 반환한 에러 응답을 그대로 전달
return ResponseEntity
.status(e.getStatusCode())
.body(e.getResponseBodyAsString());
} catch (Exception e) {
e.printStackTrace();
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of(
"code", "INTERNAL_SERVER_ERROR",
"message", "결제 승인 처리 중 오류가 발생했습니다: " + e.getMessage()
));
}
}

@GetMapping("/order/{orderId}/info")
@Operation(summary = "주문 번호로 결제 정보 조회")
public ApiResult<PaymentOrderInfoResponse> getPaymentDetail(@PathVariable Long orderId,
@AuthenticationPrincipal CustomUserDetails currentUser) {
return ApiResult.ok(paymentService.getPaymentDetail(currentUser.getId(), orderId));
}

@GetMapping("/client-key")
@Operation(summary = "Toss 클라이언트 키 조회")
public ApiResult<Map<String, String>> getClientKey() {
return ApiResult.ok(Map.of("clientKey", tossPaymentsProperties.getClientKey()));
}


Expand Down
7 changes: 6 additions & 1 deletion src/main/java/com/kt/domain/payment/Payment.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ public class Payment extends BaseEntity {
@Column(nullable = false)
private Long finalPrice; // 총 결제 금액 (상품금액 + 배송비)

@Column(nullable = false)
private String paymentKey;

@Column(nullable = false)
private boolean isDeleted = false;

Expand All @@ -44,13 +47,15 @@ public Payment(
PaymentType paymentType,
Long totalPrice,
Long deliveryFee,
Long finalPrice
Long finalPrice,
String paymentKey
) {
this.order = order;
this.paymentType = paymentType;
this.totalPrice = totalPrice;
this.deliveryFee = deliveryFee;
this.finalPrice = finalPrice;
this.paymentKey = paymentKey;
this.isDeleted = false;
}

Expand Down
8 changes: 8 additions & 0 deletions src/main/java/com/kt/dto/discount/response/DiscountInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.kt.dto.discount.response;

public record DiscountInfo(
String discountName,
String discountType,
Long discountPrice
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
public record PaymentDetailResponse(
Long id, // 결제 ID
Long orderId, // 주문 ID
String paymentKey, // 결제 고유 키
String userLoginId, // 결제자 아이디(직접 결제한 유저는 로그인한 유저)
String userName, // 결제자 이름
OrderStatus orderStatus, // 주문 상태
Expand Down
57 changes: 57 additions & 0 deletions src/main/java/com/kt/dto/payment/PaymentOrderInfoResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.kt.dto.payment;

import java.time.LocalDateTime;
import java.util.List;

import com.kt.domain.order.OrderStatus;
import com.kt.dto.order.response.OrderProductResponse;

public record PaymentOrderInfoResponse(
Long orderId,
OrderStatus orderStatus,
String orderStatusDescription,
LocalDateTime createdAt,
String receiverName,
String receiverPhone,
String receiverAddress,
UserPaymentInfo user,
List<OrderProductInfo> orderProducts,
PriceInfo priceInfo
) {
public record UserPaymentInfo(
String name,
String email,
String mobile,
MembershipInfo membership,
Long money
) {}

public record MembershipInfo(
String level
) {}

public record AppliedDiscountInfo(
String discountName,
String discountType,
Long discountAmount
) {}

public record PriceInfo(
Long totalProductPrice,
Long totalDiscountPrice,
Long deliveryFee,
Long finalPrice
) {}

public record OrderProductInfo(
Long productId,
String productName,
Long productVariantId,
Long productCount,
Long price,
Long totalPrice,
Long discountPrice,
Long discountedPrice,
List<AppliedDiscountInfo> appliedDiscounts // 할인 목록 포함
) {}
}
16 changes: 16 additions & 0 deletions src/main/java/com/kt/dto/payment/PaymentTossCancelRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.kt.dto.payment;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

public record PaymentTossCancelRequest(
@Schema(description = "취소 사유", example = "고객 변심")
@NotBlank(message = "취소 사유는 필수입니다")
String cancelReason,

@Schema(description = "취소 금액", example = "50000")
@NotNull(message = "취소 금액은 필수입니다")
Long cancelAmount
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.kt.dto.payment;

public record PaymentTossCancelResponse(
Long cancelAmount,
String cancelReason,
String canceledAt
) {
}
26 changes: 26 additions & 0 deletions src/main/java/com/kt/dto/payment/PaymentTossConfirmRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.kt.dto.payment;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

public record PaymentTossConfirmRequest(

@Schema(description = "주문 ID", example = "1")
@NotNull(message = "주문 ID는 필수입니다")
String orderId,

@Schema(description = "결제 KEY", example = "tgen_20260104212559xFAo0")
@NotBlank(message = "결제 KEY는 필수입니다")
String paymentKey,

@Schema(description = "결제 금액", example = "50000")
@NotNull(message = "결제 금액은 필수입니다")
Long amount,

@Schema(description = "결제타입", example = "간편결제")
@NotBlank(message = "결제타입은 필수입니다")
String method

) {
}
Loading
Loading