generated from pricelees/issue-pr-template
<!-- 제목 양식 --> <!-- [이슈번호] 작업 요약 (예시: [#10] Gitea 템플릿 생성) --> ## 📝 관련 이슈 및 PR **PR과 관련된 이슈 번호** - #20 ## ✨ 작업 내용 <!-- 어떤 작업을 했는지 알려주세요! --> - 기존에 공통으로 사용하던 커스텀 예외를 도메인별로 분리 - 새로 정의된 예외는 ErrorCode를 지정할 때 HttpStatusCode를 지정하도록 하여 잘못된 지정에서 오는 휴먼 에러 방지 - 커스텀 예외를 적용하는 과정에서 가독성이 낮다고 느껴지는 일부 함수형 코드는 선언형으로 수정 ## 🧪 테스트 <!-- 어떤 테스트를 생각했고 진행했는지 알려주세요! --> - 테스트 중 일부 미비된 테스트 보완 후 전체 로직 테스트 완료 ## 📚 참고 자료 및 기타 <!-- 참고한 자료, 또는 논의할 사항이 있다면 알려주세요! --> 커스텀 예외도 작업이 길어져 로깅은 이후에 진행할 예정 Reviewed-on: #21 Co-authored-by: pricelees <priceelees@gmail.com> Co-committed-by: pricelees <priceelees@gmail.com>
242 lines
8.9 KiB
Kotlin
242 lines
8.9 KiB
Kotlin
package roomescape.reservation.business
|
|
|
|
import org.springframework.data.jpa.domain.Specification
|
|
import org.springframework.data.repository.findByIdOrNull
|
|
import org.springframework.stereotype.Service
|
|
import org.springframework.transaction.annotation.Transactional
|
|
import roomescape.member.business.MemberService
|
|
import roomescape.member.infrastructure.persistence.MemberEntity
|
|
import roomescape.reservation.exception.ReservationErrorCode
|
|
import roomescape.reservation.exception.ReservationException
|
|
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
|
import roomescape.reservation.infrastructure.persistence.ReservationRepository
|
|
import roomescape.reservation.infrastructure.persistence.ReservationSearchSpecification
|
|
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
|
import roomescape.reservation.web.*
|
|
import roomescape.theme.business.ThemeService
|
|
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
|
import roomescape.time.business.TimeService
|
|
import roomescape.time.infrastructure.persistence.TimeEntity
|
|
import java.time.LocalDate
|
|
import java.time.LocalDateTime
|
|
|
|
@Service
|
|
@Transactional
|
|
class ReservationService(
|
|
private val reservationRepository: ReservationRepository,
|
|
private val timeService: TimeService,
|
|
private val memberService: MemberService,
|
|
private val themeService: ThemeService,
|
|
) {
|
|
|
|
@Transactional(readOnly = true)
|
|
fun findReservations(): ReservationRetrieveListResponse {
|
|
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
|
|
.confirmed()
|
|
.build()
|
|
|
|
return ReservationRetrieveListResponse(findAllReservationByStatus(spec))
|
|
}
|
|
|
|
@Transactional(readOnly = true)
|
|
fun findAllWaiting(): ReservationRetrieveListResponse {
|
|
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
|
|
.waiting()
|
|
.build()
|
|
|
|
return ReservationRetrieveListResponse(findAllReservationByStatus(spec))
|
|
}
|
|
|
|
private fun findAllReservationByStatus(spec: Specification<ReservationEntity>): List<ReservationRetrieveResponse> {
|
|
return reservationRepository.findAll(spec).map { it.toRetrieveResponse() }
|
|
}
|
|
|
|
fun deleteReservation(reservationId: Long, memberId: Long) {
|
|
validateIsMemberAdmin(memberId)
|
|
reservationRepository.deleteById(reservationId)
|
|
}
|
|
|
|
fun createConfirmedReservation(
|
|
request: ReservationCreateWithPaymentRequest,
|
|
memberId: Long
|
|
): ReservationEntity {
|
|
val themeId = request.themeId
|
|
val timeId = request.timeId
|
|
val date: LocalDate = request.date
|
|
validateIsReservationExist(themeId, timeId, date)
|
|
|
|
val reservation: ReservationEntity = createEntity(timeId, themeId, date, memberId, ReservationStatus.CONFIRMED)
|
|
|
|
return reservationRepository.save(reservation)
|
|
}
|
|
|
|
fun createReservationByAdmin(request: AdminReservationCreateRequest): ReservationRetrieveResponse {
|
|
validateIsReservationExist(request.themeId, request.timeId, request.date)
|
|
|
|
return addReservationWithoutPayment(
|
|
request.themeId,
|
|
request.timeId,
|
|
request.date,
|
|
request.memberId,
|
|
ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
|
)
|
|
}
|
|
|
|
fun createWaiting(request: WaitingCreateRequest, memberId: Long): ReservationRetrieveResponse {
|
|
validateMemberAlreadyReserve(request.themeId, request.timeId, request.date, memberId)
|
|
return addReservationWithoutPayment(
|
|
request.themeId,
|
|
request.timeId,
|
|
request.date,
|
|
memberId,
|
|
ReservationStatus.WAITING
|
|
)
|
|
}
|
|
|
|
private fun addReservationWithoutPayment(
|
|
themeId: Long,
|
|
timeId: Long,
|
|
date: LocalDate,
|
|
memberId: Long,
|
|
status: ReservationStatus
|
|
): ReservationRetrieveResponse = createEntity(timeId, themeId, date, memberId, status)
|
|
.also {
|
|
reservationRepository.save(it)
|
|
}.toRetrieveResponse()
|
|
|
|
private fun validateMemberAlreadyReserve(themeId: Long, timeId: Long, date: LocalDate, memberId: Long) {
|
|
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
|
|
.sameMemberId(memberId)
|
|
.sameThemeId(themeId)
|
|
.sameTimeId(timeId)
|
|
.sameDate(date)
|
|
.build()
|
|
|
|
if (reservationRepository.exists(spec)) {
|
|
throw ReservationException(ReservationErrorCode.ALREADY_RESERVE)
|
|
}
|
|
}
|
|
|
|
private fun validateIsReservationExist(themeId: Long, timeId: Long, date: LocalDate) {
|
|
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
|
|
.confirmed()
|
|
.sameThemeId(themeId)
|
|
.sameTimeId(timeId)
|
|
.sameDate(date)
|
|
.build()
|
|
|
|
if (reservationRepository.exists(spec)) {
|
|
throw ReservationException(ReservationErrorCode.RESERVATION_DUPLICATED)
|
|
}
|
|
}
|
|
|
|
private fun validateDateAndTime(
|
|
requestDate: LocalDate,
|
|
requestTime: TimeEntity
|
|
) {
|
|
val now = LocalDateTime.now()
|
|
val request = LocalDateTime.of(requestDate, requestTime.startAt)
|
|
|
|
if (request.isBefore(now)) {
|
|
throw ReservationException(ReservationErrorCode.PAST_REQUEST_DATETIME)
|
|
}
|
|
}
|
|
|
|
private fun createEntity(
|
|
timeId: Long,
|
|
themeId: Long,
|
|
date: LocalDate,
|
|
memberId: Long,
|
|
status: ReservationStatus
|
|
): ReservationEntity {
|
|
val time: TimeEntity = timeService.findById(timeId)
|
|
val theme: ThemeEntity = themeService.findById(themeId)
|
|
val member: MemberEntity = memberService.findById(memberId)
|
|
|
|
validateDateAndTime(date, time)
|
|
|
|
return ReservationEntity(
|
|
date = date,
|
|
time = time,
|
|
theme = theme,
|
|
member = member,
|
|
reservationStatus = status
|
|
)
|
|
}
|
|
|
|
@Transactional(readOnly = true)
|
|
fun searchReservations(
|
|
themeId: Long?,
|
|
memberId: Long?,
|
|
dateFrom: LocalDate?,
|
|
dateTo: LocalDate?
|
|
): ReservationRetrieveListResponse {
|
|
validateDateForSearch(dateFrom, dateTo)
|
|
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
|
|
.confirmed()
|
|
.sameThemeId(themeId)
|
|
.sameMemberId(memberId)
|
|
.dateStartFrom(dateFrom)
|
|
.dateEndAt(dateTo)
|
|
.build()
|
|
|
|
return ReservationRetrieveListResponse(findAllReservationByStatus(spec))
|
|
}
|
|
|
|
private fun validateDateForSearch(startFrom: LocalDate?, endAt: LocalDate?) {
|
|
if (startFrom == null || endAt == null) {
|
|
return
|
|
}
|
|
if (startFrom.isAfter(endAt)) {
|
|
throw ReservationException(ReservationErrorCode.INVALID_SEARCH_DATE_RANGE)
|
|
}
|
|
}
|
|
|
|
@Transactional(readOnly = true)
|
|
fun findReservationsByMemberId(memberId: Long): MyReservationRetrieveListResponse {
|
|
return MyReservationRetrieveListResponse(reservationRepository.findAllByMemberId(memberId))
|
|
}
|
|
|
|
fun confirmWaiting(reservationId: Long, memberId: Long) {
|
|
validateIsMemberAdmin(memberId)
|
|
if (reservationRepository.isExistConfirmedReservation(reservationId)) {
|
|
throw ReservationException(ReservationErrorCode.CONFIRMED_RESERVATION_ALREADY_EXISTS)
|
|
}
|
|
reservationRepository.updateStatusByReservationId(reservationId, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED)
|
|
}
|
|
|
|
fun deleteWaiting(reservationId: Long, memberId: Long) {
|
|
val reservation: ReservationEntity = findReservationOrThrow(reservationId)
|
|
if (!reservation.isWaiting()) {
|
|
throw ReservationException(ReservationErrorCode.ALREADY_CONFIRMED)
|
|
}
|
|
if (!reservation.isReservedBy(memberId)) {
|
|
throw ReservationException(ReservationErrorCode.NOT_RESERVATION_OWNER)
|
|
}
|
|
reservationRepository.delete(reservation)
|
|
}
|
|
|
|
fun rejectWaiting(reservationId: Long, memberId: Long) {
|
|
validateIsMemberAdmin(memberId)
|
|
val reservation: ReservationEntity = findReservationOrThrow(reservationId)
|
|
|
|
if (!reservation.isWaiting()) {
|
|
throw ReservationException(ReservationErrorCode.ALREADY_CONFIRMED)
|
|
}
|
|
reservationRepository.delete(reservation)
|
|
}
|
|
|
|
private fun validateIsMemberAdmin(memberId: Long) {
|
|
val member: MemberEntity = memberService.findById(memberId)
|
|
if (member.isAdmin()) {
|
|
return
|
|
}
|
|
throw ReservationException(ReservationErrorCode.NO_PERMISSION)
|
|
}
|
|
|
|
private fun findReservationOrThrow(reservationId: Long): ReservationEntity {
|
|
return reservationRepository.findByIdOrNull(reservationId)
|
|
?: throw ReservationException(ReservationErrorCode.RESERVATION_NOT_FOUND)
|
|
}
|
|
}
|