188 lines
8.1 KiB
Kotlin

package roomescape.reservation.business
import com.github.f4b6a3.tsid.TsidFactory
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import roomescape.common.config.next
import roomescape.common.dto.CurrentUserContext
import roomescape.common.dto.PrincipalType
import roomescape.common.util.DateUtils
import roomescape.member.business.UserService
import roomescape.member.web.UserContactRetrieveResponse
import roomescape.payment.business.PaymentService
import roomescape.payment.web.PaymentRetrieveResponse
import roomescape.reservation.exception.ReservationErrorCode
import roomescape.reservation.exception.ReservationException
import roomescape.reservation.infrastructure.persistence.*
import roomescape.reservation.web.*
import roomescape.schedule.business.ScheduleService
import roomescape.schedule.infrastructure.persistence.ScheduleStatus
import roomescape.schedule.web.ScheduleSummaryResponse
import roomescape.schedule.web.ScheduleUpdateRequest
import roomescape.theme.business.ThemeService
import roomescape.theme.web.ThemeInfoRetrieveResponse
import java.time.LocalDate
import java.time.LocalDateTime
private val log: KLogger = KotlinLogging.logger {}
@Service
class ReservationService(
private val reservationRepository: ReservationRepository,
private val reservationValidator: ReservationValidator,
private val scheduleService: ScheduleService,
private val userService: UserService,
private val themeService: ThemeService,
private val canceledReservationRepository: CanceledReservationRepository,
private val tsidFactory: TsidFactory,
private val paymentService: PaymentService
) {
@Transactional
fun createPendingReservation(
user: CurrentUserContext,
request: PendingReservationCreateRequest
): PendingReservationCreateResponse {
log.info { "[ReservationService.createPendingReservation] Pending 예약 생성 시작: schedule=${request.scheduleId}" }
validateCanCreate(request)
val reservation: ReservationEntity = request.toEntity(id = tsidFactory.next(), userId = user.id)
return PendingReservationCreateResponse(reservationRepository.save(reservation).id)
.also { "[ReservationService.createPendingReservation] Pending 예약 생성 완료: reservationId=${it}, schedule=${request.scheduleId}" }
}
@Transactional
fun confirmReservation(id: Long) {
log.info { "[ReservationService.confirmReservation] Pending 예약 확정 시작: reservationId=${id}" }
val reservation: ReservationEntity = findOrThrow(id)
run {
reservation.confirm()
scheduleService.updateSchedule(
reservation.scheduleId,
ScheduleUpdateRequest(status = ScheduleStatus.RESERVED)
)
}.also {
log.info { "[ReservationService.confirmReservation] Pending 예약 확정 완료: reservationId=${id}" }
}
}
@Transactional
fun cancelReservation(user: CurrentUserContext, reservationId: Long, request: ReservationCancelRequest) {
log.info { "[ReservationService.cancelReservation] 예약 취소 시작: userId=${user.id}, reservationId=${reservationId}" }
val reservation: ReservationEntity = findOrThrow(reservationId)
run {
scheduleService.updateSchedule(
reservation.scheduleId,
ScheduleUpdateRequest(status = ScheduleStatus.AVAILABLE)
)
saveCanceledReservation(user, reservation, request.cancelReason)
reservation.cancel()
}.also {
log.info { "[ReservationService.cancelReservation] 예약 취소 완료: reservationId=${reservationId}" }
}
}
@Transactional(readOnly = true)
fun findUserSummaryReservation(user: CurrentUserContext): ReservationSummaryRetrieveListResponse {
log.info { "[ReservationService.findSummaryByMemberId] 예약 조회 시작: userId=${user.id}" }
val reservations: List<ReservationEntity> = reservationRepository.findAllByUserId(user.id)
return ReservationSummaryRetrieveListResponse(reservations.map {
val schedule: ScheduleSummaryResponse = scheduleService.findSummaryById(it.scheduleId)
val theme: ThemeInfoRetrieveResponse = themeService.findSummaryById(schedule.themeId)
ReservationSummaryRetrieveResponse(
id = it.id,
themeName = theme.name,
date = schedule.date,
startAt = schedule.time,
status = it.status
)
}).also {
log.info { "[ReservationService.findSummaryByMemberId] ${it.reservations.size}개의 예약 조회 완료: userId=${user.id}" }
}
}
@Transactional(readOnly = true)
fun findDetailById(id: Long): ReservationDetailRetrieveResponse {
log.info { "[ReservationService.findDetailById] 예약 상세 조회 시작: reservationId=${id}" }
val reservation: ReservationEntity = findOrThrow(id)
val user: UserContactRetrieveResponse = userService.findContactById(reservation.userId)
val paymentDetail: PaymentRetrieveResponse = paymentService.findDetailByReservationId(id)
return reservation.toReservationDetailRetrieveResponse(
user = user,
payment = paymentDetail
).also {
log.info { "[ReservationService.findDetailById] 예약 상세 조회 완료: reservationId=${id}" }
}
}
@Transactional(readOnly = true)
fun findMostReservedThemeIds(count: Int): MostReservedThemeIdListResponse {
log.info { "[ReservationService.findMostReservedThemeIds] 인기 테마 조회 시작: count=$count" }
val previousWeekSunday = DateUtils.getSundayOfPreviousWeek(LocalDate.now())
val previousWeekSaturday = previousWeekSunday.plusDays(6)
val themeIds: List<Long> = reservationRepository.findMostReservedThemeIds(
dateFrom = previousWeekSunday,
dateTo = previousWeekSaturday,
count = count
)
return MostReservedThemeIdListResponse(themeIds = themeIds).also {
log.info { "[ReservationService.findMostReservedThemeIds] 인기 테마 조회 완료: count=${it.themeIds.size}" }
}
}
private fun findOrThrow(id: Long): ReservationEntity {
log.info { "[ReservationService.findOrThrow] 예약 조회 시작: reservationId=${id}" }
return reservationRepository.findByIdOrNull(id)
?.also { log.info { "[ReservationService.findOrThrow] 예약 조회 완료: reservationId=${id}" } }
?: run {
log.warn { "[ReservationService.findOrThrow] 예약 조회 실패: reservationId=${id}" }
throw ReservationException(ReservationErrorCode.RESERVATION_NOT_FOUND)
}
}
private fun saveCanceledReservation(
user: CurrentUserContext,
reservation: ReservationEntity,
cancelReason: String
) {
if (user.type != PrincipalType.ADMIN && reservation.userId != user.id) {
log.warn { "[ReservationService.createCanceledPayment] 예약자 본인 또는 관리자가 아닌 회원의 취소 요청: reservationId=${reservation.id}, userId=${user.id}" }
throw ReservationException(ReservationErrorCode.NO_PERMISSION_TO_CANCEL_RESERVATION)
}
CanceledReservationEntity(
id = tsidFactory.next(),
reservationId = reservation.id,
canceledBy = user.id,
cancelReason = cancelReason,
canceledAt = LocalDateTime.now(),
status = CanceledReservationStatus.PROCESSING
).also {
canceledReservationRepository.save(it)
}
}
private fun validateCanCreate(request: PendingReservationCreateRequest) {
val schedule = scheduleService.findSummaryById(request.scheduleId)
val theme = themeService.findSummaryById(schedule.themeId)
reservationValidator.validateCanCreate(schedule, theme, request)
}
}