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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import com.threestar.trainus.domain.user.entity.User;
import com.threestar.trainus.global.entity.BaseDateEntity;
import com.threestar.trainus.global.exception.domain.ErrorCode;
import com.threestar.trainus.global.exception.handler.BusinessException;

import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
Expand All @@ -15,6 +17,7 @@
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import jakarta.persistence.Version;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
Expand Down Expand Up @@ -53,10 +56,29 @@ public class UserCoupon extends BaseDateEntity {

private LocalDateTime expirationDate;

@Version
private Long version;

public UserCoupon(User user, Coupon coupon, LocalDateTime expirationDate) {
this.user = user;
this.coupon = coupon;
this.expirationDate = expirationDate;
this.status = CouponStatus.ACTIVE;
}

public void use() {
if (this.status != CouponStatus.ACTIVE) {
throw new BusinessException(ErrorCode.INVALID_COUPON);
}
this.status = CouponStatus.INACTIVE;
this.useDate = LocalDateTime.now();
}

public void restore() {
if (this.status == CouponStatus.ACTIVE) {
throw new BusinessException(ErrorCode.INVALID_COUPON_RESTORE);
}
this.status = CouponStatus.ACTIVE;
this.useDate = null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.threestar.trainus.domain.payment.controller;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.threestar.trainus.domain.payment.dto.CancelPaymentRequest;
import com.threestar.trainus.domain.payment.dto.ConfirmPaymentRequest;
import com.threestar.trainus.domain.payment.dto.PaymentClient;
import com.threestar.trainus.domain.payment.dto.PaymentRequestDto;
import com.threestar.trainus.domain.payment.dto.PaymentResponseDto;
import com.threestar.trainus.domain.payment.dto.SaveAmountRequest;
import com.threestar.trainus.domain.payment.dto.TossPaymentResponseDto;
import com.threestar.trainus.domain.payment.service.PaymentService;
import com.threestar.trainus.global.unit.BaseResponse;

import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

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

private final PaymentClient tossPaymentClient;
private final PaymentService paymentService;

@PostMapping("/prepare")
public ResponseEntity<BaseResponse<PaymentResponseDto>> preparePayment(HttpSession session,
@RequestBody PaymentRequestDto request) {
Long userId = (Long)session.getAttribute("LOGIN_USER");
PaymentResponseDto response = paymentService.preparePayment(request, userId);
//여기에서 쿠폰 가격 정해서 답변 내보내고 이를 통해 아래 호출
return BaseResponse.ok("결제 정보 준비 완료", response, HttpStatus.OK);
}

@PostMapping("/saveAmount")
public ResponseEntity<BaseResponse<Void>> saveAmount(HttpSession session, @RequestBody SaveAmountRequest request) {
session.setAttribute(request.getOrderId(), request.getAmount());
return BaseResponse.ok("Payment temp save Successful", null, HttpStatus.OK);
}

@PostMapping("/verifyAmount")
public ResponseEntity<BaseResponse<Void>> verifyAmount(HttpSession session,
@RequestBody SaveAmountRequest request) {
Integer amount = (Integer)session.getAttribute(request.getOrderId());
log.info("amount = {}", amount);

try {
if (amount == null || !amount.equals(request.getAmount())) {
return BaseResponse.error("결제 금액 정보가 유효하지 않습니다", null, HttpStatus.BAD_REQUEST);
}
return BaseResponse.ok("Payment is valid", null, HttpStatus.OK);
} finally {
session.removeAttribute(request.getOrderId());
}
}

@PostMapping("/confirm")
public ResponseEntity<BaseResponse<Void>> confirm(@RequestBody ConfirmPaymentRequest request) {
TossPaymentResponseDto tossResponse = tossPaymentClient.confirmPayment(request);
paymentService.processConfirm(tossResponse);
return BaseResponse.ok("결제 성공", null, HttpStatus.OK);
}

@PostMapping("/cancel")
public ResponseEntity<BaseResponse<Void>> cancel(@RequestBody CancelPaymentRequest request) {
TossPaymentResponseDto tossResponse = tossPaymentClient.cancelPayment(request);
paymentService.processCancel(tossResponse);
return BaseResponse.ok("결제 취소 성공", null, HttpStatus.OK);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.threestar.trainus.domain.payment.dto;

import lombok.Data;

@Data
public class CancelPaymentRequest {
private String paymentKey;
private String cancelReason;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.threestar.trainus.domain.payment.dto;

import lombok.Data;

@Data
public class ConfirmPaymentRequest {
private int amount;
private String orderId;
private String paymentKey;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.threestar.trainus.domain.payment.dto;

import java.nio.charset.StandardCharsets;
import java.util.Base64;

import org.apache.http.HttpHeaders;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;

import com.threestar.trainus.global.exception.domain.ErrorCode;
import com.threestar.trainus.global.exception.handler.BusinessException;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class PaymentClient {
private static final String BASIC_DELIMITER = ":";
private static final String AUTH_HEADER_PREFIX = "Basic ";

private final PaymentProperties paymentProperties;
private final RestClient restClient;

public PaymentClient(PaymentProperties paymentProperties) {
this.paymentProperties = paymentProperties;
this.restClient = RestClient.builder()
.baseUrl(paymentProperties.getBaseUrl())
.defaultHeader(HttpHeaders.AUTHORIZATION, createPaymentAuthHeader(paymentProperties))
.build();
}

private String createPaymentAuthHeader(PaymentProperties paymentProperties) {
log.info("secret-key : {}, url ={} baseUrl = {}, baseUrl2 = {}", paymentProperties.getSecretKey(),
paymentProperties.getBaseUrl(), paymentProperties.getConfirmEndPoint(),
paymentProperties.getCancelEndPoint());
byte[] encodedBytes = Base64.getEncoder()
.encode((paymentProperties.getSecretKey() + BASIC_DELIMITER).getBytes(StandardCharsets.UTF_8));
log.info("new Header = {}", AUTH_HEADER_PREFIX + new String(encodedBytes));
return AUTH_HEADER_PREFIX + new String(encodedBytes);
}

public TossPaymentResponseDto confirmPayment(ConfirmPaymentRequest request) {
log.info("Sending Toss confirm request: orderId={}, amount={}, paymentKey={}",
request.getOrderId(), request.getAmount(), request.getPaymentKey());

return restClient.post()
.uri(paymentProperties.getConfirmEndPoint())
.contentType(MediaType.APPLICATION_JSON)
.body(request)
.retrieve()
.onStatus(HttpStatusCode::isError, (req, res) -> {
throw new BusinessException(ErrorCode.CONFIRM_PAYMENT_FAILED);
})
.body(TossPaymentResponseDto.class);
}

public TossPaymentResponseDto cancelPayment(CancelPaymentRequest request) {
return restClient.post()
.uri(String.format(paymentProperties.getCancelEndPoint(), request.getPaymentKey()))
.contentType(MediaType.APPLICATION_JSON)
.body(request)
.retrieve()
.onStatus(HttpStatusCode::isError, (req, res) -> {
throw new BusinessException(ErrorCode.CANCEL_PAYMENT_FAILED);
})
.body(TossPaymentResponseDto.class);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.threestar.trainus.domain.payment.dto;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "payment")
public class PaymentProperties {

private String secretKey;
private String baseUrl;
private String confirmEndPoint;
private String cancelEndPoint;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.threestar.trainus.domain.payment.dto;

import com.threestar.trainus.domain.payment.entity.PaymentMethod;

import lombok.Data;

@Data
public class PaymentRequestDto {
private Long lessonId;
private Long userCouponId;
private PaymentMethod paymentMethod;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.threestar.trainus.domain.payment.dto;

import java.time.LocalDateTime;

import com.threestar.trainus.domain.payment.entity.PaymentMethod;

import lombok.Builder;
import lombok.Data;

@Builder
@Data
public class PaymentResponseDto {
private String orderId;
private String lessonTitle;
private Integer originPrice;
private Integer payPrice;
private PaymentMethod paymentMethod;
private LocalDateTime expiredAt;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.threestar.trainus.domain.payment.dto;

import lombok.Data;

@Data
public class SaveAmountRequest {
private String orderId;
private Integer amount;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.threestar.trainus.domain.payment.dto;

import java.time.LocalDateTime;

import com.threestar.trainus.domain.payment.entity.PaymentStatus;

import lombok.Getter;

@Getter
public class TossPaymentResponseDto {
private String paymentKey;
private String orderId;
private String orderName;
private String status;
private String requestedAt;
private String approvedAt;
private int totalAmount;
private String method;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.threestar.trainus.domain.payment.entity;

import java.time.LocalDateTime;

import com.threestar.trainus.domain.coupon.user.entity.UserCoupon;
import com.threestar.trainus.domain.lesson.admin.entity.Lesson;
import com.threestar.trainus.domain.user.entity.User;
import com.threestar.trainus.global.entity.BaseDateEntity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Table(name = "payments")
@Getter
@Setter
@Entity
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public class Payment extends BaseDateEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "lesson_id", nullable = false)
private Lesson lesson;

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_coupon_id")
private UserCoupon userCoupon;

@Column(nullable = false)
private String orderId;
private Integer originPrice;
private Integer payPrice;
private LocalDateTime payDate;
private LocalDateTime refundDate;
private LocalDateTime cancelledAt;

@Enumerated(EnumType.STRING)
private PaymentStatus status;

@Enumerated(EnumType.STRING)
private PaymentMethod paymentMethod;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.threestar.trainus.domain.payment.entity;

public enum PaymentMethod {
CREDIT_CARD,
BANK_TRANSFER,
TOSS_PAYMENT;

public static PaymentMethod fromTossMethod(String tossMethod) {
return switch (tossMethod) {
case "카드" -> CREDIT_CARD;
case "계좌이체" -> BANK_TRANSFER;
case "간편결제" -> TOSS_PAYMENT;
default -> throw new IllegalArgumentException("지원하지 않는 결제 수단: " + tossMethod);
};
}
}
Loading