Skip to content

Commit da0baae

Browse files
authored
feat: 결제 구현 (#97)
* feat: 결제 관련 서비스, 컨트롤러, dto 추가 * feat: 결제 로직 수정 * feat: 컨트롤러 로직 수정 * feat: 취소 기능 적용
1 parent 2e98782 commit da0baae

19 files changed

+627
-1
lines changed

src/main/java/com/threestar/trainus/domain/coupon/user/entity/UserCoupon.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
import com.threestar.trainus.domain.user.entity.User;
66
import com.threestar.trainus.global.entity.BaseDateEntity;
7+
import com.threestar.trainus.global.exception.domain.ErrorCode;
8+
import com.threestar.trainus.global.exception.handler.BusinessException;
79

810
import jakarta.persistence.Entity;
911
import jakarta.persistence.EnumType;
@@ -15,6 +17,7 @@
1517
import jakarta.persistence.ManyToOne;
1618
import jakarta.persistence.Table;
1719
import jakarta.persistence.UniqueConstraint;
20+
import jakarta.persistence.Version;
1821
import lombok.AccessLevel;
1922
import lombok.AllArgsConstructor;
2023
import lombok.Builder;
@@ -53,10 +56,29 @@ public class UserCoupon extends BaseDateEntity {
5356

5457
private LocalDateTime expirationDate;
5558

59+
@Version
60+
private Long version;
61+
5662
public UserCoupon(User user, Coupon coupon, LocalDateTime expirationDate) {
5763
this.user = user;
5864
this.coupon = coupon;
5965
this.expirationDate = expirationDate;
6066
this.status = CouponStatus.ACTIVE;
6167
}
68+
69+
public void use() {
70+
if (this.status != CouponStatus.ACTIVE) {
71+
throw new BusinessException(ErrorCode.INVALID_COUPON);
72+
}
73+
this.status = CouponStatus.INACTIVE;
74+
this.useDate = LocalDateTime.now();
75+
}
76+
77+
public void restore() {
78+
if (this.status == CouponStatus.ACTIVE) {
79+
throw new BusinessException(ErrorCode.INVALID_COUPON_RESTORE);
80+
}
81+
this.status = CouponStatus.ACTIVE;
82+
this.useDate = null;
83+
}
6284
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package com.threestar.trainus.domain.payment.controller;
2+
3+
import org.springframework.http.HttpStatus;
4+
import org.springframework.http.ResponseEntity;
5+
import org.springframework.web.bind.annotation.PathVariable;
6+
import org.springframework.web.bind.annotation.PostMapping;
7+
import org.springframework.web.bind.annotation.RequestBody;
8+
import org.springframework.web.bind.annotation.RequestMapping;
9+
import org.springframework.web.bind.annotation.RestController;
10+
11+
import com.threestar.trainus.domain.payment.dto.CancelPaymentRequest;
12+
import com.threestar.trainus.domain.payment.dto.ConfirmPaymentRequest;
13+
import com.threestar.trainus.domain.payment.dto.PaymentClient;
14+
import com.threestar.trainus.domain.payment.dto.PaymentRequestDto;
15+
import com.threestar.trainus.domain.payment.dto.PaymentResponseDto;
16+
import com.threestar.trainus.domain.payment.dto.SaveAmountRequest;
17+
import com.threestar.trainus.domain.payment.dto.TossPaymentResponseDto;
18+
import com.threestar.trainus.domain.payment.service.PaymentService;
19+
import com.threestar.trainus.global.unit.BaseResponse;
20+
21+
import jakarta.servlet.http.HttpSession;
22+
import lombok.RequiredArgsConstructor;
23+
import lombok.extern.slf4j.Slf4j;
24+
25+
@Slf4j
26+
@RestController
27+
@RequestMapping("/api/v1/payments")
28+
@RequiredArgsConstructor
29+
public class PaymentController {
30+
31+
private final PaymentClient tossPaymentClient;
32+
private final PaymentService paymentService;
33+
34+
@PostMapping("/prepare")
35+
public ResponseEntity<BaseResponse<PaymentResponseDto>> preparePayment(HttpSession session,
36+
@RequestBody PaymentRequestDto request) {
37+
Long userId = (Long)session.getAttribute("LOGIN_USER");
38+
PaymentResponseDto response = paymentService.preparePayment(request, userId);
39+
//여기에서 쿠폰 가격 정해서 답변 내보내고 이를 통해 아래 호출
40+
return BaseResponse.ok("결제 정보 준비 완료", response, HttpStatus.OK);
41+
}
42+
43+
@PostMapping("/saveAmount")
44+
public ResponseEntity<BaseResponse<Void>> saveAmount(HttpSession session, @RequestBody SaveAmountRequest request) {
45+
session.setAttribute(request.getOrderId(), request.getAmount());
46+
return BaseResponse.ok("Payment temp save Successful", null, HttpStatus.OK);
47+
}
48+
49+
@PostMapping("/verifyAmount")
50+
public ResponseEntity<BaseResponse<Void>> verifyAmount(HttpSession session,
51+
@RequestBody SaveAmountRequest request) {
52+
Integer amount = (Integer)session.getAttribute(request.getOrderId());
53+
log.info("amount = {}", amount);
54+
55+
try {
56+
if (amount == null || !amount.equals(request.getAmount())) {
57+
return BaseResponse.error("결제 금액 정보가 유효하지 않습니다", null, HttpStatus.BAD_REQUEST);
58+
}
59+
return BaseResponse.ok("Payment is valid", null, HttpStatus.OK);
60+
} finally {
61+
session.removeAttribute(request.getOrderId());
62+
}
63+
}
64+
65+
@PostMapping("/confirm")
66+
public ResponseEntity<BaseResponse<Void>> confirm(@RequestBody ConfirmPaymentRequest request) {
67+
TossPaymentResponseDto tossResponse = tossPaymentClient.confirmPayment(request);
68+
paymentService.processConfirm(tossResponse);
69+
return BaseResponse.ok("결제 성공", null, HttpStatus.OK);
70+
}
71+
72+
@PostMapping("/cancel")
73+
public ResponseEntity<BaseResponse<Void>> cancel(@RequestBody CancelPaymentRequest request) {
74+
TossPaymentResponseDto tossResponse = tossPaymentClient.cancelPayment(request);
75+
paymentService.processCancel(tossResponse);
76+
return BaseResponse.ok("결제 취소 성공", null, HttpStatus.OK);
77+
}
78+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.threestar.trainus.domain.payment.dto;
2+
3+
import lombok.Data;
4+
5+
@Data
6+
public class CancelPaymentRequest {
7+
private String paymentKey;
8+
private String cancelReason;
9+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.threestar.trainus.domain.payment.dto;
2+
3+
import lombok.Data;
4+
5+
@Data
6+
public class ConfirmPaymentRequest {
7+
private int amount;
8+
private String orderId;
9+
private String paymentKey;
10+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package com.threestar.trainus.domain.payment.dto;
2+
3+
import java.nio.charset.StandardCharsets;
4+
import java.util.Base64;
5+
6+
import org.apache.http.HttpHeaders;
7+
import org.springframework.http.HttpStatusCode;
8+
import org.springframework.http.MediaType;
9+
import org.springframework.stereotype.Component;
10+
import org.springframework.web.client.RestClient;
11+
12+
import com.threestar.trainus.global.exception.domain.ErrorCode;
13+
import com.threestar.trainus.global.exception.handler.BusinessException;
14+
15+
import lombok.extern.slf4j.Slf4j;
16+
17+
@Slf4j
18+
@Component
19+
public class PaymentClient {
20+
private static final String BASIC_DELIMITER = ":";
21+
private static final String AUTH_HEADER_PREFIX = "Basic ";
22+
23+
private final PaymentProperties paymentProperties;
24+
private final RestClient restClient;
25+
26+
public PaymentClient(PaymentProperties paymentProperties) {
27+
this.paymentProperties = paymentProperties;
28+
this.restClient = RestClient.builder()
29+
.baseUrl(paymentProperties.getBaseUrl())
30+
.defaultHeader(HttpHeaders.AUTHORIZATION, createPaymentAuthHeader(paymentProperties))
31+
.build();
32+
}
33+
34+
private String createPaymentAuthHeader(PaymentProperties paymentProperties) {
35+
log.info("secret-key : {}, url ={} baseUrl = {}, baseUrl2 = {}", paymentProperties.getSecretKey(),
36+
paymentProperties.getBaseUrl(), paymentProperties.getConfirmEndPoint(),
37+
paymentProperties.getCancelEndPoint());
38+
byte[] encodedBytes = Base64.getEncoder()
39+
.encode((paymentProperties.getSecretKey() + BASIC_DELIMITER).getBytes(StandardCharsets.UTF_8));
40+
log.info("new Header = {}", AUTH_HEADER_PREFIX + new String(encodedBytes));
41+
return AUTH_HEADER_PREFIX + new String(encodedBytes);
42+
}
43+
44+
public TossPaymentResponseDto confirmPayment(ConfirmPaymentRequest request) {
45+
log.info("Sending Toss confirm request: orderId={}, amount={}, paymentKey={}",
46+
request.getOrderId(), request.getAmount(), request.getPaymentKey());
47+
48+
return restClient.post()
49+
.uri(paymentProperties.getConfirmEndPoint())
50+
.contentType(MediaType.APPLICATION_JSON)
51+
.body(request)
52+
.retrieve()
53+
.onStatus(HttpStatusCode::isError, (req, res) -> {
54+
throw new BusinessException(ErrorCode.CONFIRM_PAYMENT_FAILED);
55+
})
56+
.body(TossPaymentResponseDto.class);
57+
}
58+
59+
public TossPaymentResponseDto cancelPayment(CancelPaymentRequest request) {
60+
return restClient.post()
61+
.uri(String.format(paymentProperties.getCancelEndPoint(), request.getPaymentKey()))
62+
.contentType(MediaType.APPLICATION_JSON)
63+
.body(request)
64+
.retrieve()
65+
.onStatus(HttpStatusCode::isError, (req, res) -> {
66+
throw new BusinessException(ErrorCode.CANCEL_PAYMENT_FAILED);
67+
})
68+
.body(TossPaymentResponseDto.class);
69+
}
70+
71+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.threestar.trainus.domain.payment.dto;
2+
3+
import org.springframework.boot.context.properties.ConfigurationProperties;
4+
import org.springframework.stereotype.Component;
5+
6+
import lombok.Getter;
7+
import lombok.Setter;
8+
9+
@Getter
10+
@Setter
11+
@Component
12+
@ConfigurationProperties(prefix = "payment")
13+
public class PaymentProperties {
14+
15+
private String secretKey;
16+
private String baseUrl;
17+
private String confirmEndPoint;
18+
private String cancelEndPoint;
19+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.threestar.trainus.domain.payment.dto;
2+
3+
import com.threestar.trainus.domain.payment.entity.PaymentMethod;
4+
5+
import lombok.Data;
6+
7+
@Data
8+
public class PaymentRequestDto {
9+
private Long lessonId;
10+
private Long userCouponId;
11+
private PaymentMethod paymentMethod;
12+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.threestar.trainus.domain.payment.dto;
2+
3+
import java.time.LocalDateTime;
4+
5+
import com.threestar.trainus.domain.payment.entity.PaymentMethod;
6+
7+
import lombok.Builder;
8+
import lombok.Data;
9+
10+
@Builder
11+
@Data
12+
public class PaymentResponseDto {
13+
private String orderId;
14+
private String lessonTitle;
15+
private Integer originPrice;
16+
private Integer payPrice;
17+
private PaymentMethod paymentMethod;
18+
private LocalDateTime expiredAt;
19+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.threestar.trainus.domain.payment.dto;
2+
3+
import lombok.Data;
4+
5+
@Data
6+
public class SaveAmountRequest {
7+
private String orderId;
8+
private Integer amount;
9+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.threestar.trainus.domain.payment.dto;
2+
3+
import java.time.LocalDateTime;
4+
5+
import com.threestar.trainus.domain.payment.entity.PaymentStatus;
6+
7+
import lombok.Getter;
8+
9+
@Getter
10+
public class TossPaymentResponseDto {
11+
private String paymentKey;
12+
private String orderId;
13+
private String orderName;
14+
private String status;
15+
private String requestedAt;
16+
private String approvedAt;
17+
private int totalAmount;
18+
private String method;
19+
}

0 commit comments

Comments
 (0)