generated from pricelees/issue-pr-template
feat: 완료되지 않은 예약 및 일정 처리 스케쥴러 도입 및 테스트 추가
This commit is contained in:
parent
459fb331ae
commit
a1621e2f65
@ -0,0 +1,43 @@
|
|||||||
|
package com.sangdol.roomescape.reservation.business.scheduler
|
||||||
|
|
||||||
|
import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationRepository
|
||||||
|
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleRepository
|
||||||
|
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus
|
||||||
|
import io.github.oshai.kotlinlogging.KLogger
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
private val log: KLogger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@EnableScheduling
|
||||||
|
class IncompletedReservationScheduler(
|
||||||
|
private val scheduleRepository: ScheduleRepository,
|
||||||
|
private val reservationRepository: ReservationRepository
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Scheduled(fixedRate = 1, timeUnit = TimeUnit.MINUTES)
|
||||||
|
@Transactional
|
||||||
|
fun processExpiredHoldSchedule() {
|
||||||
|
log.info { "[IncompletedReservationScheduler] 만료 시간이 지난 ${ScheduleStatus.HOLD} 상태의 일정 재활성화 시작" }
|
||||||
|
|
||||||
|
scheduleRepository.releaseExpiredHolds(LocalDateTime.now()).also {
|
||||||
|
log.info { "[IncompletedReservationScheduler] ${it}개의 일정 재활성화 완료" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Scheduled(fixedRate = 1, timeUnit = TimeUnit.MINUTES)
|
||||||
|
@Transactional
|
||||||
|
fun processExpiredReservation() {
|
||||||
|
log.info { "[IncompletedReservationScheduler] 결제되지 않은 예약 만료 처리 시작 " }
|
||||||
|
|
||||||
|
reservationRepository.expirePendingReservations(LocalDateTime.now()).also {
|
||||||
|
log.info { "[IncompletedReservationScheduler] ${it}개의 예약 및 일정 처리 완료" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,28 @@
|
|||||||
package com.sangdol.roomescape.reservation.infrastructure.persistence
|
package com.sangdol.roomescape.reservation.infrastructure.persistence
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
import org.springframework.data.jpa.repository.Modifying
|
||||||
|
import org.springframework.data.jpa.repository.Query
|
||||||
|
import org.springframework.data.repository.query.Param
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
interface ReservationRepository : JpaRepository<ReservationEntity, Long> {
|
interface ReservationRepository : JpaRepository<ReservationEntity, Long> {
|
||||||
|
|
||||||
fun findAllByUserIdAndStatusIsIn(userId: Long, statuses: List<ReservationStatus>): List<ReservationEntity>
|
fun findAllByUserIdAndStatusIsIn(userId: Long, statuses: List<ReservationStatus>): List<ReservationEntity>
|
||||||
|
|
||||||
|
@Modifying
|
||||||
|
@Query("""
|
||||||
|
UPDATE
|
||||||
|
reservation r
|
||||||
|
JOIN
|
||||||
|
schedule s ON r.schedule_id = s.id AND s.status = 'HOLD'
|
||||||
|
SET
|
||||||
|
r.status = 'EXPIRED',
|
||||||
|
r.updated_at = :now,
|
||||||
|
s.status = 'AVAILABLE',
|
||||||
|
s.hold_expired_at = NULL
|
||||||
|
WHERE
|
||||||
|
r.status = 'PENDING' AND r.created_at <= DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 5 MINUTE)
|
||||||
|
""", nativeQuery = true)
|
||||||
|
fun expirePendingReservations(@Param("now") now: LocalDateTime): Int
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,9 @@ import com.sangdol.roomescape.schedule.business.domain.ScheduleOverview
|
|||||||
import org.springframework.data.jpa.repository.JpaRepository
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
import org.springframework.data.jpa.repository.Modifying
|
import org.springframework.data.jpa.repository.Modifying
|
||||||
import org.springframework.data.jpa.repository.Query
|
import org.springframework.data.jpa.repository.Query
|
||||||
|
import org.springframework.data.repository.query.Param
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
import java.time.LocalDateTime
|
||||||
import java.time.LocalTime
|
import java.time.LocalTime
|
||||||
|
|
||||||
interface ScheduleRepository : JpaRepository<ScheduleEntity, Long> {
|
interface ScheduleRepository : JpaRepository<ScheduleEntity, Long> {
|
||||||
@ -103,4 +105,24 @@ interface ScheduleRepository : JpaRepository<ScheduleEntity, Long> {
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
fun changeStatus(id: Long, currentStatus: ScheduleStatus, changeStatus: ScheduleStatus): Int
|
fun changeStatus(id: Long, currentStatus: ScheduleStatus, changeStatus: ScheduleStatus): Int
|
||||||
|
|
||||||
|
@Modifying
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
UPDATE
|
||||||
|
ScheduleEntity s
|
||||||
|
SET
|
||||||
|
s.status = com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus.AVAILABLE,
|
||||||
|
s.holdExpiredAt = NULL
|
||||||
|
WHERE
|
||||||
|
s.status = com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus.HOLD
|
||||||
|
AND s.holdExpiredAt <= :now
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM ReservationEntity r
|
||||||
|
WHERE r.scheduleId = s._id
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun releaseExpiredHolds(@Param("now") now: LocalDateTime): Int
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,73 @@
|
|||||||
|
package com.sangdol.roomescape.reservation.business
|
||||||
|
|
||||||
|
import com.sangdol.common.persistence.TransactionExecutionUtil
|
||||||
|
import com.sangdol.roomescape.reservation.business.scheduler.IncompletedReservationScheduler
|
||||||
|
import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationRepository
|
||||||
|
import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationStatus
|
||||||
|
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleEntity
|
||||||
|
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleRepository
|
||||||
|
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus
|
||||||
|
import com.sangdol.roomescape.supports.FunSpecSpringbootTest
|
||||||
|
import com.sangdol.roomescape.user.infrastructure.persistence.UserEntity
|
||||||
|
import io.kotest.assertions.assertSoftly
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see com.sangdol.roomescape.reservation.business.scheduler.IncompletedReservationScheduler
|
||||||
|
* Scheduling 시간은 테스트 하지 않고, 내부 로직의 정상 동작만 확인
|
||||||
|
*/
|
||||||
|
class IncompletedReservationSchedulerTest(
|
||||||
|
private val incompletedReservationScheduler: IncompletedReservationScheduler,
|
||||||
|
private val transactionExecutionUtil: TransactionExecutionUtil,
|
||||||
|
private val jdbcTemplate: JdbcTemplate,
|
||||||
|
private val reservationRepository: ReservationRepository,
|
||||||
|
private val scheduleRepository: ScheduleRepository,
|
||||||
|
) : FunSpecSpringbootTest() {
|
||||||
|
|
||||||
|
init {
|
||||||
|
test("예약이 없고, hold_expired_at 시간이 지난 ${ScheduleStatus.HOLD} 일정을 ${ScheduleStatus.AVAILABLE} 상태로 바꾼다.") {
|
||||||
|
val schedule: ScheduleEntity = dummyInitializer.createSchedule().apply {
|
||||||
|
this.status = ScheduleStatus.HOLD
|
||||||
|
this.holdExpiredAt = LocalDateTime.now().minusSeconds(1)
|
||||||
|
}.also {
|
||||||
|
scheduleRepository.saveAndFlush(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
|
||||||
|
incompletedReservationScheduler.processExpiredHoldSchedule()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertSoftly(scheduleRepository.findByIdOrNull(schedule.id)!!) {
|
||||||
|
this.status shouldBe ScheduleStatus.AVAILABLE
|
||||||
|
this.holdExpiredAt shouldBe null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("${ReservationStatus.PENDING} 상태로 일정 시간 완료되지 않은 예약을 ${ReservationStatus.EXPIRED} 상태로 바꾼다.") {
|
||||||
|
val user: UserEntity = testAuthUtil.defaultUserLogin().first
|
||||||
|
val reservation = dummyInitializer.createPendingReservation(user = user).also {
|
||||||
|
jdbcTemplate.execute("UPDATE reservation SET created_at = DATE_SUB(NOW(), INTERVAL 5 MINUTE) WHERE id = ${it.id}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val now = LocalDateTime.now()
|
||||||
|
|
||||||
|
transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
|
||||||
|
incompletedReservationScheduler.processExpiredReservation()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertSoftly(reservationRepository.findByIdOrNull(reservation.id)!!) {
|
||||||
|
this.status shouldBe ReservationStatus.EXPIRED
|
||||||
|
this.updatedAt.hour shouldBe now.hour
|
||||||
|
this.updatedAt.minute shouldBe now.minute
|
||||||
|
}
|
||||||
|
|
||||||
|
assertSoftly(scheduleRepository.findByIdOrNull(reservation.scheduleId)!!) {
|
||||||
|
this.status shouldBe ScheduleStatus.AVAILABLE
|
||||||
|
this.holdExpiredAt shouldBe null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user