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 @@ -44,12 +44,12 @@ public ResponseEntity<?> confirmPayment(@RequestBody ConfirmTossPayRequest req)
return ResponseEntity.ok(CommonResponse.success(response));
}

@Operation(summary = "결제 실패 기록", description = "토스페이먼츠 결제 요청의 최종 실패를 기록합니다.")
@PostMapping(value = "/fail")
public ResponseEntity<?> failPayment(@RequestBody ConfirmTossPayRequest req) {
paymentService.reportFail(req);
return ResponseEntity.ok(CommonResponse.success(null));
}
// @Operation(summary = "결제 실패 기록", description = "토스페이먼츠 결제 요청의 최종 실패를 기록합니다.")
// @PostMapping(value = "/fail")
// public ResponseEntity<?> failPayment(@RequestBody ConfirmTossPayRequest req) {
// paymentService.reportFail(req);
// return ResponseEntity.ok(CommonResponse.success(null));
// }

// // 결제 취소 요청
// @PostMapping("/cancel/{paymentKey}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ public interface PaymentRepository extends JpaRepository<Payment, Long> {
Optional<Payment> findByTossPaymentKey(String tossPaymentKey);

boolean existsByTossPaymentKey(String paymentKey);

Optional<Payment> findByTossOrderId(String orderId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@
import lombok.extern.log4j.Log4j2;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

@Log4j2
Expand Down Expand Up @@ -78,25 +80,16 @@ public void confirmPayment(@Valid SavePaymentRequestDto dto, HttpSession session
// session.removeAttribute(dto.getOrderId());
}

@Transactional
/* 토스페이먼츠 결제 승인 */
public ConfirmTossPayResponse confirm(ConfirmTossPayRequest req) {
// 멱등성, 동시성 보호: paymentKey로 행 잠금
Payment payment = paymentRepository.findByTossPaymentKey(req.getPaymentKey())
.orElseGet(() -> {
Payment p = Payment.builder()
.tossOrderId(req.getOrderId())
.tossOrderId(req.getOrderId())
.status(Status.IN_PROGRESS)
.totalAmount(req.getAmount())
.build();
return paymentRepository.save(p);
});
if (payment.getStatus() == Status.DONE) {
// 멱등성 체크: 이미 처리된 주문인지 확인
Optional<Payment> existingPayment = paymentRepository.findByTossOrderId(req.getOrderId());
if (existingPayment.isPresent()) {
throw new CustomException(ErrorCode.ALREADY_COMPLETED_PAYMENT);
}
// 토스페이먼츠 승인 API 호출
final ConfirmTossPayResponse response;
ConfirmTossPayResponse response;
try {
// tossPaymentClient를 통해 호출
response = tossPaymentClient.confirmPayment(req);
} catch (FeignException.BadRequest e) {
throw new CustomException(ErrorCode.INVALID_PAYMENT_INFO);
Expand All @@ -105,14 +98,13 @@ public ConfirmTossPayResponse confirm(ConfirmTossPayRequest req) {
} catch (Exception e) {
throw new CustomException(ErrorCode.INTERNAL_SERVER_ERROR);
}
// 지갑/거래 업데이트
User user = userService.getCurrentUser();
Wallet wallet = walletRepository.findByUser(user)
.orElseThrow(() -> new CustomException(ErrorCode.WALLET_NOT_FOUND));
int amount = Math.toIntExact(response.getTotalAmount());
// 지갑 증액
int amount = Math.toIntExact(req.getAmount());
// 포인트 업데이트
wallet.updateBalance(wallet.getBalance() + amount);
// 거래 기록
// 충전(결제) 기록
WalletTransaction walletTransaction = WalletTransaction.builder()
.type(Type.CHARGE)
.amount(amount)
Expand All @@ -122,47 +114,15 @@ public ConfirmTossPayResponse confirm(ConfirmTossPayRequest req) {
.targetWallet(wallet)
.build();
walletTransactionRepository.save(walletTransaction);
payment.updateOnConfirm(response.getStatus(), response.getMethod());
walletTransaction.updatePayment(payment);
return response;
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void reportFail(ConfirmTossPayRequest req) {
// 멱등성, 동시성 보호: paymentKey로 행 잠금
Payment payment = paymentRepository.findByTossPaymentKey(req.getPaymentKey())
.orElseGet(() -> {
Payment p = Payment.builder()
.tossOrderId(req.getOrderId())
.totalAmount(req.getAmount())
.status(Status.IN_PROGRESS)
.build();
return paymentRepository.save(p);
});
// 이미 완료된 결제면 기록하지 않음
if (payment.getStatus() == Status.DONE) {
return;
}
if (req.getPaymentKey() != null &&
payment.getTossPaymentKey() != null &&
payment.getTossPaymentKey().equals(req.getPaymentKey())) {
return;
}
// 실패 기록
User user = userService.getCurrentUser();
Wallet wallet = walletRepository.findByUser(user)
.orElseThrow(() -> new CustomException(ErrorCode.WALLET_NOT_FOUND));
WalletTransaction walletTransaction = WalletTransaction.builder()
.type(Type.CHARGE)
.amount(Math.toIntExact(req.getAmount()))
.balance(wallet.getBalance())
.walletTransactionStatus(WalletTransactionStatus.FAILED)
.wallet(wallet)
.targetWallet(wallet)
Payment payment = Payment.builder()
.tossPaymentKey(response.getPaymentKey())
.tossOrderId(response.getOrderId())
.totalAmount(response.getTotalAmount())
.method(Method.from(response.getMethod()))
.status(Status.from(response.getStatus()))
.walletTransaction(walletTransaction)
.build();
walletTransactionRepository.save(walletTransaction);
// Payment 갱신
payment.updateStatus(Status.CANCELED);
walletTransaction.updatePayment(payment);
return response;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,4 @@ public ResponseEntity<?> getSettlementList(@PathVariable("clubId") final Long cl
Pageable pageable) {
return ResponseEntity.ok(CommonResponse.success(settlementService.getSettlementList(clubId, scheduleId, pageable)));
}

@Operation(summary = "유저 정산 요청 조회", description = "최근 처리된 정산 / 아직 처리되지 않은 정산 목록을 조회합니다.")
@GetMapping("/me")
public ResponseEntity<?> getMySettlementList(@PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC)
Pageable pageable) {
return ResponseEntity.ok(CommonResponse.success(settlementService.getMySettlementList(pageable)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,13 @@
@Getter
@AllArgsConstructor
public class UserSettlementDto {
private Long clubId;
private Long userId;
private String nickname;
private String profileImage;
private SettlementStatus settlementStatus;

public static UserSettlementDto from(UserSettlement userSettlement) {
return UserSettlementDto.builder()
.clubId(userSettlement.getSettlement().getSchedule().getClub().getClubId())
.userId(userSettlement.getUser().getUserId())
.nickname(userSettlement.getUser().getNickname())
.profileImage(userSettlement.getUser().getProfileImage())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.example.onlyone.domain.settlement.repository;

import com.example.onlyone.domain.schedule.entity.Schedule;
import com.example.onlyone.domain.user.dto.response.MySettlementDto;
import com.example.onlyone.domain.settlement.dto.response.UserSettlementDto;
import com.example.onlyone.domain.settlement.entity.Settlement;
import com.example.onlyone.domain.settlement.entity.SettlementStatus;
Expand All @@ -25,13 +26,12 @@ public interface UserSettlementRepository extends JpaRepository<UserSettlement,
@Query(
value = """
select new com.example.onlyone.domain.settlement.dto.response.UserSettlementDto(
c.clubId, u.userId, u.nickname, u.profileImage, us.settlementStatus
u.userId, u.nickname, u.profileImage, us.settlementStatus
)
from UserSettlement us
join us.user u
join us.settlement st
join st.schedule sch
join sch.club c
where us.settlement = :settlement
order by us.createdAt desc
""",
Expand All @@ -48,38 +48,42 @@ Page<UserSettlementDto> findAllDtoBySettlement(

@Query(
value = """
select new com.example.onlyone.domain.settlement.dto.response.UserSettlementDto(
c.clubId, u.userId, u.nickname, u.profileImage, us.settlementStatus
select new com.example.onlyone.domain.user.dto.response.MySettlementDto(
c.clubId,
sch.cost,
c.clubImage,
us.settlementStatus,
concat(c.name, ': ', sch.name),
us.createdAt
)
from UserSettlement us
join us.settlement st
join st.schedule sch
join sch.club c
where us.user = :user
and (
us.settlementStatus = com.example.onlyone.domain.settlement.entity.SettlementStatus.REQUESTED
or (
us.settlementStatus = com.example.onlyone.domain.settlement.entity.SettlementStatus.COMPLETED
and us.completedTime >= :cutoff
)
from UserSettlement us
join us.user u
join us.settlement st
join st.schedule sch
join sch.club c
where us.user = :user
and (
us.settlementStatus = com.example.onlyone.domain.settlement.entity.SettlementStatus.REQUESTED
or (
us.settlementStatus = com.example.onlyone.domain.settlement.entity.SettlementStatus.COMPLETED
and us.completedTime >= :cutoff
)
)
order by us.createdAt desc
""",
)
order by us.createdAt desc
""",
countQuery = """
select count(us)
from UserSettlement us
where us.user = :user
and (
us.settlementStatus = com.example.onlyone.domain.settlement.entity.SettlementStatus.REQUESTED
or (
us.settlementStatus = com.example.onlyone.domain.settlement.entity.SettlementStatus.COMPLETED
and us.completedTime >= :cutoff
)
)
"""
select count(us)
from UserSettlement us
where us.user = :user
and (
us.settlementStatus = com.example.onlyone.domain.settlement.entity.SettlementStatus.REQUESTED
or (
us.settlementStatus = com.example.onlyone.domain.settlement.entity.SettlementStatus.COMPLETED
and us.completedTime >= :cutoff
)
)
"""
)
Page<UserSettlementDto> findRecentOrRequestedByUser(
Page<MySettlementDto> findMyRecentOrRequested(
@Param("user") User user,
@Param("cutoff") java.time.LocalDateTime cutoff,
Pageable pageable
Expand Down
Loading
Loading