diff --git a/src/main/java/com/threestar/trainus/domain/coupon/user/entity/UserCoupon.java b/src/main/java/com/threestar/trainus/domain/coupon/user/entity/UserCoupon.java index 2677ba6..1bcd749 100644 --- a/src/main/java/com/threestar/trainus/domain/coupon/user/entity/UserCoupon.java +++ b/src/main/java/com/threestar/trainus/domain/coupon/user/entity/UserCoupon.java @@ -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; @@ -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; @@ -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; + } } diff --git a/src/main/java/com/threestar/trainus/domain/payment/controller/PaymentController.java b/src/main/java/com/threestar/trainus/domain/payment/controller/PaymentController.java new file mode 100644 index 0000000..6f0551f --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/payment/controller/PaymentController.java @@ -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> 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> 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> 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> confirm(@RequestBody ConfirmPaymentRequest request) { + TossPaymentResponseDto tossResponse = tossPaymentClient.confirmPayment(request); + paymentService.processConfirm(tossResponse); + return BaseResponse.ok("결제 성공", null, HttpStatus.OK); + } + + @PostMapping("/cancel") + public ResponseEntity> cancel(@RequestBody CancelPaymentRequest request) { + TossPaymentResponseDto tossResponse = tossPaymentClient.cancelPayment(request); + paymentService.processCancel(tossResponse); + return BaseResponse.ok("결제 취소 성공", null, HttpStatus.OK); + } +} diff --git a/src/main/java/com/threestar/trainus/domain/payment/dto/CancelPaymentRequest.java b/src/main/java/com/threestar/trainus/domain/payment/dto/CancelPaymentRequest.java new file mode 100644 index 0000000..6d7fe05 --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/payment/dto/CancelPaymentRequest.java @@ -0,0 +1,9 @@ +package com.threestar.trainus.domain.payment.dto; + +import lombok.Data; + +@Data +public class CancelPaymentRequest { + private String paymentKey; + private String cancelReason; +} diff --git a/src/main/java/com/threestar/trainus/domain/payment/dto/ConfirmPaymentRequest.java b/src/main/java/com/threestar/trainus/domain/payment/dto/ConfirmPaymentRequest.java new file mode 100644 index 0000000..0022daa --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/payment/dto/ConfirmPaymentRequest.java @@ -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; +} diff --git a/src/main/java/com/threestar/trainus/domain/payment/dto/PaymentClient.java b/src/main/java/com/threestar/trainus/domain/payment/dto/PaymentClient.java new file mode 100644 index 0000000..ce8a9c7 --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/payment/dto/PaymentClient.java @@ -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); + } + +} diff --git a/src/main/java/com/threestar/trainus/domain/payment/dto/PaymentProperties.java b/src/main/java/com/threestar/trainus/domain/payment/dto/PaymentProperties.java new file mode 100644 index 0000000..a181e7e --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/payment/dto/PaymentProperties.java @@ -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; +} diff --git a/src/main/java/com/threestar/trainus/domain/payment/dto/PaymentRequestDto.java b/src/main/java/com/threestar/trainus/domain/payment/dto/PaymentRequestDto.java new file mode 100644 index 0000000..4e39173 --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/payment/dto/PaymentRequestDto.java @@ -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; +} diff --git a/src/main/java/com/threestar/trainus/domain/payment/dto/PaymentResponseDto.java b/src/main/java/com/threestar/trainus/domain/payment/dto/PaymentResponseDto.java new file mode 100644 index 0000000..bea9074 --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/payment/dto/PaymentResponseDto.java @@ -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; +} diff --git a/src/main/java/com/threestar/trainus/domain/payment/dto/SaveAmountRequest.java b/src/main/java/com/threestar/trainus/domain/payment/dto/SaveAmountRequest.java new file mode 100644 index 0000000..39eb75f --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/payment/dto/SaveAmountRequest.java @@ -0,0 +1,9 @@ +package com.threestar.trainus.domain.payment.dto; + +import lombok.Data; + +@Data +public class SaveAmountRequest { + private String orderId; + private Integer amount; +} diff --git a/src/main/java/com/threestar/trainus/domain/payment/dto/TossPaymentResponseDto.java b/src/main/java/com/threestar/trainus/domain/payment/dto/TossPaymentResponseDto.java new file mode 100644 index 0000000..cb71f80 --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/payment/dto/TossPaymentResponseDto.java @@ -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; +} diff --git a/src/main/java/com/threestar/trainus/domain/payment/entity/Payment.java b/src/main/java/com/threestar/trainus/domain/payment/entity/Payment.java new file mode 100644 index 0000000..9fdfc5a --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/payment/entity/Payment.java @@ -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; + +} diff --git a/src/main/java/com/threestar/trainus/domain/payment/entity/PaymentMethod.java b/src/main/java/com/threestar/trainus/domain/payment/entity/PaymentMethod.java new file mode 100644 index 0000000..034fda0 --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/payment/entity/PaymentMethod.java @@ -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); + }; + } +} diff --git a/src/main/java/com/threestar/trainus/domain/payment/entity/PaymentStatus.java b/src/main/java/com/threestar/trainus/domain/payment/entity/PaymentStatus.java new file mode 100644 index 0000000..1d20ac3 --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/payment/entity/PaymentStatus.java @@ -0,0 +1,12 @@ +package com.threestar.trainus.domain.payment.entity; + +public enum PaymentStatus { + READY, + IN_PROGRESS, + WAITING_FOR_DEPOSIT, + DONE, + CANCELED, + PARTIAL_CANCELED, + ABORTED, + EXPIRED +} diff --git a/src/main/java/com/threestar/trainus/domain/payment/entity/TossPayment.java b/src/main/java/com/threestar/trainus/domain/payment/entity/TossPayment.java new file mode 100644 index 0000000..b12a8b2 --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/payment/entity/TossPayment.java @@ -0,0 +1,74 @@ +package com.threestar.trainus.domain.payment.entity; + +import java.time.LocalDateTime; + +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.OneToOne; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class TossPayment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String paymentKey; + + //토스 내부에서 관리하는 별도의 orderId + @Column(nullable = false) + private String orderId; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "payment_id", nullable = false) + private Payment payment; + + private Integer amount; + + private String orderName; + + @Enumerated(value = EnumType.STRING) + @Column(nullable = false) + private PaymentStatus paymentStatus; + + @Enumerated(value = EnumType.STRING) + @Column(nullable = false) + private PaymentMethod paymentMethod; + + @Column(nullable = false) + private LocalDateTime requestedAt; + + private LocalDateTime approvedAt; + + public void changeStatus(LocalDateTime requestedAt, LocalDateTime approvedAt, PaymentStatus paymentStatus) { + this.requestedAt = requestedAt; + this.approvedAt = approvedAt; + this.paymentStatus = paymentStatus; + } + + public boolean isCancelStatus() { + return this.paymentStatus == PaymentStatus.CANCELED; + } + + public boolean isDoneStatus() { + return this.paymentStatus == PaymentStatus.DONE; + } + +} diff --git a/src/main/java/com/threestar/trainus/domain/payment/mapper/PaymentMapper.java b/src/main/java/com/threestar/trainus/domain/payment/mapper/PaymentMapper.java new file mode 100644 index 0000000..43f3ef0 --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/payment/mapper/PaymentMapper.java @@ -0,0 +1,8 @@ +package com.threestar.trainus.domain.payment.mapper; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class PaymentMapper { +} diff --git a/src/main/java/com/threestar/trainus/domain/payment/repository/PaymentRepository.java b/src/main/java/com/threestar/trainus/domain/payment/repository/PaymentRepository.java new file mode 100644 index 0000000..bf42192 --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/payment/repository/PaymentRepository.java @@ -0,0 +1,11 @@ +package com.threestar.trainus.domain.payment.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.threestar.trainus.domain.payment.entity.Payment; + +public interface PaymentRepository extends JpaRepository { + Optional findByOrderId(String orderId); +} diff --git a/src/main/java/com/threestar/trainus/domain/payment/repository/TossPaymentRepository.java b/src/main/java/com/threestar/trainus/domain/payment/repository/TossPaymentRepository.java new file mode 100644 index 0000000..c5fd45c --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/payment/repository/TossPaymentRepository.java @@ -0,0 +1,11 @@ +package com.threestar.trainus.domain.payment.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.threestar.trainus.domain.payment.entity.TossPayment; + +public interface TossPaymentRepository extends JpaRepository { + Optional findByPaymentKey(String paymentKey); +} diff --git a/src/main/java/com/threestar/trainus/domain/payment/service/PaymentService.java b/src/main/java/com/threestar/trainus/domain/payment/service/PaymentService.java new file mode 100644 index 0000000..c5f5e43 --- /dev/null +++ b/src/main/java/com/threestar/trainus/domain/payment/service/PaymentService.java @@ -0,0 +1,150 @@ +package com.threestar.trainus.domain.payment.service; + +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.util.UUID; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.threestar.trainus.domain.coupon.user.entity.CouponStatus; +import com.threestar.trainus.domain.coupon.user.entity.UserCoupon; +import com.threestar.trainus.domain.coupon.user.repository.UserCouponRepository; +import com.threestar.trainus.domain.lesson.admin.entity.Lesson; +import com.threestar.trainus.domain.lesson.admin.service.AdminLessonService; +import com.threestar.trainus.domain.payment.dto.PaymentRequestDto; +import com.threestar.trainus.domain.payment.dto.PaymentResponseDto; +import com.threestar.trainus.domain.payment.dto.TossPaymentResponseDto; +import com.threestar.trainus.domain.payment.entity.Payment; +import com.threestar.trainus.domain.payment.entity.PaymentMethod; +import com.threestar.trainus.domain.payment.entity.PaymentStatus; +import com.threestar.trainus.domain.payment.entity.TossPayment; +import com.threestar.trainus.domain.payment.repository.PaymentRepository; +import com.threestar.trainus.domain.payment.repository.TossPaymentRepository; +import com.threestar.trainus.domain.user.entity.User; +import com.threestar.trainus.domain.user.service.UserService; +import com.threestar.trainus.global.exception.domain.ErrorCode; +import com.threestar.trainus.global.exception.handler.BusinessException; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class PaymentService { + + private final UserService userService; + private final AdminLessonService lessonService; + private final PaymentRepository paymentRepository; + private final TossPaymentRepository tossPaymentRepository; + private final UserCouponRepository userCouponRepository; + + @Transactional + public PaymentResponseDto preparePayment(PaymentRequestDto request, Long userId) { + Lesson lesson = lessonService.findLessonById(request.getLessonId()); + User user = userService.getUserById(userId); + + int originPrice = lesson.getPrice(); + + int discount = 0; + UserCoupon coupon = null; + if (request.getUserCouponId() != null) { + coupon = userCouponRepository.findById(request.getUserCouponId()) + .filter(c -> c.getStatus() == CouponStatus.ACTIVE) + .orElseThrow(() -> new BusinessException(ErrorCode.COUPON_NOT_FOUND)); + // discount = + String discountPrice = coupon.getCoupon().getDiscountPrice(); + if (discountPrice.contains("%")) { + discount = Integer.parseInt(discountPrice.substring(0, discountPrice.indexOf("%"))); + } else { + discount = Integer.parseInt(discountPrice); + } + coupon.use(); + } + + int finalPrice = Math.max(0, originPrice - discount); + + String orderId = UUID.randomUUID().toString(); + + Payment payment = Payment.builder() + .user(user) + .lesson(lesson) + .orderId(orderId) + .payPrice(finalPrice) + .originPrice(originPrice) + .payDate(LocalDateTime.now()) + .userCoupon(coupon) + .status(PaymentStatus.READY) + .paymentMethod(PaymentMethod.TOSS_PAYMENT) + .build(); + + paymentRepository.save(payment); + + return PaymentResponseDto.builder() + .originPrice(originPrice) + .lessonTitle(lesson.getLessonName()) + .paymentMethod(PaymentMethod.TOSS_PAYMENT) + .payPrice(finalPrice) + .orderId(orderId) + .build(); + + } + + @Transactional + public void processConfirm(TossPaymentResponseDto tossResponseDto) { + Payment payment = paymentRepository.findByOrderId(tossResponseDto.getOrderId()) + .orElseThrow(() -> new BusinessException(ErrorCode.INVALID_PAYMENT)); + + payment.setPayPrice(tossResponseDto.getTotalAmount()); + + DateTimeFormatter formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME; + LocalDateTime paidAt = OffsetDateTime.parse(tossResponseDto.getApprovedAt(), formatter).toLocalDateTime(); + + LocalDateTime requestAt = OffsetDateTime.parse(tossResponseDto.getRequestedAt(), formatter).toLocalDateTime(); + + payment.setPayDate(paidAt); + payment.setStatus(PaymentStatus.DONE); + payment.setPaymentMethod(PaymentMethod.fromTossMethod(tossResponseDto.getMethod())); + + TossPayment tossPayment = TossPayment.builder() + .payment(payment) + .paymentKey(tossResponseDto.getPaymentKey()) + .orderId(tossResponseDto.getOrderId()) + .amount(tossResponseDto.getTotalAmount()) + .orderName(tossResponseDto.getOrderName()) + .paymentStatus(PaymentStatus.DONE) + .paymentMethod(PaymentMethod.fromTossMethod(tossResponseDto.getMethod())) + .requestedAt(requestAt) + .approvedAt(paidAt) + .build(); + + tossPaymentRepository.save(tossPayment); + } + + @Transactional + public void processCancel(TossPaymentResponseDto tossResponseDto) { + TossPayment tossPayment = tossPaymentRepository.findByPaymentKey(tossResponseDto.getPaymentKey()) + .orElseThrow(() -> new BusinessException(ErrorCode.INVALID_PAYMENT)); + + DateTimeFormatter formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME; + LocalDateTime requestAt = OffsetDateTime.parse(tossResponseDto.getRequestedAt(), formatter).toLocalDateTime(); + + + tossPayment.changeStatus(requestAt, null, PaymentStatus.CANCELED); + + tossPaymentRepository.save(tossPayment); + + Payment payment = tossPayment.getPayment(); + payment.setStatus(PaymentStatus.CANCELED); + payment.setCancelledAt(LocalDateTime.now()); + + //쿠폰 복원 + if (payment.getUserCoupon() != null) { + UserCoupon coupon = payment.getUserCoupon(); + if (coupon.getStatus() == CouponStatus.INACTIVE) { + coupon.restore(); + userCouponRepository.save(coupon); + } + } + } +} diff --git a/src/main/java/com/threestar/trainus/global/exception/domain/ErrorCode.java b/src/main/java/com/threestar/trainus/global/exception/domain/ErrorCode.java index afe6a71..a7ca0ed 100644 --- a/src/main/java/com/threestar/trainus/global/exception/domain/ErrorCode.java +++ b/src/main/java/com/threestar/trainus/global/exception/domain/ErrorCode.java @@ -113,7 +113,15 @@ public enum ErrorCode { * Profile : 프로필 관련 예외처리 */ PROFILE_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 프로필을 찾을 수 없습니다"), - METADATA_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 메타데이터를 찾을 수 없습니다"); + METADATA_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 메타데이터를 찾을 수 없습니다"), + /* + * 결제 관련 예외처리 + */ + INVALID_COUPON(HttpStatus.BAD_REQUEST, "이미 사용되거나 사용할 수 없는 쿠폰입니다."), + INVALID_COUPON_RESTORE(HttpStatus.BAD_REQUEST, "쿠폰을 복원할 수 없는 상태입니다"), + INVALID_PAYMENT(HttpStatus.BAD_REQUEST, "결제 정보가 존재하지 않습니다."), + CANCEL_PAYMENT_FAILED(HttpStatus.BAD_REQUEST, "결제 취소가 실패되었습니다."), + CONFIRM_PAYMENT_FAILED(HttpStatus.BAD_REQUEST, "결제 승인이 실패했습니다."); //마지막 세미콜론 명시 ;