Skip to content

Commit 8e05451

Browse files
authored
fix: 추첨예매 결제 후 예매 생성 수정 (#565)
1 parent 70eb5b6 commit 8e05451

5 files changed

Lines changed: 136 additions & 9 deletions

File tree

src/main/java/com/back/b2st/domain/payment/service/LotteryPaymentFinalizer.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,24 @@
33
import org.springframework.stereotype.Component;
44
import org.springframework.transaction.annotation.Transactional;
55

6+
import com.back.b2st.domain.lottery.entry.entity.LotteryEntry;
67
import com.back.b2st.domain.lottery.result.entity.LotteryResult;
78
import com.back.b2st.domain.payment.entity.DomainType;
89
import com.back.b2st.domain.payment.entity.Payment;
910
import com.back.b2st.domain.payment.error.PaymentErrorCode;
11+
import com.back.b2st.domain.reservation.service.LotteryReservationService;
1012
import com.back.b2st.global.error.exception.BusinessException;
1113

1214
import jakarta.persistence.EntityManager;
1315
import jakarta.persistence.LockModeType;
14-
import jakarta.persistence.PersistenceContext;
1516
import lombok.RequiredArgsConstructor;
1617

1718
@Component
1819
@RequiredArgsConstructor
1920
public class LotteryPaymentFinalizer implements PaymentFinalizer {
2021

21-
@PersistenceContext
22-
private EntityManager entityManager;
22+
private final LotteryReservationService lotteryReservationService;
23+
private final EntityManager entityManager;
2324

2425
@Override
2526
public boolean supports(DomainType domainType) {
@@ -44,6 +45,12 @@ public void finalizePayment(Payment payment) {
4445
if (!lotteryResult.isPaid()) {
4546
lotteryResult.confirmPayment();
4647
}
48+
49+
LotteryEntry lotteryEntry = entityManager.find(LotteryEntry.class, lotteryResult.getLotteryEntryId());
50+
if (lotteryEntry == null) {
51+
throw new BusinessException(PaymentErrorCode.DOMAIN_NOT_FOUND);
52+
}
53+
54+
lotteryReservationService.getOrCreateCompletedReservation(payment.getMemberId(), lotteryEntry.getScheduleId());
4755
}
4856
}
49-

src/main/java/com/back/b2st/domain/reservation/repository/ReservationRepository.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@
2121
@Repository
2222
public interface ReservationRepository extends JpaRepository<Reservation, Long>, ReservationRepositoryCustom {
2323

24+
Optional<Reservation> findTopByMemberIdAndScheduleIdAndStatusOrderByIdDesc(
25+
Long memberId,
26+
Long scheduleId,
27+
ReservationStatus status
28+
);
29+
2430
/** 락 조회 */
2531
@Lock(LockModeType.PESSIMISTIC_WRITE)
2632
@Query("SELECT r FROM Reservation r WHERE r.id = :reservationId")

src/main/java/com/back/b2st/domain/reservation/service/LotteryReservationService.java

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
import java.time.LocalDateTime;
44
import java.util.List;
5+
import java.util.Optional;
56

67
import org.springframework.stereotype.Service;
78
import org.springframework.transaction.annotation.Transactional;
89

910
import com.back.b2st.domain.reservation.dto.response.LotteryReservationCreatedRes;
1011
import com.back.b2st.domain.reservation.entity.Reservation;
1112
import com.back.b2st.domain.reservation.entity.ReservationSeat;
13+
import com.back.b2st.domain.reservation.entity.ReservationStatus;
1214
import com.back.b2st.domain.reservation.repository.ReservationRepository;
1315
import com.back.b2st.domain.reservation.repository.ReservationSeatRepository;
1416
import com.back.b2st.domain.scheduleseat.entity.SeatStatus;
@@ -29,19 +31,42 @@ public class LotteryReservationService {
2931
/** === 추첨 예매 생성 (결제 완료 기준) === */
3032
@Transactional
3133
public LotteryReservationCreatedRes createCompletedReservation(Long memberId, Long scheduleId) {
34+
Reservation reservation = getOrCreateCompletedReservation(memberId, scheduleId);
35+
return LotteryReservationCreatedRes.from(reservation);
36+
}
37+
38+
@Transactional
39+
public Reservation getOrCreateCompletedReservation(Long memberId, Long scheduleId) {
3240
LocalDateTime now = LocalDateTime.now();
3341

42+
Optional<Reservation> completed =
43+
reservationRepository.findTopByMemberIdAndScheduleIdAndStatusOrderByIdDesc(
44+
memberId, scheduleId, ReservationStatus.COMPLETED
45+
);
46+
47+
if (completed.isPresent()) {
48+
return completed.get();
49+
}
50+
51+
Optional<Reservation> pending =
52+
reservationRepository.findTopByMemberIdAndScheduleIdAndStatusOrderByIdDesc(
53+
memberId, scheduleId, ReservationStatus.PENDING
54+
);
55+
56+
if (pending.isPresent()) {
57+
Reservation reservation = pending.get();
58+
reservation.complete(now);
59+
return reservation;
60+
}
61+
3462
Reservation reservation = Reservation.builder()
3563
.scheduleId(scheduleId)
3664
.memberId(memberId)
3765
.expiresAt(now)
3866
.build();
39-
40-
// 생성 즉시 예매 완료 처리
4167
reservation.complete(now);
4268

43-
Reservation saved = reservationRepository.save(reservation);
44-
return LotteryReservationCreatedRes.from(saved);
69+
return reservationRepository.save(reservation);
4570
}
4671

4772
/** === 추첨 좌석 확정 === */
@@ -70,4 +95,4 @@ public void confirmAssignedSeats(Long reservationId, Long scheduleId, List<Long>
7095
);
7196
}
7297
}
73-
}
98+
}

src/test/java/com/back/b2st/domain/payment/service/LotteryPaymentFinalizerTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@
1313
import org.mockito.Mock;
1414
import org.mockito.junit.jupiter.MockitoExtension;
1515

16+
import com.back.b2st.domain.lottery.entry.entity.LotteryEntry;
1617
import com.back.b2st.domain.lottery.result.entity.LotteryResult;
1718
import com.back.b2st.domain.payment.entity.DomainType;
1819
import com.back.b2st.domain.payment.entity.Payment;
1920
import com.back.b2st.domain.payment.error.PaymentErrorCode;
21+
import com.back.b2st.domain.reservation.entity.Reservation;
22+
import com.back.b2st.domain.reservation.service.LotteryReservationService;
2023
import com.back.b2st.global.error.exception.BusinessException;
2124

2225
import jakarta.persistence.EntityManager;
@@ -28,11 +31,16 @@ class LotteryPaymentFinalizerTest {
2831
@Mock
2932
private EntityManager entityManager;
3033

34+
@Mock
35+
private LotteryReservationService lotteryReservationService;
36+
3137
@InjectMocks
3238
private LotteryPaymentFinalizer lotteryPaymentFinalizer;
3339

3440
private static final Long LOTTERY_RESULT_ID = 10L;
41+
private static final Long LOTTERY_ENTRY_ID = 11L;
3542
private static final Long MEMBER_ID = 1L;
43+
private static final Long SCHEDULE_ID = 100L;
3644

3745
@Test
3846
@DisplayName("supports(): DomainType.LOTTERY 지원")
@@ -90,13 +98,21 @@ void finalizePayment_marksPaid_whenNotPaid() {
9098

9199
LotteryResult lotteryResult = org.mockito.Mockito.mock(LotteryResult.class);
92100
given(lotteryResult.getMemberId()).willReturn(MEMBER_ID);
101+
given(lotteryResult.getLotteryEntryId()).willReturn(LOTTERY_ENTRY_ID);
93102
given(lotteryResult.isPaid()).willReturn(false);
94103
given(entityManager.find(LotteryResult.class, LOTTERY_RESULT_ID, LockModeType.PESSIMISTIC_WRITE))
95104
.willReturn(lotteryResult);
96105

106+
LotteryEntry lotteryEntry = org.mockito.Mockito.mock(LotteryEntry.class);
107+
given(lotteryEntry.getScheduleId()).willReturn(SCHEDULE_ID);
108+
given(entityManager.find(LotteryEntry.class, LOTTERY_ENTRY_ID)).willReturn(lotteryEntry);
109+
given(lotteryReservationService.getOrCreateCompletedReservation(MEMBER_ID, SCHEDULE_ID))
110+
.willReturn(org.mockito.Mockito.mock(Reservation.class));
111+
97112
assertThatCode(() -> lotteryPaymentFinalizer.finalizePayment(payment)).doesNotThrowAnyException();
98113

99114
then(lotteryResult).should().confirmPayment();
115+
then(lotteryReservationService).should().getOrCreateCompletedReservation(MEMBER_ID, SCHEDULE_ID);
100116
}
101117

102118
@Test
@@ -108,12 +124,20 @@ void finalizePayment_idempotent_whenAlreadyPaid() {
108124

109125
LotteryResult lotteryResult = org.mockito.Mockito.mock(LotteryResult.class);
110126
given(lotteryResult.getMemberId()).willReturn(MEMBER_ID);
127+
given(lotteryResult.getLotteryEntryId()).willReturn(LOTTERY_ENTRY_ID);
111128
given(lotteryResult.isPaid()).willReturn(true);
112129
given(entityManager.find(LotteryResult.class, LOTTERY_RESULT_ID, LockModeType.PESSIMISTIC_WRITE))
113130
.willReturn(lotteryResult);
114131

132+
LotteryEntry lotteryEntry = org.mockito.Mockito.mock(LotteryEntry.class);
133+
given(lotteryEntry.getScheduleId()).willReturn(SCHEDULE_ID);
134+
given(entityManager.find(LotteryEntry.class, LOTTERY_ENTRY_ID)).willReturn(lotteryEntry);
135+
given(lotteryReservationService.getOrCreateCompletedReservation(MEMBER_ID, SCHEDULE_ID))
136+
.willReturn(org.mockito.Mockito.mock(Reservation.class));
137+
115138
assertThatCode(() -> lotteryPaymentFinalizer.finalizePayment(payment)).doesNotThrowAnyException();
116139

117140
then(lotteryResult).should(org.mockito.Mockito.never()).confirmPayment();
141+
then(lotteryReservationService).should().getOrCreateCompletedReservation(MEMBER_ID, SCHEDULE_ID);
118142
}
119143
}

src/test/java/com/back/b2st/domain/reservation/service/LotteryReservationServiceTest.java

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import static org.mockito.Mockito.*;
66

77
import java.util.List;
8+
import java.util.Optional;
89

910
import org.junit.jupiter.api.DisplayName;
1011
import org.junit.jupiter.api.Test;
@@ -17,6 +18,7 @@
1718
import com.back.b2st.domain.reservation.dto.response.LotteryReservationCreatedRes;
1819
import com.back.b2st.domain.reservation.entity.Reservation;
1920
import com.back.b2st.domain.reservation.entity.ReservationSeat;
21+
import com.back.b2st.domain.reservation.entity.ReservationStatus;
2022
import com.back.b2st.domain.reservation.repository.ReservationRepository;
2123
import com.back.b2st.domain.reservation.repository.ReservationSeatRepository;
2224
import com.back.b2st.domain.scheduleseat.entity.SeatStatus;
@@ -46,6 +48,12 @@ void createCompletedReservation_success() {
4648
Long memberId = 1L;
4749
Long scheduleId = 10L;
4850

51+
when(reservationRepository.findTopByMemberIdAndScheduleIdAndStatusOrderByIdDesc(
52+
memberId, scheduleId, ReservationStatus.COMPLETED
53+
)).thenReturn(Optional.empty());
54+
when(reservationRepository.findTopByMemberIdAndScheduleIdAndStatusOrderByIdDesc(
55+
memberId, scheduleId, ReservationStatus.PENDING
56+
)).thenReturn(Optional.empty());
4957
when(reservationRepository.save(any(Reservation.class)))
5058
.thenAnswer(invocation -> invocation.getArgument(0));
5159

@@ -62,9 +70,66 @@ void createCompletedReservation_success() {
6270
assertThat(saved.getMemberId()).isEqualTo(memberId);
6371
assertThat(saved.getScheduleId()).isEqualTo(scheduleId);
6472
assertThat(saved.getExpiresAt()).isNotNull();
73+
assertThat(saved.getStatus()).isEqualTo(ReservationStatus.COMPLETED);
6574
assertThat(res).isNotNull();
6675
}
6776

77+
@Test
78+
@DisplayName("getOrCreateCompletedReservation: 이미 완료된 예매가 있으면 저장 없이 반환한다")
79+
void getOrCreateCompletedReservation_returnsCompletedWithoutSave() {
80+
81+
// given
82+
Long memberId = 1L;
83+
Long scheduleId = 10L;
84+
85+
Reservation existing = Reservation.builder()
86+
.memberId(memberId)
87+
.scheduleId(scheduleId)
88+
.expiresAt(java.time.LocalDateTime.now())
89+
.build();
90+
existing.complete(java.time.LocalDateTime.now());
91+
92+
when(reservationRepository.findTopByMemberIdAndScheduleIdAndStatusOrderByIdDesc(
93+
memberId, scheduleId, ReservationStatus.COMPLETED
94+
)).thenReturn(Optional.of(existing));
95+
96+
// when
97+
Reservation res = lotteryReservationService.getOrCreateCompletedReservation(memberId, scheduleId);
98+
99+
// then
100+
assertThat(res).isSameAs(existing);
101+
verify(reservationRepository, never()).save(any());
102+
}
103+
104+
@Test
105+
@DisplayName("getOrCreateCompletedReservation: PENDING 예매가 있으면 완료 처리하고 반환한다")
106+
void getOrCreateCompletedReservation_completesPending() {
107+
108+
// given
109+
Long memberId = 1L;
110+
Long scheduleId = 10L;
111+
112+
Reservation pending = Reservation.builder()
113+
.memberId(memberId)
114+
.scheduleId(scheduleId)
115+
.expiresAt(java.time.LocalDateTime.now().plusMinutes(10))
116+
.build();
117+
118+
when(reservationRepository.findTopByMemberIdAndScheduleIdAndStatusOrderByIdDesc(
119+
memberId, scheduleId, ReservationStatus.COMPLETED
120+
)).thenReturn(Optional.empty());
121+
when(reservationRepository.findTopByMemberIdAndScheduleIdAndStatusOrderByIdDesc(
122+
memberId, scheduleId, ReservationStatus.PENDING
123+
)).thenReturn(Optional.of(pending));
124+
125+
// when
126+
Reservation res = lotteryReservationService.getOrCreateCompletedReservation(memberId, scheduleId);
127+
128+
// then
129+
assertThat(res.getStatus()).isEqualTo(ReservationStatus.COMPLETED);
130+
verify(reservationRepository, never()).save(any());
131+
}
132+
68133
@Test
69134
@DisplayName("confirmAssignedSeats: 좌석 확정 성공")
70135
void confirmAssignedSeats_success() {

0 commit comments

Comments
 (0)