generated from pricelees/issue-pr-template
[#16] Reservation 도메인 코드 코틀린 마이그레이션 #17
@ -6,7 +6,7 @@ import roomescape.auth.web.LoginCheckResponse
|
||||
import roomescape.auth.web.LoginRequest
|
||||
import roomescape.auth.web.TokenResponse
|
||||
import roomescape.member.business.MemberService
|
||||
import roomescape.member.infrastructure.persistence.Member
|
||||
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||
|
||||
@Service
|
||||
class AuthService(
|
||||
@ -14,7 +14,7 @@ class AuthService(
|
||||
private val jwtHandler: JwtHandler
|
||||
) {
|
||||
fun login(request: LoginRequest): TokenResponse {
|
||||
val member: Member = memberService.findMemberByEmailAndPassword(
|
||||
val member: MemberEntity = memberService.findMemberByEmailAndPassword(
|
||||
request.email,
|
||||
request.password
|
||||
)
|
||||
|
||||
@ -10,7 +10,7 @@ import roomescape.auth.infrastructure.jwt.JwtHandler
|
||||
import roomescape.common.exception.ErrorType
|
||||
import roomescape.common.exception.RoomescapeException
|
||||
import roomescape.member.business.MemberService
|
||||
import roomescape.member.infrastructure.persistence.Member
|
||||
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||
|
||||
private fun Any.isIrrelevantWith(annotationType: Class<out Annotation>): Boolean {
|
||||
if (this !is HandlerMethod) {
|
||||
@ -63,7 +63,7 @@ class AdminInterceptor(
|
||||
return true
|
||||
}
|
||||
|
||||
val member: Member?
|
||||
val member: MemberEntity?
|
||||
|
||||
try {
|
||||
val token: String? = request.accessTokenCookie().value
|
||||
|
||||
@ -6,7 +6,7 @@ import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import roomescape.common.exception.ErrorType
|
||||
import roomescape.common.exception.RoomescapeException
|
||||
import roomescape.member.infrastructure.persistence.Member
|
||||
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||
import roomescape.member.infrastructure.persistence.MemberRepository
|
||||
import roomescape.member.web.MembersResponse
|
||||
import roomescape.member.web.toResponse
|
||||
@ -22,14 +22,14 @@ class MemberService(
|
||||
.toList()
|
||||
)
|
||||
|
||||
fun findById(memberId: Long): Member = memberRepository.findByIdOrNull(memberId)
|
||||
fun findById(memberId: Long): MemberEntity = memberRepository.findByIdOrNull(memberId)
|
||||
?: throw RoomescapeException(
|
||||
ErrorType.MEMBER_NOT_FOUND,
|
||||
String.format("[memberId: %d]", memberId),
|
||||
HttpStatus.BAD_REQUEST
|
||||
)
|
||||
|
||||
fun findMemberByEmailAndPassword(email: String, password: String): Member =
|
||||
fun findMemberByEmailAndPassword(email: String, password: String): MemberEntity =
|
||||
memberRepository.findByEmailAndPassword(email, password)
|
||||
?: throw RoomescapeException(
|
||||
ErrorType.MEMBER_NOT_FOUND,
|
||||
|
||||
@ -3,7 +3,8 @@ package roomescape.member.infrastructure.persistence
|
||||
import jakarta.persistence.*
|
||||
|
||||
@Entity
|
||||
class Member(
|
||||
@Table(name = "member")
|
||||
class MemberEntity(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long? = null,
|
||||
@ -2,6 +2,6 @@ package roomescape.member.infrastructure.persistence
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
|
||||
interface MemberRepository : JpaRepository<Member, Long> {
|
||||
fun findByEmailAndPassword(email: String, password: String): Member?
|
||||
interface MemberRepository : JpaRepository<MemberEntity, Long> {
|
||||
fun findByEmailAndPassword(email: String, password: String): MemberEntity?
|
||||
}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
package roomescape.member.web
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import roomescape.member.infrastructure.persistence.Member
|
||||
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||
|
||||
fun Member.toResponse(): MemberResponse = MemberResponse(
|
||||
fun MemberEntity.toResponse(): MemberResponse = MemberResponse(
|
||||
id = id!!,
|
||||
name = name
|
||||
)
|
||||
@ -18,7 +18,7 @@ data class MemberResponse(
|
||||
) {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun fromEntity(member: Member): MemberResponse {
|
||||
fun fromEntity(member: MemberEntity): MemberResponse {
|
||||
return MemberResponse(member.id!!, member.name)
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ import roomescape.payment.web.PaymentApprove
|
||||
import roomescape.payment.web.PaymentCancel
|
||||
import roomescape.payment.web.ReservationPaymentResponse
|
||||
import roomescape.payment.web.toReservationPaymentResponse
|
||||
import roomescape.reservation.domain.Reservation
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Service
|
||||
@ -24,7 +24,7 @@ class PaymentService(
|
||||
@Transactional
|
||||
fun savePayment(
|
||||
paymentResponse: PaymentApprove.Response,
|
||||
reservation: Reservation
|
||||
reservation: ReservationEntity
|
||||
): ReservationPaymentResponse = PaymentEntity(
|
||||
orderId = paymentResponse.orderId,
|
||||
paymentKey = paymentResponse.paymentKey,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package roomescape.payment.infrastructure.persistence
|
||||
|
||||
import jakarta.persistence.*
|
||||
import roomescape.reservation.domain.Reservation
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Entity
|
||||
@ -22,7 +22,7 @@ class PaymentEntity(
|
||||
|
||||
@OneToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "reservation_id", nullable = false)
|
||||
var reservation: Reservation,
|
||||
var reservation: ReservationEntity,
|
||||
|
||||
@Column(nullable = false)
|
||||
var approvedAt: OffsetDateTime
|
||||
|
||||
@ -4,7 +4,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||
import roomescape.payment.infrastructure.client.PaymentCancelResponseDeserializer
|
||||
import roomescape.payment.infrastructure.persistence.PaymentEntity
|
||||
import roomescape.reservation.dto.response.ReservationResponse
|
||||
import roomescape.reservation.web.ReservationResponse
|
||||
import roomescape.reservation.web.toResponse
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
class PaymentApprove {
|
||||
@ -60,6 +61,6 @@ fun PaymentEntity.toReservationPaymentResponse(): ReservationPaymentResponse = R
|
||||
orderId = this.orderId,
|
||||
paymentKey = this.paymentKey,
|
||||
totalAmount = this.totalAmount,
|
||||
reservation = ReservationResponse.from(this.reservation),
|
||||
reservation = this.reservation.toResponse(),
|
||||
approvedAt = this.approvedAt
|
||||
)
|
||||
@ -0,0 +1,244 @@
|
||||
package roomescape.reservation.business
|
||||
|
||||
import org.springframework.data.jpa.domain.Specification
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import roomescape.common.exception.ErrorType
|
||||
import roomescape.common.exception.RoomescapeException
|
||||
import roomescape.member.business.MemberService
|
||||
import roomescape.reservation.infrastructure.persistence.*
|
||||
import roomescape.reservation.web.*
|
||||
import roomescape.theme.business.ThemeService
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
class ReservationService(
|
||||
private val reservationRepository: ReservationRepository,
|
||||
private val reservationTimeService: ReservationTimeService,
|
||||
private val memberService: MemberService,
|
||||
private val themeService: ThemeService,
|
||||
) {
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun findAllReservations(): ReservationsResponse {
|
||||
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
|
||||
.confirmed()
|
||||
.build()
|
||||
|
||||
|
||||
return ReservationsResponse(findAllReservationByStatus(spec))
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun findAllWaiting(): ReservationsResponse {
|
||||
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
|
||||
.waiting()
|
||||
.build()
|
||||
|
||||
return ReservationsResponse(findAllReservationByStatus(spec))
|
||||
}
|
||||
|
||||
private fun findAllReservationByStatus(spec: Specification<ReservationEntity>): List<ReservationResponse> {
|
||||
return reservationRepository.findAll(spec).map { it.toResponse() }
|
||||
}
|
||||
|
||||
fun removeReservationById(reservationId: Long, memberId: Long) {
|
||||
validateIsMemberAdmin(memberId)
|
||||
reservationRepository.deleteById(reservationId)
|
||||
}
|
||||
|
||||
fun addReservation(request: ReservationRequest, memberId: Long): ReservationEntity {
|
||||
validateIsReservationExist(request.themeId, request.timeId, request.date)
|
||||
return getReservationForSave(
|
||||
request.timeId,
|
||||
request.themeId,
|
||||
request.date,
|
||||
memberId,
|
||||
ReservationStatus.CONFIRMED
|
||||
).also {
|
||||
reservationRepository.save(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun addReservationByAdmin(request: AdminReservationRequest): ReservationResponse {
|
||||
validateIsReservationExist(request.themeId, request.timeId, request.date)
|
||||
|
||||
return addReservationWithoutPayment(
|
||||
request.themeId,
|
||||
request.timeId,
|
||||
request.date,
|
||||
request.memberId,
|
||||
ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
||||
)
|
||||
}
|
||||
|
||||
fun addWaiting(request: WaitingRequest, memberId: Long): ReservationResponse {
|
||||
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
|
||||
): ReservationResponse = getReservationForSave(timeId, themeId, date, memberId, status)
|
||||
.also {
|
||||
reservationRepository.save(it)
|
||||
}.toResponse()
|
||||
|
||||
|
||||
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 RoomescapeException(ErrorType.HAS_RESERVATION_OR_WAITING, HttpStatus.BAD_REQUEST)
|
||||
}
|
||||
}
|
||||
|
||||
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 RoomescapeException(ErrorType.RESERVATION_DUPLICATED, HttpStatus.CONFLICT)
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateDateAndTime(
|
||||
requestDate: LocalDate,
|
||||
requestReservationTime: ReservationTimeEntity
|
||||
) {
|
||||
val now = LocalDateTime.now()
|
||||
val request = LocalDateTime.of(requestDate, requestReservationTime.startAt)
|
||||
|
||||
if (request.isBefore(now)) {
|
||||
throw RoomescapeException(
|
||||
ErrorType.RESERVATION_PERIOD_IN_PAST,
|
||||
"[now: $now | request: $request]",
|
||||
HttpStatus.BAD_REQUEST
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getReservationForSave(
|
||||
timeId: Long,
|
||||
themeId: Long,
|
||||
date: LocalDate,
|
||||
memberId: Long,
|
||||
status: ReservationStatus
|
||||
): ReservationEntity {
|
||||
val time = reservationTimeService.findTimeById(timeId)
|
||||
val theme = themeService.findThemeById(themeId)
|
||||
val member = memberService.findById(memberId)
|
||||
|
||||
validateDateAndTime(date, time)
|
||||
|
||||
return ReservationEntity(
|
||||
date = date,
|
||||
reservationTime = time,
|
||||
theme = theme,
|
||||
member = member,
|
||||
reservationStatus = status
|
||||
)
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun findFilteredReservations(
|
||||
themeId: Long?,
|
||||
memberId: Long?,
|
||||
dateFrom: LocalDate?,
|
||||
dateTo: LocalDate?
|
||||
): ReservationsResponse {
|
||||
validateDateForSearch(dateFrom, dateTo)
|
||||
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
|
||||
.confirmed()
|
||||
.sameThemeId(themeId)
|
||||
.sameMemberId(memberId)
|
||||
.dateStartFrom(dateFrom)
|
||||
.dateEndAt(dateTo)
|
||||
.build()
|
||||
|
||||
return ReservationsResponse(findAllReservationByStatus(spec))
|
||||
}
|
||||
|
||||
private fun validateDateForSearch(startFrom: LocalDate?, endAt: LocalDate?) {
|
||||
if (startFrom == null || endAt == null) {
|
||||
return
|
||||
}
|
||||
if (startFrom.isAfter(endAt)) {
|
||||
throw RoomescapeException(
|
||||
ErrorType.INVALID_DATE_RANGE,
|
||||
"[startFrom: $startFrom, endAt: $endAt", HttpStatus.BAD_REQUEST
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun findMemberReservations(memberId: Long): MyReservationsResponse {
|
||||
return MyReservationsResponse(reservationRepository.findMyReservations(memberId))
|
||||
}
|
||||
|
||||
fun approveWaiting(reservationId: Long, memberId: Long) {
|
||||
validateIsMemberAdmin(memberId)
|
||||
if (reservationRepository.isExistConfirmedReservation(reservationId)) {
|
||||
throw RoomescapeException(ErrorType.RESERVATION_DUPLICATED, HttpStatus.CONFLICT)
|
||||
}
|
||||
reservationRepository.updateStatusByReservationId(reservationId, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED)
|
||||
}
|
||||
|
||||
fun cancelWaiting(reservationId: Long, memberId: Long) {
|
||||
reservationRepository.findByIdOrNull(reservationId)?.takeIf {
|
||||
it.isWaiting() && it.isSameMember(memberId)
|
||||
}?.let {
|
||||
reservationRepository.delete(it)
|
||||
} ?: throw throwReservationNotFound(reservationId)
|
||||
}
|
||||
|
||||
fun denyWaiting(reservationId: Long, memberId: Long) {
|
||||
validateIsMemberAdmin(memberId)
|
||||
reservationRepository.findByIdOrNull(reservationId)?.takeIf {
|
||||
it.isWaiting()
|
||||
}?.let {
|
||||
reservationRepository.delete(it)
|
||||
} ?: throw throwReservationNotFound(reservationId)
|
||||
}
|
||||
|
||||
private fun validateIsMemberAdmin(memberId: Long) {
|
||||
memberService.findById(memberId).takeIf {
|
||||
it.isAdmin()
|
||||
} ?: throw RoomescapeException(
|
||||
ErrorType.PERMISSION_DOES_NOT_EXIST,
|
||||
"[memberId: $memberId]",
|
||||
HttpStatus.FORBIDDEN
|
||||
)
|
||||
}
|
||||
|
||||
private fun throwReservationNotFound(reservationId: Long?): RoomescapeException {
|
||||
return RoomescapeException(
|
||||
ErrorType.RESERVATION_NOT_FOUND,
|
||||
"[reservationId: $reservationId]",
|
||||
HttpStatus.NOT_FOUND
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
package roomescape.reservation.business
|
||||
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import roomescape.common.exception.ErrorType
|
||||
import roomescape.common.exception.RoomescapeException
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationRepository
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationTimeEntity
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationTimeRepository
|
||||
import roomescape.reservation.web.*
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
|
||||
@Service
|
||||
class ReservationTimeService(
|
||||
private val reservationTimeRepository: ReservationTimeRepository,
|
||||
private val reservationRepository: ReservationRepository
|
||||
) {
|
||||
@Transactional(readOnly = true)
|
||||
fun findTimeById(id: Long): ReservationTimeEntity = reservationTimeRepository.findByIdOrNull(id)
|
||||
?: throw RoomescapeException(
|
||||
ErrorType.RESERVATION_TIME_NOT_FOUND,
|
||||
"[reservationTimeId: $id]",
|
||||
HttpStatus.BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun findAllTimes(): ReservationTimesResponse = reservationTimeRepository.findAll()
|
||||
.toResponses()
|
||||
|
||||
@Transactional
|
||||
fun addTime(reservationTimeRequest: ReservationTimeRequest): ReservationTimeResponse {
|
||||
val startAt: LocalTime = reservationTimeRequest.startAt
|
||||
|
||||
if (reservationTimeRepository.existsByStartAt(startAt)) {
|
||||
throw RoomescapeException(
|
||||
ErrorType.TIME_DUPLICATED, "[startAt: $startAt]", HttpStatus.CONFLICT
|
||||
)
|
||||
}
|
||||
|
||||
return ReservationTimeEntity(startAt = startAt)
|
||||
.also { reservationTimeRepository.save(it) }
|
||||
.toResponse()
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun removeTimeById(id: Long) {
|
||||
val reservationTime: ReservationTimeEntity = findTimeById(id)
|
||||
reservationRepository.findByReservationTime(reservationTime)
|
||||
.also {
|
||||
if (it.isNotEmpty()) {
|
||||
throw RoomescapeException(
|
||||
ErrorType.TIME_IS_USED_CONFLICT, "[timeId: $id]", HttpStatus.CONFLICT
|
||||
)
|
||||
}
|
||||
reservationTimeRepository.deleteById(id)
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun findAllAvailableTimesByDateAndTheme(date: LocalDate, themeId: Long): ReservationTimeInfosResponse {
|
||||
val allTimes = reservationTimeRepository.findAll()
|
||||
val reservations: List<ReservationEntity> = reservationRepository.findByDateAndThemeId(date, themeId)
|
||||
|
||||
return ReservationTimeInfosResponse(allTimes.map { time ->
|
||||
val alreadyBooked: Boolean = reservations.any { reservation -> reservation.reservationTime.id == time.id }
|
||||
time.toInfoResponse(alreadyBooked)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
package roomescape.reservation.business
|
||||
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import roomescape.payment.business.PaymentService
|
||||
import roomescape.payment.web.PaymentApprove
|
||||
import roomescape.payment.web.PaymentCancel
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||
import roomescape.reservation.web.ReservationRequest
|
||||
import roomescape.reservation.web.ReservationResponse
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
class ReservationWithPaymentService(
|
||||
private val reservationService: ReservationService,
|
||||
private val paymentService: PaymentService
|
||||
) {
|
||||
fun addReservationWithPayment(
|
||||
request: ReservationRequest,
|
||||
paymentInfo: PaymentApprove.Response,
|
||||
memberId: Long
|
||||
): ReservationResponse {
|
||||
val reservation: ReservationEntity = reservationService.addReservation(request, memberId)
|
||||
|
||||
return paymentService.savePayment(paymentInfo, reservation)
|
||||
.reservation
|
||||
}
|
||||
|
||||
fun saveCanceledPayment(
|
||||
cancelInfo: PaymentCancel.Response,
|
||||
approvedAt: OffsetDateTime,
|
||||
paymentKey: String
|
||||
) {
|
||||
paymentService.saveCanceledPayment(cancelInfo, approvedAt, paymentKey)
|
||||
}
|
||||
|
||||
fun removeReservationWithPayment(
|
||||
reservationId: Long,
|
||||
memberId: Long
|
||||
): PaymentCancel.Request {
|
||||
val paymentCancelRequest = paymentService.cancelPaymentByAdmin(reservationId)
|
||||
reservationService.removeReservationById(reservationId, memberId)
|
||||
|
||||
return paymentCancelRequest
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun isNotPaidReservation(reservationId: Long): Boolean = !paymentService.isReservationPaid(reservationId)
|
||||
|
||||
|
||||
fun updateCanceledTime(
|
||||
paymentKey: String,
|
||||
canceledAt: OffsetDateTime
|
||||
) {
|
||||
paymentService.updateCanceledTime(paymentKey, canceledAt)
|
||||
}
|
||||
}
|
||||
@ -1,271 +0,0 @@
|
||||
package roomescape.reservation.controller;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.headers.Header;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import roomescape.auth.web.support.Admin;
|
||||
import roomescape.auth.web.support.LoginRequired;
|
||||
import roomescape.auth.web.support.MemberId;
|
||||
import roomescape.common.dto.response.RoomescapeApiResponse;
|
||||
import roomescape.common.dto.response.RoomescapeErrorResponse;
|
||||
import roomescape.common.exception.RoomescapeException;
|
||||
import roomescape.payment.infrastructure.client.TossPaymentClient;
|
||||
import roomescape.payment.web.PaymentApprove;
|
||||
import roomescape.payment.web.PaymentCancel;
|
||||
import roomescape.reservation.dto.request.AdminReservationRequest;
|
||||
import roomescape.reservation.dto.request.ReservationRequest;
|
||||
import roomescape.reservation.dto.request.WaitingRequest;
|
||||
import roomescape.reservation.dto.response.MyReservationsResponse;
|
||||
import roomescape.reservation.dto.response.ReservationResponse;
|
||||
import roomescape.reservation.dto.response.ReservationsResponse;
|
||||
import roomescape.reservation.service.ReservationService;
|
||||
import roomescape.reservation.service.ReservationWithPaymentService;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "3. 예약 API", description = "예약 및 대기 정보를 추가 / 조회 / 삭제할 때 사용합니다.")
|
||||
public class ReservationController {
|
||||
|
||||
private final ReservationWithPaymentService reservationWithPaymentService;
|
||||
private final ReservationService reservationService;
|
||||
private final TossPaymentClient paymentClient;
|
||||
|
||||
public ReservationController(ReservationWithPaymentService reservationWithPaymentService,
|
||||
ReservationService reservationService, TossPaymentClient paymentClient) {
|
||||
this.reservationWithPaymentService = reservationWithPaymentService;
|
||||
this.reservationService = reservationService;
|
||||
this.paymentClient = paymentClient;
|
||||
}
|
||||
|
||||
@Admin
|
||||
@GetMapping("/reservations")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@Operation(summary = "모든 예약 정보 조회", tags = "관리자 로그인이 필요한 API")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)
|
||||
})
|
||||
public RoomescapeApiResponse<ReservationsResponse> getAllReservations() {
|
||||
return RoomescapeApiResponse.success(reservationService.findAllReservations());
|
||||
}
|
||||
|
||||
@LoginRequired
|
||||
@GetMapping("/reservations-mine")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@Operation(summary = "자신의 예약 및 대기 조회", tags = "로그인이 필요한 API")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)
|
||||
})
|
||||
public RoomescapeApiResponse<MyReservationsResponse> getMemberReservations(
|
||||
@MemberId @Parameter(hidden = true) Long memberId) {
|
||||
return RoomescapeApiResponse.success(reservationService.findMemberReservations(memberId));
|
||||
}
|
||||
|
||||
@Admin
|
||||
@GetMapping("/reservations/search")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@Operation(summary = "관리자의 예약 검색", description = "특정 조건에 해당되는 예약 검색", tags = "관리자 로그인이 필요한 API")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true),
|
||||
@ApiResponse(responseCode = "400", description = "날짜 범위를 지정할 때, 종료 날짜는 시작 날짜 이전일 수 없습니다.",
|
||||
content = @Content(schema = @Schema(implementation = RoomescapeErrorResponse.class)))
|
||||
})
|
||||
public RoomescapeApiResponse<ReservationsResponse> getReservationBySearching(
|
||||
@RequestParam(required = false) @Parameter(description = "테마 ID") Long themeId,
|
||||
@RequestParam(required = false) @Parameter(description = "회원 ID") Long memberId,
|
||||
@RequestParam(required = false) @Parameter(description = "yyyy-MM-dd 형식으로 입력해주세요", example = "2024-06-10") LocalDate dateFrom,
|
||||
@RequestParam(required = false) @Parameter(description = "yyyy-MM-dd 형식으로 입력해주세요", example = "2024-06-10") LocalDate dateTo
|
||||
) {
|
||||
return RoomescapeApiResponse.success(
|
||||
reservationService.findFilteredReservations(themeId, memberId, dateFrom, dateTo));
|
||||
}
|
||||
|
||||
@Admin
|
||||
@DeleteMapping("/reservations/{id}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
@Operation(summary = "관리자의 예약 취소", tags = "관리자 로그인이 필요한 API")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "204", description = "성공"),
|
||||
@ApiResponse(responseCode = "404", description = "예약 또는 결제 정보를 찾을 수 없습니다.",
|
||||
content = @Content(schema = @Schema(implementation = RoomescapeErrorResponse.class))),
|
||||
})
|
||||
public RoomescapeApiResponse<Void> removeReservation(
|
||||
@MemberId @Parameter(hidden = true) Long memberId,
|
||||
@NotNull(message = "reservationId는 null일 수 없습니다.") @PathVariable("id") @Parameter(description = "예약 ID") Long reservationId
|
||||
) {
|
||||
|
||||
if (reservationWithPaymentService.isNotPaidReservation(reservationId)) {
|
||||
reservationService.removeReservationById(reservationId, memberId);
|
||||
return RoomescapeApiResponse.success();
|
||||
}
|
||||
|
||||
PaymentCancel.Request paymentCancelRequest = reservationWithPaymentService.removeReservationWithPayment(
|
||||
reservationId, memberId);
|
||||
|
||||
PaymentCancel.Response paymentCancelResponse = paymentClient.cancelPayment(paymentCancelRequest);
|
||||
|
||||
reservationWithPaymentService.updateCanceledTime(paymentCancelRequest.paymentKey,
|
||||
paymentCancelResponse.canceledAt);
|
||||
|
||||
return RoomescapeApiResponse.success();
|
||||
}
|
||||
|
||||
@LoginRequired
|
||||
@PostMapping("/reservations")
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
@Operation(summary = "예약 추가", tags = "로그인이 필요한 API")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true,
|
||||
headers = @Header(name = HttpHeaders.LOCATION, description = "생성된 예약 정보 URL", schema = @Schema(example = "/reservations/1")))
|
||||
})
|
||||
public RoomescapeApiResponse<ReservationResponse> saveReservation(
|
||||
@Valid @RequestBody ReservationRequest reservationRequest,
|
||||
@MemberId @Parameter(hidden = true) Long memberId,
|
||||
HttpServletResponse response
|
||||
) {
|
||||
PaymentApprove.Request paymentRequest = reservationRequest.getPaymentRequest();
|
||||
PaymentApprove.Response paymentResponse = paymentClient.confirmPayment(paymentRequest);
|
||||
|
||||
try {
|
||||
ReservationResponse reservationResponse = reservationWithPaymentService.addReservationWithPayment(
|
||||
reservationRequest, paymentResponse, memberId);
|
||||
return getCreatedReservationResponse(reservationResponse, response);
|
||||
} catch (RoomescapeException e) {
|
||||
PaymentCancel.Request cancelRequest = new PaymentCancel.Request(paymentRequest.paymentKey,
|
||||
paymentRequest.amount, e.getMessage());
|
||||
|
||||
PaymentCancel.Response paymentCancelResponse = paymentClient.cancelPayment(cancelRequest);
|
||||
|
||||
reservationWithPaymentService.saveCanceledPayment(paymentCancelResponse, paymentResponse.approvedAt,
|
||||
paymentRequest.paymentKey);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Admin
|
||||
@PostMapping("/reservations/admin")
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
@Operation(summary = "관리자 예약 추가", tags = "관리자 로그인이 필요한 API")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true,
|
||||
headers = @Header(name = HttpHeaders.LOCATION, description = "생성된 예약 정보 URL", schema = @Schema(example = "/reservations/1"))),
|
||||
@ApiResponse(responseCode = "409", description = "예약이 이미 존재합니다.", content = @Content(schema = @Schema(implementation = RoomescapeErrorResponse.class)))
|
||||
})
|
||||
public RoomescapeApiResponse<ReservationResponse> saveReservationByAdmin(
|
||||
@Valid @RequestBody AdminReservationRequest adminReservationRequest,
|
||||
HttpServletResponse response
|
||||
) {
|
||||
ReservationResponse reservationResponse = reservationService.addReservationByAdmin(adminReservationRequest);
|
||||
return getCreatedReservationResponse(reservationResponse, response);
|
||||
}
|
||||
|
||||
@Admin
|
||||
@GetMapping("/reservations/waiting")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@Operation(summary = "모든 예약 대기 조회", tags = "관리자 로그인이 필요한 API")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)
|
||||
})
|
||||
public RoomescapeApiResponse<ReservationsResponse> getAllWaiting() {
|
||||
return RoomescapeApiResponse.success(reservationService.findAllWaiting());
|
||||
}
|
||||
|
||||
@LoginRequired
|
||||
@PostMapping("/reservations/waiting")
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
@Operation(summary = "예약 대기 신청", tags = "로그인이 필요한 API")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true,
|
||||
headers = @Header(name = HttpHeaders.LOCATION, description = "생성된 예약 정보 URL", schema = @Schema(example = "/reservations/1")))
|
||||
})
|
||||
public RoomescapeApiResponse<ReservationResponse> saveWaiting(
|
||||
@Valid @RequestBody WaitingRequest waitingRequest,
|
||||
@MemberId @Parameter(hidden = true) Long memberId,
|
||||
HttpServletResponse response
|
||||
) {
|
||||
ReservationResponse reservationResponse = reservationService.addWaiting(waitingRequest, memberId);
|
||||
return getCreatedReservationResponse(reservationResponse, response);
|
||||
}
|
||||
|
||||
@LoginRequired
|
||||
@DeleteMapping("/reservations/waiting/{id}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
@Operation(summary = "예약 대기 취소", tags = "로그인이 필요한 API")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "204", description = "성공"),
|
||||
@ApiResponse(responseCode = "404", description = "회원의 예약 대기 정보를 찾을 수 없습니다.",
|
||||
content = @Content(schema = @Schema(implementation = RoomescapeErrorResponse.class)))
|
||||
})
|
||||
public RoomescapeApiResponse<Void> deleteWaiting(
|
||||
@MemberId @Parameter(hidden = true) Long memberId,
|
||||
@NotNull(message = "reservationId는 null 또는 공백일 수 없습니다.") @PathVariable("id") @Parameter(description = "예약 ID") Long reservationId
|
||||
) {
|
||||
reservationService.cancelWaiting(reservationId, memberId);
|
||||
return RoomescapeApiResponse.success();
|
||||
}
|
||||
|
||||
@Admin
|
||||
@PostMapping("/reservations/waiting/{id}/approve")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@Operation(summary = "대기 중인 예약 승인", tags = "관리자 로그인이 필요한 API")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "성공"),
|
||||
@ApiResponse(responseCode = "404", description = "예약 대기 정보를 찾을 수 없습니다.",
|
||||
content = @Content(schema = @Schema(implementation = RoomescapeErrorResponse.class))),
|
||||
@ApiResponse(responseCode = "409", description = "확정된 예약이 존재하여 대기 중인 예약을 승인할 수 없습니다.",
|
||||
content = @Content(schema = @Schema(implementation = RoomescapeErrorResponse.class)))
|
||||
})
|
||||
public RoomescapeApiResponse<Void> approveWaiting(
|
||||
@MemberId @Parameter(hidden = true) Long memberId,
|
||||
@NotNull(message = "reservationId는 null 또는 공백일 수 없습니다.") @PathVariable("id") @Parameter(description = "예약 ID") Long reservationId
|
||||
) {
|
||||
reservationService.approveWaiting(reservationId, memberId);
|
||||
|
||||
return RoomescapeApiResponse.success();
|
||||
}
|
||||
|
||||
@Admin
|
||||
@PostMapping("/reservations/waiting/{id}/deny")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
@Operation(summary = "대기 중인 예약 거절", tags = "관리자 로그인이 필요한 API")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "204", description = "대기 중인 예약 거절 성공"),
|
||||
@ApiResponse(responseCode = "404", description = "예약 대기 정보를 찾을 수 없습니다.",
|
||||
content = @Content(schema = @Schema(implementation = RoomescapeErrorResponse.class)))
|
||||
})
|
||||
public RoomescapeApiResponse<Void> denyWaiting(
|
||||
@MemberId @Parameter(hidden = true) Long memberId,
|
||||
@NotNull(message = "reservationId는 null 또는 공백일 수 없습니다.") @PathVariable("id") @Parameter(description = "예약 ID") Long reservationId
|
||||
) {
|
||||
reservationService.denyWaiting(reservationId, memberId);
|
||||
|
||||
return RoomescapeApiResponse.success();
|
||||
}
|
||||
|
||||
private RoomescapeApiResponse<ReservationResponse> getCreatedReservationResponse(
|
||||
ReservationResponse reservationResponse,
|
||||
HttpServletResponse response
|
||||
) {
|
||||
response.setHeader(HttpHeaders.LOCATION, "/reservations/" + reservationResponse.id());
|
||||
return RoomescapeApiResponse.success(reservationResponse);
|
||||
}
|
||||
}
|
||||
@ -1,112 +0,0 @@
|
||||
package roomescape.reservation.controller;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import roomescape.auth.web.support.Admin;
|
||||
import roomescape.auth.web.support.LoginRequired;
|
||||
import roomescape.common.dto.response.RoomescapeApiResponse;
|
||||
import roomescape.common.dto.response.RoomescapeErrorResponse;
|
||||
import roomescape.reservation.dto.request.ReservationTimeRequest;
|
||||
import roomescape.reservation.dto.response.ReservationTimeInfosResponse;
|
||||
import roomescape.reservation.dto.response.ReservationTimeResponse;
|
||||
import roomescape.reservation.dto.response.ReservationTimesResponse;
|
||||
import roomescape.reservation.service.ReservationTimeService;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "4. 예약 시간 API", description = "예약 시간을 조회 / 추가 / 삭제할 때 사용합니다.")
|
||||
public class ReservationTimeController {
|
||||
|
||||
private final ReservationTimeService reservationTimeService;
|
||||
|
||||
public ReservationTimeController(ReservationTimeService reservationTimeService) {
|
||||
this.reservationTimeService = reservationTimeService;
|
||||
}
|
||||
|
||||
@Admin
|
||||
@GetMapping("/times")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@Operation(summary = "모든 시간 조회", tags = "관리자 로그인이 필요한 API")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)
|
||||
})
|
||||
public RoomescapeApiResponse<ReservationTimesResponse> getAllTimes() {
|
||||
return RoomescapeApiResponse.success(reservationTimeService.findAllTimes());
|
||||
}
|
||||
|
||||
@Admin
|
||||
@PostMapping("/times")
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
@Operation(summary = "시간 추가", tags = "관리자 로그인이 필요한 API")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true),
|
||||
@ApiResponse(responseCode = "409", description = "같은 시간을 추가할 수 없습니다.",
|
||||
content = @Content(schema = @Schema(implementation = RoomescapeErrorResponse.class)))
|
||||
})
|
||||
public RoomescapeApiResponse<ReservationTimeResponse> saveTime(
|
||||
@Valid @RequestBody ReservationTimeRequest reservationTimeRequest,
|
||||
HttpServletResponse response
|
||||
) {
|
||||
ReservationTimeResponse reservationTimeResponse = reservationTimeService.addTime(reservationTimeRequest);
|
||||
response.setHeader(HttpHeaders.LOCATION, "/times/" + reservationTimeResponse.id());
|
||||
|
||||
return RoomescapeApiResponse.success(reservationTimeResponse);
|
||||
}
|
||||
|
||||
@Admin
|
||||
@DeleteMapping("/times/{id}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
@Operation(summary = "시간 삭제", tags = "관리자 로그인이 필요한 API")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "204", description = "성공", useReturnTypeSchema = true),
|
||||
@ApiResponse(responseCode = "409", description = "예약된 시간은 삭제할 수 없습니다.",
|
||||
content = @Content(schema = @Schema(implementation = RoomescapeErrorResponse.class)))
|
||||
})
|
||||
public RoomescapeApiResponse<Void> removeTime(
|
||||
@NotNull(message = "timeId는 null 또는 공백일 수 없습니다.") @PathVariable @Parameter(description = "삭제하고자 하는 시간의 ID값") Long id
|
||||
) {
|
||||
reservationTimeService.removeTimeById(id);
|
||||
|
||||
return RoomescapeApiResponse.success();
|
||||
}
|
||||
|
||||
@LoginRequired
|
||||
@GetMapping("/times/filter")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@Operation(summary = "예약 가능 여부를 포함한 모든 시간 조회", tags = "로그인이 필요한 API")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)
|
||||
})
|
||||
public RoomescapeApiResponse<ReservationTimeInfosResponse> findAllAvailableReservationTimes(
|
||||
@NotNull(message = "날짜는 null일 수 없습니다.")
|
||||
@RequestParam
|
||||
@Parameter(description = "yyyy-MM-dd 형식으로 입력해주세요.", example = "2024-06-10")
|
||||
LocalDate date,
|
||||
@NotNull(message = "themeId는 null일 수 없습니다.")
|
||||
@RequestParam
|
||||
@Parameter(description = "조회할 테마의 ID를 입력해주세요.", example = "1")
|
||||
Long themeId
|
||||
) {
|
||||
return RoomescapeApiResponse.success(reservationTimeService.findAllAvailableTimesByDateAndTheme(date, themeId));
|
||||
}
|
||||
}
|
||||
138
src/main/java/roomescape/reservation/docs/ReservationAPI.kt
Normal file
138
src/main/java/roomescape/reservation/docs/ReservationAPI.kt
Normal file
@ -0,0 +1,138 @@
|
||||
package roomescape.reservation.docs
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation
|
||||
import io.swagger.v3.oas.annotations.Parameter
|
||||
import io.swagger.v3.oas.annotations.headers.Header
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses
|
||||
import io.swagger.v3.oas.annotations.tags.Tag
|
||||
import jakarta.validation.Valid
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import roomescape.auth.web.support.Admin
|
||||
import roomescape.auth.web.support.LoginRequired
|
||||
import roomescape.auth.web.support.MemberId
|
||||
import roomescape.common.dto.response.CommonApiResponse
|
||||
import roomescape.reservation.web.*
|
||||
import java.time.LocalDate
|
||||
|
||||
@Tag(name = "3. 예약 API", description = "예약 및 대기 정보를 추가 / 조회 / 삭제할 때 사용합니다.")
|
||||
interface ReservationAPI {
|
||||
|
||||
@Admin
|
||||
@Operation(summary = "모든 예약 정보 조회", tags = ["관리자 로그인이 필요한 API"])
|
||||
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
|
||||
fun getAllReservations(): ResponseEntity<CommonApiResponse<ReservationsResponse>>
|
||||
|
||||
@LoginRequired
|
||||
@Operation(summary = "자신의 예약 및 대기 조회", tags = ["로그인이 필요한 API"])
|
||||
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
|
||||
fun getMemberReservations(
|
||||
@MemberId @Parameter(hidden = true) memberId: Long
|
||||
): ResponseEntity<CommonApiResponse<MyReservationsResponse>>
|
||||
|
||||
@Admin
|
||||
@Operation(summary = "관리자의 예약 검색", description = "특정 조건에 해당되는 예약 검색", tags = ["관리자 로그인이 필요한 API"])
|
||||
@ApiResponses(
|
||||
ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)
|
||||
)
|
||||
fun getReservationBySearching(
|
||||
@RequestParam(required = false) themeId: Long?,
|
||||
@RequestParam(required = false) memberId: Long?,
|
||||
@RequestParam(required = false) dateFrom: LocalDate?,
|
||||
@RequestParam(required = false) dateTo: LocalDate?
|
||||
): ResponseEntity<CommonApiResponse<ReservationsResponse>>
|
||||
|
||||
@Admin
|
||||
@Operation(summary = "관리자의 예약 취소", tags = ["관리자 로그인이 필요한 API"])
|
||||
@ApiResponses(
|
||||
ApiResponse(responseCode = "204", description = "성공"),
|
||||
)
|
||||
fun removeReservation(
|
||||
@MemberId @Parameter(hidden = true) memberId: Long,
|
||||
@PathVariable("id") reservationId: Long
|
||||
): ResponseEntity<CommonApiResponse<Unit>>
|
||||
|
||||
@LoginRequired
|
||||
@Operation(summary = "예약 추가", tags = ["로그인이 필요한 API"])
|
||||
@ApiResponses(
|
||||
ApiResponse(
|
||||
responseCode = "201",
|
||||
description = "성공",
|
||||
useReturnTypeSchema = true,
|
||||
headers = [Header(name = HttpHeaders.LOCATION, description = "생성된 예약 정보 URL", schema = Schema(example = "/reservations/1"))]
|
||||
)
|
||||
)
|
||||
fun saveReservation(
|
||||
@Valid @RequestBody reservationRequest: ReservationRequest,
|
||||
@MemberId @Parameter(hidden = true) memberId: Long
|
||||
): ResponseEntity<CommonApiResponse<ReservationResponse>>
|
||||
|
||||
@Admin
|
||||
@Operation(summary = "관리자 예약 추가", tags = ["관리자 로그인이 필요한 API"])
|
||||
@ApiResponses(
|
||||
ApiResponse(
|
||||
responseCode = "201",
|
||||
description = "성공",
|
||||
useReturnTypeSchema = true,
|
||||
headers = [Header(name = HttpHeaders.LOCATION, description = "생성된 예약 정보 URL", schema = Schema(example = "/reservations/1"))],
|
||||
)
|
||||
)
|
||||
fun saveReservationByAdmin(
|
||||
@Valid @RequestBody adminReservationRequest: AdminReservationRequest,
|
||||
): ResponseEntity<CommonApiResponse<ReservationResponse>>
|
||||
|
||||
@Admin
|
||||
@Operation(summary = "모든 예약 대기 조회", tags = ["관리자 로그인이 필요한 API"])
|
||||
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
|
||||
fun getAllWaiting(): ResponseEntity<CommonApiResponse<ReservationsResponse>>
|
||||
|
||||
@LoginRequired
|
||||
@Operation(summary = "예약 대기 신청", tags = ["로그인이 필요한 API"])
|
||||
@ApiResponses(
|
||||
ApiResponse(
|
||||
responseCode = "201",
|
||||
description = "성공",
|
||||
useReturnTypeSchema = true,
|
||||
headers = [Header(name = HttpHeaders.LOCATION, description = "생성된 예약 정보 URL", schema = Schema(example = "/reservations/1"))]
|
||||
)
|
||||
)
|
||||
fun saveWaiting(
|
||||
@Valid @RequestBody waitingRequest: WaitingRequest,
|
||||
@MemberId @Parameter(hidden = true) memberId: Long,
|
||||
): ResponseEntity<CommonApiResponse<ReservationResponse>>
|
||||
|
||||
@LoginRequired
|
||||
@Operation(summary = "예약 대기 취소", tags = ["로그인이 필요한 API"])
|
||||
@ApiResponses(
|
||||
ApiResponse(responseCode = "204", description = "성공"),
|
||||
)
|
||||
fun deleteWaiting(
|
||||
@MemberId @Parameter(hidden = true) memberId: Long,
|
||||
@PathVariable("id") @Parameter(description = "예약 ID") reservationId: Long
|
||||
): ResponseEntity<CommonApiResponse<Unit>>
|
||||
|
||||
@Admin
|
||||
@Operation(summary = "대기 중인 예약 승인", tags = ["관리자 로그인이 필요한 API"])
|
||||
@ApiResponses(
|
||||
ApiResponse(responseCode = "200", description = "성공"),
|
||||
)
|
||||
fun approveWaiting(
|
||||
@MemberId @Parameter(hidden = true) memberId: Long,
|
||||
@PathVariable("id") @Parameter(description = "예약 ID") reservationId: Long
|
||||
): ResponseEntity<CommonApiResponse<Unit>>
|
||||
|
||||
@Admin
|
||||
@Operation(summary = "대기 중인 예약 거절", tags = ["관리자 로그인이 필요한 API"])
|
||||
@ApiResponses(
|
||||
ApiResponse(responseCode = "204", description = "대기 중인 예약 거절 성공"),
|
||||
)
|
||||
fun denyWaiting(
|
||||
@MemberId @Parameter(hidden = true) memberId: Long,
|
||||
@PathVariable("id") @Parameter(description = "예약 ID") reservationId: Long
|
||||
): ResponseEntity<CommonApiResponse<Unit>>
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
package roomescape.reservation.docs
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses
|
||||
import io.swagger.v3.oas.annotations.tags.Tag
|
||||
import jakarta.validation.Valid
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import roomescape.auth.web.support.Admin
|
||||
import roomescape.auth.web.support.LoginRequired
|
||||
import roomescape.common.dto.response.CommonApiResponse
|
||||
import roomescape.reservation.web.ReservationTimeInfosResponse
|
||||
import roomescape.reservation.web.ReservationTimeRequest
|
||||
import roomescape.reservation.web.ReservationTimeResponse
|
||||
import roomescape.reservation.web.ReservationTimesResponse
|
||||
import java.time.LocalDate
|
||||
|
||||
@Tag(name = "4. 예약 시간 API", description = "예약 시간을 조회 / 추가 / 삭제할 때 사용합니다.")
|
||||
interface ReservationTimeAPI {
|
||||
|
||||
@Admin
|
||||
@Operation(summary = "모든 시간 조회", tags = ["관리자 로그인이 필요한 API"])
|
||||
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
|
||||
fun getAllTimes(): ResponseEntity<CommonApiResponse<ReservationTimesResponse>>
|
||||
|
||||
@Admin
|
||||
@Operation(summary = "시간 추가", tags = ["관리자 로그인이 필요한 API"])
|
||||
@ApiResponses(ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true))
|
||||
fun saveTime(
|
||||
@Valid @RequestBody reservationTimeRequest: ReservationTimeRequest,
|
||||
): ResponseEntity<CommonApiResponse<ReservationTimeResponse>>
|
||||
|
||||
@Admin
|
||||
@Operation(summary = "시간 삭제", tags = ["관리자 로그인이 필요한 API"])
|
||||
@ApiResponses(ApiResponse(responseCode = "204", description = "성공", useReturnTypeSchema = true))
|
||||
fun removeTime(
|
||||
@PathVariable id: Long
|
||||
): ResponseEntity<CommonApiResponse<Unit>>
|
||||
|
||||
@LoginRequired
|
||||
@Operation(summary = "예약 가능 여부를 포함한 모든 시간 조회", tags = ["로그인이 필요한 API"])
|
||||
@ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true))
|
||||
fun findAllAvailableReservationTimes(
|
||||
@RequestParam date: LocalDate,
|
||||
@RequestParam themeId: Long
|
||||
): ResponseEntity<CommonApiResponse<ReservationTimeInfosResponse>>
|
||||
}
|
||||
@ -1,127 +0,0 @@
|
||||
package roomescape.reservation.domain;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import roomescape.common.exception.ErrorType;
|
||||
import roomescape.common.exception.RoomescapeException;
|
||||
import roomescape.member.infrastructure.persistence.Member;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity;
|
||||
|
||||
@Entity
|
||||
public class Reservation {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private LocalDate date;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "time_id", nullable = false)
|
||||
private ReservationTime reservationTime;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "theme_id", nullable = false)
|
||||
private ThemeEntity theme;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "member_id", nullable = false)
|
||||
private Member member;
|
||||
|
||||
@Enumerated(value = EnumType.STRING)
|
||||
private ReservationStatus reservationStatus;
|
||||
|
||||
protected Reservation() {
|
||||
}
|
||||
|
||||
public Reservation(
|
||||
LocalDate date,
|
||||
ReservationTime reservationTime,
|
||||
ThemeEntity theme,
|
||||
Member member,
|
||||
ReservationStatus status
|
||||
) {
|
||||
this(null, date, reservationTime, theme, member, status);
|
||||
}
|
||||
|
||||
public Reservation(
|
||||
Long id,
|
||||
LocalDate date,
|
||||
ReservationTime reservationTime,
|
||||
ThemeEntity theme,
|
||||
Member member,
|
||||
ReservationStatus status
|
||||
) {
|
||||
validateIsNull(date, reservationTime, theme, member, status);
|
||||
this.id = id;
|
||||
this.date = date;
|
||||
this.reservationTime = reservationTime;
|
||||
this.theme = theme;
|
||||
this.member = member;
|
||||
this.reservationStatus = status;
|
||||
}
|
||||
|
||||
private void validateIsNull(LocalDate date, ReservationTime reservationTime, ThemeEntity theme, Member member,
|
||||
ReservationStatus reservationStatus) {
|
||||
if (date == null || reservationTime == null || theme == null || member == null || reservationStatus == null) {
|
||||
throw new RoomescapeException(ErrorType.REQUEST_DATA_BLANK, String.format("[values: %s]", this),
|
||||
HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
public Long getMemberId() {
|
||||
return member.getId();
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public LocalDate getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public ReservationTime getReservationTime() {
|
||||
return reservationTime;
|
||||
}
|
||||
|
||||
public ThemeEntity getTheme() {
|
||||
return theme;
|
||||
}
|
||||
|
||||
public Member getMember() {
|
||||
return member;
|
||||
}
|
||||
|
||||
public ReservationStatus getReservationStatus() {
|
||||
return reservationStatus;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public boolean isSameDateAndTime(LocalDate date, ReservationTime time) {
|
||||
return this.date.equals(date) && time.getStartAt().equals(this.reservationTime.getStartAt());
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public boolean isWaiting() {
|
||||
return reservationStatus == ReservationStatus.WAITING;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public boolean isSameMember(Long memberId) {
|
||||
return getMemberId().equals(memberId);
|
||||
}
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
package roomescape.reservation.domain;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "예약 상태를 나타냅니다.", allowableValues = {"CONFIRMED", "CONFIRMED_PAYMENT_REQUIRED", "WAITING"})
|
||||
public enum ReservationStatus {
|
||||
@Schema(description = "결제가 완료된 예약")
|
||||
CONFIRMED,
|
||||
@Schema(description = "결제가 필요한 예약")
|
||||
CONFIRMED_PAYMENT_REQUIRED,
|
||||
@Schema(description = "대기 중인 예약")
|
||||
WAITING;
|
||||
}
|
||||
@ -1,59 +0,0 @@
|
||||
package roomescape.reservation.domain;
|
||||
|
||||
import java.time.LocalTime;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import roomescape.common.exception.ErrorType;
|
||||
import roomescape.common.exception.RoomescapeException;
|
||||
|
||||
@Entity
|
||||
public class ReservationTime {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private LocalTime startAt;
|
||||
|
||||
protected ReservationTime() {
|
||||
}
|
||||
|
||||
public ReservationTime(final LocalTime startAt) {
|
||||
this(null, startAt);
|
||||
}
|
||||
|
||||
public ReservationTime(final Long id, final LocalTime startAt) {
|
||||
this.id = id;
|
||||
this.startAt = startAt;
|
||||
|
||||
validateNull();
|
||||
}
|
||||
|
||||
private void validateNull() {
|
||||
if (startAt == null) {
|
||||
throw new RoomescapeException(ErrorType.REQUEST_DATA_BLANK, String.format("[values: %s]", this),
|
||||
HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public LocalTime getStartAt() {
|
||||
return startAt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ReservationTime{" +
|
||||
"id=" + id +
|
||||
", startAt=" + startAt +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
package roomescape.reservation.domain.repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import roomescape.reservation.domain.Reservation;
|
||||
import roomescape.reservation.domain.ReservationStatus;
|
||||
import roomescape.reservation.domain.ReservationTime;
|
||||
import roomescape.reservation.dto.response.MyReservationResponse;
|
||||
|
||||
public interface ReservationRepository extends JpaRepository<Reservation, Long>, JpaSpecificationExecutor<Reservation> {
|
||||
|
||||
List<Reservation> findByReservationTime(ReservationTime reservationTime);
|
||||
|
||||
List<Reservation> findByThemeId(Long themeId);
|
||||
|
||||
@Modifying
|
||||
@Query("""
|
||||
UPDATE Reservation r
|
||||
SET r.reservationStatus = :status
|
||||
WHERE r.id = :id
|
||||
""")
|
||||
int updateStatusByReservationId(@Param(value = "id") Long reservationId,
|
||||
@Param(value = "status") ReservationStatus statusForChange);
|
||||
|
||||
@Query("""
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM Reservation r
|
||||
WHERE r.theme.id = r2.theme.id
|
||||
AND r.reservationTime.id = r2.reservationTime.id
|
||||
AND r.date = r2.date
|
||||
AND r.reservationStatus != 'WAITING'
|
||||
)
|
||||
FROM Reservation r2
|
||||
WHERE r2.id = :id
|
||||
""")
|
||||
boolean isExistConfirmedReservation(@Param("id") Long reservationId);
|
||||
|
||||
@Query("""
|
||||
SELECT new roomescape.reservation.dto.response.MyReservationResponse(
|
||||
r.id,
|
||||
t.name,
|
||||
r.date,
|
||||
r.reservationTime.startAt,
|
||||
r.reservationStatus,
|
||||
(SELECT COUNT (r2) FROM Reservation r2 WHERE r2.theme = r.theme AND r2.date = r.date AND r2.reservationTime = r.reservationTime AND r2.id < r.id),
|
||||
p.paymentKey,
|
||||
p.totalAmount
|
||||
)
|
||||
FROM Reservation r
|
||||
JOIN r.theme t
|
||||
LEFT JOIN PaymentEntity p
|
||||
ON p.reservation = r
|
||||
WHERE r.member.id = :memberId
|
||||
""")
|
||||
List<MyReservationResponse> findMyReservations(Long memberId);
|
||||
}
|
||||
@ -1,87 +0,0 @@
|
||||
package roomescape.reservation.domain.repository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
|
||||
import roomescape.reservation.domain.Reservation;
|
||||
import roomescape.reservation.domain.ReservationStatus;
|
||||
|
||||
public class ReservationSearchSpecification {
|
||||
|
||||
private Specification<Reservation> spec;
|
||||
|
||||
public ReservationSearchSpecification() {
|
||||
this.spec = Specification.where(null);
|
||||
}
|
||||
|
||||
public ReservationSearchSpecification sameThemeId(Long themeId) {
|
||||
if (themeId != null) {
|
||||
this.spec = this.spec.and(
|
||||
(root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("theme").get("id"), themeId));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReservationSearchSpecification sameMemberId(Long memberId) {
|
||||
if (memberId != null) {
|
||||
this.spec = this.spec.and(
|
||||
(root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("member").get("id"), memberId));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReservationSearchSpecification sameTimeId(Long timeId) {
|
||||
if (timeId != null) {
|
||||
this.spec = this.spec.and(
|
||||
(root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("reservationTime").get("id"),
|
||||
timeId));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReservationSearchSpecification sameDate(LocalDate date) {
|
||||
if (date != null) {
|
||||
this.spec = this.spec.and(
|
||||
(root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("date"), date));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReservationSearchSpecification confirmed() {
|
||||
this.spec = this.spec.and(
|
||||
(root, query, criteriaBuilder) -> criteriaBuilder.or(
|
||||
criteriaBuilder.equal(root.get("reservationStatus"), ReservationStatus.CONFIRMED),
|
||||
criteriaBuilder.equal(root.get("reservationStatus"),
|
||||
ReservationStatus.CONFIRMED_PAYMENT_REQUIRED)
|
||||
));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReservationSearchSpecification waiting() {
|
||||
this.spec = this.spec.and(
|
||||
(root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("reservationStatus"),
|
||||
ReservationStatus.WAITING));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReservationSearchSpecification dateStartFrom(LocalDate dateFrom) {
|
||||
if (dateFrom != null) {
|
||||
this.spec = this.spec.and(
|
||||
(root, query, criteriaBuilder) -> criteriaBuilder.greaterThanOrEqualTo(root.get("date"), dateFrom));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReservationSearchSpecification dateEndAt(LocalDate toDate) {
|
||||
if (toDate != null) {
|
||||
this.spec = this.spec.and(
|
||||
(root, query, criteriaBuilder) -> criteriaBuilder.lessThanOrEqualTo(root.get("date"), toDate));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Specification<Reservation> build() {
|
||||
return this.spec;
|
||||
}
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
package roomescape.reservation.domain.repository;
|
||||
|
||||
import java.time.LocalTime;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import roomescape.reservation.domain.ReservationTime;
|
||||
|
||||
public interface ReservationTimeRepository extends JpaRepository<ReservationTime, Long> {
|
||||
|
||||
List<ReservationTime> findByStartAt(LocalTime startAt);
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
package roomescape.reservation.dto.request;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(name = "관리자 예약 저장 요청", description = "관리자의 예약 저장 요청시 사용됩니다.")
|
||||
public record AdminReservationRequest(
|
||||
@Schema(description = "예약 날짜. 지난 날짜는 지정할 수 없으며, yyyy-MM-dd 형식으로 입력해야 합니다.", type = "string", example = "2022-12-31")
|
||||
LocalDate date,
|
||||
@Schema(description = "예약 시간 ID.", example = "1")
|
||||
Long timeId,
|
||||
@Schema(description = "테마 ID", example = "1")
|
||||
Long themeId,
|
||||
@Schema(description = "회원 ID", example = "1")
|
||||
Long memberId
|
||||
) {
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
package roomescape.reservation.dto.request;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import roomescape.payment.web.PaymentApprove;
|
||||
|
||||
@Schema(name = "회원의 예약 저장 요청", description = "회원의 예약 요청시 사용됩니다.")
|
||||
public record ReservationRequest(
|
||||
@NotNull(message = "예약 날짜는 null일 수 없습니다.")
|
||||
@Schema(description = "예약 날짜. 지난 날짜는 지정할 수 없으며, yyyy-MM-dd 형식으로 입력해야 합니다.", type = "string", example = "2022-12-31")
|
||||
LocalDate date,
|
||||
@NotNull(message = "예약 요청의 timeId는 null일 수 없습니다.")
|
||||
@Schema(description = "예약 시간 ID.", example = "1")
|
||||
Long timeId,
|
||||
@NotNull(message = "예약 요청의 themeId는 null일 수 없습니다.")
|
||||
@Schema(description = "테마 ID", example = "1")
|
||||
Long themeId,
|
||||
@Schema(description = "결제 위젯을 통해 받은 결제 키")
|
||||
String paymentKey,
|
||||
@Schema(description = "결제 위젯을 통해 받은 주문번호.")
|
||||
String orderId,
|
||||
@Schema(description = "결제 위젯을 통해 받은 결제 금액")
|
||||
Long amount,
|
||||
@Schema(description = "결제 타입", example = "NORMAL")
|
||||
String paymentType
|
||||
) {
|
||||
|
||||
@JsonIgnore
|
||||
public PaymentApprove.Request getPaymentRequest() {
|
||||
return new PaymentApprove.Request(paymentKey, orderId, amount, paymentType);
|
||||
}
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
package roomescape.reservation.dto.request;
|
||||
|
||||
import java.time.LocalTime;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import io.micrometer.common.util.StringUtils;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import roomescape.common.exception.ErrorType;
|
||||
import roomescape.common.exception.RoomescapeException;
|
||||
import roomescape.reservation.domain.ReservationTime;
|
||||
|
||||
@Schema(name = "예약 시간 저장 요청", description = "예약 시간 저장 요청시 사용됩니다.")
|
||||
public record ReservationTimeRequest(
|
||||
@NotNull(message = "예약 시간은 null일 수 없습니다.")
|
||||
@Schema(description = "예약 시간. HH:mm 형식으로 입력해야 합니다.", type = "string", example = "09:00")
|
||||
LocalTime startAt
|
||||
) {
|
||||
|
||||
public ReservationTimeRequest {
|
||||
if (StringUtils.isBlank(startAt.toString())) {
|
||||
throw new RoomescapeException(ErrorType.REQUEST_DATA_BLANK,
|
||||
String.format("[values: %s]", this), HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
public ReservationTime toTime() {
|
||||
return new ReservationTime(this.startAt);
|
||||
}
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
package roomescape.reservation.dto.request;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
@Schema(name = "예약 대기 저장 요청", description = "회원의 예약 대기 요청시 사용됩니다.")
|
||||
public record WaitingRequest(
|
||||
@NotNull(message = "예약 날짜는 null일 수 없습니다.")
|
||||
@Schema(description = "예약 날짜. 지난 날짜는 지정할 수 없으며, yyyy-MM-dd 형식으로 입력해야 합니다.", type = "string", example = "2022-12-31")
|
||||
LocalDate date,
|
||||
@NotNull(message = "예약 요청의 timeId는 null일 수 없습니다.")
|
||||
@Schema(description = "예약 시간 ID", example = "1")
|
||||
Long timeId,
|
||||
@NotNull(message = "예약 요청의 themeId는 null일 수 없습니다.")
|
||||
@Schema(description = "테마 ID", example = "1")
|
||||
Long themeId
|
||||
) {
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
package roomescape.reservation.dto.response;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import roomescape.reservation.domain.ReservationStatus;
|
||||
|
||||
@Schema(name = "회원의 예약 및 대기 응답", description = "회원의 예약 및 대기 정보 응답시 사용됩니다.")
|
||||
public record MyReservationResponse(
|
||||
@Schema(description = "예약 번호. 예약을 식별할 때 사용합니다.")
|
||||
Long id,
|
||||
@Schema(description = "테마 이름")
|
||||
String themeName,
|
||||
@Schema(description = "예약 날짜", type = "string", example = "2022-12-31")
|
||||
LocalDate date,
|
||||
@Schema(description = "예약 시간", type = "string", example = "09:00")
|
||||
LocalTime time,
|
||||
@Schema(description = "예약 상태", type = "string")
|
||||
ReservationStatus status,
|
||||
@Schema(description = "예약 대기 상태일 때의 대기 순번. 확정된 예약은 0의 값을 가집니다.")
|
||||
Long rank,
|
||||
@Schema(description = "결제 키. 결제가 완료된 예약에만 값이 존재합니다.")
|
||||
String paymentKey,
|
||||
@Schema(description = "결제 금액. 결제가 완료된 예약에만 값이 존재합니다.")
|
||||
Long amount
|
||||
) {
|
||||
|
||||
public MyReservationResponse(Long id, String themeName, LocalDate date, LocalTime time, ReservationStatus status,
|
||||
Integer rank, String paymentKey, Long amount) {
|
||||
this(id, themeName, date, time, status, rank.longValue(), paymentKey, amount);
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
package roomescape.reservation.dto.response;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(name = "회원의 예약 및 대기 목록 조회 응답", description = "회원의 예약 및 대기 목록 조회 응답시 사용됩니다.")
|
||||
public record MyReservationsResponse(
|
||||
@Schema(description = "현재 로그인한 회원의 예약 및 대기 목록") List<MyReservationResponse> myReservationResponses
|
||||
) {
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
package roomescape.reservation.dto.response;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import roomescape.member.web.MemberResponse;
|
||||
import roomescape.reservation.domain.Reservation;
|
||||
import roomescape.reservation.domain.ReservationStatus;
|
||||
import roomescape.theme.web.ThemeResponse;
|
||||
|
||||
@Schema(name = "예약 정보", description = "예약 저장 및 조회 응답에 사용됩니다.")
|
||||
public record ReservationResponse(
|
||||
@Schema(description = "예약 번호. 예약을 식별할 때 사용합니다.")
|
||||
Long id,
|
||||
@Schema(description = "예약 날짜", type = "string", example = "2022-12-31")
|
||||
LocalDate date,
|
||||
@JsonProperty("member")
|
||||
@Schema(description = "예약한 회원 정보")
|
||||
MemberResponse member,
|
||||
@JsonProperty("time")
|
||||
@Schema(description = "예약 시간 정보")
|
||||
ReservationTimeResponse time,
|
||||
@JsonProperty("theme")
|
||||
@Schema(description = "예약한 테마 정보")
|
||||
ThemeResponse theme,
|
||||
@Schema(description = "예약 상태", type = "string")
|
||||
ReservationStatus status
|
||||
) {
|
||||
|
||||
public static ReservationResponse from(Reservation reservation) {
|
||||
return new ReservationResponse(
|
||||
reservation.getId(),
|
||||
reservation.getDate(),
|
||||
MemberResponse.fromEntity(reservation.getMember()),
|
||||
ReservationTimeResponse.from(reservation.getReservationTime()),
|
||||
ThemeResponse.from(reservation.getTheme()),
|
||||
reservation.getReservationStatus()
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
package roomescape.reservation.dto.response;
|
||||
|
||||
import java.time.LocalTime;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(name = "특정 테마, 날짜에 대한 시간 정보 응답", description = "특정 날짜와 테마에 대해, 예약 가능 여부를 포함한 시간 정보를 저장합니다.")
|
||||
public record ReservationTimeInfoResponse(
|
||||
@Schema(description = "예약 시간 번호. 예약 시간을 식별할 때 사용합니다.")
|
||||
Long timeId,
|
||||
@Schema(description = "예약 시간", type = "string", example = "09:00")
|
||||
LocalTime startAt,
|
||||
@Schema(description = "이미 예약이 완료된 시간인지 여부")
|
||||
boolean alreadyBooked
|
||||
) {
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
package roomescape.reservation.dto.response;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(name = "예약 시간 정보 목록 응답", description = "특정 테마, 날짜에 대한 모든 예약 가능 시간 정보를 저장합니다.")
|
||||
public record ReservationTimeInfosResponse(
|
||||
@Schema(description = "특정 테마, 날짜에 대한 예약 가능 여부를 포함한 시간 목록") List<ReservationTimeInfoResponse> reservationTimes
|
||||
) {
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
package roomescape.reservation.dto.response;
|
||||
|
||||
import java.time.LocalTime;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import roomescape.reservation.domain.ReservationTime;
|
||||
|
||||
@Schema(name = "예약 시간 정보", description = "예약 시간 추가 및 조회 응답시 사용됩니다.")
|
||||
public record ReservationTimeResponse(
|
||||
@Schema(description = "예약 시간 번호. 예약 시간을 식별할 때 사용합니다.")
|
||||
Long id,
|
||||
@Schema(description = "예약 시간", type = "string", example = "09:00")
|
||||
LocalTime startAt
|
||||
) {
|
||||
|
||||
public static ReservationTimeResponse from(ReservationTime reservationTime) {
|
||||
return new ReservationTimeResponse(reservationTime.getId(), reservationTime.getStartAt());
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
package roomescape.reservation.dto.response;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(name = "예약 시간 정보 목록 응답", description = "모든 예약 시간 조회 응답시 사용됩니다.")
|
||||
public record ReservationTimesResponse(
|
||||
@Schema(description = "모든 시간 목록") List<ReservationTimeResponse> times
|
||||
) {
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
package roomescape.reservation.dto.response;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(name = "예약 목록 조회 응답", description = "모든 예약 정보 조회 응답시 사용됩니다.")
|
||||
public record ReservationsResponse(
|
||||
@Schema(description = "모든 예약 및 대기 목록") List<ReservationResponse> reservations
|
||||
) {
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
package roomescape.reservation.infrastructure.persistence
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import jakarta.persistence.*
|
||||
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||
import java.time.LocalDate
|
||||
|
||||
@Entity
|
||||
@Table(name = "reservation")
|
||||
class ReservationEntity(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long? = null,
|
||||
|
||||
var date: LocalDate,
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "time_id", nullable = false)
|
||||
var reservationTime: ReservationTimeEntity,
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "theme_id", nullable = false)
|
||||
var theme: ThemeEntity,
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "member_id", nullable = false)
|
||||
var member: MemberEntity,
|
||||
|
||||
@Enumerated(value = EnumType.STRING)
|
||||
var reservationStatus: ReservationStatus
|
||||
) {
|
||||
@JsonIgnore
|
||||
fun isWaiting(): Boolean = reservationStatus == ReservationStatus.WAITING
|
||||
|
||||
@JsonIgnore
|
||||
fun isSameMember(memberId: Long): Boolean {
|
||||
return this.member.id == memberId
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(description = "예약 상태를 나타냅니다.", allowableValues = ["CONFIRMED", "CONFIRMED_PAYMENT_REQUIRED", "WAITING"])
|
||||
enum class ReservationStatus {
|
||||
@Schema(description = "결제가 완료된 예약")
|
||||
CONFIRMED,
|
||||
|
||||
@Schema(description = "결제가 필요한 예약")
|
||||
CONFIRMED_PAYMENT_REQUIRED,
|
||||
|
||||
@Schema(description = "대기 중인 예약")
|
||||
WAITING
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
package roomescape.reservation.infrastructure.persistence
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor
|
||||
import org.springframework.data.jpa.repository.Modifying
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
import org.springframework.data.repository.query.Param
|
||||
import roomescape.reservation.web.MyReservationResponse
|
||||
import java.time.LocalDate
|
||||
|
||||
interface ReservationRepository
|
||||
: JpaRepository<ReservationEntity, Long>, JpaSpecificationExecutor<ReservationEntity> {
|
||||
fun findByReservationTime(reservationTime: ReservationTimeEntity): List<ReservationEntity>
|
||||
|
||||
fun findByDateAndThemeId(date: LocalDate, themeId: Long): List<ReservationEntity>
|
||||
|
||||
@Modifying
|
||||
@Query("""
|
||||
UPDATE ReservationEntity r
|
||||
SET r.reservationStatus = :status
|
||||
WHERE r.id = :id
|
||||
""")
|
||||
fun updateStatusByReservationId(
|
||||
@Param(value = "id") reservationId: Long,
|
||||
@Param(value = "status") statusForChange: ReservationStatus
|
||||
): Int
|
||||
|
||||
@Query("""
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM ReservationEntity r2
|
||||
WHERE r2.id = :id
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM ReservationEntity r
|
||||
WHERE r.theme.id = r2.theme.id
|
||||
AND r.reservationTime.id = r2.reservationTime.id
|
||||
AND r.date = r2.date
|
||||
AND r.reservationStatus != 'WAITING'
|
||||
)
|
||||
)
|
||||
""")
|
||||
fun isExistConfirmedReservation(@Param("id") reservationId: Long): Boolean
|
||||
|
||||
@Query("""
|
||||
SELECT new roomescape.reservation.web.MyReservationResponse(
|
||||
r.id,
|
||||
t.name,
|
||||
r.date,
|
||||
r.reservationTime.startAt,
|
||||
r.reservationStatus,
|
||||
(SELECT COUNT (r2) * 1L FROM ReservationEntity r2 WHERE r2.theme = r.theme AND r2.date = r.date AND r2.reservationTime = r.reservationTime AND r2.id < r.id),
|
||||
p.paymentKey,
|
||||
p.totalAmount
|
||||
)
|
||||
FROM ReservationEntity r
|
||||
JOIN r.theme t
|
||||
LEFT JOIN PaymentEntity p
|
||||
ON p.reservation = r
|
||||
WHERE r.member.id = :memberId
|
||||
""")
|
||||
fun findMyReservations(memberId: Long): List<MyReservationResponse>
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
package roomescape.reservation.infrastructure.persistence
|
||||
|
||||
import org.springframework.data.jpa.domain.Specification
|
||||
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||
import java.time.LocalDate
|
||||
|
||||
class ReservationSearchSpecification(
|
||||
private var spec: Specification<ReservationEntity> = Specification { _, _, _ -> null }
|
||||
) {
|
||||
fun sameThemeId(themeId: Long?): ReservationSearchSpecification = andIfNotNull(themeId?.let {
|
||||
Specification { root, _, cb ->
|
||||
cb.equal(root.get<ThemeEntity>("theme").get<Long>("id"), themeId)
|
||||
}
|
||||
})
|
||||
|
||||
fun sameMemberId(memberId: Long?): ReservationSearchSpecification = andIfNotNull(memberId?.let {
|
||||
Specification { root, _, cb ->
|
||||
cb.equal(root.get<MemberEntity>("member").get<Long>("id"), memberId)
|
||||
}
|
||||
})
|
||||
|
||||
fun sameTimeId(timeId: Long?): ReservationSearchSpecification = andIfNotNull(timeId?.let {
|
||||
Specification { root, _, cb ->
|
||||
cb.equal(root.get<ReservationTimeEntity>("reservationTime").get<Long>("id"), timeId)
|
||||
}
|
||||
})
|
||||
|
||||
fun sameDate(date: LocalDate?): ReservationSearchSpecification = andIfNotNull(date?.let {
|
||||
Specification { root, _, cb ->
|
||||
cb.equal(root.get<LocalDate>("date"), date)
|
||||
}
|
||||
})
|
||||
|
||||
fun confirmed(): ReservationSearchSpecification = andIfNotNull { root, _, cb ->
|
||||
cb.or(
|
||||
cb.equal(
|
||||
root.get<ReservationStatus>("reservationStatus"),
|
||||
ReservationStatus.CONFIRMED
|
||||
),
|
||||
cb.equal(
|
||||
root.get<ReservationStatus>("reservationStatus"),
|
||||
ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun waiting(): ReservationSearchSpecification = andIfNotNull { root, _, cb ->
|
||||
cb.equal(
|
||||
root.get<ReservationStatus>("reservationStatus"),
|
||||
ReservationStatus.WAITING
|
||||
)
|
||||
}
|
||||
|
||||
fun dateStartFrom(dateFrom: LocalDate?): ReservationSearchSpecification = andIfNotNull(dateFrom?.let {
|
||||
Specification { root, _, cb ->
|
||||
cb.greaterThanOrEqualTo(root.get("date"), dateFrom)
|
||||
}
|
||||
})
|
||||
|
||||
fun dateEndAt(dateTo: LocalDate?): ReservationSearchSpecification = andIfNotNull(dateTo?.let {
|
||||
Specification { root, _, cb ->
|
||||
cb.lessThanOrEqualTo(root.get("date"), dateTo)
|
||||
}
|
||||
})
|
||||
|
||||
fun build(): Specification<ReservationEntity> {
|
||||
return this.spec
|
||||
}
|
||||
|
||||
private fun andIfNotNull(condition: Specification<ReservationEntity>?): ReservationSearchSpecification {
|
||||
condition?.let { this.spec = this.spec.and(condition) }
|
||||
return this
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package roomescape.reservation.infrastructure.persistence
|
||||
|
||||
import jakarta.persistence.*
|
||||
import java.time.LocalTime
|
||||
|
||||
@Entity
|
||||
@Table(name = "reservation_time")
|
||||
class ReservationTimeEntity(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long? = null,
|
||||
var startAt: LocalTime
|
||||
)
|
||||
@ -0,0 +1,8 @@
|
||||
package roomescape.reservation.infrastructure.persistence
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import java.time.LocalTime
|
||||
|
||||
interface ReservationTimeRepository : JpaRepository<ReservationTimeEntity, Long> {
|
||||
fun existsByStartAt(startAt: LocalTime): Boolean
|
||||
}
|
||||
@ -1,227 +0,0 @@
|
||||
package roomescape.reservation.service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import roomescape.common.exception.ErrorType;
|
||||
import roomescape.common.exception.RoomescapeException;
|
||||
import roomescape.member.business.MemberService;
|
||||
import roomescape.member.infrastructure.persistence.Member;
|
||||
import roomescape.reservation.domain.Reservation;
|
||||
import roomescape.reservation.domain.ReservationStatus;
|
||||
import roomescape.reservation.domain.ReservationTime;
|
||||
import roomescape.reservation.domain.repository.ReservationRepository;
|
||||
import roomescape.reservation.domain.repository.ReservationSearchSpecification;
|
||||
import roomescape.reservation.dto.request.AdminReservationRequest;
|
||||
import roomescape.reservation.dto.request.ReservationRequest;
|
||||
import roomescape.reservation.dto.request.WaitingRequest;
|
||||
import roomescape.reservation.dto.response.MyReservationsResponse;
|
||||
import roomescape.reservation.dto.response.ReservationResponse;
|
||||
import roomescape.reservation.dto.response.ReservationsResponse;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity;
|
||||
import roomescape.theme.business.ThemeService;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
public class ReservationService {
|
||||
|
||||
private final ReservationRepository reservationRepository;
|
||||
private final ReservationTimeService reservationTimeService;
|
||||
private final MemberService memberService;
|
||||
private final ThemeService themeService;
|
||||
|
||||
public ReservationService(
|
||||
ReservationRepository reservationRepository,
|
||||
ReservationTimeService reservationTimeService,
|
||||
MemberService memberService,
|
||||
ThemeService themeService
|
||||
) {
|
||||
this.reservationRepository = reservationRepository;
|
||||
this.reservationTimeService = reservationTimeService;
|
||||
this.memberService = memberService;
|
||||
this.themeService = themeService;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ReservationsResponse findAllReservations() {
|
||||
Specification<Reservation> spec = new ReservationSearchSpecification().confirmed().build();
|
||||
List<ReservationResponse> response = findAllReservationByStatus(spec);
|
||||
|
||||
return new ReservationsResponse(response);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ReservationsResponse findAllWaiting() {
|
||||
Specification<Reservation> spec = new ReservationSearchSpecification().waiting().build();
|
||||
List<ReservationResponse> response = findAllReservationByStatus(spec);
|
||||
|
||||
return new ReservationsResponse(response);
|
||||
}
|
||||
|
||||
private List<ReservationResponse> findAllReservationByStatus(Specification<Reservation> spec) {
|
||||
return reservationRepository.findAll(spec)
|
||||
.stream()
|
||||
.map(ReservationResponse::from)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public void removeReservationById(Long reservationId, Long memberId) {
|
||||
validateIsMemberAdmin(memberId);
|
||||
reservationRepository.deleteById(reservationId);
|
||||
}
|
||||
|
||||
public Reservation addReservation(ReservationRequest request, Long memberId) {
|
||||
validateIsReservationExist(request.themeId(), request.timeId(), request.date());
|
||||
Reservation reservation = getReservationForSave(request.timeId(), request.themeId(), request.date(), memberId,
|
||||
ReservationStatus.CONFIRMED);
|
||||
return reservationRepository.save(reservation);
|
||||
}
|
||||
|
||||
public ReservationResponse addReservationByAdmin(AdminReservationRequest request) {
|
||||
validateIsReservationExist(request.themeId(), request.timeId(), request.date());
|
||||
return addReservationWithoutPayment(request.themeId(), request.timeId(), request.date(),
|
||||
request.memberId(), ReservationStatus.CONFIRMED_PAYMENT_REQUIRED);
|
||||
}
|
||||
|
||||
public ReservationResponse addWaiting(WaitingRequest request, Long memberId) {
|
||||
validateMemberAlreadyReserve(request.themeId(), request.timeId(), request.date(), memberId);
|
||||
return addReservationWithoutPayment(request.themeId(), request.timeId(), request.date(), memberId,
|
||||
ReservationStatus.WAITING);
|
||||
}
|
||||
|
||||
private ReservationResponse addReservationWithoutPayment(Long themeId, Long timeId, LocalDate date, Long memberId,
|
||||
ReservationStatus status) {
|
||||
Reservation reservation = getReservationForSave(timeId, themeId, date, memberId, status);
|
||||
Reservation saved = reservationRepository.save(reservation);
|
||||
return ReservationResponse.from(saved);
|
||||
}
|
||||
|
||||
private void validateMemberAlreadyReserve(Long themeId, Long timeId, LocalDate date, Long memberId) {
|
||||
Specification<Reservation> spec = new ReservationSearchSpecification()
|
||||
.sameMemberId(memberId)
|
||||
.sameThemeId(themeId)
|
||||
.sameTimeId(timeId)
|
||||
.sameDate(date)
|
||||
.build();
|
||||
|
||||
if (reservationRepository.exists(spec)) {
|
||||
throw new RoomescapeException(ErrorType.HAS_RESERVATION_OR_WAITING, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateIsReservationExist(Long themeId, Long timeId, LocalDate date) {
|
||||
Specification<Reservation> spec = new ReservationSearchSpecification()
|
||||
.confirmed()
|
||||
.sameThemeId(themeId)
|
||||
.sameTimeId(timeId)
|
||||
.sameDate(date)
|
||||
.build();
|
||||
|
||||
if (reservationRepository.exists(spec)) {
|
||||
throw new RoomescapeException(ErrorType.RESERVATION_DUPLICATED, HttpStatus.CONFLICT);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateDateAndTime(
|
||||
LocalDate requestDate,
|
||||
ReservationTime requestReservationTime
|
||||
) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime request = LocalDateTime.of(requestDate, requestReservationTime.getStartAt());
|
||||
if (request.isBefore(now)) {
|
||||
throw new RoomescapeException(ErrorType.RESERVATION_PERIOD_IN_PAST,
|
||||
String.format("[now: %s %s | request: %s %s]",
|
||||
now.toLocalDate(), now.toLocalTime(), requestDate, requestReservationTime.getStartAt()),
|
||||
HttpStatus.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private Reservation getReservationForSave(Long timeId, Long themeId, LocalDate date, Long memberId,
|
||||
ReservationStatus status) {
|
||||
ReservationTime time = reservationTimeService.findTimeById(timeId);
|
||||
ThemeEntity theme = themeService.findThemeById(themeId);
|
||||
Member member = memberService.findById(memberId);
|
||||
|
||||
validateDateAndTime(date, time);
|
||||
return new Reservation(date, time, theme, member, status);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ReservationsResponse findFilteredReservations(Long themeId, Long memberId, LocalDate dateFrom,
|
||||
LocalDate dateTo) {
|
||||
validateDateForSearch(dateFrom, dateTo);
|
||||
Specification<Reservation> spec = new ReservationSearchSpecification()
|
||||
.confirmed()
|
||||
.sameThemeId(themeId)
|
||||
.sameMemberId(memberId)
|
||||
.dateStartFrom(dateFrom)
|
||||
.dateEndAt(dateTo)
|
||||
.build();
|
||||
|
||||
List<ReservationResponse> response = reservationRepository.findAll(spec)
|
||||
.stream()
|
||||
.map(ReservationResponse::from)
|
||||
.toList();
|
||||
|
||||
return new ReservationsResponse(response);
|
||||
}
|
||||
|
||||
private void validateDateForSearch(LocalDate startFrom, LocalDate endAt) {
|
||||
if (startFrom == null || endAt == null) {
|
||||
return;
|
||||
}
|
||||
if (startFrom.isAfter(endAt)) {
|
||||
throw new RoomescapeException(ErrorType.INVALID_DATE_RANGE,
|
||||
String.format("[startFrom: %s, endAt: %s", startFrom, endAt), HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public MyReservationsResponse findMemberReservations(Long memberId) {
|
||||
return new MyReservationsResponse(reservationRepository.findMyReservations(memberId));
|
||||
}
|
||||
|
||||
public void approveWaiting(Long reservationId, Long memberId) {
|
||||
validateIsMemberAdmin(memberId);
|
||||
if (reservationRepository.isExistConfirmedReservation(reservationId)) {
|
||||
throw new RoomescapeException(ErrorType.RESERVATION_DUPLICATED, HttpStatus.CONFLICT);
|
||||
}
|
||||
reservationRepository.updateStatusByReservationId(reservationId, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED);
|
||||
}
|
||||
|
||||
public void cancelWaiting(Long reservationId, Long memberId) {
|
||||
Reservation waiting = reservationRepository.findById(reservationId)
|
||||
.filter(Reservation::isWaiting)
|
||||
.filter(r -> r.isSameMember(memberId))
|
||||
.orElseThrow(() -> throwReservationNotFound(reservationId));
|
||||
reservationRepository.delete(waiting);
|
||||
}
|
||||
|
||||
public void denyWaiting(Long reservationId, Long memberId) {
|
||||
validateIsMemberAdmin(memberId);
|
||||
Reservation waiting = reservationRepository.findById(reservationId)
|
||||
.filter(Reservation::isWaiting)
|
||||
.orElseThrow(() -> throwReservationNotFound(reservationId));
|
||||
reservationRepository.delete(waiting);
|
||||
}
|
||||
|
||||
private void validateIsMemberAdmin(Long memberId) {
|
||||
Member member = memberService.findById(memberId);
|
||||
if (member.isAdmin()) {
|
||||
return;
|
||||
}
|
||||
throw new RoomescapeException(ErrorType.PERMISSION_DOES_NOT_EXIST, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
private RoomescapeException throwReservationNotFound(Long reservationId) {
|
||||
return new RoomescapeException(ErrorType.RESERVATION_NOT_FOUND,
|
||||
String.format("[reservationId: %d]", reservationId), HttpStatus.NOT_FOUND);
|
||||
}
|
||||
}
|
||||
@ -1,100 +0,0 @@
|
||||
package roomescape.reservation.service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import roomescape.common.exception.ErrorType;
|
||||
import roomescape.common.exception.RoomescapeException;
|
||||
import roomescape.reservation.domain.Reservation;
|
||||
import roomescape.reservation.domain.ReservationTime;
|
||||
import roomescape.reservation.domain.repository.ReservationRepository;
|
||||
import roomescape.reservation.domain.repository.ReservationTimeRepository;
|
||||
import roomescape.reservation.dto.request.ReservationTimeRequest;
|
||||
import roomescape.reservation.dto.response.ReservationTimeInfoResponse;
|
||||
import roomescape.reservation.dto.response.ReservationTimeInfosResponse;
|
||||
import roomescape.reservation.dto.response.ReservationTimeResponse;
|
||||
import roomescape.reservation.dto.response.ReservationTimesResponse;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
public class ReservationTimeService {
|
||||
|
||||
private final ReservationTimeRepository reservationTimeRepository;
|
||||
private final ReservationRepository reservationRepository;
|
||||
|
||||
public ReservationTimeService(
|
||||
ReservationTimeRepository reservationTimeRepository,
|
||||
ReservationRepository reservationRepository
|
||||
) {
|
||||
this.reservationTimeRepository = reservationTimeRepository;
|
||||
this.reservationRepository = reservationRepository;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ReservationTime findTimeById(Long id) {
|
||||
return reservationTimeRepository.findById(id)
|
||||
.orElseThrow(() -> new RoomescapeException(ErrorType.RESERVATION_TIME_NOT_FOUND,
|
||||
String.format("[reservationTimeId: %d]", id), HttpStatus.BAD_REQUEST));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ReservationTimesResponse findAllTimes() {
|
||||
List<ReservationTimeResponse> response = reservationTimeRepository.findAll()
|
||||
.stream()
|
||||
.map(ReservationTimeResponse::from)
|
||||
.toList();
|
||||
|
||||
return new ReservationTimesResponse(response);
|
||||
}
|
||||
|
||||
public ReservationTimeResponse addTime(ReservationTimeRequest reservationTimeRequest) {
|
||||
validateTimeDuplication(reservationTimeRequest);
|
||||
ReservationTime reservationTime = reservationTimeRepository.save(reservationTimeRequest.toTime());
|
||||
|
||||
return ReservationTimeResponse.from(reservationTime);
|
||||
}
|
||||
|
||||
private void validateTimeDuplication(ReservationTimeRequest reservationTimeRequest) {
|
||||
List<ReservationTime> duplicateReservationTimes = reservationTimeRepository.findByStartAt(
|
||||
reservationTimeRequest.startAt());
|
||||
|
||||
if (!duplicateReservationTimes.isEmpty()) {
|
||||
throw new RoomescapeException(ErrorType.TIME_DUPLICATED,
|
||||
String.format("[startAt: %s]", reservationTimeRequest.startAt()), HttpStatus.CONFLICT);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeTimeById(Long id) {
|
||||
ReservationTime reservationTime = findTimeById(id);
|
||||
List<Reservation> usingTimeReservations = reservationRepository.findByReservationTime(reservationTime);
|
||||
|
||||
if (!usingTimeReservations.isEmpty()) {
|
||||
throw new RoomescapeException(ErrorType.TIME_IS_USED_CONFLICT, String.format("[timeId: %d]", id),
|
||||
HttpStatus.CONFLICT);
|
||||
}
|
||||
|
||||
reservationTimeRepository.deleteById(id);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ReservationTimeInfosResponse findAllAvailableTimesByDateAndTheme(LocalDate date, Long themeId) {
|
||||
List<ReservationTime> allTimes = reservationTimeRepository.findAll();
|
||||
List<Reservation> reservations = reservationRepository.findByThemeId(themeId);
|
||||
|
||||
List<ReservationTimeInfoResponse> response = allTimes.stream()
|
||||
.map(time -> new ReservationTimeInfoResponse(time.getId(), time.getStartAt(),
|
||||
isReservationBooked(reservations, date, time)))
|
||||
.toList();
|
||||
|
||||
return new ReservationTimeInfosResponse(response);
|
||||
}
|
||||
|
||||
private boolean isReservationBooked(List<Reservation> reservations, LocalDate date, ReservationTime time) {
|
||||
return reservations.stream()
|
||||
.anyMatch(reservation -> reservation.isSameDateAndTime(date, time));
|
||||
}
|
||||
}
|
||||
@ -1,56 +0,0 @@
|
||||
package roomescape.reservation.service;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import roomescape.payment.business.PaymentService;
|
||||
import roomescape.payment.web.PaymentApprove;
|
||||
import roomescape.payment.web.PaymentCancel;
|
||||
import roomescape.payment.web.ReservationPaymentResponse;
|
||||
import roomescape.reservation.domain.Reservation;
|
||||
import roomescape.reservation.dto.request.ReservationRequest;
|
||||
import roomescape.reservation.dto.response.ReservationResponse;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
public class ReservationWithPaymentService {
|
||||
|
||||
private final ReservationService reservationService;
|
||||
private final PaymentService paymentService;
|
||||
|
||||
public ReservationWithPaymentService(ReservationService reservationService,
|
||||
PaymentService paymentService) {
|
||||
this.reservationService = reservationService;
|
||||
this.paymentService = paymentService;
|
||||
}
|
||||
|
||||
public ReservationResponse addReservationWithPayment(ReservationRequest request,
|
||||
PaymentApprove.Response paymentInfo,
|
||||
Long memberId) {
|
||||
Reservation reservation = reservationService.addReservation(request, memberId);
|
||||
ReservationPaymentResponse reservationPaymentResponse = paymentService.savePayment(paymentInfo, reservation);
|
||||
|
||||
return reservationPaymentResponse.reservation();
|
||||
}
|
||||
|
||||
public void saveCanceledPayment(PaymentCancel.Response cancelInfo, OffsetDateTime approvedAt, String paymentKey) {
|
||||
paymentService.saveCanceledPayment(cancelInfo, approvedAt, paymentKey);
|
||||
}
|
||||
|
||||
public PaymentCancel.Request removeReservationWithPayment(Long reservationId, Long memberId) {
|
||||
PaymentCancel.Request paymentCancelRequest = paymentService.cancelPaymentByAdmin(reservationId);
|
||||
reservationService.removeReservationById(reservationId, memberId);
|
||||
return paymentCancelRequest;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public boolean isNotPaidReservation(Long reservationId) {
|
||||
return !paymentService.isReservationPaid(reservationId);
|
||||
}
|
||||
|
||||
public void updateCanceledTime(String paymentKey, OffsetDateTime canceledAt) {
|
||||
paymentService.updateCanceledTime(paymentKey, canceledAt);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,159 @@
|
||||
package roomescape.reservation.web
|
||||
|
||||
import io.swagger.v3.oas.annotations.Parameter
|
||||
import jakarta.validation.Valid
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import roomescape.auth.web.support.MemberId
|
||||
import roomescape.common.dto.response.CommonApiResponse
|
||||
import roomescape.common.exception.RoomescapeException
|
||||
import roomescape.payment.infrastructure.client.TossPaymentClient
|
||||
import roomescape.payment.web.PaymentApprove
|
||||
import roomescape.payment.web.PaymentCancel
|
||||
import roomescape.reservation.business.ReservationService
|
||||
import roomescape.reservation.business.ReservationWithPaymentService
|
||||
import roomescape.reservation.docs.ReservationAPI
|
||||
import java.net.URI
|
||||
import java.time.LocalDate
|
||||
|
||||
@RestController
|
||||
class ReservationController(
|
||||
private val reservationWithPaymentService: ReservationWithPaymentService,
|
||||
private val reservationService: ReservationService,
|
||||
private val paymentClient: TossPaymentClient
|
||||
) : ReservationAPI {
|
||||
@GetMapping("/reservations")
|
||||
override fun getAllReservations(): ResponseEntity<CommonApiResponse<ReservationsResponse>> {
|
||||
val response: ReservationsResponse = reservationService.findAllReservations()
|
||||
|
||||
return ResponseEntity.ok(CommonApiResponse(response))
|
||||
}
|
||||
|
||||
@GetMapping("/reservations-mine")
|
||||
override fun getMemberReservations(
|
||||
@MemberId @Parameter(hidden = true) memberId: Long
|
||||
): ResponseEntity<CommonApiResponse<MyReservationsResponse>> {
|
||||
val response: MyReservationsResponse = reservationService.findMemberReservations(memberId)
|
||||
|
||||
return ResponseEntity.ok(CommonApiResponse(response))
|
||||
}
|
||||
|
||||
@GetMapping("/reservations/search")
|
||||
override fun getReservationBySearching(
|
||||
@RequestParam(required = false) themeId: Long?,
|
||||
@RequestParam(required = false) memberId: Long?,
|
||||
@RequestParam(required = false) dateFrom: LocalDate?,
|
||||
@RequestParam(required = false) dateTo: LocalDate?
|
||||
): ResponseEntity<CommonApiResponse<ReservationsResponse>> {
|
||||
val response: ReservationsResponse = reservationService.findFilteredReservations(themeId, memberId, dateFrom, dateTo)
|
||||
|
||||
return ResponseEntity.ok(CommonApiResponse(response))
|
||||
}
|
||||
|
||||
@DeleteMapping("/reservations/{id}")
|
||||
override fun removeReservation(
|
||||
@MemberId @Parameter(hidden = true) memberId: Long,
|
||||
@PathVariable("id") reservationId: Long
|
||||
): ResponseEntity<CommonApiResponse<Unit>> {
|
||||
if (reservationWithPaymentService.isNotPaidReservation(reservationId)) {
|
||||
reservationService.removeReservationById(reservationId, memberId)
|
||||
return ResponseEntity.noContent().build()
|
||||
}
|
||||
|
||||
val paymentCancelRequest = reservationWithPaymentService.removeReservationWithPayment(
|
||||
reservationId, memberId)
|
||||
val paymentCancelResponse = paymentClient.cancelPayment(paymentCancelRequest)
|
||||
reservationWithPaymentService.updateCanceledTime(paymentCancelRequest.paymentKey,
|
||||
paymentCancelResponse.canceledAt)
|
||||
|
||||
return ResponseEntity.noContent().build()
|
||||
}
|
||||
|
||||
@PostMapping("/reservations")
|
||||
override fun saveReservation(
|
||||
@Valid @RequestBody reservationRequest: ReservationRequest,
|
||||
@MemberId @Parameter(hidden = true) memberId: Long
|
||||
): ResponseEntity<CommonApiResponse<ReservationResponse>> {
|
||||
val paymentRequest: PaymentApprove.Request = reservationRequest.paymentRequest
|
||||
val paymentResponse: PaymentApprove.Response = paymentClient.confirmPayment(paymentRequest)
|
||||
|
||||
try {
|
||||
val reservationResponse: ReservationResponse = reservationWithPaymentService.addReservationWithPayment(
|
||||
reservationRequest,
|
||||
paymentResponse,
|
||||
memberId
|
||||
)
|
||||
return ResponseEntity.created(URI.create("/reservations/${reservationResponse.id}"))
|
||||
.body(CommonApiResponse(reservationResponse))
|
||||
} catch (e: RoomescapeException) {
|
||||
val cancelRequest = PaymentCancel.Request(paymentRequest.paymentKey,
|
||||
paymentRequest.amount, e.message!!)
|
||||
val paymentCancelResponse = paymentClient.cancelPayment(cancelRequest)
|
||||
reservationWithPaymentService.saveCanceledPayment(paymentCancelResponse, paymentResponse.approvedAt,
|
||||
paymentRequest.paymentKey)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/reservations/admin")
|
||||
override fun saveReservationByAdmin(
|
||||
@Valid @RequestBody adminReservationRequest: AdminReservationRequest
|
||||
): ResponseEntity<CommonApiResponse<ReservationResponse>> {
|
||||
val response: ReservationResponse =
|
||||
reservationService.addReservationByAdmin(adminReservationRequest)
|
||||
|
||||
return ResponseEntity.created(URI.create("/reservations/${response.id}"))
|
||||
.body(CommonApiResponse(response))
|
||||
}
|
||||
|
||||
@GetMapping("/reservations/waiting")
|
||||
override fun getAllWaiting(): ResponseEntity<CommonApiResponse<ReservationsResponse>> {
|
||||
val response: ReservationsResponse = reservationService.findAllWaiting()
|
||||
|
||||
return ResponseEntity.ok(CommonApiResponse(response))
|
||||
}
|
||||
|
||||
@PostMapping("/reservations/waiting")
|
||||
override fun saveWaiting(
|
||||
@Valid @RequestBody waitingRequest: WaitingRequest,
|
||||
@MemberId @Parameter(hidden = true) memberId: Long,
|
||||
): ResponseEntity<CommonApiResponse<ReservationResponse>> {
|
||||
val response: ReservationResponse = reservationService.addWaiting(
|
||||
waitingRequest,
|
||||
memberId
|
||||
)
|
||||
|
||||
return ResponseEntity.created(URI.create("/reservations/${response.id}"))
|
||||
.body(CommonApiResponse(response))
|
||||
}
|
||||
|
||||
@DeleteMapping("/reservations/waiting/{id}")
|
||||
override fun deleteWaiting(
|
||||
@MemberId @Parameter(hidden = true) memberId: Long,
|
||||
@PathVariable("id") reservationId: Long
|
||||
): ResponseEntity<CommonApiResponse<Unit>> {
|
||||
reservationService.cancelWaiting(reservationId, memberId)
|
||||
|
||||
return ResponseEntity.noContent().build()
|
||||
}
|
||||
|
||||
@PostMapping("/reservations/waiting/{id}/approve")
|
||||
override fun approveWaiting(
|
||||
@MemberId @Parameter(hidden = true) memberId: Long,
|
||||
@PathVariable("id") reservationId: Long
|
||||
): ResponseEntity<CommonApiResponse<Unit>> {
|
||||
reservationService.approveWaiting(reservationId, memberId)
|
||||
|
||||
return ResponseEntity.ok().build()
|
||||
}
|
||||
|
||||
@PostMapping("/reservations/waiting/{id}/deny")
|
||||
override fun denyWaiting(
|
||||
@MemberId @Parameter(hidden = true) memberId: Long,
|
||||
@PathVariable("id") reservationId: Long
|
||||
): ResponseEntity<CommonApiResponse<Unit>> {
|
||||
reservationService.denyWaiting(reservationId, memberId)
|
||||
|
||||
return ResponseEntity.noContent().build()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
package roomescape.reservation.web
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import roomescape.payment.web.PaymentApprove
|
||||
import java.time.LocalDate
|
||||
|
||||
@Schema(name = "관리자 예약 저장 요청", description = "관리자의 예약 저장 요청시 사용됩니다.")
|
||||
@JvmRecord
|
||||
data class AdminReservationRequest(
|
||||
@JvmField @field:Schema(description = "예약 날짜. 지난 날짜는 지정할 수 없으며, yyyy-MM-dd 형식으로 입력해야 합니다.", type = "string", example = "2022-12-31")
|
||||
val date: LocalDate,
|
||||
@JvmField @field:Schema(description = "예약 시간 ID.", example = "1")
|
||||
val timeId: Long,
|
||||
@JvmField @field:Schema(description = "테마 ID", example = "1")
|
||||
val themeId: Long,
|
||||
@JvmField @field:Schema(description = "회원 ID", example = "1")
|
||||
val memberId: Long
|
||||
)
|
||||
|
||||
@Schema(name = "회원의 예약 저장 요청", description = "회원의 예약 요청시 사용됩니다.")
|
||||
@JvmRecord
|
||||
data class ReservationRequest(
|
||||
@JvmField
|
||||
@field:Schema(description = "예약 날짜. 지난 날짜는 지정할 수 없으며, yyyy-MM-dd 형식으로 입력해야 합니다.", type = "string", example = "2022-12-31")
|
||||
val date: LocalDate,
|
||||
|
||||
@JvmField
|
||||
@field:Schema(description = "예약 시간 ID.", example = "1")
|
||||
val timeId: Long,
|
||||
|
||||
@JvmField @field:Schema(description = "테마 ID", example = "1")
|
||||
val themeId: Long,
|
||||
|
||||
@field:Schema(description = "결제 위젯을 통해 받은 결제 키")
|
||||
val paymentKey: String,
|
||||
|
||||
@field:Schema(description = "결제 위젯을 통해 받은 주문번호.")
|
||||
val orderId: String,
|
||||
|
||||
@field:Schema(description = "결제 위젯을 통해 받은 결제 금액")
|
||||
val amount: Long,
|
||||
|
||||
@field:Schema(description = "결제 타입", example = "NORMAL")
|
||||
val paymentType: String
|
||||
) {
|
||||
@get:JsonIgnore
|
||||
val paymentRequest: PaymentApprove.Request
|
||||
get() = PaymentApprove.Request(paymentKey, orderId, amount, paymentType)
|
||||
}
|
||||
|
||||
@Schema(name = "예약 대기 저장 요청", description = "회원의 예약 대기 요청시 사용됩니다.")
|
||||
@JvmRecord
|
||||
data class WaitingRequest(
|
||||
@JvmField
|
||||
@field:Schema(description = "예약 날짜. 지난 날짜는 지정할 수 없으며, yyyy-MM-dd 형식으로 입력해야 합니다.", type = "string", example = "2022-12-31")
|
||||
val date: LocalDate,
|
||||
|
||||
@JvmField
|
||||
@field:Schema(description = "예약 시간 ID", example = "1")
|
||||
val timeId: Long,
|
||||
|
||||
@JvmField
|
||||
@field:Schema(description = "테마 ID", example = "1")
|
||||
val themeId: Long
|
||||
)
|
||||
103
src/main/java/roomescape/reservation/web/ReservationResponse.kt
Normal file
103
src/main/java/roomescape/reservation/web/ReservationResponse.kt
Normal file
@ -0,0 +1,103 @@
|
||||
package roomescape.reservation.web
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import roomescape.member.web.MemberResponse
|
||||
import roomescape.member.web.toResponse
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
||||
import roomescape.theme.web.ThemeResponse
|
||||
import roomescape.theme.web.toResponse
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
|
||||
@Schema(name = "회원의 예약 및 대기 응답", description = "회원의 예약 및 대기 정보 응답시 사용됩니다.")
|
||||
@JvmRecord
|
||||
data class MyReservationResponse(
|
||||
@field:Schema(description = "예약 번호. 예약을 식별할 때 사용합니다.")
|
||||
val id: Long,
|
||||
|
||||
@field:Schema(description = "테마 이름")
|
||||
val themeName: String,
|
||||
|
||||
@field:Schema(description = "예약 날짜", type = "string", example = "2022-12-31")
|
||||
val date: LocalDate,
|
||||
|
||||
@field:Schema(description = "예약 시간", type = "string", example = "09:00")
|
||||
val time: LocalTime,
|
||||
|
||||
@field:Schema(description = "예약 상태", type = "string")
|
||||
val status: ReservationStatus,
|
||||
|
||||
@field:Schema(description = "예약 대기 상태일 때의 대기 순번. 확정된 예약은 0의 값을 가집니다.")
|
||||
val rank: Long,
|
||||
|
||||
@field:Schema(description = "결제 키. 결제가 완료된 예약에만 값이 존재합니다.")
|
||||
val paymentKey: String?,
|
||||
|
||||
@field:Schema(description = "결제 금액. 결제가 완료된 예약에만 값이 존재합니다.")
|
||||
val amount: Long?
|
||||
)
|
||||
|
||||
@Schema(name = "회원의 예약 및 대기 목록 조회 응답", description = "회원의 예약 및 대기 목록 조회 응답시 사용됩니다.")
|
||||
@JvmRecord
|
||||
data class MyReservationsResponse(
|
||||
@field:Schema(description = "현재 로그인한 회원의 예약 및 대기 목록")
|
||||
val reservations: List<MyReservationResponse>
|
||||
)
|
||||
|
||||
@Schema(name = "예약 정보", description = "예약 저장 및 조회 응답에 사용됩니다.")
|
||||
@JvmRecord
|
||||
data class ReservationResponse(
|
||||
@JvmField
|
||||
@field:Schema(description = "예약 번호. 예약을 식별할 때 사용합니다.")
|
||||
val id: Long,
|
||||
|
||||
@field:Schema(description = "예약 날짜", type = "string", example = "2022-12-31")
|
||||
val date: LocalDate,
|
||||
|
||||
@field:Schema(description = "예약한 회원 정보")
|
||||
@field:JsonProperty("member")
|
||||
val member: MemberResponse,
|
||||
|
||||
@field:Schema(description = "예약 시간 정보")
|
||||
@field:JsonProperty("time")
|
||||
val time: ReservationTimeResponse,
|
||||
|
||||
@field:Schema(description = "예약한 테마 정보")
|
||||
@field:JsonProperty("theme")
|
||||
val theme: ThemeResponse,
|
||||
|
||||
@field:Schema(description = "예약 상태", type = "string")
|
||||
val status: ReservationStatus
|
||||
) {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun from(reservation: ReservationEntity): ReservationResponse {
|
||||
return ReservationResponse(
|
||||
reservation.id!!,
|
||||
reservation.date,
|
||||
reservation.member.toResponse(),
|
||||
reservation.reservationTime.toResponse(),
|
||||
reservation.theme.toResponse(),
|
||||
reservation.reservationStatus
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ReservationEntity.toResponse(): ReservationResponse = ReservationResponse(
|
||||
id = this.id!!,
|
||||
date = this.date,
|
||||
member = this.member.toResponse(),
|
||||
time = this.reservationTime.toResponse(),
|
||||
theme = this.theme.toResponse(),
|
||||
status = this.reservationStatus
|
||||
)
|
||||
|
||||
@Schema(name = "예약 목록 조회 응답", description = "모든 예약 정보 조회 응답시 사용됩니다.")
|
||||
@JvmRecord
|
||||
data class ReservationsResponse(
|
||||
@field:Schema(description = "모든 예약 및 대기 목록")
|
||||
val reservations: List<ReservationResponse>
|
||||
)
|
||||
@ -0,0 +1,51 @@
|
||||
package roomescape.reservation.web
|
||||
|
||||
import jakarta.validation.Valid
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import roomescape.common.dto.response.CommonApiResponse
|
||||
import roomescape.reservation.business.ReservationTimeService
|
||||
import roomescape.reservation.docs.ReservationTimeAPI
|
||||
import java.net.URI
|
||||
import java.time.LocalDate
|
||||
|
||||
@RestController
|
||||
class ReservationTimeController(
|
||||
private val reservationTimeService: ReservationTimeService
|
||||
) : ReservationTimeAPI {
|
||||
|
||||
@GetMapping("/times")
|
||||
override fun getAllTimes(): ResponseEntity<CommonApiResponse<ReservationTimesResponse>> {
|
||||
val response: ReservationTimesResponse = reservationTimeService.findAllTimes()
|
||||
|
||||
return ResponseEntity.ok(CommonApiResponse(response))
|
||||
}
|
||||
|
||||
@PostMapping("/times")
|
||||
override fun saveTime(
|
||||
@Valid @RequestBody reservationTimeRequest: ReservationTimeRequest,
|
||||
): ResponseEntity<CommonApiResponse<ReservationTimeResponse>> {
|
||||
val response: ReservationTimeResponse = reservationTimeService.addTime(reservationTimeRequest)
|
||||
|
||||
return ResponseEntity
|
||||
.created(URI.create("/times/${response.id}"))
|
||||
.body(CommonApiResponse(response))
|
||||
}
|
||||
|
||||
@DeleteMapping("/times/{id}")
|
||||
override fun removeTime(@PathVariable id: Long): ResponseEntity<CommonApiResponse<Unit>> {
|
||||
reservationTimeService.removeTimeById(id)
|
||||
|
||||
return ResponseEntity.noContent().build()
|
||||
}
|
||||
|
||||
@GetMapping("/times/filter")
|
||||
override fun findAllAvailableReservationTimes(
|
||||
@RequestParam date: LocalDate,
|
||||
@RequestParam themeId: Long
|
||||
): ResponseEntity<CommonApiResponse<ReservationTimeInfosResponse>> {
|
||||
val response: ReservationTimeInfosResponse = reservationTimeService.findAllAvailableTimesByDateAndTheme(date, themeId)
|
||||
|
||||
return ResponseEntity.ok(CommonApiResponse(response))
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
package roomescape.reservation.web
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationTimeEntity
|
||||
import java.time.LocalTime
|
||||
|
||||
@Schema(name = "예약 시간 저장 요청", description = "예약 시간 저장 요청시 사용됩니다.")
|
||||
@JvmRecord
|
||||
data class ReservationTimeRequest(
|
||||
@JvmField
|
||||
@field:Schema(description = "예약 시간. HH:mm 형식으로 입력해야 합니다.", type = "string", example = "09:00")
|
||||
val startAt: LocalTime
|
||||
)
|
||||
|
||||
@Schema(name = "예약 시간 정보", description = "예약 시간 추가 및 조회 응답시 사용됩니다.")
|
||||
@JvmRecord
|
||||
data class ReservationTimeResponse(
|
||||
@JvmField
|
||||
@field:Schema(description = "예약 시간 번호. 예약 시간을 식별할 때 사용합니다.")
|
||||
val id: Long,
|
||||
|
||||
@field:Schema(description = "예약 시간", type = "string", example = "09:00")
|
||||
val startAt: LocalTime
|
||||
) {
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun from(reservationTime: ReservationTimeEntity): ReservationTimeResponse {
|
||||
return ReservationTimeResponse(reservationTime.id!!, reservationTime.startAt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ReservationTimeEntity.toResponse(): ReservationTimeResponse = ReservationTimeResponse(
|
||||
this.id!!, this.startAt
|
||||
)
|
||||
|
||||
@Schema(name = "예약 시간 정보 목록 응답", description = "모든 예약 시간 조회 응답시 사용됩니다.")
|
||||
@JvmRecord
|
||||
data class ReservationTimesResponse(
|
||||
@field:Schema(description = "모든 시간 목록")
|
||||
val times: List<ReservationTimeResponse>
|
||||
)
|
||||
|
||||
fun List<ReservationTimeEntity>.toResponses(): ReservationTimesResponse = ReservationTimesResponse(
|
||||
this.map { it.toResponse() }
|
||||
)
|
||||
|
||||
@Schema(name = "특정 테마, 날짜에 대한 시간 정보 응답", description = "특정 날짜와 테마에 대해, 예약 가능 여부를 포함한 시간 정보를 저장합니다.")
|
||||
@JvmRecord
|
||||
data class ReservationTimeInfoResponse(
|
||||
@field:Schema(description = "예약 시간 번호. 예약 시간을 식별할 때 사용합니다.")
|
||||
val id: Long,
|
||||
|
||||
@field:Schema(description = "예약 시간", type = "string", example = "09:00")
|
||||
val startAt: LocalTime,
|
||||
|
||||
@field:Schema(description = "이미 예약이 완료된 시간인지 여부")
|
||||
val alreadyBooked: Boolean
|
||||
)
|
||||
|
||||
fun ReservationTimeEntity.toInfoResponse(alreadyBooked: Boolean): ReservationTimeInfoResponse = ReservationTimeInfoResponse(
|
||||
id = this.id!!,
|
||||
startAt = this.startAt,
|
||||
alreadyBooked = alreadyBooked
|
||||
)
|
||||
|
||||
@Schema(name = "예약 시간 정보 목록 응답", description = "특정 테마, 날짜에 대한 모든 예약 가능 시간 정보를 저장합니다.")
|
||||
@JvmRecord
|
||||
data class ReservationTimeInfosResponse(
|
||||
@field:Schema(description = "특정 테마, 날짜에 대한 예약 가능 여부를 포함한 시간 목록")
|
||||
val times: List<ReservationTimeInfoResponse>
|
||||
)
|
||||
@ -9,7 +9,7 @@ interface ThemeRepository : JpaRepository<ThemeEntity, Long> {
|
||||
@Query(value = """
|
||||
SELECT t
|
||||
FROM ThemeEntity t
|
||||
RIGHT JOIN Reservation r ON t.id = r.theme.id
|
||||
RIGHT JOIN ReservationEntity r ON t.id = r.theme.id
|
||||
WHERE r.date BETWEEN :startDate AND :endDate
|
||||
GROUP BY r.theme.id
|
||||
ORDER BY COUNT(r.theme.id) DESC, t.id ASC
|
||||
@ -24,7 +24,7 @@ interface ThemeRepository : JpaRepository<ThemeEntity, Long> {
|
||||
@Query(value = """
|
||||
SELECT EXISTS(
|
||||
SELECT 1
|
||||
FROM Reservation r
|
||||
FROM ReservationEntity r
|
||||
WHERE r.theme.id = :id
|
||||
)
|
||||
""")
|
||||
|
||||
@ -11,7 +11,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
function render(data) {
|
||||
const tableBody = document.getElementById('table-body');
|
||||
tableBody.innerHTML = '';
|
||||
data.data.myReservationResponses.forEach(item => {
|
||||
data.data.reservations.forEach(item => {
|
||||
const row = tableBody.insertRow();
|
||||
|
||||
const theme = item.themeName;
|
||||
|
||||
@ -7,15 +7,15 @@ import io.kotest.matchers.shouldBe
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import roomescape.util.JwtFixture
|
||||
import roomescape.util.MemberFixture
|
||||
import roomescape.member.business.MemberService
|
||||
import roomescape.member.infrastructure.persistence.Member
|
||||
import roomescape.member.infrastructure.persistence.MemberRepository
|
||||
import roomescape.auth.infrastructure.jwt.JwtHandler
|
||||
import roomescape.auth.service.AuthService
|
||||
import roomescape.common.exception.ErrorType
|
||||
import roomescape.common.exception.RoomescapeException
|
||||
import roomescape.member.business.MemberService
|
||||
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||
import roomescape.member.infrastructure.persistence.MemberRepository
|
||||
import roomescape.util.JwtFixture
|
||||
import roomescape.util.MemberFixture
|
||||
|
||||
|
||||
class AuthServiceTest : BehaviorSpec({
|
||||
@ -24,7 +24,7 @@ class AuthServiceTest : BehaviorSpec({
|
||||
val jwtHandler: JwtHandler = JwtFixture.create()
|
||||
|
||||
val authService = AuthService(memberService, jwtHandler)
|
||||
val user: Member = MemberFixture.user()
|
||||
val user: MemberEntity = MemberFixture.user()
|
||||
|
||||
Given("로그인 요청을 받으면") {
|
||||
When("이메일과 비밀번호로 회원을 찾고") {
|
||||
|
||||
@ -5,9 +5,9 @@ import io.jsonwebtoken.SignatureAlgorithm
|
||||
import io.kotest.assertions.throwables.shouldThrow
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import roomescape.util.JwtFixture
|
||||
import roomescape.common.exception.ErrorType
|
||||
import roomescape.common.exception.RoomescapeException
|
||||
import roomescape.util.JwtFixture
|
||||
import java.util.*
|
||||
import kotlin.random.Random
|
||||
|
||||
|
||||
@ -1,147 +0,0 @@
|
||||
package roomescape.payment.business;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.jdbc.Sql;
|
||||
import org.springframework.test.context.jdbc.Sql.ExecutionPhase;
|
||||
|
||||
import roomescape.common.exception.RoomescapeException;
|
||||
import roomescape.member.infrastructure.persistence.Member;
|
||||
import roomescape.member.infrastructure.persistence.MemberRepository;
|
||||
import roomescape.member.infrastructure.persistence.Role;
|
||||
import roomescape.payment.infrastructure.persistence.CanceledPaymentEntity;
|
||||
import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository;
|
||||
import roomescape.payment.web.PaymentApprove;
|
||||
import roomescape.payment.web.PaymentCancel;
|
||||
import roomescape.payment.web.ReservationPaymentResponse;
|
||||
import roomescape.reservation.domain.Reservation;
|
||||
import roomescape.reservation.domain.ReservationStatus;
|
||||
import roomescape.reservation.domain.ReservationTime;
|
||||
import roomescape.reservation.domain.repository.ReservationRepository;
|
||||
import roomescape.reservation.domain.repository.ReservationTimeRepository;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeRepository;
|
||||
|
||||
@SpringBootTest
|
||||
@Sql(scripts = "/truncate.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD)
|
||||
class PaymentServiceTest {
|
||||
|
||||
@Autowired
|
||||
private PaymentService paymentService;
|
||||
@Autowired
|
||||
private ReservationRepository reservationRepository;
|
||||
@Autowired
|
||||
private MemberRepository memberRepository;
|
||||
@Autowired
|
||||
private ReservationTimeRepository reservationTimeRepository;
|
||||
@Autowired
|
||||
private ThemeRepository themeRepository;
|
||||
@Autowired
|
||||
private CanceledPaymentRepository canceledPaymentRepository;
|
||||
|
||||
@Test
|
||||
@DisplayName("결제 정보를 저장한다.")
|
||||
void savePayment() {
|
||||
// given
|
||||
PaymentApprove.Response paymentInfo = new PaymentApprove.Response("payment-key", "order-id",
|
||||
OffsetDateTime.now(), 10000L);
|
||||
LocalDateTime localDateTime = LocalDateTime.now().plusHours(1L);
|
||||
LocalDate date = localDateTime.toLocalDate();
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime()));
|
||||
Member member = memberRepository.save(new Member(null, "member", "email@email.com", "password", Role.MEMBER));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "desc", "thumbnail"));
|
||||
Reservation reservation = reservationRepository.save(new Reservation(date, time, theme, member,
|
||||
ReservationStatus.CONFIRMED));
|
||||
|
||||
// when
|
||||
ReservationPaymentResponse reservationPaymentResponse = paymentService.savePayment(paymentInfo, reservation);
|
||||
|
||||
// then
|
||||
assertThat(reservationPaymentResponse.reservation().id()).isEqualTo(reservation.getId());
|
||||
assertThat(reservationPaymentResponse.paymentKey()).isEqualTo(paymentInfo.paymentKey);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("예약 ID로 결제 정보를 제거하고, 결제 취소 테이블에 취소 정보를 저장한다.")
|
||||
void cancelPaymentByAdmin() {
|
||||
// given
|
||||
PaymentApprove.Response paymentInfo = new PaymentApprove.Response("payment-key", "order-id",
|
||||
OffsetDateTime.now(), 10000L);
|
||||
LocalDateTime localDateTime = LocalDateTime.now().plusHours(1L);
|
||||
LocalDate date = localDateTime.toLocalDate();
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime()));
|
||||
Member member = memberRepository.save(new Member(null, "member", "email@email.com", "password", Role.MEMBER));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "desc", "thumbnail"));
|
||||
Reservation reservation = reservationRepository.save(new Reservation(date, time, theme, member,
|
||||
ReservationStatus.CONFIRMED));
|
||||
|
||||
paymentService.savePayment(paymentInfo, reservation);
|
||||
|
||||
// when
|
||||
PaymentCancel.Request paymentCancelRequest = paymentService.cancelPaymentByAdmin(reservation.getId());
|
||||
|
||||
// then
|
||||
assertThat(canceledPaymentRepository.findByPaymentKey("payment-key")).isNotNull();
|
||||
assertThat(paymentCancelRequest.paymentKey).isEqualTo(paymentInfo.paymentKey);
|
||||
assertThat(paymentCancelRequest.cancelReason).isEqualTo("고객 요청");
|
||||
assertThat(paymentCancelRequest.amount).isEqualTo(10000L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("입력된 예약 ID에 대한 결제 정보가 없으면 예외가 발생한다.")
|
||||
void cancelPaymentByAdminWithNonExistentReservationId() {
|
||||
// given
|
||||
Long nonExistentReservationId = 1L;
|
||||
|
||||
// when
|
||||
assertThatThrownBy(() -> paymentService.cancelPaymentByAdmin(nonExistentReservationId))
|
||||
.isInstanceOf(RoomescapeException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("결제 취소 정보에 있는 취소 시간을 업데이트한다.")
|
||||
void updateCanceledTime() {
|
||||
// given
|
||||
PaymentApprove.Response paymentInfo = new PaymentApprove.Response("payment-key", "order-id",
|
||||
OffsetDateTime.now(), 10000L);
|
||||
LocalDateTime localDateTime = LocalDateTime.now().plusHours(1L);
|
||||
LocalDate date = localDateTime.toLocalDate();
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime()));
|
||||
Member member = memberRepository.save(new Member(null, "member", "email@email.com", "password", Role.MEMBER));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "desc", "thumbnail"));
|
||||
Reservation reservation = reservationRepository.save(new Reservation(date, time, theme, member,
|
||||
ReservationStatus.CONFIRMED));
|
||||
|
||||
paymentService.savePayment(paymentInfo, reservation);
|
||||
paymentService.cancelPaymentByAdmin(reservation.getId());
|
||||
|
||||
// when
|
||||
OffsetDateTime canceledAt = OffsetDateTime.now().plusHours(2L);
|
||||
paymentService.updateCanceledTime(paymentInfo.paymentKey, canceledAt);
|
||||
|
||||
// then
|
||||
CanceledPaymentEntity canceledPayment = canceledPaymentRepository.findByPaymentKey(paymentInfo.paymentKey);
|
||||
|
||||
assertThat(canceledPayment).isNotNull();
|
||||
assertThat(canceledPayment.getCanceledAt()).isEqualTo(canceledAt);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("결제 취소 시간을 업데이트 할 때, 입력한 paymentKey가 존재하지 않으면 예외가 발생한다.")
|
||||
void updateCanceledTimeWithNonExistentPaymentKey() {
|
||||
// given
|
||||
OffsetDateTime canceledAt = OffsetDateTime.now().plusHours(2L);
|
||||
|
||||
// when
|
||||
assertThatThrownBy(() -> paymentService.updateCanceledTime("non-existent-payment-key", canceledAt))
|
||||
.isInstanceOf(RoomescapeException.class);
|
||||
}
|
||||
}
|
||||
@ -17,7 +17,7 @@ import roomescape.payment.web.PaymentCancel
|
||||
import roomescape.util.PaymentFixture
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
class PaymentServiceKTest : FunSpec({
|
||||
class PaymentServiceTest : FunSpec({
|
||||
val paymentRepository: PaymentRepository = mockk()
|
||||
val canceledPaymentRepository: CanceledPaymentRepository = mockk()
|
||||
|
||||
@ -6,6 +6,7 @@ import io.kotest.matchers.shouldBe
|
||||
import jakarta.persistence.EntityManager
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||
import roomescape.util.PaymentFixture
|
||||
import roomescape.util.ReservationFixture
|
||||
|
||||
@ -15,23 +16,23 @@ class PaymentRepositoryTest(
|
||||
@Autowired val entityManager: EntityManager
|
||||
) : FunSpec() {
|
||||
|
||||
var reservationId: Long = 0L
|
||||
lateinit var reservation: ReservationEntity
|
||||
|
||||
init {
|
||||
context("existsByReservationId") {
|
||||
beforeTest {
|
||||
reservationId = setupReservation()
|
||||
PaymentFixture.create(reservationId = reservationId)
|
||||
reservation = setupReservation()
|
||||
PaymentFixture.create(reservation = reservation)
|
||||
.also { paymentRepository.save(it) }
|
||||
}
|
||||
|
||||
test("true") {
|
||||
paymentRepository.existsByReservationId(reservationId)
|
||||
paymentRepository.existsByReservationId(reservation.id!!)
|
||||
.also { it shouldBe true }
|
||||
}
|
||||
|
||||
test("false") {
|
||||
paymentRepository.existsByReservationId(reservationId + 1)
|
||||
paymentRepository.existsByReservationId(reservation.id!! + 1L)
|
||||
.also { it shouldBe false }
|
||||
}
|
||||
}
|
||||
@ -40,20 +41,20 @@ class PaymentRepositoryTest(
|
||||
lateinit var paymentKey: String
|
||||
|
||||
beforeTest {
|
||||
reservationId = setupReservation()
|
||||
paymentKey = PaymentFixture.create(reservationId = reservationId)
|
||||
reservation = setupReservation()
|
||||
paymentKey = PaymentFixture.create(reservation = reservation)
|
||||
.also { paymentRepository.save(it) }
|
||||
.paymentKey
|
||||
}
|
||||
|
||||
test("정상 반환") {
|
||||
paymentRepository.findPaymentKeyByReservationId(reservationId)
|
||||
paymentRepository.findPaymentKeyByReservationId(reservation.id!!)
|
||||
?.let { it shouldBe paymentKey }
|
||||
?: throw AssertionError("Unexpected null value")
|
||||
}
|
||||
|
||||
test("null 반환") {
|
||||
paymentRepository.findPaymentKeyByReservationId(reservationId + 1)
|
||||
paymentRepository.findPaymentKeyByReservationId(reservation.id!! + 1)
|
||||
.also { it shouldBe null }
|
||||
}
|
||||
}
|
||||
@ -62,8 +63,8 @@ class PaymentRepositoryTest(
|
||||
lateinit var payment: PaymentEntity
|
||||
|
||||
beforeTest {
|
||||
reservationId = setupReservation()
|
||||
payment = PaymentFixture.create(reservationId = reservationId)
|
||||
reservation = setupReservation()
|
||||
payment = PaymentFixture.create(reservation = reservation)
|
||||
.also { paymentRepository.save(it) }
|
||||
}
|
||||
|
||||
@ -89,7 +90,7 @@ class PaymentRepositoryTest(
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupReservation(): Long {
|
||||
private fun setupReservation(): ReservationEntity {
|
||||
return ReservationFixture.create().also {
|
||||
entityManager.persist(it.member)
|
||||
entityManager.persist(it.theme)
|
||||
@ -98,6 +99,6 @@ class PaymentRepositoryTest(
|
||||
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,175 @@
|
||||
package roomescape.reservation.business
|
||||
|
||||
import io.kotest.assertions.throwables.shouldThrow
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import roomescape.common.exception.ErrorType
|
||||
import roomescape.common.exception.RoomescapeException
|
||||
import roomescape.member.business.MemberService
|
||||
import roomescape.member.infrastructure.persistence.Role
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationRepository
|
||||
import roomescape.theme.business.ThemeService
|
||||
import roomescape.util.MemberFixture
|
||||
import roomescape.util.ReservationFixture
|
||||
import roomescape.util.ReservationTimeFixture
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
|
||||
class ReservationServiteTest : FunSpec({
|
||||
|
||||
val reservationRepository: ReservationRepository = mockk()
|
||||
val reservationTimeService: ReservationTimeService = mockk()
|
||||
val memberService: MemberService = mockk()
|
||||
val themeService: ThemeService = mockk()
|
||||
val reservationService = ReservationService(
|
||||
reservationRepository,
|
||||
reservationTimeService,
|
||||
memberService,
|
||||
themeService
|
||||
)
|
||||
|
||||
context("예약을 추가할 때") {
|
||||
test("이미 예약이 있으면 예외를 던진다.") {
|
||||
every {
|
||||
reservationRepository.exists(any())
|
||||
} returns true
|
||||
|
||||
val reservationRequest = ReservationFixture.createRequest()
|
||||
|
||||
shouldThrow<RoomescapeException> {
|
||||
reservationService.addReservation(reservationRequest, 1L)
|
||||
}.also {
|
||||
it.errorType shouldBe ErrorType.RESERVATION_DUPLICATED
|
||||
}
|
||||
}
|
||||
|
||||
context("날짜, 시간이 잘못 입력되면 예외를 던진다.") {
|
||||
every {
|
||||
reservationRepository.exists(any())
|
||||
} returns false
|
||||
|
||||
every {
|
||||
themeService.findThemeById(any())
|
||||
} returns mockk()
|
||||
|
||||
every {
|
||||
memberService.findById(any())
|
||||
} returns mockk()
|
||||
|
||||
|
||||
test("지난 날짜이면 예외를 던진다.") {
|
||||
val reservationRequest = ReservationFixture.createRequest().copy(
|
||||
date = LocalDate.now().minusDays(1)
|
||||
)
|
||||
|
||||
every {
|
||||
reservationTimeService.findTimeById(any())
|
||||
} returns ReservationTimeFixture.create()
|
||||
|
||||
shouldThrow<RoomescapeException> {
|
||||
reservationService.addReservation(reservationRequest, 1L)
|
||||
}.also {
|
||||
it.errorType shouldBe ErrorType.RESERVATION_PERIOD_IN_PAST
|
||||
}
|
||||
}
|
||||
|
||||
test("지난 시간이면 예외를 던진다.") {
|
||||
val reservationRequest = ReservationFixture.createRequest().copy(
|
||||
date = LocalDate.now(),
|
||||
)
|
||||
|
||||
every {
|
||||
reservationTimeService.findTimeById(reservationRequest.timeId)
|
||||
} returns ReservationTimeFixture.create(
|
||||
startAt = LocalTime.now().minusMinutes(1)
|
||||
)
|
||||
|
||||
shouldThrow<RoomescapeException> {
|
||||
reservationService.addReservation(reservationRequest, 1L)
|
||||
}.also {
|
||||
it.errorType shouldBe ErrorType.RESERVATION_PERIOD_IN_PAST
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("예약 대기를 걸 때") {
|
||||
test("이미 예약한 회원이 같은 날짜와 테마로 대기를 걸면 예외를 던진다.") {
|
||||
val reservationRequest = ReservationFixture.createRequest().copy(
|
||||
date = LocalDate.now(),
|
||||
themeId = 1L,
|
||||
timeId = 1L,
|
||||
)
|
||||
|
||||
every {
|
||||
reservationRepository.exists(any())
|
||||
} returns true
|
||||
|
||||
shouldThrow<RoomescapeException> {
|
||||
val waitingRequest = ReservationFixture.createWaitingRequest(
|
||||
date = reservationRequest.date,
|
||||
themeId = reservationRequest.themeId,
|
||||
timeId = reservationRequest.timeId
|
||||
)
|
||||
reservationService.addWaiting(waitingRequest, 1L)
|
||||
}.also {
|
||||
it.errorType shouldBe ErrorType.HAS_RESERVATION_OR_WAITING
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("예약을 조회할 때") {
|
||||
test("종료 날짜가 시작 날짜보다 이전이면 예외를 던진다.") {
|
||||
val startFrom = LocalDate.now()
|
||||
val endAt = startFrom.minusDays(1)
|
||||
|
||||
shouldThrow<RoomescapeException> {
|
||||
reservationService.findFilteredReservations(
|
||||
null,
|
||||
null,
|
||||
startFrom,
|
||||
endAt
|
||||
)
|
||||
}.also {
|
||||
it.errorType shouldBe ErrorType.INVALID_DATE_RANGE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("대기중인 예약을 승인할 때") {
|
||||
test("관리자가 아니면 예외를 던진다.") {
|
||||
val member = MemberFixture.create(id = 1L, role = Role.MEMBER)
|
||||
|
||||
every {
|
||||
memberService.findById(any())
|
||||
} returns member
|
||||
|
||||
shouldThrow<RoomescapeException> {
|
||||
reservationService.approveWaiting(1L, member.id!!)
|
||||
}.also {
|
||||
it.errorType shouldBe ErrorType.PERMISSION_DOES_NOT_EXIST
|
||||
}
|
||||
}
|
||||
|
||||
test("이미 확정된 예약이 있으면 예외를 던진다.") {
|
||||
val member = MemberFixture.create(id = 1L, role = Role.ADMIN)
|
||||
val reservationId = 1L
|
||||
|
||||
every {
|
||||
memberService.findById(any())
|
||||
} returns member
|
||||
|
||||
every {
|
||||
reservationRepository.isExistConfirmedReservation(reservationId)
|
||||
} returns true
|
||||
|
||||
shouldThrow<RoomescapeException> {
|
||||
reservationService.approveWaiting(reservationId, member.id!!)
|
||||
}.also {
|
||||
it.errorType shouldBe ErrorType.RESERVATION_DUPLICATED
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -0,0 +1,92 @@
|
||||
package roomescape.reservation.business
|
||||
|
||||
import io.kotest.assertions.throwables.shouldThrow
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.http.HttpStatus
|
||||
import roomescape.common.exception.ErrorType
|
||||
import roomescape.common.exception.RoomescapeException
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationRepository
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationTimeRepository
|
||||
import roomescape.reservation.web.ReservationTimeRequest
|
||||
import roomescape.util.ReservationTimeFixture
|
||||
import java.time.LocalTime
|
||||
|
||||
class ReservationTimeServiceTest : FunSpec({
|
||||
val reservationTimeRepository: ReservationTimeRepository = mockk()
|
||||
val reservationRepository: ReservationRepository = mockk()
|
||||
|
||||
val reservationTimeService = ReservationTimeService(
|
||||
reservationTimeRepository = reservationTimeRepository,
|
||||
reservationRepository = reservationRepository
|
||||
)
|
||||
|
||||
context("findTimeById") {
|
||||
test("시간을 찾을 수 없으면 400 에러를 던진다.") {
|
||||
val id = 1L
|
||||
|
||||
// Mocking the behavior of reservationTimeRepository.findByIdOrNull
|
||||
every { reservationTimeRepository.findByIdOrNull(id) } returns null
|
||||
|
||||
shouldThrow<RoomescapeException> {
|
||||
reservationTimeService.findTimeById(id)
|
||||
}.apply {
|
||||
errorType shouldBe ErrorType.RESERVATION_TIME_NOT_FOUND
|
||||
httpStatus shouldBe HttpStatus.BAD_REQUEST
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("addTime") {
|
||||
test("중복된 시간이 있으면 409 에러를 던진다.") {
|
||||
val request = ReservationTimeRequest(startAt = LocalTime.of(10, 0))
|
||||
|
||||
// Mocking the behavior of reservationTimeRepository.findByStartAt
|
||||
every { reservationTimeRepository.existsByStartAt(request.startAt) } returns true
|
||||
|
||||
shouldThrow<RoomescapeException> {
|
||||
reservationTimeService.addTime(request)
|
||||
}.apply {
|
||||
errorType shouldBe ErrorType.TIME_DUPLICATED
|
||||
httpStatus shouldBe HttpStatus.CONFLICT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("removeTimeById") {
|
||||
test("시간을 찾을 수 없으면 400 에러를 던진다.") {
|
||||
val id = 1L
|
||||
|
||||
// Mocking the behavior of reservationTimeRepository.findByIdOrNull
|
||||
every { reservationTimeRepository.findByIdOrNull(id) } returns null
|
||||
|
||||
shouldThrow<RoomescapeException> {
|
||||
reservationTimeService.removeTimeById(id)
|
||||
}.apply {
|
||||
errorType shouldBe ErrorType.RESERVATION_TIME_NOT_FOUND
|
||||
httpStatus shouldBe HttpStatus.BAD_REQUEST
|
||||
}
|
||||
}
|
||||
|
||||
test("예약이 있는 시간이면 409 에러를 던진다.") {
|
||||
val id = 1L
|
||||
val reservationTime = ReservationTimeFixture.create()
|
||||
|
||||
// Mocking the behavior of reservationTimeRepository.findByIdOrNull
|
||||
every { reservationTimeRepository.findByIdOrNull(id) } returns reservationTime
|
||||
|
||||
// Mocking the behavior of reservationRepository.findByReservationTime
|
||||
every { reservationRepository.findByReservationTime(reservationTime) } returns listOf(mockk())
|
||||
|
||||
shouldThrow<RoomescapeException> {
|
||||
reservationTimeService.removeTimeById(id)
|
||||
}.apply {
|
||||
errorType shouldBe ErrorType.TIME_IS_USED_CONFLICT
|
||||
httpStatus shouldBe HttpStatus.CONFLICT
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -0,0 +1,122 @@
|
||||
package roomescape.reservation.business
|
||||
|
||||
import io.kotest.assertions.assertSoftly
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import roomescape.payment.business.PaymentService
|
||||
import roomescape.payment.infrastructure.persistence.PaymentEntity
|
||||
import roomescape.payment.web.PaymentCancel
|
||||
import roomescape.payment.web.toReservationPaymentResponse
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
||||
import roomescape.reservation.web.ReservationRequest
|
||||
import roomescape.reservation.web.ReservationResponse
|
||||
import roomescape.util.*
|
||||
|
||||
class ReservationWithPaymentServiceTest : FunSpec({
|
||||
val reservationService: ReservationService = mockk()
|
||||
val paymentService: PaymentService = mockk()
|
||||
|
||||
val reservationWithPaymentService = ReservationWithPaymentService(
|
||||
reservationService = reservationService,
|
||||
paymentService = paymentService
|
||||
)
|
||||
|
||||
val reservationRequest: ReservationRequest = ReservationFixture.createRequest()
|
||||
val paymentApproveResponse = PaymentFixture.createApproveResponse()
|
||||
val memberId = 1L
|
||||
val reservationEntity: ReservationEntity = ReservationFixture.create(
|
||||
id = 1L,
|
||||
date = reservationRequest.date,
|
||||
reservationTime = ReservationTimeFixture.create(id = reservationRequest.timeId),
|
||||
theme = ThemeFixture.create(id = reservationRequest.themeId),
|
||||
member = MemberFixture.create(id = memberId),
|
||||
status = ReservationStatus.CONFIRMED
|
||||
)
|
||||
val paymentEntity: PaymentEntity = PaymentFixture.create(
|
||||
id = 1L,
|
||||
orderId = reservationRequest.orderId,
|
||||
paymentKey = reservationRequest.paymentKey,
|
||||
totalAmount = reservationRequest.amount,
|
||||
reservation = reservationEntity,
|
||||
)
|
||||
|
||||
context("addReservationWithPayment") {
|
||||
test("예약 및 결제 정보를 저장한다.") {
|
||||
every {
|
||||
reservationService.addReservation(reservationRequest, memberId)
|
||||
} returns reservationEntity
|
||||
|
||||
every {
|
||||
paymentService.savePayment(paymentApproveResponse, reservationEntity)
|
||||
} returns paymentEntity.toReservationPaymentResponse()
|
||||
|
||||
val result: ReservationResponse = reservationWithPaymentService.addReservationWithPayment(
|
||||
request = reservationRequest,
|
||||
paymentInfo = paymentApproveResponse,
|
||||
memberId = memberId
|
||||
)
|
||||
|
||||
assertSoftly(result) {
|
||||
this.id shouldBe reservationEntity.id
|
||||
this.date shouldBe reservationEntity.date
|
||||
this.member.id shouldBe reservationEntity.member.id
|
||||
this.time.id shouldBe reservationEntity.reservationTime.id
|
||||
this.theme.id shouldBe reservationEntity.theme.id
|
||||
this.status shouldBe ReservationStatus.CONFIRMED
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
context("removeReservationWithPayment") {
|
||||
test("예약 및 결제 정보를 삭제하고, 결제 취소 정보를 저장한다.") {
|
||||
val paymentCancelRequest: PaymentCancel.Request = PaymentFixture.createCancelRequest().copy(
|
||||
paymentKey = paymentEntity.paymentKey,
|
||||
amount = paymentEntity.totalAmount,
|
||||
cancelReason = "고객 요청"
|
||||
)
|
||||
|
||||
every {
|
||||
paymentService.cancelPaymentByAdmin(reservationEntity.id!!)
|
||||
} returns paymentCancelRequest
|
||||
|
||||
every {
|
||||
reservationService.removeReservationById(reservationEntity.id!!, reservationEntity.member.id!!)
|
||||
} just Runs
|
||||
|
||||
val result: PaymentCancel.Request = reservationWithPaymentService.removeReservationWithPayment(
|
||||
reservationId = reservationEntity.id!!,
|
||||
memberId = reservationEntity.member.id!!
|
||||
)
|
||||
|
||||
result shouldBe paymentCancelRequest
|
||||
}
|
||||
}
|
||||
|
||||
context("isNotPaidReservation") {
|
||||
test("결제된 예약이면 true를 반환한다.") {
|
||||
every {
|
||||
paymentService.isReservationPaid(reservationEntity.id!!)
|
||||
} returns false
|
||||
|
||||
val result: Boolean = reservationWithPaymentService.isNotPaidReservation(reservationEntity.id!!)
|
||||
|
||||
result shouldBe true
|
||||
}
|
||||
|
||||
test("결제되지 않은 예약이면 false를 반환한다.") {
|
||||
every {
|
||||
paymentService.isReservationPaid(reservationEntity.id!!)
|
||||
} returns true
|
||||
|
||||
val result: Boolean = reservationWithPaymentService.isNotPaidReservation(reservationEntity.id!!)
|
||||
|
||||
result shouldBe false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -1,621 +0,0 @@
|
||||
package roomescape.reservation.controller;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.test.context.jdbc.Sql;
|
||||
import org.springframework.test.context.jdbc.Sql.ExecutionPhase;
|
||||
|
||||
import io.restassured.RestAssured;
|
||||
import io.restassured.http.ContentType;
|
||||
import io.restassured.http.Header;
|
||||
import roomescape.member.infrastructure.persistence.Member;
|
||||
import roomescape.member.infrastructure.persistence.MemberRepository;
|
||||
import roomescape.member.infrastructure.persistence.Role;
|
||||
import roomescape.payment.infrastructure.client.TossPaymentClient;
|
||||
import roomescape.payment.infrastructure.persistence.CanceledPaymentEntity;
|
||||
import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository;
|
||||
import roomescape.payment.infrastructure.persistence.PaymentEntity;
|
||||
import roomescape.payment.infrastructure.persistence.PaymentRepository;
|
||||
import roomescape.payment.web.PaymentApprove;
|
||||
import roomescape.payment.web.PaymentCancel;
|
||||
import roomescape.reservation.domain.Reservation;
|
||||
import roomescape.reservation.domain.ReservationStatus;
|
||||
import roomescape.reservation.domain.ReservationTime;
|
||||
import roomescape.reservation.domain.repository.ReservationRepository;
|
||||
import roomescape.reservation.domain.repository.ReservationTimeRepository;
|
||||
import roomescape.reservation.dto.request.AdminReservationRequest;
|
||||
import roomescape.reservation.dto.request.ReservationRequest;
|
||||
import roomescape.reservation.dto.request.WaitingRequest;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeRepository;
|
||||
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@Sql(scripts = "/truncate.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD)
|
||||
public class ReservationControllerTest {
|
||||
|
||||
@Autowired
|
||||
private ReservationRepository reservationRepository;
|
||||
@Autowired
|
||||
private ReservationTimeRepository reservationTimeRepository;
|
||||
@Autowired
|
||||
private ThemeRepository themeRepository;
|
||||
@Autowired
|
||||
private MemberRepository memberRepository;
|
||||
@Autowired
|
||||
private PaymentRepository paymentRepository;
|
||||
@Autowired
|
||||
private CanceledPaymentRepository canceledPaymentRepository;
|
||||
|
||||
@MockBean
|
||||
private TossPaymentClient paymentClient;
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("처음으로 등록하는 예약의 id는 1이다.")
|
||||
void firstPost() {
|
||||
String accessTokenCookie = getAdminAccessTokenCookieByLogin("admin@admin.com", "12341234");
|
||||
|
||||
LocalTime time = LocalTime.of(17, 30);
|
||||
LocalDate date = LocalDate.now().plusDays(1L);
|
||||
|
||||
reservationTimeRepository.save(new ReservationTime(time));
|
||||
themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
|
||||
Map<String, String> reservationParams = Map.of(
|
||||
"date", date.toString(),
|
||||
"timeId", "1",
|
||||
"themeId", "1",
|
||||
"paymentKey", "pk",
|
||||
"orderId", "oi",
|
||||
"amount", "1000",
|
||||
"paymentType", "DEFAULT"
|
||||
);
|
||||
|
||||
when(paymentClient.confirmPayment(any(PaymentApprove.Request.class))).thenReturn(
|
||||
new PaymentApprove.Response("pk", "oi", OffsetDateTime.of(date, time, ZoneOffset.ofHours(9)), 1000L));
|
||||
|
||||
RestAssured.given().log().all()
|
||||
.contentType(ContentType.JSON)
|
||||
.port(port)
|
||||
.header("Cookie", accessTokenCookie)
|
||||
.body(reservationParams)
|
||||
.when().post("/reservations")
|
||||
.then().log().all()
|
||||
.statusCode(201)
|
||||
.body("data.id", is(1))
|
||||
.header("Location", "/reservations/1");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("대기중인 예약을 취소한다.")
|
||||
void cancelWaiting() {
|
||||
// given
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
String accessTokenCookie = getAccessTokenCookieByLogin("email@email.com", "password");
|
||||
|
||||
ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30)));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member member1 = memberRepository.save(new Member(null, "name1", "email1r@email.com", "password", Role.MEMBER));
|
||||
|
||||
// when
|
||||
reservationRepository.save(new Reservation(LocalDate.now().plusDays(1), reservationTime, theme, member1,
|
||||
ReservationStatus.CONFIRMED));
|
||||
Reservation waiting = reservationRepository.save(
|
||||
new Reservation(LocalDate.now().plusDays(1), reservationTime, theme, member,
|
||||
ReservationStatus.WAITING));
|
||||
|
||||
// then
|
||||
RestAssured.given().log().all()
|
||||
.port(port)
|
||||
.header("Cookie", accessTokenCookie)
|
||||
.when().delete("/reservations/waiting/{id}", waiting.getId())
|
||||
.then().log().all()
|
||||
.statusCode(204);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("회원은 자신이 아닌 다른 회원의 예약을 취소할 수 없다.")
|
||||
void cantCancelOtherMembersWaiting() {
|
||||
// given
|
||||
Member confirmedMember = memberRepository.save(
|
||||
new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
String accessTokenCookie = getAccessTokenCookieByLogin("email@email.com", "password");
|
||||
|
||||
ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30)));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member waitingMember = memberRepository.save(
|
||||
new Member(null, "name1", "email1r@email.com", "password", Role.MEMBER));
|
||||
|
||||
// when
|
||||
reservationRepository.save(new Reservation(LocalDate.now().plusDays(1), reservationTime, theme, confirmedMember,
|
||||
ReservationStatus.CONFIRMED));
|
||||
Reservation waiting = reservationRepository.save(
|
||||
new Reservation(LocalDate.now().plusDays(1), reservationTime, theme, waitingMember,
|
||||
ReservationStatus.WAITING));
|
||||
|
||||
// then
|
||||
RestAssured.given().log().all()
|
||||
.port(port)
|
||||
.header("Cookie", accessTokenCookie)
|
||||
.when().delete("/reservations/waiting/{id}", waiting.getId())
|
||||
.then().log().all()
|
||||
.statusCode(404);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("관리자 권한이 있으면 전체 예약정보를 조회할 수 있다.")
|
||||
void readEmptyReservations() {
|
||||
// given
|
||||
String accessTokenCookie = getAdminAccessTokenCookieByLogin("admin@admin.com", "12341234");
|
||||
|
||||
ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30)));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
|
||||
// when
|
||||
reservationRepository.save(
|
||||
new Reservation(LocalDate.now(), reservationTime, theme, member, ReservationStatus.CONFIRMED));
|
||||
reservationRepository.save(new Reservation(LocalDate.now().plusDays(1), reservationTime, theme, member,
|
||||
ReservationStatus.CONFIRMED));
|
||||
reservationRepository.save(new Reservation(LocalDate.now().plusDays(2), reservationTime, theme, member,
|
||||
ReservationStatus.CONFIRMED));
|
||||
|
||||
// then
|
||||
RestAssured.given().log().all()
|
||||
.port(port)
|
||||
.header(new Header("Cookie", accessTokenCookie))
|
||||
.when().get("/reservations")
|
||||
.then().log().all()
|
||||
.statusCode(200)
|
||||
.body("data.reservations.size()", is(3));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("예약 취소는 관리자만 할 수 있다.")
|
||||
void canRemoveMyReservation() {
|
||||
// given
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
String accessTokenCookie = getAccessTokenCookieByLogin(member.getEmail(), member.getPassword());
|
||||
|
||||
ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30)));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Reservation reservation = reservationRepository.save(
|
||||
new Reservation(LocalDate.now(), reservationTime, theme, member, ReservationStatus.CONFIRMED));
|
||||
|
||||
// when & then
|
||||
RestAssured.given().log().all()
|
||||
.port(port)
|
||||
.header("Cookie", accessTokenCookie)
|
||||
.when().delete("/reservations/" + reservation.getId())
|
||||
.then().log().all()
|
||||
.statusCode(302);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("관리자가 대기중인 예약을 거절한다.")
|
||||
void denyWaiting() {
|
||||
// given
|
||||
String adminTokenCookie = getAdminAccessTokenCookieByLogin("admin@email.com", "password");
|
||||
|
||||
ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30)));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member confirmedMember = memberRepository.save(
|
||||
new Member(null, "name1", "email@email.com", "password", Role.MEMBER));
|
||||
Member waitingMember = memberRepository.save(
|
||||
new Member(null, "name1", "email1@email.com", "password", Role.MEMBER));
|
||||
|
||||
reservationRepository.save(
|
||||
new Reservation(LocalDate.now(), reservationTime, theme, confirmedMember, ReservationStatus.CONFIRMED));
|
||||
Reservation waiting = reservationRepository.save(
|
||||
new Reservation(LocalDate.now(), reservationTime, theme, waitingMember, ReservationStatus.WAITING));
|
||||
|
||||
// when & then
|
||||
RestAssured.given().log().all()
|
||||
.port(port)
|
||||
.header("Cookie", adminTokenCookie)
|
||||
.when().post("/reservations/waiting/{id}/deny", waiting.getId())
|
||||
.then().log().all()
|
||||
.statusCode(204);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("본인의 예약이 아니더라도 관리자 권한이 있으면 예약 정보를 삭제할 수 있다.")
|
||||
void readReservationsSizeAfterPostAndDelete() {
|
||||
// given
|
||||
Member member = memberRepository.save(new Member(null, "name", "admin@admin.com", "password", Role.ADMIN));
|
||||
String accessTokenCookie = getAccessTokenCookieByLogin(member.getEmail(), member.getPassword());
|
||||
|
||||
ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30)));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member anotherMember = memberRepository.save(
|
||||
new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
|
||||
Reservation reservation = reservationRepository.save(
|
||||
new Reservation(LocalDate.now(), reservationTime, theme, anotherMember, ReservationStatus.CONFIRMED));
|
||||
|
||||
// when & then
|
||||
RestAssured.given().log().all()
|
||||
.port(port)
|
||||
.header("Cookie", accessTokenCookie)
|
||||
.when().delete("/reservations/" + reservation.getId())
|
||||
.then().log().all()
|
||||
.statusCode(204);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("requestValidateSource")
|
||||
@DisplayName("예약 생성 시, 요청 값에 공백 또는 null이 포함되어 있으면 400 에러를 발생한다.")
|
||||
void validateBlankRequest(Map<String, String> invalidRequestBody) {
|
||||
RestAssured.given().log().all()
|
||||
.contentType(ContentType.JSON)
|
||||
.port(port)
|
||||
.body(invalidRequestBody)
|
||||
.header("Cookie", getAdminAccessTokenCookieByLogin("a@a.a", "a"))
|
||||
.when().post("/reservations")
|
||||
.then().log().all()
|
||||
.statusCode(400);
|
||||
}
|
||||
|
||||
private static Stream<Map<String, String>> requestValidateSource() {
|
||||
return Stream.of(
|
||||
Map.of("timeId", "1",
|
||||
"themeId", "1"),
|
||||
|
||||
Map.of("date", LocalDate.now().plusDays(1L).toString(),
|
||||
"themeId", "1"),
|
||||
|
||||
Map.of("date", LocalDate.now().plusDays(1L).toString(),
|
||||
"timeId", "1"),
|
||||
|
||||
Map.of("date", " ",
|
||||
"timeId", "1",
|
||||
"themeId", "1"),
|
||||
|
||||
Map.of("date", LocalDate.now().plusDays(1L).toString(),
|
||||
"timeId", " ",
|
||||
"themeId", "1"),
|
||||
|
||||
Map.of("date", LocalDate.now().plusDays(1L).toString(),
|
||||
"timeId", "1",
|
||||
"themeId", " ")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("예약 생성 시, 정수 요청 데이터에 문자가 입력되어오면 400 에러를 발생한다.")
|
||||
void validateRequestDataFormat() {
|
||||
Map<String, String> invalidTypeRequestBody = Map.of(
|
||||
"date", LocalDate.now().plusDays(1L).toString(),
|
||||
"timeId", "1",
|
||||
"themeId", "한글"
|
||||
);
|
||||
|
||||
RestAssured.given().log().all()
|
||||
.contentType(ContentType.JSON)
|
||||
.port(port)
|
||||
.header("Cookie", getAdminAccessTokenCookieByLogin("a@a.a", "a"))
|
||||
.body(invalidTypeRequestBody)
|
||||
.when().post("/reservations")
|
||||
.then().log().all()
|
||||
.statusCode(400);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@DisplayName("모든 예약 / 대기 중인 예약 / 현재 로그인된 회원의 예약 및 대기를 조회한다.")
|
||||
@CsvSource(value = {"/reservations, reservations, 2", "/reservations/waiting, reservations, 1",
|
||||
"/reservations-mine, myReservationResponses, 3"}, delimiter = ',')
|
||||
void getAllReservations(String requestURI, String responseFieldName, int expectedSize) {
|
||||
// given
|
||||
LocalDate date = LocalDate.now().plusDays(1);
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30)));
|
||||
ReservationTime time1 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(18, 30)));
|
||||
ReservationTime time2 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(19, 30)));
|
||||
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.ADMIN));
|
||||
String accessToken = getAccessTokenCookieByLogin("email@email.com", "password");
|
||||
|
||||
// when : 예약은 2개, 예약 대기는 1개 조회되어야 한다.
|
||||
reservationRepository.save(new Reservation(date, time, theme, member, ReservationStatus.CONFIRMED));
|
||||
reservationRepository.save(
|
||||
new Reservation(date, time1, theme, member, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED));
|
||||
reservationRepository.save(new Reservation(date, time2, theme, member, ReservationStatus.WAITING));
|
||||
|
||||
// then
|
||||
RestAssured.given().log().all()
|
||||
.port(port)
|
||||
.header("Cookie", accessToken)
|
||||
.when().get(requestURI)
|
||||
.then().log().all()
|
||||
.statusCode(200)
|
||||
.body("data." + responseFieldName + ".size()", is(expectedSize));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("예약을 삭제할 때, 승인되었으나 결제 대기중인 예약은 결제 취소 없이 바로 삭제한다.")
|
||||
void removeNotPaidReservation() {
|
||||
// given
|
||||
LocalDate date = LocalDate.now().plusDays(1);
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30)));
|
||||
String accessToken = getAdminAccessTokenCookieByLogin("admin@email.com", "password");
|
||||
|
||||
// when
|
||||
Reservation saved = reservationRepository.save(new Reservation(date, time, theme,
|
||||
memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)),
|
||||
ReservationStatus.CONFIRMED_PAYMENT_REQUIRED));
|
||||
|
||||
// then
|
||||
RestAssured.given().log().all()
|
||||
.port(port)
|
||||
.header("Cookie", accessToken)
|
||||
.when().delete("/reservations/{id}", saved.getId())
|
||||
.then().log().all()
|
||||
.statusCode(204);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("이미 결제가 된 예약은 삭제 후 결제 취소를 요청한다.")
|
||||
void removePaidReservation() {
|
||||
// given
|
||||
String accessToken = getAdminAccessTokenCookieByLogin("admin@email.com", "password");
|
||||
LocalDate date = LocalDate.now().plusDays(1);
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30)));
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
|
||||
Reservation saved = reservationRepository.save(
|
||||
new Reservation(date, time, theme, member, ReservationStatus.CONFIRMED));
|
||||
PaymentEntity savedPaymentEntity = paymentRepository.save(
|
||||
new PaymentEntity(null, "pk", "oi", 1000L, saved, OffsetDateTime.now().minusHours(1L)));
|
||||
|
||||
// when
|
||||
when(paymentClient.cancelPayment(any(PaymentCancel.Request.class)))
|
||||
.thenReturn(new PaymentCancel.Response("pk", "고객 요청", savedPaymentEntity.getTotalAmount(),
|
||||
OffsetDateTime.now()));
|
||||
|
||||
// then
|
||||
RestAssured.given().log().all()
|
||||
.port(port)
|
||||
.header("Cookie", accessToken)
|
||||
.when().delete("/reservations/{id}", saved.getId())
|
||||
.then().log().all()
|
||||
.statusCode(204);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("예약을 추가할 때, 결제 승인 이후에 예외가 발생하면 결제를 취소한 뒤 결제 취소 테이블에 취소 정보를 저장한다.")
|
||||
void saveReservationWithCancelPayment() {
|
||||
// given
|
||||
LocalDateTime localDateTime = LocalDateTime.now().minusHours(1L).withNano(0);
|
||||
LocalDate date = localDateTime.toLocalDate();
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime()));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
String accessToken = getAccessTokenCookieByLogin(member.getEmail(), member.getPassword());
|
||||
|
||||
// when : 이전 날짜의 예약을 추가하여 결제 승인 이후 DB 저장 과정에서 예외를 발생시킨다.
|
||||
String paymentKey = "pk";
|
||||
OffsetDateTime canceledAt = OffsetDateTime.now().plusHours(1L).withNano(0);
|
||||
OffsetDateTime approvedAt = OffsetDateTime.of(localDateTime, ZoneOffset.ofHours(9));
|
||||
when(paymentClient.confirmPayment(any(PaymentApprove.Request.class)))
|
||||
.thenReturn(new PaymentApprove.Response(paymentKey, "oi", approvedAt, 1000L));
|
||||
|
||||
when(paymentClient.cancelPayment(any(PaymentCancel.Request.class)))
|
||||
.thenReturn(new PaymentCancel.Response(paymentKey, "고객 요청", 1000L, canceledAt));
|
||||
|
||||
RestAssured.given().log().all()
|
||||
.contentType(ContentType.JSON)
|
||||
.port(port)
|
||||
.header("Cookie", accessToken)
|
||||
.body(new ReservationRequest(date, time.getId(), theme.getId(), "pk", "oi", 1000L, "DEFAULT"))
|
||||
.when().post("/reservations")
|
||||
.then().log().all()
|
||||
.statusCode(400);
|
||||
|
||||
// then
|
||||
CanceledPaymentEntity canceledPayment = canceledPaymentRepository.findByPaymentKey(paymentKey);
|
||||
assertThat(canceledPayment).isNotNull();
|
||||
assertThat(canceledPayment.getCanceledAt()).isEqualTo(canceledAt);
|
||||
assertThat(canceledPayment.getCancelReason()).isEqualTo("고객 요청");
|
||||
assertThat(canceledPayment.getCancelAmount()).isEqualTo(1000L);
|
||||
assertThat(canceledPayment.getApprovedAt()).isEqualTo(approvedAt);
|
||||
}
|
||||
|
||||
@DisplayName("테마만을 이용하여 예약을 조회한다.")
|
||||
@ParameterizedTest(name = "테마 ID={0}로 조회 시 {1}개의 예약이 조회된다.")
|
||||
@CsvSource(value = {"1/4", "2/3"}, delimiter = '/')
|
||||
@Sql({"/truncate.sql", "/test_search_data.sql"})
|
||||
void searchByTheme(String themeId, int expectedCount) {
|
||||
RestAssured.given().log().all()
|
||||
.port(port)
|
||||
.param("themeId", themeId)
|
||||
.param("memberId", "")
|
||||
.param("dateFrom", "")
|
||||
.param("dateTo", "")
|
||||
.header("cookie", getAdminAccessTokenCookieByLogin("admin@email.com", "password"))
|
||||
.when().get("/reservations/search")
|
||||
.then().log().all()
|
||||
.statusCode(HttpStatus.OK.value())
|
||||
.body("data.reservations.size()", is(expectedCount));
|
||||
}
|
||||
|
||||
@DisplayName("시작 날짜만을 이용하여 예약을 조회한다.")
|
||||
@ParameterizedTest(name = "오늘 날짜보다 {0}일 전인 날짜를 시작 날짜로 조회 시 {1}개의 예약이 조회된다.")
|
||||
@CsvSource(value = {"1/1", "7/7"}, delimiter = '/')
|
||||
@Sql({"/truncate.sql", "/test_search_data.sql"})
|
||||
void searchByFromDate(int minusDays, int expectedCount) {
|
||||
RestAssured.given().log().all()
|
||||
.port(port)
|
||||
.param("themeId", "")
|
||||
.param("memberId", "")
|
||||
.param("dateFrom", LocalDate.now().minusDays(minusDays).toString())
|
||||
.param("dateTo", "")
|
||||
.header("cookie", getAdminAccessTokenCookieByLogin("admin@email.com", "password"))
|
||||
.when().get("/reservations/search")
|
||||
.then().log().all()
|
||||
.statusCode(HttpStatus.OK.value())
|
||||
.body("data.reservations.size()", is(expectedCount));
|
||||
}
|
||||
|
||||
@DisplayName("종료 날짜만을 이용하여 예약을 조회한다..")
|
||||
@ParameterizedTest(name = "오늘 날짜보다 {0}일 전인 날짜를 종료 날짜로 조회 시 {1}개의 예약이 조회된다.")
|
||||
@CsvSource(value = {"1/7", "3/5", "7/1"}, delimiter = '/')
|
||||
@Sql({"/truncate.sql", "/test_search_data.sql"})
|
||||
void searchByToDate(int minusDays, int expectedCount) {
|
||||
RestAssured.given().log().all()
|
||||
.port(port)
|
||||
.param("themeId", "")
|
||||
.param("memberId", "")
|
||||
.param("dateFrom", "")
|
||||
.param("dateTo", LocalDate.now().minusDays(minusDays).toString())
|
||||
.header("cookie", getAdminAccessTokenCookieByLogin("admin@email.com", "password"))
|
||||
.when().get("/reservations/search")
|
||||
.then().log().all()
|
||||
.statusCode(HttpStatus.OK.value())
|
||||
.body("data.reservations.size()", is(expectedCount));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("예약 대기를 추가한다.")
|
||||
void addWaiting() {
|
||||
// given
|
||||
LocalDateTime localDateTime = LocalDateTime.now().plusDays(1L).withNano(0);
|
||||
LocalDate date = localDateTime.toLocalDate();
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime()));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
Member member1 = memberRepository.save(new Member(null, "name1", "email1@email.com", "password", Role.MEMBER));
|
||||
|
||||
String accessToken = getAccessTokenCookieByLogin(member.getEmail(), member.getPassword());
|
||||
reservationRepository.save(new Reservation(date, time, theme, member1, ReservationStatus.CONFIRMED));
|
||||
|
||||
// when & then
|
||||
RestAssured.given().log().all()
|
||||
.port(port)
|
||||
.contentType(ContentType.JSON)
|
||||
.header("Cookie", accessToken)
|
||||
.body(new WaitingRequest(date, time.getId(), theme.getId()))
|
||||
.when().post("/reservations/waiting")
|
||||
.then().log().all()
|
||||
.statusCode(201)
|
||||
.body("data.status", is("WAITING"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("대기중인 예약을 승인한다.")
|
||||
void approveWaiting() {
|
||||
// given
|
||||
LocalDateTime localDateTime = LocalDateTime.now().plusHours(1L);
|
||||
LocalDate date = localDateTime.toLocalDate();
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime()));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
|
||||
String accessToken = getAdminAccessTokenCookieByLogin("admin@email.com", "password");
|
||||
Reservation waiting = reservationRepository.save(
|
||||
new Reservation(date, time, theme, member, ReservationStatus.WAITING));
|
||||
|
||||
// when
|
||||
RestAssured.given().log().all()
|
||||
.port(port)
|
||||
.header("Cookie", accessToken)
|
||||
.when().post("/reservations/waiting/{id}/approve", waiting.getId())
|
||||
.then().log().all()
|
||||
.statusCode(200);
|
||||
|
||||
// then
|
||||
reservationRepository.findById(waiting.getId())
|
||||
.ifPresent(r -> assertThat(r.getReservationStatus()).isEqualTo(
|
||||
ReservationStatus.CONFIRMED_PAYMENT_REQUIRED));
|
||||
|
||||
}
|
||||
|
||||
private String getAccessTokenCookieByLogin(final String email, final String password) {
|
||||
Map<String, String> loginParams = Map.of(
|
||||
"email", email,
|
||||
"password", password
|
||||
);
|
||||
|
||||
String accessToken = RestAssured.given().log().all()
|
||||
.contentType(ContentType.JSON)
|
||||
.port(port)
|
||||
.body(loginParams)
|
||||
.when().post("/login")
|
||||
.then().log().all().extract().cookie("accessToken");
|
||||
|
||||
return "accessToken=" + accessToken;
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("관리자가 직접 예약을 추가한다.")
|
||||
void addReservationByAdmin() {
|
||||
// given
|
||||
LocalDateTime localDateTime = LocalDateTime.now().plusDays(1L).withNano(0);
|
||||
LocalDate date = localDateTime.toLocalDate();
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime()));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
|
||||
String adminAccessToken = getAdminAccessTokenCookieByLogin("admin@email.com", "password");
|
||||
|
||||
// when & then
|
||||
RestAssured.given().log().all()
|
||||
.port(port)
|
||||
.contentType(ContentType.JSON)
|
||||
.header("Cookie", adminAccessToken)
|
||||
.body(new AdminReservationRequest(date, time.getId(), theme.getId(), member.getId()))
|
||||
.when().post("/reservations/admin")
|
||||
.then().log().all()
|
||||
.statusCode(201);
|
||||
}
|
||||
|
||||
private String getAdminAccessTokenCookieByLogin(final String email, final String password) {
|
||||
memberRepository.save(new Member(null, "이름", email, password, Role.ADMIN));
|
||||
|
||||
Map<String, String> loginParams = Map.of(
|
||||
"email", email,
|
||||
"password", password
|
||||
);
|
||||
|
||||
String accessToken = RestAssured.given().log().all()
|
||||
.contentType(ContentType.JSON)
|
||||
.port(port)
|
||||
.body(loginParams)
|
||||
.when().post("/login")
|
||||
.then().log().all().extract().cookie("accessToken");
|
||||
|
||||
return "accessToken=" + accessToken;
|
||||
}
|
||||
}
|
||||
@ -1,247 +0,0 @@
|
||||
package roomescape.reservation.controller;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||
import org.springframework.test.context.jdbc.Sql;
|
||||
import org.springframework.test.context.jdbc.Sql.ExecutionPhase;
|
||||
|
||||
import io.restassured.RestAssured;
|
||||
import io.restassured.http.ContentType;
|
||||
import io.restassured.http.Header;
|
||||
import roomescape.member.infrastructure.persistence.Member;
|
||||
import roomescape.member.infrastructure.persistence.MemberRepository;
|
||||
import roomescape.member.infrastructure.persistence.Role;
|
||||
import roomescape.reservation.domain.Reservation;
|
||||
import roomescape.reservation.domain.ReservationStatus;
|
||||
import roomescape.reservation.domain.ReservationTime;
|
||||
import roomescape.reservation.domain.repository.ReservationRepository;
|
||||
import roomescape.reservation.domain.repository.ReservationTimeRepository;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeRepository;
|
||||
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
|
||||
@Sql(scripts = "/truncate.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD)
|
||||
public class ReservationTimeControllerTest {
|
||||
|
||||
@Autowired
|
||||
private ReservationTimeRepository reservationTimeRepository;
|
||||
|
||||
@Autowired
|
||||
private ThemeRepository themeRepository;
|
||||
|
||||
@Autowired
|
||||
private ReservationRepository reservationRepository;
|
||||
|
||||
@Autowired
|
||||
private MemberRepository memberRepository;
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
|
||||
private final Map<String, String> params = Map.of(
|
||||
"startAt", "17:00"
|
||||
);
|
||||
|
||||
@Test
|
||||
@DisplayName("처음으로 등록하는 시간의 id는 1이다.")
|
||||
void firstPost() {
|
||||
String adminAccessTokenCookie = getAdminAccessTokenCookieByLogin("email@email.com", "password");
|
||||
|
||||
RestAssured.given().log().all()
|
||||
.contentType(ContentType.JSON)
|
||||
.port(port)
|
||||
.header(new Header("Cookie", adminAccessTokenCookie))
|
||||
.body(params)
|
||||
.when().post("/times")
|
||||
.then().log().all()
|
||||
.statusCode(201)
|
||||
.body("data.id", is(1))
|
||||
.header("Location", "/times/1");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("아무 시간도 등록 하지 않은 경우, 시간 목록 조회 결과 개수는 0개이다.")
|
||||
void readEmptyTimes() {
|
||||
String adminAccessTokenCookie = getAdminAccessTokenCookieByLogin("email@email.com", "password");
|
||||
|
||||
RestAssured.given().log().all()
|
||||
.port(port)
|
||||
.header(new Header("Cookie", adminAccessTokenCookie))
|
||||
.when().get("/times")
|
||||
.then().log().all()
|
||||
.statusCode(200)
|
||||
.body("data.times.size()", is(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("하나의 시간만 등록한 경우, 시간 목록 조회 결과 개수는 1개이다.")
|
||||
void readTimesSizeAfterFirstPost() {
|
||||
String adminAccessTokenCookie = getAdminAccessTokenCookieByLogin("email@email.com", "password");
|
||||
|
||||
RestAssured.given().log().all()
|
||||
.contentType(ContentType.JSON)
|
||||
.port(port)
|
||||
.header(new Header("Cookie", adminAccessTokenCookie))
|
||||
.body(params)
|
||||
.when().post("/times")
|
||||
.then().log().all()
|
||||
.statusCode(201)
|
||||
.body("data.id", is(1))
|
||||
.header("Location", "/times/1");
|
||||
|
||||
RestAssured.given().log().all()
|
||||
.port(port)
|
||||
.header(new Header("Cookie", adminAccessTokenCookie))
|
||||
.when().get("/times")
|
||||
.then().log().all()
|
||||
.statusCode(200)
|
||||
.body("data.times.size()", is(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("하나의 시간만 등록한 경우, 시간 삭제 뒤 시간 목록 조회 결과 개수는 0개이다.")
|
||||
void readTimesSizeAfterPostAndDelete() {
|
||||
String adminAccessTokenCookie = getAdminAccessTokenCookieByLogin("email@email.com", "password");
|
||||
|
||||
RestAssured.given().log().all()
|
||||
.contentType(ContentType.JSON)
|
||||
.port(port)
|
||||
.header(new Header("Cookie", adminAccessTokenCookie))
|
||||
.body(params)
|
||||
.when().post("/times")
|
||||
.then().log().all()
|
||||
.statusCode(201)
|
||||
.body("data.id", is(1))
|
||||
.header("Location", "/times/1");
|
||||
|
||||
RestAssured.given().log().all()
|
||||
.port(port)
|
||||
.header(new Header("Cookie", adminAccessTokenCookie))
|
||||
.when().delete("/times/1")
|
||||
.then().log().all()
|
||||
.statusCode(204);
|
||||
|
||||
RestAssured.given().log().all()
|
||||
.port(port)
|
||||
.header(new Header("Cookie", adminAccessTokenCookie))
|
||||
.when().get("/times")
|
||||
.then().log().all()
|
||||
.statusCode(200)
|
||||
.body("data.times.size()", is(0));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("validateRequestDataFormatSource")
|
||||
@DisplayName("예약 시간 생성 시, 시간 요청 데이터에 시간 포맷이 아닌 값이 입력되어오면 400 에러를 발생한다.")
|
||||
void validateRequestDataFormat(Map<String, String> request) {
|
||||
String adminAccessTokenCookie = getAdminAccessTokenCookieByLogin("email@email.com", "password");
|
||||
|
||||
RestAssured.given().log().all()
|
||||
.contentType(ContentType.JSON)
|
||||
.header(new Header("Cookie", adminAccessTokenCookie))
|
||||
.port(port)
|
||||
.body(request)
|
||||
.when().post("/times")
|
||||
.then().log().all()
|
||||
.statusCode(400);
|
||||
}
|
||||
|
||||
static Stream<Map<String, String>> validateRequestDataFormatSource() {
|
||||
return Stream.of(
|
||||
Map.of(
|
||||
"startAt", "24:59"
|
||||
),
|
||||
Map.of(
|
||||
"startAt", "hihi")
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("validateBlankRequestSource")
|
||||
@DisplayName("예약 시간 생성 시, 요청 값에 공백 또는 null이 포함되어 있으면 400 에러를 발생한다.")
|
||||
void validateBlankRequest(Map<String, String> request) {
|
||||
String adminAccessTokenCookie = getAdminAccessTokenCookieByLogin("email@email.com", "password");
|
||||
|
||||
RestAssured.given().log().all()
|
||||
.contentType(ContentType.JSON)
|
||||
.header(new Header("Cookie", adminAccessTokenCookie))
|
||||
.port(port)
|
||||
.body(request)
|
||||
.when().post("/times")
|
||||
.then().log().all()
|
||||
.statusCode(400);
|
||||
}
|
||||
|
||||
static Stream<Map<String, String>> validateBlankRequestSource() {
|
||||
return Stream.of(
|
||||
Map.of(
|
||||
),
|
||||
Map.of(
|
||||
"startAt", ""
|
||||
),
|
||||
Map.of(
|
||||
"startAt", " "
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private String getAdminAccessTokenCookieByLogin(String email, String password) {
|
||||
memberRepository.save(new Member(null, "이름", email, password, Role.ADMIN));
|
||||
|
||||
Map<String, String> loginParams = Map.of(
|
||||
"email", email,
|
||||
"password", password
|
||||
);
|
||||
|
||||
String accessToken = RestAssured.given().log().all()
|
||||
.contentType(ContentType.JSON)
|
||||
.port(port)
|
||||
.body(loginParams)
|
||||
.when().post("/login")
|
||||
.then().log().all().extract().cookie("accessToken");
|
||||
|
||||
return "accessToken=" + accessToken;
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("특정 날짜의 특정 테마 예약 현황을 조회한다.")
|
||||
void readReservationByDateAndThemeId() {
|
||||
// given
|
||||
LocalDate today = LocalDate.now();
|
||||
ReservationTime reservationTime1 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 0)));
|
||||
ReservationTime reservationTime2 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30)));
|
||||
ReservationTime reservationTime3 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(18, 30)));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명1", "설명", "썸네일URL"));
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
|
||||
reservationRepository.save(
|
||||
new Reservation(today.plusDays(1), reservationTime1, theme, member, ReservationStatus.CONFIRMED));
|
||||
reservationRepository.save(
|
||||
new Reservation(today.plusDays(1), reservationTime2, theme, member, ReservationStatus.CONFIRMED));
|
||||
reservationRepository.save(
|
||||
new Reservation(today.plusDays(1), reservationTime3, theme, member, ReservationStatus.CONFIRMED));
|
||||
|
||||
// when & then
|
||||
RestAssured.given().log().all()
|
||||
.contentType(ContentType.JSON)
|
||||
.port(port)
|
||||
.header("Cookie", getAdminAccessTokenCookieByLogin("a@a.a", "a"))
|
||||
.when().get("/times/filter?date={date}&themeId={themeId}", today.plusDays(1).toString(), theme.getId())
|
||||
.then().log().all()
|
||||
.statusCode(200)
|
||||
.body("data.reservationTimes.size()", is(3));
|
||||
}
|
||||
}
|
||||
@ -1,55 +0,0 @@
|
||||
package roomescape.reservation.domain;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import roomescape.member.infrastructure.persistence.Member;
|
||||
import roomescape.member.infrastructure.persistence.Role;
|
||||
import roomescape.common.exception.RoomescapeException;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity;
|
||||
|
||||
public class ReservationTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("validateConstructorParameterBlankSource")
|
||||
@DisplayName("객체 생성 시, null 또는 공백이 존재하면 예외를 발생한다.")
|
||||
void validateConstructorParameterBlank(LocalDate date, ReservationTime reservationTime, ThemeEntity theme,
|
||||
Member member) {
|
||||
|
||||
// when & then
|
||||
Assertions.assertThatThrownBy(
|
||||
() -> new Reservation(date, reservationTime, theme, member, ReservationStatus.CONFIRMED))
|
||||
.isInstanceOf(RoomescapeException.class);
|
||||
}
|
||||
|
||||
static Stream<Arguments> validateConstructorParameterBlankSource() {
|
||||
return Stream.of(
|
||||
Arguments.of(null,
|
||||
new ReservationTime(LocalTime.now().plusHours(1)),
|
||||
new ThemeEntity(null, "테마명", "설명", "썸네일URI"),
|
||||
new Member(null, "name", "email@email.com", "password", Role.MEMBER)),
|
||||
Arguments.of(
|
||||
LocalDate.now(),
|
||||
null,
|
||||
new ThemeEntity(null, "테마명", "설명", "썸네일URI"),
|
||||
new Member(null, "name", "email@email.com", "password", Role.MEMBER)),
|
||||
Arguments.of(
|
||||
LocalDate.now(),
|
||||
new ReservationTime(LocalTime.now().plusHours(1)),
|
||||
null,
|
||||
new Member(null, "name", "email@email.com", "password", Role.MEMBER)),
|
||||
Arguments.of(
|
||||
LocalDate.now(),
|
||||
new ReservationTime(LocalTime.now().plusHours(1)),
|
||||
new ThemeEntity(null, "테마명", "설명", "썸네일URI"),
|
||||
null)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
package roomescape.reservation.domain;
|
||||
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import roomescape.common.exception.RoomescapeException;
|
||||
|
||||
class ReservationTimeTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("객체 생성 시, null이 존재하면 예외를 발생한다.")
|
||||
void validateConstructorParameterNull() {
|
||||
|
||||
// when & then
|
||||
Assertions.assertThatThrownBy(() -> new ReservationTime(null))
|
||||
.isInstanceOf(RoomescapeException.class);
|
||||
}
|
||||
}
|
||||
@ -1,175 +0,0 @@
|
||||
package roomescape.reservation.domain.repository;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
|
||||
import roomescape.member.infrastructure.persistence.Member;
|
||||
import roomescape.member.infrastructure.persistence.MemberRepository;
|
||||
import roomescape.member.infrastructure.persistence.Role;
|
||||
import roomescape.reservation.domain.Reservation;
|
||||
import roomescape.reservation.domain.ReservationStatus;
|
||||
import roomescape.reservation.domain.ReservationTime;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeRepository;
|
||||
|
||||
@DataJpaTest
|
||||
class ReservationSearchSpecificationTest {
|
||||
|
||||
@Autowired
|
||||
private ReservationRepository reservationRepository;
|
||||
|
||||
@Autowired
|
||||
private ReservationTimeRepository timeRepository;
|
||||
|
||||
@Autowired
|
||||
private ThemeRepository themeRepository;
|
||||
|
||||
@Autowired
|
||||
private MemberRepository memberRepository;
|
||||
|
||||
/**
|
||||
* 시간은 모두 현재 시간(LocalTime.now()), 테마, 회원은 동일 확정된 예약은 오늘, 결제 대기인 예약은 어제, 대기 상태인 예약은 내일
|
||||
*/
|
||||
// 현재 시간으로 확정 예약
|
||||
private Reservation reservation1;
|
||||
// 확정되었으나 결제 대기인 하루 전 예약
|
||||
private Reservation reservation2;
|
||||
// 대기 상태인 내일 예약
|
||||
private Reservation reservation3;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
LocalDateTime dateTime = LocalDateTime.now();
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
ReservationTime time = timeRepository.save(new ReservationTime(dateTime.toLocalTime()));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "description", "thumbnail"));
|
||||
|
||||
reservation1 = reservationRepository.save(
|
||||
new Reservation(dateTime.toLocalDate(), time, theme, member, ReservationStatus.CONFIRMED));
|
||||
reservation2 = reservationRepository.save(
|
||||
new Reservation(dateTime.toLocalDate().minusDays(1), time, theme, member,
|
||||
ReservationStatus.CONFIRMED_PAYMENT_REQUIRED));
|
||||
reservation3 = reservationRepository.save(
|
||||
new Reservation(dateTime.toLocalDate().plusDays(1), time, theme, member, ReservationStatus.WAITING));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("동일한 테마의 예약을 찾는다.")
|
||||
void searchByThemeId() {
|
||||
// given
|
||||
Long themeId = reservation1.getTheme().getId();
|
||||
Specification<Reservation> spec = new ReservationSearchSpecification().sameThemeId(themeId).build();
|
||||
|
||||
// when
|
||||
List<Reservation> found = reservationRepository.findAll(spec);
|
||||
|
||||
// then
|
||||
assertThat(found).containsExactly(reservation1, reservation2, reservation3);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("동일한 회원의 예약을 찾는다.")
|
||||
void searchByMemberId() {
|
||||
// given
|
||||
Long memberId = reservation1.getMember().getId();
|
||||
Specification<Reservation> spec = new ReservationSearchSpecification().sameMemberId(memberId).build();
|
||||
|
||||
// when
|
||||
List<Reservation> found = reservationRepository.findAll(spec);
|
||||
|
||||
// then
|
||||
assertThat(found).containsExactly(reservation1, reservation2, reservation3);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("동일한 시간의 예약을 찾는다.")
|
||||
void searchByTimeId() {
|
||||
// given
|
||||
Long timeId = reservation1.getReservationTime().getId();
|
||||
Specification<Reservation> spec = new ReservationSearchSpecification().sameTimeId(timeId).build();
|
||||
|
||||
// when
|
||||
List<Reservation> found = reservationRepository.findAll(spec);
|
||||
|
||||
// then
|
||||
assertThat(found).containsExactly(reservation1, reservation2, reservation3);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("동일한 날짜의 예약을 찾는다.")
|
||||
void searchByDate() {
|
||||
// given
|
||||
LocalDate date = reservation1.getDate();
|
||||
Specification<Reservation> spec = new ReservationSearchSpecification().sameDate(date).build();
|
||||
|
||||
// when
|
||||
List<Reservation> found = reservationRepository.findAll(spec);
|
||||
|
||||
// then
|
||||
assertThat(found).containsExactly(reservation1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("확정 상태인 예약을 찾는다.")
|
||||
void searchConfirmedReservation() {
|
||||
// given
|
||||
Specification<Reservation> spec = new ReservationSearchSpecification().confirmed().build();
|
||||
|
||||
// when
|
||||
List<Reservation> found = reservationRepository.findAll(spec);
|
||||
|
||||
// then
|
||||
assertThat(found).containsExactly(reservation1, reservation2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("대기 중인 예약을 찾는다.")
|
||||
void searchWaitingReservation() {
|
||||
// given
|
||||
Specification<Reservation> spec = new ReservationSearchSpecification().waiting().build();
|
||||
|
||||
// when
|
||||
List<Reservation> found = reservationRepository.findAll(spec);
|
||||
|
||||
// then
|
||||
assertThat(found).containsExactly(reservation3);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("특정 날짜 이후의 예약을 찾는다.")
|
||||
void searchDateStartFrom() {
|
||||
// given : 어제 이후의 예약을 조회하면, 모든 예약이 조회되어야 한다.
|
||||
LocalDate date = LocalDate.now().minusDays(1L);
|
||||
Specification<Reservation> spec = new ReservationSearchSpecification().dateStartFrom(date).build();
|
||||
|
||||
// when
|
||||
List<Reservation> found = reservationRepository.findAll(spec);
|
||||
|
||||
// then
|
||||
assertThat(found).containsExactly(reservation1, reservation2, reservation3);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("특정 날짜 이전의 예약을 찾는다.")
|
||||
void searchDateEndAt() {
|
||||
// given : 내일 이전의 예약을 조회하면, 모든 예약이 조회되어야 한다.
|
||||
LocalDate date = LocalDate.now().plusDays(1L);
|
||||
Specification<Reservation> spec = new ReservationSearchSpecification().dateEndAt(date).build();
|
||||
|
||||
// when
|
||||
List<Reservation> found = reservationRepository.findAll(spec);
|
||||
|
||||
// then
|
||||
assertThat(found).containsExactly(reservation1, reservation2, reservation3);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,200 @@
|
||||
package roomescape.reservation.infrastructure.persistence
|
||||
|
||||
import io.kotest.assertions.assertSoftly
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.collections.shouldHaveSize
|
||||
import io.kotest.matchers.shouldBe
|
||||
import jakarta.persistence.EntityManager
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import roomescape.payment.infrastructure.persistence.PaymentEntity
|
||||
import roomescape.reservation.web.MyReservationResponse
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||
import roomescape.util.PaymentFixture
|
||||
import roomescape.util.ReservationFixture
|
||||
import roomescape.util.ReservationTimeFixture
|
||||
import roomescape.util.ThemeFixture
|
||||
|
||||
@DataJpaTest
|
||||
class ReservationRepositoryTest(
|
||||
val entityManager: EntityManager,
|
||||
val reservationRepository: ReservationRepository,
|
||||
) : FunSpec() {
|
||||
init {
|
||||
context("findByReservationTime") {
|
||||
val time = ReservationTimeFixture.create()
|
||||
|
||||
beforeTest {
|
||||
listOf(
|
||||
ReservationFixture.create(reservationTime = time),
|
||||
ReservationFixture.create(reservationTime = ReservationTimeFixture.create(
|
||||
startAt = time.startAt.plusSeconds(1)
|
||||
))
|
||||
).forEach {
|
||||
persistReservation(it)
|
||||
}
|
||||
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}
|
||||
|
||||
test("입력된 시간과 일치하는 예약을 반환한다.") {
|
||||
assertSoftly(reservationRepository.findByReservationTime(time)) {
|
||||
it shouldHaveSize 1
|
||||
assertSoftly(it.first().reservationTime.startAt) { result ->
|
||||
result.hour shouldBe time.startAt.hour
|
||||
result.minute shouldBe time.startAt.minute
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("findByDateAndThemeId") {
|
||||
val date = ReservationFixture.create().date
|
||||
lateinit var theme1: ThemeEntity
|
||||
lateinit var theme2: ThemeEntity
|
||||
|
||||
beforeTest {
|
||||
theme1 = ThemeFixture.create(name = "theme1").also {
|
||||
entityManager.persist(it)
|
||||
}
|
||||
|
||||
theme2 = ThemeFixture.create(name = "theme2").also {
|
||||
entityManager.persist(it)
|
||||
}
|
||||
|
||||
listOf(
|
||||
ReservationFixture.create(date = date, theme = theme1),
|
||||
ReservationFixture.create(date = date.plusDays(1), theme = theme1),
|
||||
ReservationFixture.create(date = date, theme = theme2),
|
||||
).forEach {
|
||||
entityManager.persist(it.reservationTime)
|
||||
entityManager.persist(it.member)
|
||||
entityManager.persist(it)
|
||||
}
|
||||
}
|
||||
|
||||
test("입력된 날짜와 테마 ID에 해당하는 예약을 반환한다.") {
|
||||
assertSoftly(reservationRepository.findByDateAndThemeId(date, theme1.id!!)) {
|
||||
it shouldHaveSize 1
|
||||
it.first().theme shouldBe theme1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("updateStatusByReservationId") {
|
||||
lateinit var reservation: ReservationEntity
|
||||
|
||||
beforeTest {
|
||||
reservation = ReservationFixture.create().also {
|
||||
persistReservation(it)
|
||||
}
|
||||
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}
|
||||
|
||||
test("예약 상태를 업데이트한다.") {
|
||||
ReservationStatus.entries.forEach {
|
||||
val reservationId = reservation.id!!
|
||||
val updatedRows = reservationRepository.updateStatusByReservationId(reservationId, it)
|
||||
updatedRows shouldBe 1
|
||||
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
|
||||
reservationRepository.findByIdOrNull(reservationId)?.also { updated ->
|
||||
updated.reservationStatus shouldBe it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("isExistConfirmedReservation") {
|
||||
lateinit var waiting: ReservationEntity
|
||||
lateinit var confirmed: ReservationEntity
|
||||
lateinit var confirmedPaymentRequired: ReservationEntity
|
||||
|
||||
beforeTest {
|
||||
waiting = ReservationFixture.create(status = ReservationStatus.WAITING).also {
|
||||
persistReservation(it)
|
||||
}
|
||||
|
||||
confirmed = ReservationFixture.create(status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED).also {
|
||||
persistReservation(it)
|
||||
}
|
||||
|
||||
confirmedPaymentRequired = ReservationFixture.create(status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED).also {
|
||||
persistReservation(it)
|
||||
}
|
||||
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}
|
||||
|
||||
test("예약이 없으면 false를 반환한다.") {
|
||||
val maxId: Long = listOf(waiting, confirmed, confirmedPaymentRequired)
|
||||
.maxOfOrNull { it.id ?: 0L } ?: 0L
|
||||
reservationRepository.isExistConfirmedReservation(maxId + 1L) shouldBe false
|
||||
}
|
||||
|
||||
test("예약이 대기중이면 false를 반환한다.") {
|
||||
reservationRepository.isExistConfirmedReservation(waiting.id!!) shouldBe false
|
||||
}
|
||||
|
||||
test("예약이 결제 완료 상태이면 true를 반환한다.") {
|
||||
reservationRepository.isExistConfirmedReservation(confirmed.id!!) shouldBe true
|
||||
}
|
||||
|
||||
test("예약이 결제 대기 상태이면 true를 반환한다.") {
|
||||
reservationRepository.isExistConfirmedReservation(confirmedPaymentRequired.id!!) shouldBe true
|
||||
}
|
||||
}
|
||||
|
||||
context("findMyReservations") {
|
||||
lateinit var reservation: ReservationEntity
|
||||
|
||||
beforeTest {
|
||||
reservation = ReservationFixture.create()
|
||||
persistReservation(reservation)
|
||||
}
|
||||
|
||||
test("결제 정보를 포함한 회원의 예약 목록을 반환한다.") {
|
||||
val payment: PaymentEntity = PaymentFixture.create(
|
||||
reservation = reservation
|
||||
).also {
|
||||
entityManager.persist(it)
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}
|
||||
|
||||
val result: List<MyReservationResponse> = reservationRepository.findMyReservations(reservation.member.id!!)
|
||||
|
||||
result shouldHaveSize 1
|
||||
assertSoftly(result.first()) {
|
||||
it.id shouldBe reservation.id
|
||||
it.paymentKey shouldBe payment.paymentKey
|
||||
it.amount shouldBe payment.totalAmount
|
||||
}
|
||||
}
|
||||
|
||||
test("결제 정보가 없다면 paymentKey와 amount는 null로 반환한다.") {
|
||||
val result: List<MyReservationResponse> = reservationRepository.findMyReservations(reservation.member.id!!)
|
||||
|
||||
result shouldHaveSize 1
|
||||
assertSoftly(result.first()) {
|
||||
it.id shouldBe reservation.id
|
||||
it.paymentKey shouldBe null
|
||||
it.amount shouldBe null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun persistReservation(reservation: ReservationEntity) {
|
||||
entityManager.persist(reservation.reservationTime)
|
||||
entityManager.persist(reservation.theme)
|
||||
entityManager.persist(reservation.member)
|
||||
entityManager.persist(reservation)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,179 @@
|
||||
package roomescape.reservation.infrastructure.persistence
|
||||
|
||||
import io.kotest.assertions.assertSoftly
|
||||
import io.kotest.core.spec.style.StringSpec
|
||||
import io.kotest.matchers.collections.shouldContainExactly
|
||||
import io.kotest.matchers.collections.shouldHaveSize
|
||||
import jakarta.persistence.EntityManager
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||
import roomescape.util.MemberFixture
|
||||
import roomescape.util.ReservationFixture
|
||||
import roomescape.util.ReservationTimeFixture
|
||||
import roomescape.util.ThemeFixture
|
||||
import java.time.LocalDate
|
||||
|
||||
@DataJpaTest
|
||||
class ReservationSearchSpecificationTest(
|
||||
val entityManager: EntityManager,
|
||||
val reservationRepository: ReservationRepository
|
||||
) : StringSpec() {
|
||||
|
||||
init {
|
||||
lateinit var confirmedNow: ReservationEntity
|
||||
lateinit var confirmedNotPaidYesterday: ReservationEntity
|
||||
lateinit var waitingTomorrow: ReservationEntity
|
||||
lateinit var member: MemberEntity
|
||||
lateinit var reservationTime: ReservationTimeEntity
|
||||
lateinit var theme: ThemeEntity
|
||||
|
||||
"동일한 테마의 예약을 조회한다" {
|
||||
val spec = ReservationSearchSpecification()
|
||||
.sameThemeId(theme.id)
|
||||
.build()
|
||||
|
||||
val results: List<ReservationEntity> = reservationRepository.findAll(spec)
|
||||
|
||||
assertSoftly(results) {
|
||||
this shouldHaveSize 3
|
||||
this shouldContainExactly listOf(confirmedNow, confirmedNotPaidYesterday, waitingTomorrow)
|
||||
}
|
||||
}
|
||||
|
||||
"동일한 회원의 예약을 조회한다" {
|
||||
val spec = ReservationSearchSpecification()
|
||||
.sameMemberId(member.id)
|
||||
.build()
|
||||
|
||||
val results: List<ReservationEntity> = reservationRepository.findAll(spec)
|
||||
|
||||
assertSoftly(results) {
|
||||
this shouldHaveSize 3
|
||||
this shouldContainExactly listOf(confirmedNow, confirmedNotPaidYesterday, waitingTomorrow)
|
||||
}
|
||||
}
|
||||
|
||||
"동일한 예약 시간의 예약을 조회한다" {
|
||||
val spec = ReservationSearchSpecification()
|
||||
.sameTimeId(reservationTime.id)
|
||||
.build()
|
||||
|
||||
val results: List<ReservationEntity> = reservationRepository.findAll(spec)
|
||||
|
||||
assertSoftly(results) {
|
||||
this shouldHaveSize 3
|
||||
this shouldContainExactly listOf(confirmedNow, confirmedNotPaidYesterday, waitingTomorrow)
|
||||
}
|
||||
}
|
||||
|
||||
"동일한 날짜의 예약을 조회한다" {
|
||||
val spec = ReservationSearchSpecification()
|
||||
.sameDate(LocalDate.now())
|
||||
.build()
|
||||
|
||||
val results: List<ReservationEntity> = reservationRepository.findAll(spec)
|
||||
|
||||
assertSoftly(results) {
|
||||
this shouldHaveSize 1
|
||||
this shouldContainExactly listOf(confirmedNow)
|
||||
}
|
||||
}
|
||||
|
||||
"확정 상태인 예약을 조회한다" {
|
||||
val spec = ReservationSearchSpecification()
|
||||
.confirmed()
|
||||
.build()
|
||||
|
||||
val results: List<ReservationEntity> = reservationRepository.findAll(spec)
|
||||
|
||||
assertSoftly(results) {
|
||||
this shouldHaveSize 2
|
||||
this shouldContainExactly listOf(confirmedNow, confirmedNotPaidYesterday)
|
||||
}
|
||||
}
|
||||
|
||||
"대기 상태인 예약을 조회한다" {
|
||||
val spec = ReservationSearchSpecification()
|
||||
.waiting()
|
||||
.build()
|
||||
|
||||
val results: List<ReservationEntity> = reservationRepository.findAll(spec)
|
||||
|
||||
assertSoftly(results) {
|
||||
this shouldHaveSize 1
|
||||
this shouldContainExactly listOf(waitingTomorrow)
|
||||
}
|
||||
}
|
||||
|
||||
"예약 날짜가 오늘 이후인 예약을 조회한다" {
|
||||
val spec = ReservationSearchSpecification()
|
||||
.dateStartFrom(LocalDate.now())
|
||||
.build()
|
||||
|
||||
val results: List<ReservationEntity> = reservationRepository.findAll(spec)
|
||||
|
||||
assertSoftly(results) {
|
||||
this shouldHaveSize 2
|
||||
this shouldContainExactly listOf(confirmedNow, waitingTomorrow)
|
||||
}
|
||||
}
|
||||
|
||||
"예약 날짜가 내일 이전인 예약을 조회한다" {
|
||||
val spec = ReservationSearchSpecification()
|
||||
.dateEndAt(LocalDate.now().plusDays(1))
|
||||
.build()
|
||||
|
||||
val results: List<ReservationEntity> = reservationRepository.findAll(spec)
|
||||
|
||||
assertSoftly(results) {
|
||||
this shouldHaveSize 3
|
||||
this shouldContainExactly listOf(confirmedNow, confirmedNotPaidYesterday, waitingTomorrow)
|
||||
}
|
||||
}
|
||||
|
||||
beforeTest {
|
||||
member = MemberFixture.create().also {
|
||||
entityManager.persist(it)
|
||||
}
|
||||
reservationTime = ReservationTimeFixture.create().also {
|
||||
entityManager.persist(it)
|
||||
}
|
||||
theme = ThemeFixture.create().also {
|
||||
entityManager.persist(it)
|
||||
}
|
||||
|
||||
confirmedNow = ReservationFixture.create(
|
||||
reservationTime = reservationTime,
|
||||
member = member,
|
||||
theme = theme,
|
||||
date = LocalDate.now(),
|
||||
status = ReservationStatus.CONFIRMED
|
||||
).also {
|
||||
entityManager.persist(it)
|
||||
}
|
||||
|
||||
confirmedNotPaidYesterday = ReservationFixture.create(
|
||||
reservationTime = reservationTime,
|
||||
member = member,
|
||||
theme = theme,
|
||||
date = LocalDate.now().minusDays(1),
|
||||
status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
||||
).also {
|
||||
entityManager.persist(it)
|
||||
}
|
||||
|
||||
waitingTomorrow = ReservationFixture.create(
|
||||
reservationTime = reservationTime,
|
||||
member = member,
|
||||
theme = theme,
|
||||
date = LocalDate.now().plusDays(1),
|
||||
status = ReservationStatus.WAITING
|
||||
).also {
|
||||
entityManager.persist(it)
|
||||
}
|
||||
|
||||
entityManager.flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package roomescape.reservation.infrastructure.persistence
|
||||
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import jakarta.persistence.EntityManager
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||
import roomescape.util.ReservationTimeFixture
|
||||
import java.time.LocalTime
|
||||
|
||||
@DataJpaTest
|
||||
class ReservationTimeRepositoryTest(
|
||||
val entityManager: EntityManager,
|
||||
val reservationTimeRepository: ReservationTimeRepository,
|
||||
) : FunSpec({
|
||||
|
||||
context("existsByStartAt") {
|
||||
val startAt = LocalTime.of(10, 0)
|
||||
|
||||
beforeTest {
|
||||
entityManager.persist(ReservationTimeFixture.create(startAt = startAt))
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}
|
||||
|
||||
test("동일한 시간이 있으면 true 반환") {
|
||||
reservationTimeRepository.existsByStartAt(startAt) shouldBe true
|
||||
}
|
||||
|
||||
test("동일한 시간이 없으면 false 반환") {
|
||||
reservationTimeRepository.existsByStartAt(startAt.plusSeconds(1)) shouldBe false
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -1,219 +0,0 @@
|
||||
package roomescape.reservation.service;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.jdbc.Sql;
|
||||
import org.springframework.test.context.jdbc.Sql.ExecutionPhase;
|
||||
|
||||
import roomescape.member.business.MemberService;
|
||||
import roomescape.member.infrastructure.persistence.Member;
|
||||
import roomescape.member.infrastructure.persistence.MemberRepository;
|
||||
import roomescape.member.infrastructure.persistence.Role;
|
||||
import roomescape.reservation.domain.Reservation;
|
||||
import roomescape.reservation.domain.ReservationStatus;
|
||||
import roomescape.reservation.domain.ReservationTime;
|
||||
import roomescape.reservation.domain.repository.ReservationRepository;
|
||||
import roomescape.reservation.domain.repository.ReservationTimeRepository;
|
||||
import roomescape.reservation.dto.request.ReservationRequest;
|
||||
import roomescape.reservation.dto.request.WaitingRequest;
|
||||
import roomescape.reservation.dto.response.ReservationResponse;
|
||||
import roomescape.common.exception.RoomescapeException;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeRepository;
|
||||
import roomescape.theme.business.ThemeService;
|
||||
|
||||
@SpringBootTest
|
||||
@Sql(scripts = "/truncate.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD)
|
||||
@Import({ReservationService.class, MemberService.class, ReservationTimeService.class, ThemeService.class})
|
||||
class ReservationServiceTest {
|
||||
|
||||
@Autowired
|
||||
ReservationTimeRepository reservationTimeRepository;
|
||||
@Autowired
|
||||
ReservationRepository reservationRepository;
|
||||
@Autowired
|
||||
ThemeRepository themeRepository;
|
||||
@Autowired
|
||||
MemberRepository memberRepository;
|
||||
@Autowired
|
||||
private ReservationService reservationService;
|
||||
|
||||
@Test
|
||||
@DisplayName("예약을 추가할때 이미 예약이 존재하면 예외가 발생한다.")
|
||||
void reservationAlreadyExistFail() {
|
||||
// given
|
||||
ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 30)));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member member1 = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
Member member2 = memberRepository.save(new Member(null, "name2", "email2@email.com", "password", Role.MEMBER));
|
||||
LocalDate date = LocalDate.now().plusDays(1L);
|
||||
|
||||
// when
|
||||
reservationService.addReservation(
|
||||
new ReservationRequest(date, reservationTime.getId(), theme.getId(), "paymentKey", "orderId",
|
||||
1000L, "paymentType"), member2.getId());
|
||||
|
||||
// then
|
||||
assertThatThrownBy(() -> reservationService.addReservation(
|
||||
new ReservationRequest(date, reservationTime.getId(), theme.getId(), "paymentKey", "orderId",
|
||||
1000L, "paymentType"), member1.getId()))
|
||||
.isInstanceOf(RoomescapeException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("이미 예약한 멤버가 같은 테마에 대기를 신청하면 예외가 발생한다.")
|
||||
void requestWaitWhenAlreadyReserveFail() {
|
||||
// given
|
||||
ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 30)));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
LocalDate date = LocalDate.now().plusDays(1L);
|
||||
|
||||
// when
|
||||
reservationService.addReservation(
|
||||
new ReservationRequest(date, reservationTime.getId(), theme.getId(), "paymentKey", "orderId",
|
||||
1000L, "paymentType"), member.getId());
|
||||
|
||||
// then
|
||||
assertThatThrownBy(() -> reservationService.addWaiting(
|
||||
new WaitingRequest(date, reservationTime.getId(), theme.getId()), member.getId()))
|
||||
.isInstanceOf(RoomescapeException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("예약 대기를 두 번 이상 요청하면 예외가 발생한다.")
|
||||
void requestWaitTwiceFail() {
|
||||
// given
|
||||
ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 30)));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
Member member1 = memberRepository.save(new Member(null, "name1", "email1@email.com", "password", Role.MEMBER));
|
||||
LocalDate date = LocalDate.now().plusDays(1L);
|
||||
|
||||
// when
|
||||
reservationService.addReservation(
|
||||
new ReservationRequest(date, reservationTime.getId(), theme.getId(), "paymentKey", "orderId",
|
||||
1000L, "paymentType"), member.getId());
|
||||
|
||||
reservationService.addWaiting(
|
||||
new WaitingRequest(date, reservationTime.getId(), theme.getId()), member1.getId());
|
||||
|
||||
// then
|
||||
assertThatThrownBy(() -> reservationService.addWaiting(
|
||||
new WaitingRequest(date, reservationTime.getId(), theme.getId()), member1.getId()))
|
||||
.isInstanceOf(RoomescapeException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("이미 지난 날짜로 예약을 생성하면 예외가 발생한다.")
|
||||
void beforeDateReservationFail() {
|
||||
// given
|
||||
ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 30)));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
LocalDate beforeDate = LocalDate.now().minusDays(1L);
|
||||
|
||||
// when & then
|
||||
assertThatThrownBy(() -> reservationService.addReservation(
|
||||
new ReservationRequest(beforeDate, reservationTime.getId(), theme.getId(), "paymentKey", "orderId",
|
||||
1000L, "paymentType"), member.getId()))
|
||||
.isInstanceOf(RoomescapeException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("현재 날짜가 예약 당일이지만, 이미 지난 시간으로 예약을 생성하면 예외가 발생한다.")
|
||||
void beforeTimeReservationFail() {
|
||||
// given
|
||||
LocalDateTime beforeTime = LocalDateTime.now().minusHours(1L).withNano(0);
|
||||
ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(beforeTime.toLocalTime()));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
|
||||
// when & then
|
||||
assertThatThrownBy(() -> reservationService.addReservation(
|
||||
new ReservationRequest(beforeTime.toLocalDate(), reservationTime.getId(), theme.getId(), "paymentKey",
|
||||
"orderId", 1000L, "paymentType"), member.getId()))
|
||||
.isInstanceOf(RoomescapeException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("존재하지 않는 회원이 예약을 생성하려고 하면 예외가 발생한다.")
|
||||
void notExistMemberReservationFail() {
|
||||
// given
|
||||
LocalDateTime beforeTime = LocalDateTime.now().minusDays(1L).withNano(0);
|
||||
ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(beforeTime.toLocalTime()));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Long NotExistMemberId = 1L;
|
||||
|
||||
// when & then
|
||||
assertThatThrownBy(() -> reservationService.addReservation(
|
||||
new ReservationRequest(beforeTime.toLocalDate(), reservationTime.getId(), theme.getId(), "paymentKey",
|
||||
"orderId", 1000L, "paymentType"),
|
||||
NotExistMemberId))
|
||||
.isInstanceOf(RoomescapeException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("예약을 조회할 때 종료 날짜가 시작 날짜 이전이면 예외가 발생한다.")
|
||||
void invalidDateRange() {
|
||||
// given
|
||||
LocalDate dateFrom = LocalDate.now().plusDays(1);
|
||||
LocalDate dateTo = LocalDate.now();
|
||||
|
||||
// when & then
|
||||
assertThatThrownBy(() -> reservationService.findFilteredReservations(null, null, dateFrom, dateTo))
|
||||
.isInstanceOf(RoomescapeException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("대기중인 예약을 승인할 때, 기존에 예약이 존재하면 예외가 발생한다.")
|
||||
void confirmWaitingWhenReservationExist() {
|
||||
// given
|
||||
ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 30)));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member admin = memberRepository.save(new Member(null, "admin", "admin@email.com", "password", Role.ADMIN));
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
Member member1 = memberRepository.save(new Member(null, "name1", "email1@email.com", "password", Role.MEMBER));
|
||||
|
||||
reservationService.addReservation(
|
||||
new ReservationRequest(LocalDate.now().plusDays(1L), reservationTime.getId(), theme.getId(),
|
||||
"paymentKey", "orderId",
|
||||
1000L, "paymentType"), member.getId());
|
||||
ReservationResponse waiting = reservationService.addWaiting(
|
||||
new WaitingRequest(LocalDate.now().plusDays(1L), reservationTime.getId(), theme.getId()),
|
||||
member1.getId());
|
||||
|
||||
// when & then
|
||||
assertThatThrownBy(() -> reservationService.approveWaiting(waiting.id(), admin.getId()))
|
||||
.isInstanceOf(RoomescapeException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("대기중인 예약을 확정한다.")
|
||||
void approveWaiting() {
|
||||
// given
|
||||
ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 30)));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member admin = memberRepository.save(new Member(null, "admin", "admin@email.com", "password", Role.ADMIN));
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
|
||||
// when
|
||||
ReservationResponse waiting = reservationService.addWaiting(
|
||||
new WaitingRequest(LocalDate.now().plusDays(1L), reservationTime.getId(), theme.getId()),
|
||||
member.getId());
|
||||
reservationService.approveWaiting(waiting.id(), admin.getId());
|
||||
|
||||
// then
|
||||
Reservation confirmed = reservationRepository.findById(waiting.id()).get();
|
||||
assertThat(confirmed.getReservationStatus()).isEqualTo(ReservationStatus.CONFIRMED_PAYMENT_REQUIRED);
|
||||
}
|
||||
}
|
||||
@ -1,88 +0,0 @@
|
||||
package roomescape.reservation.service;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.jdbc.Sql;
|
||||
import org.springframework.test.context.jdbc.Sql.ExecutionPhase;
|
||||
|
||||
import roomescape.member.infrastructure.persistence.Member;
|
||||
import roomescape.member.infrastructure.persistence.MemberRepository;
|
||||
import roomescape.member.infrastructure.persistence.Role;
|
||||
import roomescape.reservation.domain.Reservation;
|
||||
import roomescape.reservation.domain.ReservationStatus;
|
||||
import roomescape.reservation.domain.ReservationTime;
|
||||
import roomescape.reservation.domain.repository.ReservationRepository;
|
||||
import roomescape.reservation.domain.repository.ReservationTimeRepository;
|
||||
import roomescape.reservation.dto.request.ReservationTimeRequest;
|
||||
import roomescape.common.exception.RoomescapeException;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeRepository;
|
||||
|
||||
@SpringBootTest
|
||||
@Import(ReservationTimeService.class)
|
||||
@Sql(scripts = "/truncate.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD)
|
||||
class ReservationTimeServiceTest {
|
||||
|
||||
@Autowired
|
||||
private ReservationTimeService reservationTimeService;
|
||||
@Autowired
|
||||
private ReservationTimeRepository reservationTimeRepository;
|
||||
@Autowired
|
||||
private ReservationRepository reservationRepository;
|
||||
@Autowired
|
||||
private ThemeRepository themeRepository;
|
||||
@Autowired
|
||||
private MemberRepository memberRepository;
|
||||
|
||||
@Test
|
||||
@DisplayName("중복된 예약 시간을 등록하는 경우 예외가 발생한다.")
|
||||
void duplicateTimeFail() {
|
||||
// given
|
||||
reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 30)));
|
||||
|
||||
// when & then
|
||||
assertThatThrownBy(() -> reservationTimeService.addTime(new ReservationTimeRequest(LocalTime.of(12, 30))))
|
||||
.isInstanceOf(RoomescapeException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("존재하지 않는 ID로 시간을 조회하면 예외가 발생한다.")
|
||||
void findTimeByIdFail() {
|
||||
// given
|
||||
ReservationTime saved = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 30)));
|
||||
|
||||
// when
|
||||
Long invalidTimeId = saved.getId() + 1;
|
||||
|
||||
// when & then
|
||||
assertThatThrownBy(() -> reservationTimeService.findTimeById(invalidTimeId))
|
||||
.isInstanceOf(RoomescapeException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("삭제하려는 시간에 예약이 존재하면 예외를 발생한다.")
|
||||
void usingTimeDeleteFail() {
|
||||
// given
|
||||
LocalDateTime localDateTime = LocalDateTime.now().plusDays(1L).withNano(0);
|
||||
ReservationTime reservationTime = reservationTimeRepository.save(
|
||||
new ReservationTime(localDateTime.toLocalTime()));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "테마명", "설명", "썸네일URL"));
|
||||
Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER));
|
||||
|
||||
// when
|
||||
reservationRepository.save(new Reservation(localDateTime.toLocalDate(), reservationTime, theme, member,
|
||||
ReservationStatus.CONFIRMED));
|
||||
|
||||
// then
|
||||
assertThatThrownBy(() -> reservationTimeService.removeTimeById(reservationTime.getId()))
|
||||
.isInstanceOf(RoomescapeException.class);
|
||||
}
|
||||
}
|
||||
@ -1,162 +0,0 @@
|
||||
package roomescape.reservation.service;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.jdbc.Sql;
|
||||
import org.springframework.test.context.jdbc.Sql.ExecutionPhase;
|
||||
|
||||
import roomescape.member.infrastructure.persistence.Member;
|
||||
import roomescape.member.infrastructure.persistence.MemberRepository;
|
||||
import roomescape.member.infrastructure.persistence.Role;
|
||||
import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository;
|
||||
import roomescape.payment.infrastructure.persistence.PaymentEntity;
|
||||
import roomescape.payment.infrastructure.persistence.PaymentRepository;
|
||||
import roomescape.payment.web.PaymentApprove;
|
||||
import roomescape.payment.web.PaymentCancel;
|
||||
import roomescape.reservation.domain.Reservation;
|
||||
import roomescape.reservation.domain.ReservationStatus;
|
||||
import roomescape.reservation.domain.ReservationTime;
|
||||
import roomescape.reservation.domain.repository.ReservationRepository;
|
||||
import roomescape.reservation.domain.repository.ReservationTimeRepository;
|
||||
import roomescape.reservation.dto.request.ReservationRequest;
|
||||
import roomescape.reservation.dto.response.ReservationResponse;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity;
|
||||
import roomescape.theme.infrastructure.persistence.ThemeRepository;
|
||||
|
||||
@SpringBootTest
|
||||
@Sql(scripts = "/truncate.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD)
|
||||
class ReservationWithPaymentServiceTest {
|
||||
|
||||
@Autowired
|
||||
private ReservationWithPaymentService reservationWithPaymentService;
|
||||
@Autowired
|
||||
private ReservationRepository reservationRepository;
|
||||
@Autowired
|
||||
private MemberRepository memberRepository;
|
||||
@Autowired
|
||||
private ReservationTimeRepository reservationTimeRepository;
|
||||
@Autowired
|
||||
private ThemeRepository themeRepository;
|
||||
@Autowired
|
||||
private PaymentRepository paymentRepository;
|
||||
@Autowired
|
||||
private CanceledPaymentRepository canceledPaymentRepository;
|
||||
|
||||
@Test
|
||||
@DisplayName("예약과 결제 정보를 추가한다.")
|
||||
void addReservationWithPayment() {
|
||||
// given
|
||||
PaymentApprove.Response paymentInfo = new PaymentApprove.Response("payment-key", "order-id",
|
||||
OffsetDateTime.now(), 10000L);
|
||||
LocalDateTime localDateTime = LocalDateTime.now().plusDays(1L).withNano(0);
|
||||
LocalDate date = localDateTime.toLocalDate();
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime()));
|
||||
Member member = memberRepository.save(new Member(null, "member", "email@email.com", "password", Role.MEMBER));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "desc", "thumbnail"));
|
||||
ReservationRequest reservationRequest = new ReservationRequest(date, time.getId(), theme.getId(), "payment-key",
|
||||
"order-id", 10000L, "NORMAL");
|
||||
|
||||
// when
|
||||
ReservationResponse reservationResponse = reservationWithPaymentService.addReservationWithPayment(
|
||||
reservationRequest, paymentInfo, member.getId());
|
||||
|
||||
// then
|
||||
reservationRepository.findById(reservationResponse.id())
|
||||
.ifPresent(reservation -> {
|
||||
assertThat(reservation.getMember().getId()).isEqualTo(member.getId());
|
||||
assertThat(reservation.getTheme().getId()).isEqualTo(theme.getId());
|
||||
assertThat(reservation.getDate()).isEqualTo(date);
|
||||
assertThat(reservation.getReservationTime().getId()).isEqualTo(time.getId());
|
||||
assertThat(reservation.getReservationStatus()).isEqualTo(ReservationStatus.CONFIRMED);
|
||||
});
|
||||
|
||||
PaymentEntity payment = paymentRepository.findByPaymentKey("payment-key");
|
||||
assertThat(payment).isNotNull();
|
||||
assertThat(payment.getReservation().getId()).isEqualTo(reservationResponse.id());
|
||||
assertThat(payment.getPaymentKey()).isEqualTo("payment-key");
|
||||
assertThat(payment.getOrderId()).isEqualTo("order-id");
|
||||
assertThat(payment.getTotalAmount()).isEqualTo(10000L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("예약 ID를 이용하여 예약과 결제 정보를 제거하고, 결제 취소 정보를 저장한다.")
|
||||
void removeReservationWithPayment() {
|
||||
// given
|
||||
PaymentApprove.Response paymentInfo = new PaymentApprove.Response("payment-key", "order-id",
|
||||
OffsetDateTime.now(), 10000L);
|
||||
LocalDateTime localDateTime = LocalDateTime.now().plusDays(1L).withNano(0);
|
||||
LocalDate date = localDateTime.toLocalDate();
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime()));
|
||||
Member member = memberRepository.save(new Member(null, "member", "admin@email.com", "password", Role.ADMIN));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "desc", "thumbnail"));
|
||||
ReservationRequest reservationRequest = new ReservationRequest(date, time.getId(), theme.getId(), "payment-key",
|
||||
"order-id", 10000L, "NORMAL");
|
||||
|
||||
ReservationResponse reservationResponse = reservationWithPaymentService.addReservationWithPayment(
|
||||
reservationRequest, paymentInfo, member.getId());
|
||||
|
||||
// when
|
||||
PaymentCancel.Request paymentCancelRequest = reservationWithPaymentService.removeReservationWithPayment(
|
||||
reservationResponse.id(), member.getId());
|
||||
|
||||
// then
|
||||
assertThat(paymentCancelRequest.cancelReason).isEqualTo("고객 요청");
|
||||
assertThat(reservationRepository.findById(reservationResponse.id())).isEmpty();
|
||||
assertThat(paymentRepository.findByPaymentKey("payment-key")).isNull();
|
||||
assertThat(canceledPaymentRepository.findByPaymentKey("payment-key")).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("결제 정보가 없으면 True를 반환한다.")
|
||||
void isNotPaidReservation() {
|
||||
// given
|
||||
PaymentApprove.Response paymentInfo = new PaymentApprove.Response("payment-key", "order-id",
|
||||
OffsetDateTime.now(), 10000L);
|
||||
LocalDateTime localDateTime = LocalDateTime.now().plusHours(1L);
|
||||
LocalDate date = localDateTime.toLocalDate();
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime()));
|
||||
Member member = memberRepository.save(new Member(null, "member", "admin@email.com", "password", Role.ADMIN));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "desc", "thumbnail"));
|
||||
|
||||
Reservation saved = reservationRepository.save(
|
||||
new Reservation(date, time, theme, member, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED));
|
||||
|
||||
// when
|
||||
boolean result = reservationWithPaymentService.isNotPaidReservation(saved.getId());
|
||||
|
||||
// then
|
||||
assertThat(result).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("결제 정보가 있으면 False를 반환한다.")
|
||||
void isPaidReservation() {
|
||||
// given
|
||||
PaymentApprove.Response paymentInfo = new PaymentApprove.Response("payment-key", "order-id",
|
||||
OffsetDateTime.now(), 10000L);
|
||||
LocalDateTime localDateTime = LocalDateTime.now().plusDays(1L).withNano(0);
|
||||
LocalDate date = localDateTime.toLocalDate();
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime()));
|
||||
Member member = memberRepository.save(new Member(null, "member", "admin@email.com", "password", Role.ADMIN));
|
||||
ThemeEntity theme = themeRepository.save(new ThemeEntity(null, "name", "desc", "thumbnail"));
|
||||
ReservationRequest reservationRequest = new ReservationRequest(date, time.getId(), theme.getId(), "payment-key",
|
||||
"order-id", 10000L, "NORMAL");
|
||||
|
||||
ReservationResponse reservationResponse = reservationWithPaymentService.addReservationWithPayment(
|
||||
reservationRequest, paymentInfo, member.getId());
|
||||
|
||||
// when
|
||||
boolean result = reservationWithPaymentService.isNotPaidReservation(reservationResponse.id());
|
||||
|
||||
// then
|
||||
assertThat(result).isFalse();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,792 @@
|
||||
package roomescape.reservation.web
|
||||
|
||||
import com.ninjasquad.springmockk.MockkBean
|
||||
import com.ninjasquad.springmockk.SpykBean
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.mockk.every
|
||||
import io.restassured.module.kotlin.extensions.Given
|
||||
import io.restassured.module.kotlin.extensions.Then
|
||||
import io.restassured.module.kotlin.extensions.When
|
||||
import jakarta.persistence.EntityManager
|
||||
import org.hamcrest.Matchers.containsString
|
||||
import org.hamcrest.Matchers.equalTo
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.boot.test.web.server.LocalServerPort
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.transaction.support.TransactionTemplate
|
||||
import roomescape.auth.web.support.AdminInterceptor
|
||||
import roomescape.auth.web.support.LoginInterceptor
|
||||
import roomescape.auth.web.support.MemberIdResolver
|
||||
import roomescape.common.exception.ErrorType
|
||||
import roomescape.common.exception.RoomescapeException
|
||||
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||
import roomescape.member.infrastructure.persistence.Role
|
||||
import roomescape.payment.infrastructure.client.TossPaymentClient
|
||||
import roomescape.payment.infrastructure.persistence.PaymentEntity
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationTimeEntity
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||
import roomescape.util.*
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
class ReservationControllerTest(
|
||||
@LocalServerPort val port: Int,
|
||||
val entityManager: EntityManager,
|
||||
val transactionTemplate: TransactionTemplate
|
||||
) : FunSpec({
|
||||
extension(DatabaseCleanerExtension(mode = CleanerMode.AFTER_EACH_TEST))
|
||||
}) {
|
||||
@MockkBean
|
||||
lateinit var paymentClient: TossPaymentClient
|
||||
|
||||
@SpykBean
|
||||
lateinit var loginInterceptor: LoginInterceptor
|
||||
|
||||
@SpykBean
|
||||
lateinit var adminInterceptor: AdminInterceptor
|
||||
|
||||
@SpykBean
|
||||
lateinit var memberIdResolver: MemberIdResolver
|
||||
|
||||
init {
|
||||
context("POST /reservations") {
|
||||
lateinit var member: MemberEntity
|
||||
beforeTest {
|
||||
member = login(MemberFixture.create(role = Role.MEMBER))
|
||||
}
|
||||
|
||||
test("정상 응답") {
|
||||
val reservationRequest = createRequest()
|
||||
val paymentApproveResponse = PaymentFixture.createApproveResponse().copy(
|
||||
paymentKey = reservationRequest.paymentKey,
|
||||
orderId = reservationRequest.orderId,
|
||||
totalAmount = reservationRequest.amount,
|
||||
)
|
||||
|
||||
every {
|
||||
paymentClient.confirmPayment(any())
|
||||
} returns paymentApproveResponse
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
body(reservationRequest)
|
||||
}.When {
|
||||
post("/reservations")
|
||||
}.Then {
|
||||
log().all()
|
||||
statusCode(201)
|
||||
body("data.date", equalTo(reservationRequest.date.toString()))
|
||||
body("data.status", equalTo(ReservationStatus.CONFIRMED.name))
|
||||
}
|
||||
}
|
||||
|
||||
test("결제 과정에서 발생하는 에러는 그대로 응답") {
|
||||
val reservationRequest = createRequest()
|
||||
val paymentException = RoomescapeException(
|
||||
ErrorType.PAYMENT_SERVER_ERROR,
|
||||
HttpStatus.INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
every {
|
||||
paymentClient.confirmPayment(any())
|
||||
} throws paymentException
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
body(reservationRequest)
|
||||
}.When {
|
||||
post("/reservations")
|
||||
}.Then {
|
||||
log().all()
|
||||
statusCode(paymentException.httpStatus.value())
|
||||
body("errorType", equalTo(paymentException.errorType.name))
|
||||
}
|
||||
}
|
||||
|
||||
test("결제 완료 후 예약 / 결제 정보 저장 과정에서 에러 발생시 결제 취소 후 에러 응답") {
|
||||
val reservationRequest = createRequest()
|
||||
val paymentApproveResponse = PaymentFixture.createApproveResponse().copy(
|
||||
paymentKey = reservationRequest.paymentKey,
|
||||
orderId = reservationRequest.orderId,
|
||||
totalAmount = reservationRequest.amount,
|
||||
)
|
||||
|
||||
every {
|
||||
paymentClient.confirmPayment(any())
|
||||
} returns paymentApproveResponse
|
||||
|
||||
// 예약 저장 과정에서 테마가 없는 예외
|
||||
val invalidRequest = reservationRequest.copy(themeId = reservationRequest.themeId + 1)
|
||||
val expectedException = RoomescapeException(ErrorType.THEME_NOT_FOUND, HttpStatus.BAD_REQUEST)
|
||||
|
||||
every {
|
||||
paymentClient.cancelPayment(any())
|
||||
} returns PaymentFixture.createCancelResponse()
|
||||
|
||||
val canceledPaymentSizeBeforeApiCall: Long = entityManager.createQuery(
|
||||
"SELECT COUNT(c) FROM CanceledPaymentEntity c",
|
||||
Long::class.java
|
||||
).singleResult
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
body(invalidRequest)
|
||||
}.When {
|
||||
post("/reservations")
|
||||
}.Then {
|
||||
log().all()
|
||||
statusCode(expectedException.httpStatus.value())
|
||||
body("errorType", equalTo(expectedException.errorType.name))
|
||||
}
|
||||
|
||||
val canceledPaymentSizeAfterApiCall: Long = entityManager.createQuery(
|
||||
"SELECT COUNT(c) FROM CanceledPaymentEntity c",
|
||||
Long::class.java
|
||||
).singleResult
|
||||
|
||||
canceledPaymentSizeAfterApiCall shouldBe canceledPaymentSizeBeforeApiCall + 1L
|
||||
}
|
||||
}
|
||||
|
||||
context("GET /reservations") {
|
||||
lateinit var reservations: MutableMap<MemberEntity, MutableList<ReservationEntity>>
|
||||
beforeTest {
|
||||
reservations = createDummyReservations()
|
||||
}
|
||||
|
||||
test("관리자이면 정상 응답") {
|
||||
login(MemberFixture.create(role = Role.ADMIN))
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
}.When {
|
||||
get("/reservations")
|
||||
}.Then {
|
||||
log().all()
|
||||
statusCode(200)
|
||||
body("data.reservations.size()", equalTo(reservations.values.sumOf { it.size }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("GET /reservations-mine") {
|
||||
lateinit var reservations: MutableMap<MemberEntity, MutableList<ReservationEntity>>
|
||||
beforeTest {
|
||||
reservations = createDummyReservations()
|
||||
}
|
||||
|
||||
test("로그인한 회원이 자신의 예약 목록을 조회한다.") {
|
||||
val member: MemberEntity = login(reservations.keys.first())
|
||||
val expectedReservations: Int = reservations[member]?.size ?: 0
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
}.When {
|
||||
get("/reservations-mine")
|
||||
}.Then {
|
||||
log().all()
|
||||
statusCode(200)
|
||||
body("data.reservations.size()", equalTo(expectedReservations))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("GET /reservations/search") {
|
||||
lateinit var reservations: MutableMap<MemberEntity, MutableList<ReservationEntity>>
|
||||
beforeTest {
|
||||
reservations = createDummyReservations()
|
||||
}
|
||||
|
||||
test("관리자만 검색할 수 있다.") {
|
||||
login(reservations.keys.first())
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
}.When {
|
||||
get("/reservations/search")
|
||||
}.Then {
|
||||
log().all()
|
||||
header(HttpHeaders.CONTENT_TYPE, containsString(MediaType.TEXT_HTML_VALUE))
|
||||
}
|
||||
}
|
||||
|
||||
test("파라미터를 지정하지 않으면 전체 목록 응답") {
|
||||
login(MemberFixture.create(role = Role.ADMIN))
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
}.When {
|
||||
get("/reservations/search")
|
||||
}.Then {
|
||||
log().all()
|
||||
statusCode(200)
|
||||
body("data.reservations.size()", equalTo(reservations.values.sumOf { it.size }))
|
||||
}
|
||||
}
|
||||
|
||||
test("시작 날짜가 종료 날짜 이전이면 예외 응답") {
|
||||
login(MemberFixture.create(role = Role.ADMIN))
|
||||
|
||||
val startDate = LocalDate.now().plusDays(1)
|
||||
val endDate = LocalDate.now()
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
param("dateFrom", startDate.toString())
|
||||
param("dateTo", endDate.toString())
|
||||
}.When {
|
||||
get("/reservations/search")
|
||||
}.Then {
|
||||
log().all()
|
||||
statusCode(HttpStatus.BAD_REQUEST.value())
|
||||
body("errorType", equalTo(ErrorType.INVALID_DATE_RANGE.name))
|
||||
}
|
||||
}
|
||||
|
||||
test("동일한 회원의 모든 예약 응답") {
|
||||
login(MemberFixture.create(role = Role.ADMIN))
|
||||
val member: MemberEntity = reservations.keys.first()
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
param("memberId", member.id)
|
||||
}.When {
|
||||
get("/reservations/search")
|
||||
}.Then {
|
||||
log().all()
|
||||
statusCode(200)
|
||||
body("data.reservations.size()", equalTo(reservations[member]?.size ?: 0))
|
||||
}
|
||||
}
|
||||
|
||||
test("동일한 테마의 모든 예약 응답") {
|
||||
login(MemberFixture.create(role = Role.ADMIN))
|
||||
val themes = reservations.values.flatten().map { it.theme }
|
||||
val requestThemeId: Long = themes.first().id!!
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
param("themeId", requestThemeId)
|
||||
}.When {
|
||||
get("/reservations/search")
|
||||
}.Then {
|
||||
log().all()
|
||||
statusCode(200)
|
||||
body("data.reservations.size()", equalTo(themes.filter { it.id == requestThemeId }.size))
|
||||
}
|
||||
}
|
||||
|
||||
test("시작 날짜와 종료 날짜 사이의 예약 응답") {
|
||||
login(MemberFixture.create(role = Role.ADMIN))
|
||||
val dateFrom: LocalDate = reservations.values.flatten().minOf { it.date }
|
||||
val dateTo: LocalDate = reservations.values.flatten().maxOf { it.date }
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
param("dateFrom", dateFrom.toString())
|
||||
param("dateTo", dateTo.toString())
|
||||
}.When {
|
||||
get("/reservations/search")
|
||||
}.Then {
|
||||
log().all()
|
||||
statusCode(200)
|
||||
body("data.reservations.size()", equalTo(reservations.values.sumOf { it.size }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("DELETE /reservations/{id}") {
|
||||
lateinit var reservations: MutableMap<MemberEntity, MutableList<ReservationEntity>>
|
||||
beforeTest {
|
||||
reservations = createDummyReservations()
|
||||
}
|
||||
|
||||
test("관리자만 예약을 삭제할 수 있다.") {
|
||||
login(MemberFixture.create(role = Role.MEMBER))
|
||||
val reservation: ReservationEntity = reservations.values.flatten().first()
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
}.When {
|
||||
delete("/reservations/${reservation.id}")
|
||||
}.Then {
|
||||
log().all()
|
||||
statusCode(302)
|
||||
header(HttpHeaders.LOCATION, containsString("/login"))
|
||||
}
|
||||
}
|
||||
|
||||
test("결제되지 않은 예약은 바로 제거") {
|
||||
login(MemberFixture.create(role = Role.ADMIN))
|
||||
val reservationId: Long = reservations.values.flatten().first().id!!
|
||||
|
||||
transactionTemplate.execute {
|
||||
val reservation: ReservationEntity = entityManager.find(
|
||||
ReservationEntity::class.java,
|
||||
reservationId
|
||||
)
|
||||
reservation.reservationStatus = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
||||
entityManager.persist(reservation)
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
}.When {
|
||||
delete("/reservations/$reservationId")
|
||||
}.Then {
|
||||
log().all()
|
||||
statusCode(HttpStatus.NO_CONTENT.value())
|
||||
}
|
||||
|
||||
// 예약이 삭제되었는지 확인
|
||||
transactionTemplate.executeWithoutResult {
|
||||
val deletedReservation = entityManager.find(
|
||||
ReservationEntity::class.java,
|
||||
reservationId
|
||||
)
|
||||
deletedReservation shouldBe null
|
||||
}
|
||||
}
|
||||
|
||||
test("결제된 예약은 취소 후 제거") {
|
||||
login(MemberFixture.create(role = Role.ADMIN))
|
||||
val reservation: ReservationEntity = reservations.values.flatten().first()
|
||||
lateinit var payment: PaymentEntity
|
||||
|
||||
transactionTemplate.execute {
|
||||
payment = PaymentFixture.create(reservation = reservation).also {
|
||||
entityManager.persist(it)
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}
|
||||
}
|
||||
|
||||
every {
|
||||
paymentClient.cancelPayment(any())
|
||||
} returns PaymentFixture.createCancelResponse()
|
||||
|
||||
val canceledPaymentSizeBeforeApiCall: Long = entityManager.createQuery(
|
||||
"SELECT COUNT(c) FROM CanceledPaymentEntity c",
|
||||
Long::class.java
|
||||
).singleResult
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
}.When {
|
||||
delete("/reservations/${reservation.id}")
|
||||
}.Then {
|
||||
log().all()
|
||||
statusCode(HttpStatus.NO_CONTENT.value())
|
||||
}
|
||||
|
||||
val canceledPaymentSizeAfterApiCall: Long = entityManager.createQuery(
|
||||
"SELECT COUNT(c) FROM CanceledPaymentEntity c",
|
||||
Long::class.java
|
||||
).singleResult
|
||||
|
||||
canceledPaymentSizeAfterApiCall shouldBe canceledPaymentSizeBeforeApiCall + 1L
|
||||
}
|
||||
}
|
||||
|
||||
context("POST /reservations/admin") {
|
||||
test("관리자가 예약을 추가하면 결제 대기 상태로 예약 생성") {
|
||||
val member = login(MemberFixture.create(role = Role.ADMIN))
|
||||
val adminRequest: AdminReservationRequest = createRequest().let {
|
||||
AdminReservationRequest(
|
||||
date = it.date,
|
||||
themeId = it.themeId,
|
||||
timeId = it.timeId,
|
||||
memberId = member.id!!,
|
||||
)
|
||||
}
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
body(adminRequest)
|
||||
}.When {
|
||||
post("/reservations/admin")
|
||||
}.Then {
|
||||
log().all()
|
||||
statusCode(201)
|
||||
body("data.status", equalTo(ReservationStatus.CONFIRMED_PAYMENT_REQUIRED.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("GET /reservations/waiting") {
|
||||
lateinit var reservations: MutableMap<MemberEntity, MutableList<ReservationEntity>>
|
||||
beforeTest {
|
||||
reservations = createDummyReservations()
|
||||
}
|
||||
|
||||
test("관리자가 아니면 조회할 수 없다.") {
|
||||
login(MemberFixture.create(role = Role.MEMBER))
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
}.When {
|
||||
get("/reservations/waiting")
|
||||
}.Then {
|
||||
log().all()
|
||||
header(HttpHeaders.CONTENT_TYPE, containsString(MediaType.TEXT_HTML_VALUE))
|
||||
}
|
||||
}
|
||||
|
||||
test("대기 중인 예약 목록을 조회한다.") {
|
||||
login(MemberFixture.create(role = Role.ADMIN))
|
||||
val expected = reservations.values.flatten()
|
||||
.count { it.reservationStatus == ReservationStatus.WAITING }
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
}.When {
|
||||
get("/reservations/waiting")
|
||||
}.Then {
|
||||
log().all()
|
||||
statusCode(200)
|
||||
body("data.reservations.size()", equalTo(expected))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("POST /reservations/waiting") {
|
||||
test("회원이 대기 예약을 추가한다.") {
|
||||
val member = login(MemberFixture.create(role = Role.MEMBER))
|
||||
val waitingRequest: WaitingRequest = createRequest().let {
|
||||
WaitingRequest(
|
||||
date = it.date,
|
||||
themeId = it.themeId,
|
||||
timeId = it.timeId
|
||||
)
|
||||
}
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
body(waitingRequest)
|
||||
}.When {
|
||||
post("/reservations/waiting")
|
||||
}.Then {
|
||||
log().all()
|
||||
statusCode(201)
|
||||
body("data.member.id", equalTo(member.id!!.toInt()))
|
||||
body("data.status", equalTo(ReservationStatus.WAITING.name))
|
||||
}
|
||||
}
|
||||
|
||||
test("이미 예약된 시간, 테마로 대기 예약 요청 시 예외 응답") {
|
||||
val member = login(MemberFixture.create(role = Role.MEMBER))
|
||||
val reservationRequest = createRequest()
|
||||
|
||||
transactionTemplate.executeWithoutResult {
|
||||
val reservation = ReservationFixture.create(
|
||||
date = reservationRequest.date,
|
||||
theme = entityManager.find(ThemeEntity::class.java, reservationRequest.themeId),
|
||||
reservationTime = entityManager.find(ReservationTimeEntity::class.java, reservationRequest.timeId),
|
||||
member = member,
|
||||
status = ReservationStatus.WAITING
|
||||
)
|
||||
entityManager.persist(reservation)
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}
|
||||
|
||||
// 이미 예약된 시간, 테마로 대기 예약 요청
|
||||
val waitingRequest = WaitingRequest(
|
||||
date = reservationRequest.date,
|
||||
themeId = reservationRequest.themeId,
|
||||
timeId = reservationRequest.timeId
|
||||
)
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
body(waitingRequest)
|
||||
}.When {
|
||||
post("/reservations/waiting")
|
||||
}.Then {
|
||||
log().all()
|
||||
statusCode(HttpStatus.BAD_REQUEST.value())
|
||||
body("errorType", equalTo(ErrorType.HAS_RESERVATION_OR_WAITING.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("DELETE /reservations/waiting/{id}") {
|
||||
lateinit var reservations: MutableMap<MemberEntity, MutableList<ReservationEntity>>
|
||||
beforeTest {
|
||||
reservations = createDummyReservations()
|
||||
}
|
||||
|
||||
test("대기 중인 예약을 취소한다.") {
|
||||
val member = login(MemberFixture.create(role = Role.MEMBER))
|
||||
val waiting: ReservationEntity = createSingleReservation(
|
||||
member = member,
|
||||
status = ReservationStatus.WAITING
|
||||
)
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
}.When {
|
||||
delete("/reservations/waiting/${waiting.id}")
|
||||
}.Then {
|
||||
log().all()
|
||||
statusCode(HttpStatus.NO_CONTENT.value())
|
||||
}
|
||||
|
||||
transactionTemplate.executeWithoutResult { _ ->
|
||||
entityManager.find(
|
||||
ReservationEntity::class.java,
|
||||
waiting.id
|
||||
) shouldBe null
|
||||
}
|
||||
}
|
||||
|
||||
test("이미 완료된 예약은 삭제할 수 없다.") {
|
||||
val member = login(MemberFixture.create(role = Role.MEMBER))
|
||||
val reservation: ReservationEntity = createSingleReservation(
|
||||
member = member,
|
||||
status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
||||
)
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
}.When {
|
||||
delete("/reservations/waiting/{id}", reservation.id)
|
||||
}.Then {
|
||||
log().all()
|
||||
body("errorType", equalTo(ErrorType.RESERVATION_NOT_FOUND.name))
|
||||
statusCode(HttpStatus.NOT_FOUND.value())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("POST /reservations/waiting/{id}/approve") {
|
||||
test("관리자만 승인할 수 있다.") {
|
||||
login(MemberFixture.create(role = Role.MEMBER))
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
}.When {
|
||||
post("/reservations/waiting/1/approve")
|
||||
}.Then {
|
||||
log().all()
|
||||
statusCode(302)
|
||||
header(HttpHeaders.LOCATION, containsString("/login"))
|
||||
}
|
||||
}
|
||||
|
||||
test("대기 예약을 승인하면 결제 대기 상태로 변경") {
|
||||
val member = login(MemberFixture.create(role = Role.ADMIN))
|
||||
val reservation = createSingleReservation(
|
||||
member = member,
|
||||
status = ReservationStatus.WAITING
|
||||
)
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
}.When {
|
||||
post("/reservations/waiting/${reservation.id!!}/approve")
|
||||
}.Then {
|
||||
log().all()
|
||||
statusCode(200)
|
||||
}
|
||||
|
||||
transactionTemplate.executeWithoutResult { _ ->
|
||||
entityManager.find(
|
||||
ReservationEntity::class.java,
|
||||
reservation.id
|
||||
)?.also {
|
||||
it.reservationStatus shouldBe ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
||||
} ?: throw AssertionError("Reservation not found")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("POST /reservations/waiting/{id}/deny") {
|
||||
test("관리자만 거절할 수 있다.") {
|
||||
login(MemberFixture.create(role = Role.MEMBER))
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
}.When {
|
||||
post("/reservations/waiting/1/deny")
|
||||
}.Then {
|
||||
log().all()
|
||||
statusCode(302)
|
||||
header(HttpHeaders.LOCATION, containsString("/login"))
|
||||
}
|
||||
}
|
||||
|
||||
test("거절된 예약은 삭제된다.") {
|
||||
val member = login(MemberFixture.create(role = Role.ADMIN))
|
||||
val reservation = createSingleReservation(
|
||||
member = member,
|
||||
status = ReservationStatus.WAITING
|
||||
)
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
}.When {
|
||||
post("/reservations/waiting/${reservation.id!!}/deny")
|
||||
}.Then {
|
||||
log().all()
|
||||
statusCode(204)
|
||||
}
|
||||
|
||||
transactionTemplate.executeWithoutResult { _ ->
|
||||
entityManager.find(
|
||||
ReservationEntity::class.java,
|
||||
reservation.id
|
||||
) shouldBe null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createSingleReservation(
|
||||
date: LocalDate = LocalDate.now().plusDays(1),
|
||||
time: LocalTime = LocalTime.now(),
|
||||
themeName: String = "Default Theme",
|
||||
member: MemberEntity = MemberFixture.create(role = Role.MEMBER),
|
||||
status: ReservationStatus = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
||||
): ReservationEntity {
|
||||
return ReservationFixture.create(
|
||||
date = date,
|
||||
theme = ThemeFixture.create(name = themeName),
|
||||
reservationTime = ReservationTimeFixture.create(startAt = time),
|
||||
member = member,
|
||||
status = status
|
||||
).also { it ->
|
||||
transactionTemplate.execute { _ ->
|
||||
if (member.id == null) {
|
||||
entityManager.persist(member)
|
||||
}
|
||||
entityManager.persist(it.reservationTime)
|
||||
entityManager.persist(it.theme)
|
||||
entityManager.persist(it)
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createDummyReservations(): MutableMap<MemberEntity, MutableList<ReservationEntity>> {
|
||||
val reservations: MutableMap<MemberEntity, MutableList<ReservationEntity>> = mutableMapOf()
|
||||
val members: List<MemberEntity> = listOf(
|
||||
MemberFixture.create(role = Role.MEMBER),
|
||||
MemberFixture.create(role = Role.MEMBER)
|
||||
)
|
||||
|
||||
transactionTemplate.executeWithoutResult {
|
||||
members.forEach { member ->
|
||||
entityManager.persist(member)
|
||||
}
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}
|
||||
|
||||
transactionTemplate.executeWithoutResult {
|
||||
repeat(10) { index ->
|
||||
val theme = ThemeFixture.create(name = "theme$index")
|
||||
val time = ReservationTimeFixture.create(startAt = LocalTime.now().plusMinutes(index.toLong()))
|
||||
entityManager.persist(theme)
|
||||
entityManager.persist(time)
|
||||
|
||||
val reservation = ReservationFixture.create(
|
||||
date = LocalDate.now().plusDays(index.toLong()),
|
||||
theme = theme,
|
||||
reservationTime = time,
|
||||
member = members[index % members.size],
|
||||
status = ReservationStatus.CONFIRMED
|
||||
)
|
||||
entityManager.persist(reservation)
|
||||
reservations.getOrPut(reservation.member) { mutableListOf() }.add(reservation)
|
||||
}
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}
|
||||
|
||||
return reservations
|
||||
}
|
||||
|
||||
fun createRequest(
|
||||
theme: ThemeEntity = ThemeFixture.create(),
|
||||
time: ReservationTimeEntity = ReservationTimeFixture.create(),
|
||||
): ReservationRequest {
|
||||
lateinit var reservationRequest: ReservationRequest
|
||||
|
||||
transactionTemplate.executeWithoutResult {
|
||||
entityManager.persist(theme)
|
||||
entityManager.persist(time)
|
||||
|
||||
reservationRequest = ReservationFixture.createRequest(
|
||||
themeId = theme.id!!,
|
||||
timeId = time.id!!,
|
||||
)
|
||||
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}
|
||||
|
||||
return reservationRequest
|
||||
}
|
||||
|
||||
fun login(member: MemberEntity): MemberEntity {
|
||||
if (member.id == null) {
|
||||
transactionTemplate.executeWithoutResult {
|
||||
entityManager.persist(member)
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}
|
||||
}
|
||||
|
||||
if (member.isAdmin()) {
|
||||
loginAsAdmin()
|
||||
} else {
|
||||
loginAsUser()
|
||||
}
|
||||
resolveMemberId(member.id!!)
|
||||
|
||||
return member
|
||||
}
|
||||
|
||||
private fun loginAsUser() {
|
||||
every {
|
||||
loginInterceptor.preHandle(any(), any(), any())
|
||||
} returns true
|
||||
}
|
||||
|
||||
private fun loginAsAdmin() {
|
||||
every {
|
||||
adminInterceptor.preHandle(any(), any(), any())
|
||||
} returns true
|
||||
}
|
||||
|
||||
private fun resolveMemberId(memberId: Long) {
|
||||
every {
|
||||
memberIdResolver.resolveArgument(any(), any(), any(), any())
|
||||
} returns memberId
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,306 @@
|
||||
package roomescape.reservation.web
|
||||
|
||||
import com.ninjasquad.springmockk.MockkBean
|
||||
import com.ninjasquad.springmockk.SpykBean
|
||||
import io.kotest.assertions.assertSoftly
|
||||
import io.kotest.matchers.collections.shouldHaveSize
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.mockk.every
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
|
||||
import org.springframework.context.annotation.Import
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import roomescape.common.config.JacksonConfig
|
||||
import roomescape.common.exception.ErrorType
|
||||
import roomescape.reservation.business.ReservationTimeService
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationRepository
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationTimeEntity
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationTimeRepository
|
||||
import roomescape.util.ReservationFixture
|
||||
import roomescape.util.ReservationTimeFixture
|
||||
import roomescape.util.RoomescapeApiTest
|
||||
import roomescape.util.ThemeFixture
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
|
||||
@WebMvcTest(ReservationTimeController::class)
|
||||
@Import(JacksonConfig::class)
|
||||
class ReservationTimeControllerTest(
|
||||
val mockMvc: MockMvc,
|
||||
) : RoomescapeApiTest() {
|
||||
|
||||
@SpykBean
|
||||
private lateinit var reservationTimeService: ReservationTimeService
|
||||
|
||||
@MockkBean
|
||||
private lateinit var reservationTimeRepository: ReservationTimeRepository
|
||||
|
||||
@MockkBean
|
||||
private lateinit var reservationRepository: ReservationRepository
|
||||
|
||||
init {
|
||||
Given("등록된 모든 시간을 조회할 때") {
|
||||
val endpoint = "/times"
|
||||
|
||||
When("관리자인 경우") {
|
||||
beforeTest {
|
||||
loginAsAdmin()
|
||||
}
|
||||
|
||||
Then("정상 응답") {
|
||||
every {
|
||||
reservationTimeRepository.findAll()
|
||||
} returns listOf(
|
||||
ReservationTimeFixture.create(id = 1L),
|
||||
ReservationTimeFixture.create(id = 2L)
|
||||
)
|
||||
|
||||
runGetTest(
|
||||
mockMvc = mockMvc,
|
||||
endpoint = endpoint,
|
||||
log = true
|
||||
) {
|
||||
status { isOk() }
|
||||
content {
|
||||
contentType(MediaType.APPLICATION_JSON)
|
||||
jsonPath("$.data.times[0].id") { value(1) }
|
||||
jsonPath("$.data.times[1].id") { value(2) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
When("관리자가 아닌 경우") {
|
||||
loginAsUser()
|
||||
|
||||
Then("로그인 페이지로 이동") {
|
||||
runGetTest(
|
||||
mockMvc = mockMvc,
|
||||
endpoint = endpoint,
|
||||
log = true
|
||||
) {
|
||||
status { is3xxRedirection() }
|
||||
header { string("Location", "/login") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Given("시간을 추가할 때") {
|
||||
val endpoint = "/times"
|
||||
|
||||
When("관리자인 경우") {
|
||||
beforeTest {
|
||||
loginAsAdmin()
|
||||
}
|
||||
val time = LocalTime.of(10, 0)
|
||||
val request = ReservationTimeRequest(startAt = time)
|
||||
|
||||
Then("시간 형식이 HH:mm이 아니거나, 범위를 벗어나면 400 응답") {
|
||||
listOf(
|
||||
"{\"startAt\": \"23:30:30\"}",
|
||||
"{\"startAt\": \"24:59\"}",
|
||||
).forEach {
|
||||
runPostTest(
|
||||
mockMvc = mockMvc,
|
||||
endpoint = endpoint,
|
||||
body = it,
|
||||
log = true
|
||||
) {
|
||||
status { isBadRequest() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Then("정상 응답") {
|
||||
every {
|
||||
reservationTimeService.addTime(request)
|
||||
} returns ReservationTimeResponse(id = 1, startAt = time)
|
||||
|
||||
runPostTest(
|
||||
mockMvc = mockMvc,
|
||||
endpoint = endpoint,
|
||||
body = request,
|
||||
log = true
|
||||
) {
|
||||
status { isCreated() }
|
||||
content {
|
||||
contentType(MediaType.APPLICATION_JSON)
|
||||
jsonPath("$.data.id") { value(1) }
|
||||
jsonPath("$.data.startAt") { value("10:00") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Then("동일한 시간이 존재하면 409 응답") {
|
||||
every {
|
||||
reservationTimeRepository.existsByStartAt(time)
|
||||
} returns true
|
||||
|
||||
runPostTest(
|
||||
mockMvc = mockMvc,
|
||||
endpoint = endpoint,
|
||||
body = request,
|
||||
log = true
|
||||
) {
|
||||
status { isConflict() }
|
||||
content {
|
||||
contentType(MediaType.APPLICATION_JSON)
|
||||
jsonPath("$.errorType") { value(ErrorType.TIME_DUPLICATED.name) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
When("관리자가 아닌 경우") {
|
||||
loginAsUser()
|
||||
|
||||
Then("로그인 페이지로 이동") {
|
||||
runPostTest(
|
||||
mockMvc = mockMvc,
|
||||
endpoint = endpoint,
|
||||
body = ReservationTimeFixture.create(),
|
||||
log = true
|
||||
) {
|
||||
status { is3xxRedirection() }
|
||||
header { string("Location", "/login") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Given("시간을 삭제할 때") {
|
||||
val endpoint = "/times/1"
|
||||
|
||||
When("관리자인 경우") {
|
||||
beforeTest {
|
||||
loginAsAdmin()
|
||||
}
|
||||
|
||||
Then("정상 응답") {
|
||||
every {
|
||||
reservationTimeService.removeTimeById(1L)
|
||||
} returns Unit
|
||||
|
||||
runDeleteTest(
|
||||
mockMvc = mockMvc,
|
||||
endpoint = endpoint,
|
||||
log = true
|
||||
) {
|
||||
status { isNoContent() }
|
||||
}
|
||||
}
|
||||
|
||||
Then("없는 시간을 조회하면 400 응답") {
|
||||
val id = 1L
|
||||
every {
|
||||
reservationTimeRepository.findByIdOrNull(id)
|
||||
} returns null
|
||||
|
||||
runDeleteTest(
|
||||
mockMvc = mockMvc,
|
||||
endpoint = "/times/$id",
|
||||
log = true
|
||||
) {
|
||||
status { isBadRequest() }
|
||||
content {
|
||||
contentType(MediaType.APPLICATION_JSON)
|
||||
jsonPath("$.errorType") { value(ErrorType.RESERVATION_TIME_NOT_FOUND.name) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Then("예약이 있는 시간을 삭제하면 409 응답") {
|
||||
val id = 1L
|
||||
every {
|
||||
reservationTimeRepository.findByIdOrNull(id)
|
||||
} returns ReservationTimeFixture.create(id = id)
|
||||
|
||||
every {
|
||||
reservationRepository.findByReservationTime(any())
|
||||
} returns listOf(ReservationFixture.create())
|
||||
|
||||
runDeleteTest(
|
||||
mockMvc = mockMvc,
|
||||
endpoint = "/times/$id",
|
||||
log = true
|
||||
) {
|
||||
status { isConflict() }
|
||||
content {
|
||||
contentType(MediaType.APPLICATION_JSON)
|
||||
jsonPath("$.errorType") { value(ErrorType.TIME_IS_USED_CONFLICT.name) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
When("관리자가 아닌 경우") {
|
||||
loginAsUser()
|
||||
|
||||
Then("로그인 페이지로 이동") {
|
||||
runDeleteTest(
|
||||
mockMvc = mockMvc,
|
||||
endpoint = endpoint,
|
||||
log = true
|
||||
) {
|
||||
status { is3xxRedirection() }
|
||||
header { string("Location", "/login") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Given("날짜, 테마가 주어졌을 때") {
|
||||
loginAsUser()
|
||||
|
||||
val date: LocalDate = LocalDate.now()
|
||||
val themeId = 1L
|
||||
|
||||
When("저장된 예약 시간이 있으면") {
|
||||
val times: List<ReservationTimeEntity> = listOf(
|
||||
ReservationTimeFixture.create(id = 1L, startAt = LocalTime.of(10, 0)),
|
||||
ReservationTimeFixture.create(id = 2L, startAt = LocalTime.of(11, 0))
|
||||
)
|
||||
|
||||
every {
|
||||
reservationTimeRepository.findAll()
|
||||
} returns times
|
||||
|
||||
Then("그 시간과, 해당 날짜와 테마에 대한 예약 여부가 담긴 목록을 응답") {
|
||||
|
||||
every {
|
||||
reservationRepository.findByDateAndThemeId(date, themeId)
|
||||
} returns listOf(
|
||||
ReservationFixture.create(
|
||||
id = 1L,
|
||||
date = date,
|
||||
theme = ThemeFixture.create(id = themeId),
|
||||
reservationTime = times[0]
|
||||
)
|
||||
)
|
||||
|
||||
val response = runGetTest(
|
||||
mockMvc = mockMvc,
|
||||
endpoint = "/times/filter?date=$date&themeId=$themeId",
|
||||
log = true
|
||||
) {
|
||||
status { isOk() }
|
||||
content {
|
||||
contentType(MediaType.APPLICATION_JSON)
|
||||
}
|
||||
}.andReturn().readValue(ReservationTimeInfosResponse::class.java)
|
||||
|
||||
assertSoftly(response.times) {
|
||||
this shouldHaveSize times.size
|
||||
this[0].id shouldBe times[0].id
|
||||
this[0].alreadyBooked shouldBe true
|
||||
|
||||
this[1].id shouldBe times[1].id
|
||||
this[1].alreadyBooked shouldBe false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,9 @@
|
||||
package roomescape.theme.util
|
||||
|
||||
import jakarta.persistence.EntityManager
|
||||
import roomescape.member.infrastructure.persistence.Member
|
||||
import roomescape.reservation.domain.ReservationStatus
|
||||
import roomescape.reservation.domain.ReservationTime
|
||||
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationTimeEntity
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||
import roomescape.util.MemberFixture
|
||||
import roomescape.util.ReservationFixture
|
||||
@ -20,16 +20,16 @@ object TestThemeCreateUtil {
|
||||
date: LocalDate,
|
||||
): ThemeEntity {
|
||||
val themeEntity: ThemeEntity = ThemeFixture.create(name = name).also { entityManager.persist(it) }
|
||||
val member: Member = MemberFixture.create().also { entityManager.persist(it) }
|
||||
val member: MemberEntity = MemberFixture.create().also { entityManager.persist(it) }
|
||||
|
||||
for (i in 1..reservedCount) {
|
||||
val time: ReservationTime = ReservationTimeFixture.create(
|
||||
val time: ReservationTimeEntity = ReservationTimeFixture.create(
|
||||
startAt = LocalTime.now().plusMinutes(i.toLong())
|
||||
).also { entityManager.persist(it) }
|
||||
|
||||
ReservationFixture.create(
|
||||
date = date,
|
||||
themeEntity = themeEntity,
|
||||
theme = themeEntity,
|
||||
member = member,
|
||||
reservationTime = time,
|
||||
status = ReservationStatus.CONFIRMED
|
||||
|
||||
@ -8,10 +8,11 @@ import jakarta.persistence.EntityManager
|
||||
import org.hamcrest.Matchers.equalTo
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.boot.test.web.server.LocalServerPort
|
||||
import org.springframework.test.context.jdbc.Sql
|
||||
import org.springframework.transaction.support.TransactionTemplate
|
||||
import roomescape.theme.business.ThemeService
|
||||
import roomescape.theme.util.TestThemeCreateUtil
|
||||
import roomescape.util.CleanerMode
|
||||
import roomescape.util.DatabaseCleanerExtension
|
||||
import java.time.LocalDate
|
||||
import kotlin.random.Random
|
||||
|
||||
@ -21,13 +22,14 @@ import kotlin.random.Random
|
||||
* 날짜 범위, 예약 수만 검증
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@Sql(value = ["/truncate.sql"], executionPhase = Sql.ExecutionPhase.AFTER_TEST_CLASS)
|
||||
class MostReservedThemeAPITest(
|
||||
@LocalServerPort val port: Int,
|
||||
val themeService: ThemeService,
|
||||
val transactionTemplate: TransactionTemplate,
|
||||
val entityManager: EntityManager,
|
||||
) : FunSpec() {
|
||||
) : FunSpec({
|
||||
extension(DatabaseCleanerExtension(mode = CleanerMode.AFTER_SPEC))
|
||||
}) {
|
||||
init {
|
||||
beforeSpec {
|
||||
transactionTemplate.executeWithoutResult {
|
||||
|
||||
@ -201,12 +201,13 @@ class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() {
|
||||
)
|
||||
|
||||
every {
|
||||
themeRepository.existsByName(request.name)
|
||||
} returns false
|
||||
|
||||
every {
|
||||
themeRepository.save(any())
|
||||
} returns theme
|
||||
themeService.save(request)
|
||||
} returns ThemeResponse(
|
||||
id = theme.id!!,
|
||||
name = theme.name,
|
||||
description = theme.description,
|
||||
thumbnail = theme.thumbnail
|
||||
)
|
||||
|
||||
Then("201 응답을 받는다.") {
|
||||
runPostTest(
|
||||
|
||||
64
src/test/java/roomescape/util/DatabaseCleaner.kt
Normal file
64
src/test/java/roomescape/util/DatabaseCleaner.kt
Normal file
@ -0,0 +1,64 @@
|
||||
package roomescape.util
|
||||
|
||||
import io.kotest.core.listeners.AfterSpecListener
|
||||
import io.kotest.core.listeners.AfterTestListener
|
||||
import io.kotest.core.spec.Spec
|
||||
import io.kotest.core.test.TestCase
|
||||
import io.kotest.core.test.TestResult
|
||||
import io.kotest.extensions.spring.testContextManager
|
||||
import jakarta.persistence.EntityManager
|
||||
import org.springframework.jdbc.core.JdbcTemplate
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
@Component
|
||||
class DatabaseCleaner(
|
||||
val entityManager: EntityManager,
|
||||
val jdbcTemplate: JdbcTemplate,
|
||||
) {
|
||||
val tables: List<String> by lazy {
|
||||
jdbcTemplate.query("SHOW TABLES") { rs, _ ->
|
||||
rs.getString(1).lowercase()
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
entityManager.clear()
|
||||
|
||||
jdbcTemplate.execute("SET REFERENTIAL_INTEGRITY FALSE")
|
||||
tables.forEach {
|
||||
jdbcTemplate.execute("TRUNCATE TABLE $it RESTART IDENTITY")
|
||||
}
|
||||
jdbcTemplate.execute("SET REFERENTIAL_INTEGRITY TRUE")
|
||||
}
|
||||
}
|
||||
|
||||
enum class CleanerMode {
|
||||
AFTER_EACH_TEST,
|
||||
AFTER_SPEC
|
||||
}
|
||||
|
||||
class DatabaseCleanerExtension(
|
||||
private val mode: CleanerMode
|
||||
) : AfterTestListener, AfterSpecListener {
|
||||
override suspend fun afterTest(testCase: TestCase, result: TestResult) {
|
||||
super.afterTest(testCase, result)
|
||||
when (mode) {
|
||||
CleanerMode.AFTER_EACH_TEST -> getCleaner().clear()
|
||||
CleanerMode.AFTER_SPEC -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun afterSpec(spec: Spec) {
|
||||
super.afterSpec(spec)
|
||||
when (mode) {
|
||||
CleanerMode.AFTER_EACH_TEST -> Unit
|
||||
CleanerMode.AFTER_SPEC -> getCleaner().clear()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getCleaner(): DatabaseCleaner {
|
||||
return testContextManager().testContext
|
||||
.applicationContext
|
||||
.getBean(DatabaseCleaner::class.java)
|
||||
}
|
||||
}
|
||||
@ -2,15 +2,17 @@ package roomescape.util
|
||||
|
||||
import roomescape.auth.infrastructure.jwt.JwtHandler
|
||||
import roomescape.auth.web.LoginRequest
|
||||
import roomescape.member.infrastructure.persistence.Member
|
||||
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||
import roomescape.member.infrastructure.persistence.Role
|
||||
import roomescape.payment.infrastructure.persistence.CanceledPaymentEntity
|
||||
import roomescape.payment.infrastructure.persistence.PaymentEntity
|
||||
import roomescape.payment.web.PaymentApprove
|
||||
import roomescape.payment.web.PaymentCancel
|
||||
import roomescape.reservation.domain.Reservation
|
||||
import roomescape.reservation.domain.ReservationStatus
|
||||
import roomescape.reservation.domain.ReservationTime
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationTimeEntity
|
||||
import roomescape.reservation.web.ReservationRequest
|
||||
import roomescape.reservation.web.WaitingRequest
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
@ -26,23 +28,25 @@ object MemberFixture {
|
||||
account: String = "default",
|
||||
password: String = "password",
|
||||
role: Role = Role.ADMIN
|
||||
): Member = Member(id, name, "$account@email.com", password, role)
|
||||
): MemberEntity = MemberEntity(id, name, "$account@email.com", password, role)
|
||||
|
||||
fun admin(): Member = create(
|
||||
fun admin(): MemberEntity = create(
|
||||
id = 2L,
|
||||
account = "admin",
|
||||
role = Role.ADMIN
|
||||
)
|
||||
|
||||
fun adminLoginRequest(): LoginRequest = LoginRequest(
|
||||
email = admin().email,
|
||||
password = admin().password
|
||||
)
|
||||
|
||||
fun user(): Member = create(
|
||||
fun user(): MemberEntity = create(
|
||||
id = 1L,
|
||||
account = "user",
|
||||
role = Role.MEMBER
|
||||
)
|
||||
|
||||
fun userLoginRequest(): LoginRequest = LoginRequest(
|
||||
email = user().email,
|
||||
password = user().password
|
||||
@ -53,7 +57,7 @@ object ReservationTimeFixture {
|
||||
fun create(
|
||||
id: Long? = null,
|
||||
startAt: LocalTime = LocalTime.now().plusHours(1),
|
||||
): ReservationTime = ReservationTime(id, startAt)
|
||||
): ReservationTimeEntity = ReservationTimeEntity(id, startAt)
|
||||
}
|
||||
|
||||
object ThemeFixture {
|
||||
@ -69,11 +73,39 @@ object ReservationFixture {
|
||||
fun create(
|
||||
id: Long? = null,
|
||||
date: LocalDate = LocalDate.now().plusWeeks(1),
|
||||
themeEntity: ThemeEntity = ThemeFixture.create(),
|
||||
reservationTime: ReservationTime = ReservationTimeFixture.create(),
|
||||
member: Member = MemberFixture.create(),
|
||||
theme: ThemeEntity = ThemeFixture.create(),
|
||||
reservationTime: ReservationTimeEntity = ReservationTimeFixture.create(),
|
||||
member: MemberEntity = MemberFixture.create(),
|
||||
status: ReservationStatus = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
||||
): Reservation = Reservation(id, date, reservationTime, themeEntity, member, status)
|
||||
): ReservationEntity = ReservationEntity(id, date, reservationTime, theme, member, status)
|
||||
|
||||
fun createRequest(
|
||||
date: LocalDate = LocalDate.now().plusWeeks(1),
|
||||
themeId: Long = 1L,
|
||||
timeId: Long = 1L,
|
||||
paymentKey: String = "paymentKey",
|
||||
orderId: String = "orderId",
|
||||
amount: Long = 10000L,
|
||||
paymentType: String = "NORMAL",
|
||||
): ReservationRequest = ReservationRequest(
|
||||
date = date,
|
||||
timeId = timeId,
|
||||
themeId = themeId,
|
||||
paymentKey = paymentKey,
|
||||
orderId = orderId,
|
||||
amount = amount,
|
||||
paymentType = paymentType
|
||||
)
|
||||
|
||||
fun createWaitingRequest(
|
||||
date: LocalDate = LocalDate.now().plusWeeks(1),
|
||||
themeId: Long = 1L,
|
||||
timeId: Long = 1L
|
||||
): WaitingRequest = WaitingRequest(
|
||||
date = date,
|
||||
timeId = timeId,
|
||||
themeId = themeId
|
||||
)
|
||||
}
|
||||
|
||||
object JwtFixture {
|
||||
@ -96,14 +128,14 @@ object PaymentFixture {
|
||||
orderId: String = ORDER_ID,
|
||||
paymentKey: String = PAYMENT_KEY,
|
||||
totalAmount: Long = AMOUNT,
|
||||
reservationId: Long = Random.nextLong(),
|
||||
reservation: ReservationEntity = ReservationFixture.create(id = 1L),
|
||||
approvedAt: OffsetDateTime = OffsetDateTime.now()
|
||||
): PaymentEntity = PaymentEntity(
|
||||
id = id,
|
||||
orderId = orderId,
|
||||
paymentKey = paymentKey,
|
||||
totalAmount = totalAmount,
|
||||
reservation = ReservationFixture.create(id = reservationId),
|
||||
reservation = reservation,
|
||||
approvedAt = approvedAt
|
||||
)
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package roomescape.util
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.ninjasquad.springmockk.MockkBean
|
||||
import com.ninjasquad.springmockk.SpykBean
|
||||
import io.kotest.core.spec.style.BehaviorSpec
|
||||
@ -15,10 +14,11 @@ import roomescape.auth.infrastructure.jwt.JwtHandler
|
||||
import roomescape.auth.web.support.AdminInterceptor
|
||||
import roomescape.auth.web.support.LoginInterceptor
|
||||
import roomescape.auth.web.support.MemberIdResolver
|
||||
import roomescape.common.config.JacksonConfig
|
||||
import roomescape.common.exception.ErrorType
|
||||
import roomescape.common.exception.RoomescapeException
|
||||
import roomescape.member.business.MemberService
|
||||
import roomescape.member.infrastructure.persistence.Member
|
||||
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||
import roomescape.member.infrastructure.persistence.MemberRepository
|
||||
import roomescape.util.MemberFixture.NOT_LOGGED_IN_USERID
|
||||
|
||||
@ -42,9 +42,9 @@ abstract class RoomescapeApiTest : BehaviorSpec() {
|
||||
@MockkBean
|
||||
lateinit var jwtHandler: JwtHandler
|
||||
|
||||
val objectMapper: ObjectMapper = jacksonObjectMapper()
|
||||
val admin: Member = MemberFixture.admin()
|
||||
val user: Member = MemberFixture.user()
|
||||
val objectMapper: ObjectMapper = JacksonConfig().objectMapper()
|
||||
val admin: MemberEntity = MemberFixture.admin()
|
||||
val user: MemberEntity = MemberFixture.user()
|
||||
|
||||
fun runGetTest(
|
||||
mockMvc: MockMvc,
|
||||
@ -71,9 +71,7 @@ abstract class RoomescapeApiTest : BehaviorSpec() {
|
||||
}
|
||||
}.apply {
|
||||
log.takeIf { it }?.let { this.andDo { print() } }
|
||||
}.andExpect {
|
||||
assert
|
||||
}
|
||||
}.andExpect(assert)
|
||||
|
||||
fun runDeleteTest(
|
||||
mockMvc: MockMvc,
|
||||
@ -84,9 +82,7 @@ abstract class RoomescapeApiTest : BehaviorSpec() {
|
||||
header(HttpHeaders.COOKIE, "accessToken=token")
|
||||
}.apply {
|
||||
log.takeIf { it }?.let { this.andDo { print() } }
|
||||
}.andExpect {
|
||||
assert
|
||||
}
|
||||
}.andExpect(assert)
|
||||
|
||||
fun loginAsAdmin() {
|
||||
every {
|
||||
|
||||
@ -1,194 +0,0 @@
|
||||
INSERT INTO member (name, password, email, role)
|
||||
VALUES ('이름', '12341234', 'test@test.com', 'MEMBER');
|
||||
INSERT INTO member (name, password, email, role)
|
||||
VALUES ('관리자', '12341234', 'admin@admin.com', 'ADMIN');
|
||||
|
||||
|
||||
-- 테마 목록 : 11개
|
||||
INSERT INTO theme (name, description, thumbnail)
|
||||
VALUES ('테마1', '재밌는 테마입니다',
|
||||
'https://www.google.co.kr/url?sa=i&url=http%3A%2F%2Fwww.code-k.co.kr%2Fsub%2Fcode_sub03.html%3FR_JIJEM%3DS1&psig=AOvVaw20fNjL28MSMMiR0Nb57Eh-&ust=1714695060162000&source=images&cd=vfe&opi=89978449&ved=0CBIQjRxqFwoTCOiO2oLX7YUDFQAAAAAdAAAAABAE');
|
||||
INSERT INTO theme (name, description, thumbnail)
|
||||
VALUES ('테마2', '재밌는 테마입니다',
|
||||
'https://www.google.co.kr/url?sa=i&url=http%3A%2F%2Fwww.code-k.co.kr%2Fsub%2Fcode_sub03.html%3FR_JIJEM%3DS1&psig=AOvVaw20fNjL28MSMMiR0Nb57Eh-&ust=1714695060162000&source=images&cd=vfe&opi=89978449&ved=0CBIQjRxqFwoTCOiO2oLX7YUDFQAAAAAdAAAAABAE');
|
||||
INSERT INTO theme (name, description, thumbnail)
|
||||
VALUES ('테마3', '재밌는 테마입니다',
|
||||
'https://www.google.co.kr/url?sa=i&url=http%3A%2F%2Fwww.code-k.co.kr%2Fsub%2Fcode_sub03.html%3FR_JIJEM%3DS1&psig=AOvVaw20fNjL28MSMMiR0Nb57Eh-&ust=1714695060162000&source=images&cd=vfe&opi=89978449&ved=0CBIQjRxqFwoTCOiO2oLX7YUDFQAAAAAdAAAAABAE');
|
||||
INSERT INTO theme (name, description, thumbnail)
|
||||
VALUES ('테마4', '재밌는 테마입니다',
|
||||
'https://www.google.co.kr/url?sa=i&url=http%3A%2F%2Fwww.code-k.co.kr%2Fsub%2Fcode_sub03.html%3FR_JIJEM%3DS1&psig=AOvVaw20fNjL28MSMMiR0Nb57Eh-&ust=1714695060162000&source=images&cd=vfe&opi=89978449&ved=0CBIQjRxqFwoTCOiO2oLX7YUDFQAAAAAdAAAAABAE');
|
||||
INSERT INTO theme (name, description, thumbnail)
|
||||
VALUES ('테마5', '재밌는 테마입니다',
|
||||
'https://www.google.co.kr/url?sa=i&url=http%3A%2F%2Fwww.code-k.co.kr%2Fsub%2Fcode_sub03.html%3FR_JIJEM%3DS1&psig=AOvVaw20fNjL28MSMMiR0Nb57Eh-&ust=1714695060162000&source=images&cd=vfe&opi=89978449&ved=0CBIQjRxqFwoTCOiO2oLX7YUDFQAAAAAdAAAAABAE');
|
||||
INSERT INTO theme (name, description, thumbnail)
|
||||
VALUES ('테마6', '재밌는 테마입니다',
|
||||
'https://www.google.co.kr/url?sa=i&url=http%3A%2F%2Fwww.code-k.co.kr%2Fsub%2Fcode_sub03.html%3FR_JIJEM%3DS1&psig=AOvVaw20fNjL28MSMMiR0Nb57Eh-&ust=1714695060162000&source=images&cd=vfe&opi=89978449&ved=0CBIQjRxqFwoTCOiO2oLX7YUDFQAAAAAdAAAAABAE');
|
||||
INSERT INTO theme (name, description, thumbnail)
|
||||
VALUES ('테마7', '재밌는 테마입니다',
|
||||
'https://www.google.co.kr/url?sa=i&url=http%3A%2F%2Fwww.code-k.co.kr%2Fsub%2Fcode_sub03.html%3FR_JIJEM%3DS1&psig=AOvVaw20fNjL28MSMMiR0Nb57Eh-&ust=1714695060162000&source=images&cd=vfe&opi=89978449&ved=0CBIQjRxqFwoTCOiO2oLX7YUDFQAAAAAdAAAAABAE');
|
||||
INSERT INTO theme (name, description, thumbnail)
|
||||
VALUES ('테마8', '재밌는 테마입니다',
|
||||
'https://www.google.co.kr/url?sa=i&url=http%3A%2F%2Fwww.code-k.co.kr%2Fsub%2Fcode_sub03.html%3FR_JIJEM%3DS1&psig=AOvVaw20fNjL28MSMMiR0Nb57Eh-&ust=1714695060162000&source=images&cd=vfe&opi=89978449&ved=0CBIQjRxqFwoTCOiO2oLX7YUDFQAAAAAdAAAAABAE');
|
||||
INSERT INTO theme (name, description, thumbnail)
|
||||
VALUES ('테마9', '재밌는 테마입니다',
|
||||
'https://www.google.co.kr/url?sa=i&url=http%3A%2F%2Fwww.code-k.co.kr%2Fsub%2Fcode_sub03.html%3FR_JIJEM%3DS1&psig=AOvVaw20fNjL28MSMMiR0Nb57Eh-&ust=1714695060162000&source=images&cd=vfe&opi=89978449&ved=0CBIQjRxqFwoTCOiO2oLX7YUDFQAAAAAdAAAAABAE');
|
||||
INSERT INTO theme (name, description, thumbnail)
|
||||
VALUES ('테마10', '재밌는 테마입니다',
|
||||
'https://www.google.co.kr/url?sa=i&url=http%3A%2F%2Fwww.code-k.co.kr%2Fsub%2Fcode_sub03.html%3FR_JIJEM%3DS1&psig=AOvVaw20fNjL28MSMMiR0Nb57Eh-&ust=1714695060162000&source=images&cd=vfe&opi=89978449&ved=0CBIQjRxqFwoTCOiO2oLX7YUDFQAAAAAdAAAAABAE');
|
||||
INSERT INTO theme (name, description, thumbnail)
|
||||
VALUES ('테마11', '재밌는 테마입니다',
|
||||
'https://www.google.co.kr/url?sa=i&url=http%3A%2F%2Fwww.code-k.co.kr%2Fsub%2Fcode_sub03.html%3FR_JIJEM%3DS1&psig=AOvVaw20fNjL28MSMMiR0Nb57Eh-&ust=1714695060162000&source=images&cd=vfe&opi=89978449&ved=0CBIQjRxqFwoTCOiO2oLX7YUDFQAAAAAdAAAAABAE');
|
||||
|
||||
-- 예약 시간 목록 : 5개
|
||||
INSERT INTO reservation_time (start_at)
|
||||
VALUES ('08:00');
|
||||
INSERT INTO reservation_time (start_at)
|
||||
VALUES ('10:00');
|
||||
INSERT INTO reservation_time (start_at)
|
||||
VALUES ('13:00');
|
||||
INSERT INTO reservation_time (start_at)
|
||||
VALUES ('21:00');
|
||||
INSERT INTO reservation_time (start_at)
|
||||
VALUES ('23:00');
|
||||
|
||||
-- 5,4,2,5,2,3,1,1,1,1,1
|
||||
-- 내림차순 정렬 ID : 4/1, 2, 6, 3/5, 7/8/9/10/11
|
||||
|
||||
-- 테마 1 예약 목록 : 5개
|
||||
INSERT INTO reservation (date, time_id, theme_id, member_id, reservation_status)
|
||||
VALUES (DATEADD('DAY', -3, CURRENT_DATE), 1, 1, 1, 'CONFIRMED');
|
||||
INSERT INTO reservation (date, time_id, theme_id, member_id, reservation_status)
|
||||
VALUES (DATEADD('DAY', -3, CURRENT_DATE), 2, 1, 1, 'CONFIRMED');
|
||||
INSERT INTO reservation (date, time_id, theme_id, member_id, reservation_status)
|
||||
VALUES (DATEADD('DAY', -3, CURRENT_DATE), 3, 1, 1, 'CONFIRMED');
|
||||
INSERT INTO reservation (date, time_id, theme_id, member_id, reservation_status)
|
||||
VALUES (DATEADD('DAY', -3, CURRENT_DATE), 4, 1, 1, 'CONFIRMED');
|
||||
INSERT INTO reservation (date, time_id, theme_id, member_id, reservation_status)
|
||||
VALUES (DATEADD('DAY', -3, CURRENT_DATE), 5, 1, 1, 'CONFIRMED');
|
||||
|
||||
insert into payment(order_id, payment_key, total_amount, reservation_id, approved_at)
|
||||
values ('orderId-1', 'paymentKey-1', 10000, 1, CURRENT_DATE);
|
||||
insert into payment(order_id, payment_key, total_amount, reservation_id, approved_at)
|
||||
values ('orderId-2', 'paymentKey-2', 20000, 2, CURRENT_DATE);
|
||||
insert into payment(order_id, payment_key, total_amount, reservation_id, approved_at)
|
||||
values ('orderId-3', 'paymentKey-3', 30000, 3, CURRENT_DATE);
|
||||
insert into payment(order_id, payment_key, total_amount, reservation_id, approved_at)
|
||||
values ('orderId-4', 'paymentKey-4', 40000, 4, CURRENT_DATE);
|
||||
insert into payment(order_id, payment_key, total_amount, reservation_id, approved_at)
|
||||
values ('orderId-5', 'paymentKey-5', 50000, 5, CURRENT_DATE);
|
||||
|
||||
-- 테마 2 예약 목록 : 4개
|
||||
INSERT INTO reservation (date, time_id, theme_id, member_id, reservation_status)
|
||||
VALUES (DATEADD('DAY', -3, CURRENT_DATE), 1, 2, 1, 'CONFIRMED');
|
||||
INSERT INTO reservation (date, time_id, theme_id, member_id, reservation_status)
|
||||
VALUES (DATEADD('DAY', -3, CURRENT_DATE), 2, 2, 1, 'CONFIRMED');
|
||||
INSERT INTO reservation (date, time_id, theme_id, member_id, reservation_status)
|
||||
VALUES (DATEADD('DAY', -3, CURRENT_DATE), 3, 2, 1, 'CONFIRMED');
|
||||
INSERT INTO reservation (date, time_id, theme_id, member_id, reservation_status)
|
||||
VALUES (DATEADD('DAY', -3, CURRENT_DATE), 4, 2, 1, 'CONFIRMED');
|
||||
|
||||
insert into payment(order_id, payment_key, total_amount, reservation_id, approved_at)
|
||||
values ('orderId-6', 'paymentKey-6', 50000, 6, CURRENT_DATE);
|
||||
insert into payment(order_id, payment_key, total_amount, reservation_id, approved_at)
|
||||
values ('orderId-7', 'paymentKey-7', 50000, 7, CURRENT_DATE);
|
||||
insert into payment(order_id, payment_key, total_amount, reservation_id, approved_at)
|
||||
values ('orderId-8', 'paymentKey-8', 50000, 8, CURRENT_DATE);
|
||||
insert into payment(order_id, payment_key, total_amount, reservation_id, approved_at)
|
||||
values ('orderId-9', 'paymentKey-9', 50000, 9, CURRENT_DATE);
|
||||
|
||||
|
||||
-- 테마 3 예약 목록 : 2개
|
||||
INSERT INTO reservation (date, time_id, theme_id, member_id, reservation_status)
|
||||
VALUES (DATEADD('DAY', -3, CURRENT_DATE), 1, 3, 1, 'CONFIRMED');
|
||||
INSERT INTO reservation (date, time_id, theme_id, member_id, reservation_status)
|
||||
VALUES (DATEADD('DAY', -3, CURRENT_DATE), 2, 3, 1, 'CONFIRMED');
|
||||
|
||||
insert into payment(order_id, payment_key, total_amount, reservation_id, approved_at)
|
||||
values ('orderId-10', 'paymentKey-10', 50000, 10, CURRENT_DATE);
|
||||
insert into payment(order_id, payment_key, total_amount, reservation_id, approved_at)
|
||||
values ('orderId-11', 'paymentKey-11', 50000, 11, CURRENT_DATE);
|
||||
|
||||
-- 테마 4 예약 목록 : 5개
|
||||
INSERT INTO reservation (date, time_id, theme_id, member_id, reservation_status)
|
||||
VALUES (DATEADD('DAY', -3, CURRENT_DATE), 1, 4, 1, 'CONFIRMED');
|
||||
INSERT INTO reservation (date, time_id, theme_id, member_id, reservation_status)
|
||||
VALUES (DATEADD('DAY', -3, CURRENT_DATE), 2, 4, 1, 'CONFIRMED');
|
||||
INSERT INTO reservation (date, time_id, theme_id, member_id, reservation_status)
|
||||
VALUES (DATEADD('DAY', -3, CURRENT_DATE), 3, 4, 1, 'CONFIRMED');
|
||||
INSERT INTO reservation (date, time_id, theme_id, member_id, reservation_status)
|
||||
VALUES (DATEADD('DAY', -3, CURRENT_DATE), 4, 4, 1, 'CONFIRMED');
|
||||
INSERT INTO reservation (date, time_id, theme_id, member_id, reservation_status)
|
||||
VALUES (DATEADD('DAY', -3, CURRENT_DATE), 5, 4, 1, 'CONFIRMED');
|
||||
|
||||
insert into payment(order_id, payment_key, total_amount, reservation_id, approved_at)
|
||||
values ('orderId-12', 'paymentKey-12', 50000, 12, CURRENT_DATE);
|
||||
insert into payment(order_id, payment_key, total_amount, reservation_id, approved_at)
|
||||
values ('orderId-13', 'paymentKey-13', 50000, 13, CURRENT_DATE);
|
||||
insert into payment(order_id, payment_key, total_amount, reservation_id, approved_at)
|
||||
values ('orderId-14', 'paymentKey-14', 50000, 14, CURRENT_DATE);
|
||||
insert into payment(order_id, payment_key, total_amount, reservation_id, approved_at)
|
||||
values ('orderId-15', 'paymentKey-15', 50000, 15, CURRENT_DATE);
|
||||
insert into payment(order_id, payment_key, total_amount, reservation_id, approved_at)
|
||||
values ('orderId-16', 'paymentKey-16', 50000, 16, CURRENT_DATE);
|
||||
|
||||
-- 테마 5 예약 목록 : 2개
|
||||
INSERT INTO reservation (date, time_id, theme_id, member_id, reservation_status)
|
||||
VALUES (DATEADD('DAY', -3, CURRENT_DATE), 1, 5, 1, 'CONFIRMED');
|
||||
INSERT INTO reservation (date, time_id, theme_id, member_id, reservation_status)
|
||||
VALUES (DATEADD('DAY', -3, CURRENT_DATE), 5, 5, 1, 'CONFIRMED');
|
||||
|
||||
insert into payment(order_id, payment_key, total_amount, reservation_id, approved_at)
|
||||
values ('orderId-17', 'paymentKey-17', 50000, 17, CURRENT_DATE);
|
||||
insert into payment(order_id, payment_key, total_amount, reservation_id, approved_at)
|
||||
values ('orderId-18', 'paymentKey-18', 50000, 18, CURRENT_DATE);
|
||||
|
||||
-- 테마 6 예약 목록 : 3개
|
||||
INSERT INTO reservation (date, time_id, theme_id, member_id, reservation_status)
|
||||
VALUES (DATEADD('DAY', -3, CURRENT_DATE), 1, 6, 1, 'CONFIRMED');
|
||||
INSERT INTO reservation (date, time_id, theme_id, member_id, reservation_status)
|
||||
VALUES (DATEADD('DAY', -3, CURRENT_DATE), 2, 6, 1, 'CONFIRMED');
|
||||
INSERT INTO reservation (date, time_id, theme_id, member_id, reservation_status)
|
||||
VALUES (DATEADD('DAY', -3, CURRENT_DATE), 3, 6, 1, 'CONFIRMED');
|
||||
|
||||
insert into payment(order_id, payment_key, total_amount, reservation_id, approved_at)
|
||||
values ('orderId-19', 'paymentKey-19', 50000, 19, CURRENT_DATE);
|
||||
insert into payment(order_id, payment_key, total_amount, reservation_id, approved_at)
|
||||
values ('orderId-20', 'paymentKey-20', 50000, 20, CURRENT_DATE);
|
||||
insert into payment(order_id, payment_key, total_amount, reservation_id, approved_at)
|
||||
values ('orderId-21', 'paymentKey-21', 50000, 21, CURRENT_DATE);
|
||||
|
||||
-- 테마 7 예약 목록
|
||||
INSERT INTO reservation (date, time_id, theme_id, member_id, reservation_status)
|
||||
VALUES (DATEADD('DAY', -3, CURRENT_DATE), 1, 7, 1, 'CONFIRMED');
|
||||
|
||||
insert into payment(order_id, payment_key, total_amount, reservation_id, approved_at)
|
||||
values ('orderId-22', 'paymentKey-22', 50000, 22, CURRENT_DATE);
|
||||
|
||||
-- 테마 8 예약 목록
|
||||
INSERT INTO reservation (date, time_id, theme_id, member_id, reservation_status)
|
||||
VALUES (DATEADD('DAY', -3, CURRENT_DATE), 1, 8, 1, 'CONFIRMED');
|
||||
|
||||
insert into payment(order_id, payment_key, total_amount, reservation_id, approved_at)
|
||||
values ('orderId-23', 'paymentKey-23', 50000, 23, CURRENT_DATE);
|
||||
|
||||
|
||||
-- 테마 9 예약 목록
|
||||
INSERT INTO reservation (date, time_id, theme_id, member_id, reservation_status)
|
||||
VALUES (DATEADD('DAY', -3, CURRENT_DATE), 1, 9, 1, 'CONFIRMED');
|
||||
|
||||
insert into payment(order_id, payment_key, total_amount, reservation_id, approved_at)
|
||||
values ('orderId-24', 'paymentKey-24', 50000, 24, CURRENT_DATE);
|
||||
|
||||
-- 테마 10 예약 목록
|
||||
INSERT INTO reservation (date, time_id, theme_id, member_id, reservation_status)
|
||||
VALUES (DATEADD('DAY', -3, CURRENT_DATE), 1, 10, 1, 'CONFIRMED');
|
||||
|
||||
insert into payment(order_id, payment_key, total_amount, reservation_id, approved_at)
|
||||
values ('orderId-25', 'paymentKey-25', 50000, 25, CURRENT_DATE);
|
||||
|
||||
-- 테마 11 예약 목록
|
||||
INSERT INTO reservation (date, time_id, theme_id, member_id, reservation_status)
|
||||
VALUES (DATEADD('DAY', -3, CURRENT_DATE), 5, 11, 1, 'CONFIRMED');
|
||||
|
||||
insert into payment(order_id, payment_key, total_amount, reservation_id, approved_at)
|
||||
values ('orderId-26', 'paymentKey-26', 50000, 26, CURRENT_DATE);
|
||||
@ -1,41 +0,0 @@
|
||||
-- 관리자가 특정 조건에 해당되는 예약을 조회하는 테스트에서만 사용되는 데이터입니다.
|
||||
insert into reservation_time(start_at)
|
||||
values ('15:00');
|
||||
|
||||
insert into theme(name, description, thumbnail)
|
||||
values ('테스트1', '테스트중', 'https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg');
|
||||
insert into theme(name, description, thumbnail)
|
||||
values ('테스트2', '테스트중', 'https://i.pinimg.com/236x/6e/bc/46/6ebc461a94a49f9ea3b8bbe2204145d4.jpg');
|
||||
|
||||
insert into member(name, email, password, role)
|
||||
values ('어드민', 'a@a.a', 'a', 'ADMIN');
|
||||
insert into member(name, email, password, role)
|
||||
values ('1호', '1@1.1', '1', 'MEMBER');
|
||||
|
||||
-- 예약
|
||||
-- 시간은 같은 시간으로, 날짜는 어제부터 7일 전까지
|
||||
-- memberId = 1인 멤버는 3개의 예약, memberId = 2인 멤버는 4개의 예약이 있음
|
||||
-- themeId = 1인 테마는 4개의 예약, themeId = 2인 테마는 3개의 예약이 있음
|
||||
insert into reservation(date, time_id, theme_id, member_id, reservation_status)
|
||||
values (DATEADD('DAY', -1, CURRENT_DATE()), 1, 1, 1, 'CONFIRMED');
|
||||
insert into reservation(date, time_id, theme_id, member_id, reservation_status)
|
||||
values (DATEADD('DAY', -2, CURRENT_DATE()), 1, 1, 1, 'CONFIRMED');
|
||||
insert into reservation(date, time_id, theme_id, member_id, reservation_status)
|
||||
values (DATEADD('DAY', -3, CURRENT_DATE()), 1, 1, 1, 'CONFIRMED');
|
||||
insert into reservation(date, time_id, theme_id, member_id, reservation_status)
|
||||
values (DATEADD('DAY', -4, CURRENT_DATE()), 1, 1, 2, 'CONFIRMED');
|
||||
insert into reservation(date, time_id, theme_id, member_id, reservation_status)
|
||||
values (DATEADD('DAY', -5, CURRENT_DATE()), 1, 2, 2, 'CONFIRMED');
|
||||
insert into reservation(date, time_id, theme_id, member_id, reservation_status)
|
||||
values (DATEADD('DAY', -6, CURRENT_DATE()), 1, 2, 2, 'CONFIRMED');
|
||||
insert into reservation(date, time_id, theme_id, member_id, reservation_status)
|
||||
values (DATEADD('DAY', -7, CURRENT_DATE()), 1, 2, 2, 'CONFIRMED');
|
||||
|
||||
-- 예약 대기
|
||||
-- 예약 대기는 조회되면 안됨.
|
||||
insert into reservation(date, time_id, theme_id, member_id, reservation_status)
|
||||
values (DATEADD('DAY', 7, CURRENT_DATE()), 1, 1, 1, 'WAITING');
|
||||
insert into reservation(date, time_id, theme_id, member_id, reservation_status)
|
||||
values (DATEADD('DAY', 8, CURRENT_DATE()), 1, 1, 1, 'WAITING');
|
||||
insert into reservation(date, time_id, theme_id, member_id, reservation_status)
|
||||
values (DATEADD('DAY', 9, CURRENT_DATE()), 1, 1, 2, 'WAITING');
|
||||
@ -1,25 +0,0 @@
|
||||
DELETE
|
||||
FROM payment;
|
||||
DELETE
|
||||
FROM canceled_payment;
|
||||
DELETE
|
||||
FROM reservation;
|
||||
DELETE
|
||||
FROM reservation_time;
|
||||
DELETE
|
||||
FROM theme;
|
||||
DELETE
|
||||
FROM member;
|
||||
|
||||
ALTER TABLE payment
|
||||
ALTER COLUMN id RESTART WITH 1;
|
||||
ALTER TABLE canceled_payment
|
||||
ALTER COLUMN id RESTART WITH 1;
|
||||
ALTER TABLE reservation
|
||||
ALTER COLUMN id RESTART WITH 1;
|
||||
ALTER TABLE reservation_time
|
||||
ALTER COLUMN id RESTART WITH 1;
|
||||
ALTER TABLE theme
|
||||
ALTER COLUMN id RESTART WITH 1;
|
||||
ALTER TABLE member
|
||||
ALTER COLUMN id RESTART WITH 1;
|
||||
Loading…
x
Reference in New Issue
Block a user