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 @@ -3,23 +3,24 @@
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import com.back.b2st.domain.lottery.entry.entity.LotteryEntry;
import com.back.b2st.domain.lottery.result.entity.LotteryResult;
import com.back.b2st.domain.payment.entity.DomainType;
import com.back.b2st.domain.payment.entity.Payment;
import com.back.b2st.domain.payment.error.PaymentErrorCode;
import com.back.b2st.domain.reservation.service.LotteryReservationService;
import com.back.b2st.global.error.exception.BusinessException;

import jakarta.persistence.EntityManager;
import jakarta.persistence.LockModeType;
import jakarta.persistence.PersistenceContext;
import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class LotteryPaymentFinalizer implements PaymentFinalizer {

@PersistenceContext
private EntityManager entityManager;
private final LotteryReservationService lotteryReservationService;
private final EntityManager entityManager;

@Override
public boolean supports(DomainType domainType) {
Expand All @@ -44,6 +45,12 @@ public void finalizePayment(Payment payment) {
if (!lotteryResult.isPaid()) {
lotteryResult.confirmPayment();
}

LotteryEntry lotteryEntry = entityManager.find(LotteryEntry.class, lotteryResult.getLotteryEntryId());
if (lotteryEntry == null) {
throw new BusinessException(PaymentErrorCode.DOMAIN_NOT_FOUND);
}

lotteryReservationService.getOrCreateCompletedReservation(payment.getMemberId(), lotteryEntry.getScheduleId());
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@
@Repository
public interface ReservationRepository extends JpaRepository<Reservation, Long>, ReservationRepositoryCustom {

Optional<Reservation> findTopByMemberIdAndScheduleIdAndStatusOrderByIdDesc(
Long memberId,
Long scheduleId,
ReservationStatus status
);

/** 락 조회 */
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT r FROM Reservation r WHERE r.id = :reservationId")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.back.b2st.domain.reservation.dto.response.LotteryReservationCreatedRes;
import com.back.b2st.domain.reservation.entity.Reservation;
import com.back.b2st.domain.reservation.entity.ReservationSeat;
import com.back.b2st.domain.reservation.entity.ReservationStatus;
import com.back.b2st.domain.reservation.repository.ReservationRepository;
import com.back.b2st.domain.reservation.repository.ReservationSeatRepository;
import com.back.b2st.domain.scheduleseat.entity.SeatStatus;
Expand All @@ -29,19 +31,42 @@ public class LotteryReservationService {
/** === 추첨 예매 생성 (결제 완료 기준) === */
@Transactional
public LotteryReservationCreatedRes createCompletedReservation(Long memberId, Long scheduleId) {
Reservation reservation = getOrCreateCompletedReservation(memberId, scheduleId);
return LotteryReservationCreatedRes.from(reservation);
}

@Transactional
public Reservation getOrCreateCompletedReservation(Long memberId, Long scheduleId) {
LocalDateTime now = LocalDateTime.now();

Optional<Reservation> completed =
reservationRepository.findTopByMemberIdAndScheduleIdAndStatusOrderByIdDesc(
memberId, scheduleId, ReservationStatus.COMPLETED
);

if (completed.isPresent()) {
return completed.get();
}

Optional<Reservation> pending =
reservationRepository.findTopByMemberIdAndScheduleIdAndStatusOrderByIdDesc(
memberId, scheduleId, ReservationStatus.PENDING
);

if (pending.isPresent()) {
Reservation reservation = pending.get();
reservation.complete(now);
return reservation;
}

Reservation reservation = Reservation.builder()
.scheduleId(scheduleId)
.memberId(memberId)
.expiresAt(now)
.build();

// 생성 즉시 예매 완료 처리
reservation.complete(now);

Reservation saved = reservationRepository.save(reservation);
return LotteryReservationCreatedRes.from(saved);
return reservationRepository.save(reservation);
}

/** === 추첨 좌석 확정 === */
Expand Down Expand Up @@ -70,4 +95,4 @@ public void confirmAssignedSeats(Long reservationId, Long scheduleId, List<Long>
);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import com.back.b2st.domain.lottery.entry.entity.LotteryEntry;
import com.back.b2st.domain.lottery.result.entity.LotteryResult;
import com.back.b2st.domain.payment.entity.DomainType;
import com.back.b2st.domain.payment.entity.Payment;
import com.back.b2st.domain.payment.error.PaymentErrorCode;
import com.back.b2st.domain.reservation.entity.Reservation;
import com.back.b2st.domain.reservation.service.LotteryReservationService;
import com.back.b2st.global.error.exception.BusinessException;

import jakarta.persistence.EntityManager;
Expand All @@ -28,11 +31,16 @@ class LotteryPaymentFinalizerTest {
@Mock
private EntityManager entityManager;

@Mock
private LotteryReservationService lotteryReservationService;

@InjectMocks
private LotteryPaymentFinalizer lotteryPaymentFinalizer;

private static final Long LOTTERY_RESULT_ID = 10L;
private static final Long LOTTERY_ENTRY_ID = 11L;
private static final Long MEMBER_ID = 1L;
private static final Long SCHEDULE_ID = 100L;

@Test
@DisplayName("supports(): DomainType.LOTTERY 지원")
Expand Down Expand Up @@ -90,13 +98,21 @@ void finalizePayment_marksPaid_whenNotPaid() {

LotteryResult lotteryResult = org.mockito.Mockito.mock(LotteryResult.class);
given(lotteryResult.getMemberId()).willReturn(MEMBER_ID);
given(lotteryResult.getLotteryEntryId()).willReturn(LOTTERY_ENTRY_ID);
given(lotteryResult.isPaid()).willReturn(false);
given(entityManager.find(LotteryResult.class, LOTTERY_RESULT_ID, LockModeType.PESSIMISTIC_WRITE))
.willReturn(lotteryResult);

LotteryEntry lotteryEntry = org.mockito.Mockito.mock(LotteryEntry.class);
given(lotteryEntry.getScheduleId()).willReturn(SCHEDULE_ID);
given(entityManager.find(LotteryEntry.class, LOTTERY_ENTRY_ID)).willReturn(lotteryEntry);
given(lotteryReservationService.getOrCreateCompletedReservation(MEMBER_ID, SCHEDULE_ID))
.willReturn(org.mockito.Mockito.mock(Reservation.class));

assertThatCode(() -> lotteryPaymentFinalizer.finalizePayment(payment)).doesNotThrowAnyException();

then(lotteryResult).should().confirmPayment();
then(lotteryReservationService).should().getOrCreateCompletedReservation(MEMBER_ID, SCHEDULE_ID);
}

@Test
Expand All @@ -108,12 +124,20 @@ void finalizePayment_idempotent_whenAlreadyPaid() {

LotteryResult lotteryResult = org.mockito.Mockito.mock(LotteryResult.class);
given(lotteryResult.getMemberId()).willReturn(MEMBER_ID);
given(lotteryResult.getLotteryEntryId()).willReturn(LOTTERY_ENTRY_ID);
given(lotteryResult.isPaid()).willReturn(true);
given(entityManager.find(LotteryResult.class, LOTTERY_RESULT_ID, LockModeType.PESSIMISTIC_WRITE))
.willReturn(lotteryResult);

LotteryEntry lotteryEntry = org.mockito.Mockito.mock(LotteryEntry.class);
given(lotteryEntry.getScheduleId()).willReturn(SCHEDULE_ID);
given(entityManager.find(LotteryEntry.class, LOTTERY_ENTRY_ID)).willReturn(lotteryEntry);
given(lotteryReservationService.getOrCreateCompletedReservation(MEMBER_ID, SCHEDULE_ID))
.willReturn(org.mockito.Mockito.mock(Reservation.class));

assertThatCode(() -> lotteryPaymentFinalizer.finalizePayment(payment)).doesNotThrowAnyException();

then(lotteryResult).should(org.mockito.Mockito.never()).confirmPayment();
then(lotteryReservationService).should().getOrCreateCompletedReservation(MEMBER_ID, SCHEDULE_ID);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import static org.mockito.Mockito.*;

import java.util.List;
import java.util.Optional;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
Expand All @@ -17,6 +18,7 @@
import com.back.b2st.domain.reservation.dto.response.LotteryReservationCreatedRes;
import com.back.b2st.domain.reservation.entity.Reservation;
import com.back.b2st.domain.reservation.entity.ReservationSeat;
import com.back.b2st.domain.reservation.entity.ReservationStatus;
import com.back.b2st.domain.reservation.repository.ReservationRepository;
import com.back.b2st.domain.reservation.repository.ReservationSeatRepository;
import com.back.b2st.domain.scheduleseat.entity.SeatStatus;
Expand Down Expand Up @@ -46,6 +48,12 @@ void createCompletedReservation_success() {
Long memberId = 1L;
Long scheduleId = 10L;

when(reservationRepository.findTopByMemberIdAndScheduleIdAndStatusOrderByIdDesc(
memberId, scheduleId, ReservationStatus.COMPLETED
)).thenReturn(Optional.empty());
when(reservationRepository.findTopByMemberIdAndScheduleIdAndStatusOrderByIdDesc(
memberId, scheduleId, ReservationStatus.PENDING
)).thenReturn(Optional.empty());
when(reservationRepository.save(any(Reservation.class)))
.thenAnswer(invocation -> invocation.getArgument(0));

Expand All @@ -62,9 +70,66 @@ void createCompletedReservation_success() {
assertThat(saved.getMemberId()).isEqualTo(memberId);
assertThat(saved.getScheduleId()).isEqualTo(scheduleId);
assertThat(saved.getExpiresAt()).isNotNull();
assertThat(saved.getStatus()).isEqualTo(ReservationStatus.COMPLETED);
assertThat(res).isNotNull();
}

@Test
@DisplayName("getOrCreateCompletedReservation: 이미 완료된 예매가 있으면 저장 없이 반환한다")
void getOrCreateCompletedReservation_returnsCompletedWithoutSave() {

// given
Long memberId = 1L;
Long scheduleId = 10L;

Reservation existing = Reservation.builder()
.memberId(memberId)
.scheduleId(scheduleId)
.expiresAt(java.time.LocalDateTime.now())
.build();
existing.complete(java.time.LocalDateTime.now());

when(reservationRepository.findTopByMemberIdAndScheduleIdAndStatusOrderByIdDesc(
memberId, scheduleId, ReservationStatus.COMPLETED
)).thenReturn(Optional.of(existing));

// when
Reservation res = lotteryReservationService.getOrCreateCompletedReservation(memberId, scheduleId);

// then
assertThat(res).isSameAs(existing);
verify(reservationRepository, never()).save(any());
}

@Test
@DisplayName("getOrCreateCompletedReservation: PENDING 예매가 있으면 완료 처리하고 반환한다")
void getOrCreateCompletedReservation_completesPending() {

// given
Long memberId = 1L;
Long scheduleId = 10L;

Reservation pending = Reservation.builder()
.memberId(memberId)
.scheduleId(scheduleId)
.expiresAt(java.time.LocalDateTime.now().plusMinutes(10))
.build();

when(reservationRepository.findTopByMemberIdAndScheduleIdAndStatusOrderByIdDesc(
memberId, scheduleId, ReservationStatus.COMPLETED
)).thenReturn(Optional.empty());
when(reservationRepository.findTopByMemberIdAndScheduleIdAndStatusOrderByIdDesc(
memberId, scheduleId, ReservationStatus.PENDING
)).thenReturn(Optional.of(pending));

// when
Reservation res = lotteryReservationService.getOrCreateCompletedReservation(memberId, scheduleId);

// then
assertThat(res.getStatus()).isEqualTo(ReservationStatus.COMPLETED);
verify(reservationRepository, never()).save(any());
}

@Test
@DisplayName("confirmAssignedSeats: 좌석 확정 성공")
void confirmAssignedSeats_success() {
Expand Down