Skip to content

Commit

Permalink
Merge pull request #108 from prgrms-be-devcourse/feature/NAYB-138
Browse files Browse the repository at this point in the history
[NAYB-138] 결제 실패할 경우를 처리한다.
  • Loading branch information
pushedrumex committed Sep 15, 2023
2 parents b378d69 + f8a499d commit c8a7c80
Show file tree
Hide file tree
Showing 16 changed files with 348 additions and 176 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,12 @@ public void use() {
}
isUsed = true;
}

public void unUse() {
if (isUsed == false) {
throw new InvalidUsedCouponException("사용하지 않은 쿠폰입니다.");
}
isUsed = false;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

Expand All @@ -20,6 +21,10 @@ public interface ItemRepository extends JpaRepository<Item, Long>, ItemRepositor
@Query("select i from Item i where i.itemId = :itemId")
Optional<Item> findByItemId(@Param("itemId") Long itemId);

@Modifying(flushAutomatically = true, clearAutomatically = true)
@Query("update Item i set i.quantity = i.quantity + :quantity where i.itemId = :itemId")
void increaseQuantity(@Param("itemId") Long itemId, @Param("quantity") int quantity);

@Query("select i from Item i where i.itemId in ?1")
List<Item> findByItemIdIn(Collection<Long> itemIds);

Expand Down
10 changes: 8 additions & 2 deletions src/main/java/com/prgrms/nabmart/domain/order/Order.java
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ public void setUserCoupon(final UserCoupon userCoupon) {
this.price -= userCoupon.getDiscount();

}

private void calculateDeliveryFee(final int totalPrice) {
if (totalPrice >= 43000) {
this.deliveryFee = 0;
Expand Down Expand Up @@ -150,9 +150,15 @@ public void changeStatus(OrderStatus orderStatus) {
this.status = orderStatus;
}

public void redeemCoupon() {
public void useCoupon() {
if (userCoupon != null) {
userCoupon.use();
}
}

public void unUseCoupon() {
if (userCoupon != null) {
userCoupon.unUse();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ public Order getOrderByOrderIdAndUserId(final Long orderId, final Long userId) {
.orElseThrow(() -> new NotFoundOrderException("order 가 존재하지 않습니다"));
}

public Order getOrderByUuidAndUserId(final String uuid, final Long userId) {
return orderRepository.findByUuidAndUser_UserId(uuid, userId)
.orElseThrow(() -> new NotFoundOrderException("order 가 존재하지 않습니다"));
}

private User findUserByUserId(final Long userId) {
return userRepository.findById(userId)
.orElseThrow(() -> new NotFoundUserException("존재하지 않은 사용자입니다."));
Expand All @@ -154,4 +159,14 @@ private Item findItemByItemId(final Long itemId) {
return itemRepository.findByItemId(itemId)
.orElseThrow(() -> new NotFoundItemException("존재하지 않는 상품입니다."));
}

@Transactional
public void cancelOrder(final Order order) {
order.updateOrderStatus(OrderStatus.CANCELED);
order.unUseCoupon();
order.getOrderItems().forEach(
orderItem -> itemRepository.increaseQuantity(orderItem.getItem().getItemId(),
orderItem.getQuantity())
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
public enum PaymentStatus {
PENDING,
SUCCESS,
CANCELED
CANCELED,
FAILED
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.prgrms.nabmart.domain.payment.controller;

import com.prgrms.nabmart.domain.payment.service.PaymentClient;
import com.prgrms.nabmart.domain.payment.service.PaymentService;
import com.prgrms.nabmart.domain.payment.service.response.PaymentRequestResponse;
import com.prgrms.nabmart.domain.payment.service.response.PaymentResponse;
Expand All @@ -19,6 +20,7 @@
public class PaymentController {

private final PaymentService paymentService;
private final PaymentClient paymentClient;

@PostMapping("/{orderId}")
public ResponseEntity<PaymentRequestResponse> pay(
Expand All @@ -34,8 +36,20 @@ public ResponseEntity<PaymentResponse> paySuccess(
@RequestParam("paymentKey") String paymentKey,
@RequestParam("amount") Integer amount,
@LoginUser Long userId
) {
paymentClient.confirmPayment(uuid, paymentKey, amount);

return ResponseEntity.ok(
paymentService.processSuccessPayment(userId, uuid, paymentKey, amount));
}

@GetMapping("/toss/fail")
public ResponseEntity<PaymentResponse> payFail(
@RequestParam("orderId") String uuid,
@RequestParam("message") String errorMessage,
@LoginUser Long userId
) {
return ResponseEntity.ok(
paymentService.confirmPayment(userId, uuid, paymentKey, amount));
paymentService.processFailPayment(userId, uuid, errorMessage));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.prgrms.nabmart.domain.payment.service;

import com.prgrms.nabmart.domain.payment.exception.PaymentFailException;
import com.prgrms.nabmart.domain.payment.service.response.TossPaymentApiResponse;
import com.prgrms.nabmart.global.infrastructure.ApiService;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import lombok.RequiredArgsConstructor;
import net.minidev.json.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class PaymentClient {

private final ApiService apiService;

@Value("${payment.toss.secret-key}")
private String secretKey;

@Value("${payment.toss.confirm-url}")
private String confirmUrl;

public void confirmPayment(final String uuid, final String paymentKey, final Integer amount) {
HttpHeaders httpHeaders = getHttpHeaders();
JSONObject params = getParams(uuid, paymentKey, amount);
TossPaymentApiResponse paymentApiResponse = requestPaymentApi(httpHeaders, params);

validatePaymentResult(paymentApiResponse);
}

private HttpHeaders getHttpHeaders() {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setBasicAuth(getEncodeAuth());
httpHeaders.setContentType(MediaType.APPLICATION_JSON);

return httpHeaders;
}

private String getEncodeAuth() {
return new String(
Base64.getEncoder()
.encode((secretKey + ":").getBytes(StandardCharsets.UTF_8))
);
}

private JSONObject getParams(String uuid, String paymentKey, Integer amount) {
JSONObject params = new JSONObject();
params.put("paymentKey", paymentKey);
params.put("orderId", uuid);
params.put("amount", amount);

return params;
}

private TossPaymentApiResponse requestPaymentApi(HttpHeaders httpHeaders, JSONObject params) {
return apiService.getResult(
new HttpEntity<>(params, httpHeaders),
confirmUrl,
TossPaymentApiResponse.class
);
}

private void validatePaymentResult(TossPaymentApiResponse paymentApiResponse) {
if (!paymentApiResponse.status().equals("DONE")) {
throw new PaymentFailException("결제가 실패되었습니다.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,18 @@

import com.prgrms.nabmart.domain.order.Order;
import com.prgrms.nabmart.domain.order.OrderStatus;
import com.prgrms.nabmart.domain.order.exception.NotFoundOrderException;
import com.prgrms.nabmart.domain.order.exception.NotPayingOrderException;
import com.prgrms.nabmart.domain.order.repository.OrderRepository;
import com.prgrms.nabmart.domain.order.service.OrderService;
import com.prgrms.nabmart.domain.payment.Payment;
import com.prgrms.nabmart.domain.payment.PaymentStatus;
import com.prgrms.nabmart.domain.payment.exception.DuplicatePayException;
import com.prgrms.nabmart.domain.payment.exception.NotFoundPaymentException;
import com.prgrms.nabmart.domain.payment.exception.PaymentAmountMismatchException;
import com.prgrms.nabmart.domain.payment.exception.PaymentFailException;
import com.prgrms.nabmart.domain.payment.repository.PaymentRepository;
import com.prgrms.nabmart.domain.payment.service.response.PaymentRequestResponse;
import com.prgrms.nabmart.domain.payment.service.response.PaymentResponse;
import com.prgrms.nabmart.domain.payment.service.response.TossPaymentApiResponse;
import com.prgrms.nabmart.global.infrastructure.ApiService;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import lombok.RequiredArgsConstructor;
import net.minidev.json.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -32,21 +22,14 @@
public class PaymentService {

private final PaymentRepository paymentRepository;
private final OrderRepository orderRepository;
private final ApiService apiService;
private final OrderService orderService;

@Value("${payment.toss.success-url}")
private String successCallBackUrl;

@Value("${payment.toss.fail-url}")
private String failCallBackUrl;

@Value("${payment.toss.secret-key}")
private String secretKey;

@Value("${payment.toss.confirm-url}")
private String confirmUrl;

@Transactional
public PaymentRequestResponse pay(final Long orderId, final Long userId) {
final Order order = getOrderByOrderIdAndUserId(
Expand All @@ -56,7 +39,7 @@ public PaymentRequestResponse pay(final Long orderId, final Long userId) {

validateOrderStatusWithPending(order);
order.changeStatus(OrderStatus.PAYING);
order.redeemCoupon();
order.useCoupon();

final Payment payment = buildPayment(order);
paymentRepository.save(payment);
Expand All @@ -73,13 +56,11 @@ public PaymentRequestResponse pay(final Long orderId, final Long userId) {


private Order getOrderByOrderIdAndUserId(Long orderId, Long userId) {
return orderRepository.findByOrderIdAndUser_UserId(orderId, userId)
.orElseThrow(() -> new NotFoundOrderException("주문 존재하지 않습니다."));
return orderService.getOrderByOrderIdAndUserId(orderId, userId);
}

private Order getOrderByUuidAndUserId(String uuid, Long userId) {
return orderRepository.findByUuidAndUser_UserId(uuid, userId)
.orElseThrow(() -> new NotFoundOrderException("주문 존재하지 않습니다."));
return orderService.getOrderByUuidAndUserId(uuid, userId);
}

private void validateOrderStatusWithPending(final Order order) {
Expand All @@ -96,7 +77,7 @@ private Payment buildPayment(Order order) {
}

@Transactional
public PaymentResponse confirmPayment(
public PaymentResponse processSuccessPayment(
Long userId,
String uuid,
String paymentKey,
Expand All @@ -108,19 +89,12 @@ public PaymentResponse confirmPayment(
Order order = getOrderByUuidAndUserId(uuid, userId);
validateOrderStatusWithPaying(order);

HttpHeaders httpHeaders = getHttpHeaders();
JSONObject params = getParams(uuid, paymentKey, amount);

TossPaymentApiResponse paymentApiResponse = requestPaymentApi(httpHeaders, params);

validatePaymentResult(paymentApiResponse);

payment.changeStatus(PaymentStatus.SUCCESS);
payment.setPaymentKey(paymentKey);

order.changeStatus(OrderStatus.PAYED);

return new PaymentResponse(payment.getPaymentStatus().toString());
return new PaymentResponse(payment.getPaymentStatus().toString(), null);
}

private void validateOrderStatusWithPaying(final Order order) {
Expand All @@ -129,63 +103,39 @@ private void validateOrderStatusWithPaying(final Order order) {
}
}

private TossPaymentApiResponse requestPaymentApi(HttpHeaders httpHeaders, JSONObject params) {
return apiService.getResult(
new HttpEntity<>(params, httpHeaders),
confirmUrl,
TossPaymentApiResponse.class
);
}

private void validatePayment(Integer amount, Payment payment) {
validatePaymentStatus(payment);
validatePaymentStatusWithPending(payment);
validatePrice(amount, payment);
}

private void validatePaymentStatus(final Payment payment) {
private void validatePaymentStatusWithPending(final Payment payment) {
if (payment.isMisMatchStatus(PaymentStatus.PENDING)) {
throw new DuplicatePayException("이미 처리된 결제입니다.");
}
}

private void validatePaymentResult(TossPaymentApiResponse paymentApiResponse) {
if (!paymentApiResponse.status().equals("DONE")) {
throw new PaymentFailException("결제가 실패되었습니다.");
}
}

private JSONObject getParams(String uuid, String paymentKey, Integer amount) {
JSONObject params = new JSONObject();
params.put("paymentKey", paymentKey);
params.put("orderId", uuid);
params.put("amount", amount);

return params;
}

private HttpHeaders getHttpHeaders() {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setBasicAuth(getEncodeAuth());
httpHeaders.setContentType(MediaType.APPLICATION_JSON);

return httpHeaders;
}

private void validatePrice(Integer amount, Payment payment) {
if (payment.isMisMatchPrice(amount)) {
throw new PaymentAmountMismatchException("결제 금액이 일치하지 않습니다.");
}
}

private String getEncodeAuth() {
return new String(
Base64.getEncoder()
.encode((secretKey + ":").getBytes(StandardCharsets.UTF_8))
);
}

private Payment getPaymentByUuidAndUserId(String uuid, Long userId) {
return paymentRepository.findByOrder_UuidAndUser_UserId(uuid, userId)
.orElseThrow(() -> new NotFoundPaymentException("결제가 존재하지 않습니다."));
}

@Transactional
public PaymentResponse processFailPayment(Long userId, String uuid, String errorMessage) {
Payment payment = getPaymentByUuidAndUserId(uuid, userId);
validatePaymentStatusWithPending(payment);
payment.changeStatus(PaymentStatus.FAILED);

Order order = getOrderByUuidAndUserId(uuid, userId);
validateOrderStatusWithPaying(order);

orderService.cancelOrder(order);

return new PaymentResponse(payment.getPaymentStatus().toString(), errorMessage);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package com.prgrms.nabmart.domain.payment.service.response;

public record PaymentResponse(String status) {
public record PaymentResponse(String status, String message) {

}
Loading

0 comments on commit c8a7c80

Please sign in to comment.