diff --git a/src/main/java/roomescape/auth/service/AuthService.kt b/src/main/java/roomescape/auth/service/AuthService.kt index ccc9c95c..b6ee6e00 100644 --- a/src/main/java/roomescape/auth/service/AuthService.kt +++ b/src/main/java/roomescape/auth/service/AuthService.kt @@ -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 ) diff --git a/src/main/java/roomescape/auth/web/support/AuthInterceptors.kt b/src/main/java/roomescape/auth/web/support/AuthInterceptors.kt index 20b5b53d..42c6e958 100644 --- a/src/main/java/roomescape/auth/web/support/AuthInterceptors.kt +++ b/src/main/java/roomescape/auth/web/support/AuthInterceptors.kt @@ -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): 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 diff --git a/src/main/java/roomescape/member/business/MemberService.kt b/src/main/java/roomescape/member/business/MemberService.kt index 6dc20c7c..512f947d 100644 --- a/src/main/java/roomescape/member/business/MemberService.kt +++ b/src/main/java/roomescape/member/business/MemberService.kt @@ -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, diff --git a/src/main/java/roomescape/member/infrastructure/persistence/Member.kt b/src/main/java/roomescape/member/infrastructure/persistence/MemberEntity.kt similarity index 90% rename from src/main/java/roomescape/member/infrastructure/persistence/Member.kt rename to src/main/java/roomescape/member/infrastructure/persistence/MemberEntity.kt index 1e63e863..f9ec13eb 100644 --- a/src/main/java/roomescape/member/infrastructure/persistence/Member.kt +++ b/src/main/java/roomescape/member/infrastructure/persistence/MemberEntity.kt @@ -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, diff --git a/src/main/java/roomescape/member/infrastructure/persistence/MemberRepository.kt b/src/main/java/roomescape/member/infrastructure/persistence/MemberRepository.kt index c08e3920..667d9df8 100644 --- a/src/main/java/roomescape/member/infrastructure/persistence/MemberRepository.kt +++ b/src/main/java/roomescape/member/infrastructure/persistence/MemberRepository.kt @@ -2,6 +2,6 @@ package roomescape.member.infrastructure.persistence import org.springframework.data.jpa.repository.JpaRepository -interface MemberRepository : JpaRepository { - fun findByEmailAndPassword(email: String, password: String): Member? +interface MemberRepository : JpaRepository { + fun findByEmailAndPassword(email: String, password: String): MemberEntity? } diff --git a/src/main/java/roomescape/member/web/MemberDTO.kt b/src/main/java/roomescape/member/web/MemberDTO.kt index e20bac35..8b8b4e32 100644 --- a/src/main/java/roomescape/member/web/MemberDTO.kt +++ b/src/main/java/roomescape/member/web/MemberDTO.kt @@ -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) } } diff --git a/src/main/java/roomescape/payment/business/PaymentService.kt b/src/main/java/roomescape/payment/business/PaymentService.kt index 1b4e6deb..8a406fad 100644 --- a/src/main/java/roomescape/payment/business/PaymentService.kt +++ b/src/main/java/roomescape/payment/business/PaymentService.kt @@ -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, diff --git a/src/main/java/roomescape/payment/infrastructure/persistence/PaymentEntity.kt b/src/main/java/roomescape/payment/infrastructure/persistence/PaymentEntity.kt index 8de5d4da..88f25dbe 100644 --- a/src/main/java/roomescape/payment/infrastructure/persistence/PaymentEntity.kt +++ b/src/main/java/roomescape/payment/infrastructure/persistence/PaymentEntity.kt @@ -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 diff --git a/src/main/java/roomescape/payment/web/PaymentDTO.kt b/src/main/java/roomescape/payment/web/PaymentDTO.kt index 8b4b12e2..b346c20c 100644 --- a/src/main/java/roomescape/payment/web/PaymentDTO.kt +++ b/src/main/java/roomescape/payment/web/PaymentDTO.kt @@ -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 ) \ No newline at end of file diff --git a/src/main/java/roomescape/reservation/business/ReservationService.kt b/src/main/java/roomescape/reservation/business/ReservationService.kt new file mode 100644 index 00000000..1047b9c7 --- /dev/null +++ b/src/main/java/roomescape/reservation/business/ReservationService.kt @@ -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 = ReservationSearchSpecification() + .confirmed() + .build() + + + return ReservationsResponse(findAllReservationByStatus(spec)) + } + + @Transactional(readOnly = true) + fun findAllWaiting(): ReservationsResponse { + val spec: Specification = ReservationSearchSpecification() + .waiting() + .build() + + return ReservationsResponse(findAllReservationByStatus(spec)) + } + + private fun findAllReservationByStatus(spec: Specification): List { + 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 = 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 = 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 = 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 + ) + } +} diff --git a/src/main/java/roomescape/reservation/business/ReservationTimeService.kt b/src/main/java/roomescape/reservation/business/ReservationTimeService.kt new file mode 100644 index 00000000..5f940834 --- /dev/null +++ b/src/main/java/roomescape/reservation/business/ReservationTimeService.kt @@ -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 = reservationRepository.findByDateAndThemeId(date, themeId) + + return ReservationTimeInfosResponse(allTimes.map { time -> + val alreadyBooked: Boolean = reservations.any { reservation -> reservation.reservationTime.id == time.id } + time.toInfoResponse(alreadyBooked) + }) + } +} diff --git a/src/main/java/roomescape/reservation/business/ReservationWithPaymentService.kt b/src/main/java/roomescape/reservation/business/ReservationWithPaymentService.kt new file mode 100644 index 00000000..a3b0c47b --- /dev/null +++ b/src/main/java/roomescape/reservation/business/ReservationWithPaymentService.kt @@ -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) + } +} diff --git a/src/main/java/roomescape/reservation/controller/ReservationController.java b/src/main/java/roomescape/reservation/controller/ReservationController.java deleted file mode 100644 index f5df5f57..00000000 --- a/src/main/java/roomescape/reservation/controller/ReservationController.java +++ /dev/null @@ -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 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 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 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 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 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 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 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 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 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 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 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 getCreatedReservationResponse( - ReservationResponse reservationResponse, - HttpServletResponse response - ) { - response.setHeader(HttpHeaders.LOCATION, "/reservations/" + reservationResponse.id()); - return RoomescapeApiResponse.success(reservationResponse); - } -} diff --git a/src/main/java/roomescape/reservation/controller/ReservationTimeController.java b/src/main/java/roomescape/reservation/controller/ReservationTimeController.java deleted file mode 100644 index 18d5abb5..00000000 --- a/src/main/java/roomescape/reservation/controller/ReservationTimeController.java +++ /dev/null @@ -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 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 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 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 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)); - } -} diff --git a/src/main/java/roomescape/reservation/docs/ReservationAPI.kt b/src/main/java/roomescape/reservation/docs/ReservationAPI.kt new file mode 100644 index 00000000..05fe2883 --- /dev/null +++ b/src/main/java/roomescape/reservation/docs/ReservationAPI.kt @@ -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> + + @LoginRequired + @Operation(summary = "자신의 예약 및 대기 조회", tags = ["로그인이 필요한 API"]) + @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) + fun getMemberReservations( + @MemberId @Parameter(hidden = true) memberId: Long + ): ResponseEntity> + + @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> + + @Admin + @Operation(summary = "관리자의 예약 취소", tags = ["관리자 로그인이 필요한 API"]) + @ApiResponses( + ApiResponse(responseCode = "204", description = "성공"), + ) + fun removeReservation( + @MemberId @Parameter(hidden = true) memberId: Long, + @PathVariable("id") reservationId: Long + ): ResponseEntity> + + @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> + + @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> + + @Admin + @Operation(summary = "모든 예약 대기 조회", tags = ["관리자 로그인이 필요한 API"]) + @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) + fun getAllWaiting(): ResponseEntity> + + @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> + + @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> + + @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> + + @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> +} diff --git a/src/main/java/roomescape/reservation/docs/ReservationTimeAPI.kt b/src/main/java/roomescape/reservation/docs/ReservationTimeAPI.kt new file mode 100644 index 00000000..3b1535e2 --- /dev/null +++ b/src/main/java/roomescape/reservation/docs/ReservationTimeAPI.kt @@ -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> + + @Admin + @Operation(summary = "시간 추가", tags = ["관리자 로그인이 필요한 API"]) + @ApiResponses(ApiResponse(responseCode = "201", description = "성공", useReturnTypeSchema = true)) + fun saveTime( + @Valid @RequestBody reservationTimeRequest: ReservationTimeRequest, + ): ResponseEntity> + + @Admin + @Operation(summary = "시간 삭제", tags = ["관리자 로그인이 필요한 API"]) + @ApiResponses(ApiResponse(responseCode = "204", description = "성공", useReturnTypeSchema = true)) + fun removeTime( + @PathVariable id: Long + ): ResponseEntity> + + @LoginRequired + @Operation(summary = "예약 가능 여부를 포함한 모든 시간 조회", tags = ["로그인이 필요한 API"]) + @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) + fun findAllAvailableReservationTimes( + @RequestParam date: LocalDate, + @RequestParam themeId: Long + ): ResponseEntity> +} diff --git a/src/main/java/roomescape/reservation/domain/Reservation.java b/src/main/java/roomescape/reservation/domain/Reservation.java deleted file mode 100644 index 6509596f..00000000 --- a/src/main/java/roomescape/reservation/domain/Reservation.java +++ /dev/null @@ -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); - } -} diff --git a/src/main/java/roomescape/reservation/domain/ReservationStatus.java b/src/main/java/roomescape/reservation/domain/ReservationStatus.java deleted file mode 100644 index d8978397..00000000 --- a/src/main/java/roomescape/reservation/domain/ReservationStatus.java +++ /dev/null @@ -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; -} diff --git a/src/main/java/roomescape/reservation/domain/ReservationTime.java b/src/main/java/roomescape/reservation/domain/ReservationTime.java deleted file mode 100644 index e54a39b0..00000000 --- a/src/main/java/roomescape/reservation/domain/ReservationTime.java +++ /dev/null @@ -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 + - '}'; - } -} diff --git a/src/main/java/roomescape/reservation/domain/repository/ReservationRepository.java b/src/main/java/roomescape/reservation/domain/repository/ReservationRepository.java deleted file mode 100644 index af83b3dc..00000000 --- a/src/main/java/roomescape/reservation/domain/repository/ReservationRepository.java +++ /dev/null @@ -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, JpaSpecificationExecutor { - - List findByReservationTime(ReservationTime reservationTime); - - List 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 findMyReservations(Long memberId); -} diff --git a/src/main/java/roomescape/reservation/domain/repository/ReservationSearchSpecification.java b/src/main/java/roomescape/reservation/domain/repository/ReservationSearchSpecification.java deleted file mode 100644 index 69eb914a..00000000 --- a/src/main/java/roomescape/reservation/domain/repository/ReservationSearchSpecification.java +++ /dev/null @@ -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 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 build() { - return this.spec; - } -} diff --git a/src/main/java/roomescape/reservation/domain/repository/ReservationTimeRepository.java b/src/main/java/roomescape/reservation/domain/repository/ReservationTimeRepository.java deleted file mode 100644 index 791077d2..00000000 --- a/src/main/java/roomescape/reservation/domain/repository/ReservationTimeRepository.java +++ /dev/null @@ -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 { - - List findByStartAt(LocalTime startAt); -} diff --git a/src/main/java/roomescape/reservation/dto/request/AdminReservationRequest.java b/src/main/java/roomescape/reservation/dto/request/AdminReservationRequest.java deleted file mode 100644 index 5b2ea0e1..00000000 --- a/src/main/java/roomescape/reservation/dto/request/AdminReservationRequest.java +++ /dev/null @@ -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 -) { -} diff --git a/src/main/java/roomescape/reservation/dto/request/ReservationRequest.java b/src/main/java/roomescape/reservation/dto/request/ReservationRequest.java deleted file mode 100644 index e15b00fb..00000000 --- a/src/main/java/roomescape/reservation/dto/request/ReservationRequest.java +++ /dev/null @@ -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); - } -} diff --git a/src/main/java/roomescape/reservation/dto/request/ReservationTimeRequest.java b/src/main/java/roomescape/reservation/dto/request/ReservationTimeRequest.java deleted file mode 100644 index dd2f24c5..00000000 --- a/src/main/java/roomescape/reservation/dto/request/ReservationTimeRequest.java +++ /dev/null @@ -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); - } -} diff --git a/src/main/java/roomescape/reservation/dto/request/WaitingRequest.java b/src/main/java/roomescape/reservation/dto/request/WaitingRequest.java deleted file mode 100644 index 0568a670..00000000 --- a/src/main/java/roomescape/reservation/dto/request/WaitingRequest.java +++ /dev/null @@ -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 -) { -} diff --git a/src/main/java/roomescape/reservation/dto/response/MyReservationResponse.java b/src/main/java/roomescape/reservation/dto/response/MyReservationResponse.java deleted file mode 100644 index 8667c371..00000000 --- a/src/main/java/roomescape/reservation/dto/response/MyReservationResponse.java +++ /dev/null @@ -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); - } -} diff --git a/src/main/java/roomescape/reservation/dto/response/MyReservationsResponse.java b/src/main/java/roomescape/reservation/dto/response/MyReservationsResponse.java deleted file mode 100644 index a32ef460..00000000 --- a/src/main/java/roomescape/reservation/dto/response/MyReservationsResponse.java +++ /dev/null @@ -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 myReservationResponses -) { -} diff --git a/src/main/java/roomescape/reservation/dto/response/ReservationResponse.java b/src/main/java/roomescape/reservation/dto/response/ReservationResponse.java deleted file mode 100644 index f1fa1704..00000000 --- a/src/main/java/roomescape/reservation/dto/response/ReservationResponse.java +++ /dev/null @@ -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() - ); - } -} diff --git a/src/main/java/roomescape/reservation/dto/response/ReservationTimeInfoResponse.java b/src/main/java/roomescape/reservation/dto/response/ReservationTimeInfoResponse.java deleted file mode 100644 index 1105bf03..00000000 --- a/src/main/java/roomescape/reservation/dto/response/ReservationTimeInfoResponse.java +++ /dev/null @@ -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 -) { -} diff --git a/src/main/java/roomescape/reservation/dto/response/ReservationTimeInfosResponse.java b/src/main/java/roomescape/reservation/dto/response/ReservationTimeInfosResponse.java deleted file mode 100644 index ef02deeb..00000000 --- a/src/main/java/roomescape/reservation/dto/response/ReservationTimeInfosResponse.java +++ /dev/null @@ -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 reservationTimes -) { -} diff --git a/src/main/java/roomescape/reservation/dto/response/ReservationTimeResponse.java b/src/main/java/roomescape/reservation/dto/response/ReservationTimeResponse.java deleted file mode 100644 index fc27d3d5..00000000 --- a/src/main/java/roomescape/reservation/dto/response/ReservationTimeResponse.java +++ /dev/null @@ -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()); - } -} diff --git a/src/main/java/roomescape/reservation/dto/response/ReservationTimesResponse.java b/src/main/java/roomescape/reservation/dto/response/ReservationTimesResponse.java deleted file mode 100644 index 1cbff917..00000000 --- a/src/main/java/roomescape/reservation/dto/response/ReservationTimesResponse.java +++ /dev/null @@ -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 times -) { -} diff --git a/src/main/java/roomescape/reservation/dto/response/ReservationsResponse.java b/src/main/java/roomescape/reservation/dto/response/ReservationsResponse.java deleted file mode 100644 index 83386f1d..00000000 --- a/src/main/java/roomescape/reservation/dto/response/ReservationsResponse.java +++ /dev/null @@ -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 reservations -) { -} diff --git a/src/main/java/roomescape/reservation/infrastructure/persistence/ReservationEntity.kt b/src/main/java/roomescape/reservation/infrastructure/persistence/ReservationEntity.kt new file mode 100644 index 00000000..f513d5c2 --- /dev/null +++ b/src/main/java/roomescape/reservation/infrastructure/persistence/ReservationEntity.kt @@ -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 +} diff --git a/src/main/java/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt b/src/main/java/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt new file mode 100644 index 00000000..5d17d47a --- /dev/null +++ b/src/main/java/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt @@ -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, JpaSpecificationExecutor { + fun findByReservationTime(reservationTime: ReservationTimeEntity): List + + fun findByDateAndThemeId(date: LocalDate, themeId: Long): List + + @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 +} diff --git a/src/main/java/roomescape/reservation/infrastructure/persistence/ReservationSearchSpecification.kt b/src/main/java/roomescape/reservation/infrastructure/persistence/ReservationSearchSpecification.kt new file mode 100644 index 00000000..ca55ce2e --- /dev/null +++ b/src/main/java/roomescape/reservation/infrastructure/persistence/ReservationSearchSpecification.kt @@ -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 = Specification { _, _, _ -> null } +) { + fun sameThemeId(themeId: Long?): ReservationSearchSpecification = andIfNotNull(themeId?.let { + Specification { root, _, cb -> + cb.equal(root.get("theme").get("id"), themeId) + } + }) + + fun sameMemberId(memberId: Long?): ReservationSearchSpecification = andIfNotNull(memberId?.let { + Specification { root, _, cb -> + cb.equal(root.get("member").get("id"), memberId) + } + }) + + fun sameTimeId(timeId: Long?): ReservationSearchSpecification = andIfNotNull(timeId?.let { + Specification { root, _, cb -> + cb.equal(root.get("reservationTime").get("id"), timeId) + } + }) + + fun sameDate(date: LocalDate?): ReservationSearchSpecification = andIfNotNull(date?.let { + Specification { root, _, cb -> + cb.equal(root.get("date"), date) + } + }) + + fun confirmed(): ReservationSearchSpecification = andIfNotNull { root, _, cb -> + cb.or( + cb.equal( + root.get("reservationStatus"), + ReservationStatus.CONFIRMED + ), + cb.equal( + root.get("reservationStatus"), + ReservationStatus.CONFIRMED_PAYMENT_REQUIRED + ) + ) + } + + fun waiting(): ReservationSearchSpecification = andIfNotNull { root, _, cb -> + cb.equal( + root.get("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 { + return this.spec + } + + private fun andIfNotNull(condition: Specification?): ReservationSearchSpecification { + condition?.let { this.spec = this.spec.and(condition) } + return this + } +} diff --git a/src/main/java/roomescape/reservation/infrastructure/persistence/ReservationTimeEntity.kt b/src/main/java/roomescape/reservation/infrastructure/persistence/ReservationTimeEntity.kt new file mode 100644 index 00000000..73479a7b --- /dev/null +++ b/src/main/java/roomescape/reservation/infrastructure/persistence/ReservationTimeEntity.kt @@ -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 +) diff --git a/src/main/java/roomescape/reservation/infrastructure/persistence/ReservationTimeRepository.kt b/src/main/java/roomescape/reservation/infrastructure/persistence/ReservationTimeRepository.kt new file mode 100644 index 00000000..d4a03bcd --- /dev/null +++ b/src/main/java/roomescape/reservation/infrastructure/persistence/ReservationTimeRepository.kt @@ -0,0 +1,8 @@ +package roomescape.reservation.infrastructure.persistence + +import org.springframework.data.jpa.repository.JpaRepository +import java.time.LocalTime + +interface ReservationTimeRepository : JpaRepository { + fun existsByStartAt(startAt: LocalTime): Boolean +} diff --git a/src/main/java/roomescape/reservation/service/ReservationService.java b/src/main/java/roomescape/reservation/service/ReservationService.java deleted file mode 100644 index 0a3098ac..00000000 --- a/src/main/java/roomescape/reservation/service/ReservationService.java +++ /dev/null @@ -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 spec = new ReservationSearchSpecification().confirmed().build(); - List response = findAllReservationByStatus(spec); - - return new ReservationsResponse(response); - } - - @Transactional(readOnly = true) - public ReservationsResponse findAllWaiting() { - Specification spec = new ReservationSearchSpecification().waiting().build(); - List response = findAllReservationByStatus(spec); - - return new ReservationsResponse(response); - } - - private List findAllReservationByStatus(Specification 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 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 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 spec = new ReservationSearchSpecification() - .confirmed() - .sameThemeId(themeId) - .sameMemberId(memberId) - .dateStartFrom(dateFrom) - .dateEndAt(dateTo) - .build(); - - List 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); - } -} diff --git a/src/main/java/roomescape/reservation/service/ReservationTimeService.java b/src/main/java/roomescape/reservation/service/ReservationTimeService.java deleted file mode 100644 index 276a0daa..00000000 --- a/src/main/java/roomescape/reservation/service/ReservationTimeService.java +++ /dev/null @@ -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 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 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 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 allTimes = reservationTimeRepository.findAll(); - List reservations = reservationRepository.findByThemeId(themeId); - - List response = allTimes.stream() - .map(time -> new ReservationTimeInfoResponse(time.getId(), time.getStartAt(), - isReservationBooked(reservations, date, time))) - .toList(); - - return new ReservationTimeInfosResponse(response); - } - - private boolean isReservationBooked(List reservations, LocalDate date, ReservationTime time) { - return reservations.stream() - .anyMatch(reservation -> reservation.isSameDateAndTime(date, time)); - } -} diff --git a/src/main/java/roomescape/reservation/service/ReservationWithPaymentService.java b/src/main/java/roomescape/reservation/service/ReservationWithPaymentService.java deleted file mode 100644 index 853e6e30..00000000 --- a/src/main/java/roomescape/reservation/service/ReservationWithPaymentService.java +++ /dev/null @@ -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); - } -} diff --git a/src/main/java/roomescape/reservation/web/ReservationController.kt b/src/main/java/roomescape/reservation/web/ReservationController.kt new file mode 100644 index 00000000..4b3d7e98 --- /dev/null +++ b/src/main/java/roomescape/reservation/web/ReservationController.kt @@ -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> { + val response: ReservationsResponse = reservationService.findAllReservations() + + return ResponseEntity.ok(CommonApiResponse(response)) + } + + @GetMapping("/reservations-mine") + override fun getMemberReservations( + @MemberId @Parameter(hidden = true) memberId: Long + ): ResponseEntity> { + 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> { + 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> { + 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> { + 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> { + val response: ReservationResponse = + reservationService.addReservationByAdmin(adminReservationRequest) + + return ResponseEntity.created(URI.create("/reservations/${response.id}")) + .body(CommonApiResponse(response)) + } + + @GetMapping("/reservations/waiting") + override fun getAllWaiting(): ResponseEntity> { + 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> { + 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> { + 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> { + 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> { + reservationService.denyWaiting(reservationId, memberId) + + return ResponseEntity.noContent().build() + } +} diff --git a/src/main/java/roomescape/reservation/web/ReservationRequest.kt b/src/main/java/roomescape/reservation/web/ReservationRequest.kt new file mode 100644 index 00000000..fa081bc9 --- /dev/null +++ b/src/main/java/roomescape/reservation/web/ReservationRequest.kt @@ -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 +) diff --git a/src/main/java/roomescape/reservation/web/ReservationResponse.kt b/src/main/java/roomescape/reservation/web/ReservationResponse.kt new file mode 100644 index 00000000..3776d150 --- /dev/null +++ b/src/main/java/roomescape/reservation/web/ReservationResponse.kt @@ -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 +) + +@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 +) diff --git a/src/main/java/roomescape/reservation/web/ReservationTimeController.kt b/src/main/java/roomescape/reservation/web/ReservationTimeController.kt new file mode 100644 index 00000000..6fb0147e --- /dev/null +++ b/src/main/java/roomescape/reservation/web/ReservationTimeController.kt @@ -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> { + val response: ReservationTimesResponse = reservationTimeService.findAllTimes() + + return ResponseEntity.ok(CommonApiResponse(response)) + } + + @PostMapping("/times") + override fun saveTime( + @Valid @RequestBody reservationTimeRequest: ReservationTimeRequest, + ): ResponseEntity> { + 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> { + reservationTimeService.removeTimeById(id) + + return ResponseEntity.noContent().build() + } + + @GetMapping("/times/filter") + override fun findAllAvailableReservationTimes( + @RequestParam date: LocalDate, + @RequestParam themeId: Long + ): ResponseEntity> { + val response: ReservationTimeInfosResponse = reservationTimeService.findAllAvailableTimesByDateAndTheme(date, themeId) + + return ResponseEntity.ok(CommonApiResponse(response)) + } +} diff --git a/src/main/java/roomescape/reservation/web/ReservationTimeDTO.kt b/src/main/java/roomescape/reservation/web/ReservationTimeDTO.kt new file mode 100644 index 00000000..e0b06678 --- /dev/null +++ b/src/main/java/roomescape/reservation/web/ReservationTimeDTO.kt @@ -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 +) + +fun List.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 +) diff --git a/src/main/java/roomescape/theme/infrastructure/persistence/ThemeRepository.kt b/src/main/java/roomescape/theme/infrastructure/persistence/ThemeRepository.kt index f0ea0b5a..83b02cfe 100644 --- a/src/main/java/roomescape/theme/infrastructure/persistence/ThemeRepository.kt +++ b/src/main/java/roomescape/theme/infrastructure/persistence/ThemeRepository.kt @@ -9,7 +9,7 @@ interface ThemeRepository : JpaRepository { @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 { @Query(value = """ SELECT EXISTS( SELECT 1 - FROM Reservation r + FROM ReservationEntity r WHERE r.theme.id = :id ) """) diff --git a/src/main/java/roomescape/theme/web/ThemeDTO.kt b/src/main/java/roomescape/theme/web/ThemeDTO.kt index 27c4ea61..b814a43a 100644 --- a/src/main/java/roomescape/theme/web/ThemeDTO.kt +++ b/src/main/java/roomescape/theme/web/ThemeDTO.kt @@ -64,5 +64,5 @@ data class ThemesResponse( ) fun List.toResponse(): ThemesResponse = ThemesResponse( - themes = this.map { it.toResponse()} + themes = this.map { it.toResponse() } ) diff --git a/src/main/resources/static/js/reservation-mine.js b/src/main/resources/static/js/reservation-mine.js index ae17a8af..80a4d903 100644 --- a/src/main/resources/static/js/reservation-mine.js +++ b/src/main/resources/static/js/reservation-mine.js @@ -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; diff --git a/src/test/java/roomescape/auth/business/AuthServiceTest.kt b/src/test/java/roomescape/auth/business/AuthServiceTest.kt index 31cdef51..90fb95cb 100644 --- a/src/test/java/roomescape/auth/business/AuthServiceTest.kt +++ b/src/test/java/roomescape/auth/business/AuthServiceTest.kt @@ -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("이메일과 비밀번호로 회원을 찾고") { diff --git a/src/test/java/roomescape/auth/infrastructure/jwt/JwtHandlerTest.kt b/src/test/java/roomescape/auth/infrastructure/jwt/JwtHandlerTest.kt index 4626bab1..53f12279 100644 --- a/src/test/java/roomescape/auth/infrastructure/jwt/JwtHandlerTest.kt +++ b/src/test/java/roomescape/auth/infrastructure/jwt/JwtHandlerTest.kt @@ -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 diff --git a/src/test/java/roomescape/payment/business/PaymentServiceTest.java b/src/test/java/roomescape/payment/business/PaymentServiceTest.java deleted file mode 100644 index 57f057a8..00000000 --- a/src/test/java/roomescape/payment/business/PaymentServiceTest.java +++ /dev/null @@ -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); - } -} diff --git a/src/test/java/roomescape/payment/business/PaymentServiceKTest.kt b/src/test/java/roomescape/payment/business/PaymentServiceTest.kt similarity index 99% rename from src/test/java/roomescape/payment/business/PaymentServiceKTest.kt rename to src/test/java/roomescape/payment/business/PaymentServiceTest.kt index e72b228c..be9c05fa 100644 --- a/src/test/java/roomescape/payment/business/PaymentServiceKTest.kt +++ b/src/test/java/roomescape/payment/business/PaymentServiceTest.kt @@ -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() diff --git a/src/test/java/roomescape/payment/infrastructure/persistence/CanceledPaymentRepositoryTest.kt b/src/test/java/roomescape/payment/infrastructure/persistence/CanceledPaymentRepositoryTest.kt index 1043a737..a764351f 100644 --- a/src/test/java/roomescape/payment/infrastructure/persistence/CanceledPaymentRepositoryTest.kt +++ b/src/test/java/roomescape/payment/infrastructure/persistence/CanceledPaymentRepositoryTest.kt @@ -11,7 +11,7 @@ import java.util.* @DataJpaTest class CanceledPaymentRepositoryTest( @Autowired val canceledPaymentRepository: CanceledPaymentRepository, -): FunSpec() { +) : FunSpec() { init { context("paymentKey로 CanceledPaymentEntity 조회") { val paymentKey = "test-payment-key" diff --git a/src/test/java/roomescape/payment/infrastructure/persistence/PaymentRepositoryTest.kt b/src/test/java/roomescape/payment/infrastructure/persistence/PaymentRepositoryTest.kt index ab3217d0..4b202077 100644 --- a/src/test/java/roomescape/payment/infrastructure/persistence/PaymentRepositoryTest.kt +++ b/src/test/java/roomescape/payment/infrastructure/persistence/PaymentRepositoryTest.kt @@ -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 + } } } diff --git a/src/test/java/roomescape/reservation/business/ReservationServiteTest.kt b/src/test/java/roomescape/reservation/business/ReservationServiteTest.kt new file mode 100644 index 00000000..c9b2fa6a --- /dev/null +++ b/src/test/java/roomescape/reservation/business/ReservationServiteTest.kt @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + reservationService.approveWaiting(reservationId, member.id!!) + }.also { + it.errorType shouldBe ErrorType.RESERVATION_DUPLICATED + } + } + } +}) diff --git a/src/test/java/roomescape/reservation/business/ReservationTimeServiceTest.kt b/src/test/java/roomescape/reservation/business/ReservationTimeServiceTest.kt new file mode 100644 index 00000000..a1bedcfe --- /dev/null +++ b/src/test/java/roomescape/reservation/business/ReservationTimeServiceTest.kt @@ -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 { + 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 { + 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 { + 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 { + reservationTimeService.removeTimeById(id) + }.apply { + errorType shouldBe ErrorType.TIME_IS_USED_CONFLICT + httpStatus shouldBe HttpStatus.CONFLICT + } + } + } +}) diff --git a/src/test/java/roomescape/reservation/business/ReservationWithPaymentServiceTest.kt b/src/test/java/roomescape/reservation/business/ReservationWithPaymentServiceTest.kt new file mode 100644 index 00000000..d4e1e804 --- /dev/null +++ b/src/test/java/roomescape/reservation/business/ReservationWithPaymentServiceTest.kt @@ -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 + } + } + } +}) diff --git a/src/test/java/roomescape/reservation/controller/ReservationControllerTest.java b/src/test/java/roomescape/reservation/controller/ReservationControllerTest.java deleted file mode 100644 index 5a888a55..00000000 --- a/src/test/java/roomescape/reservation/controller/ReservationControllerTest.java +++ /dev/null @@ -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 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 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> 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 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 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 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; - } -} diff --git a/src/test/java/roomescape/reservation/controller/ReservationTimeControllerTest.java b/src/test/java/roomescape/reservation/controller/ReservationTimeControllerTest.java deleted file mode 100644 index baaa53a3..00000000 --- a/src/test/java/roomescape/reservation/controller/ReservationTimeControllerTest.java +++ /dev/null @@ -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 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 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> validateRequestDataFormatSource() { - return Stream.of( - Map.of( - "startAt", "24:59" - ), - Map.of( - "startAt", "hihi") - ); - } - - @ParameterizedTest - @MethodSource("validateBlankRequestSource") - @DisplayName("예약 시간 생성 시, 요청 값에 공백 또는 null이 포함되어 있으면 400 에러를 발생한다.") - void validateBlankRequest(Map 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> 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 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)); - } -} diff --git a/src/test/java/roomescape/reservation/domain/ReservationTest.java b/src/test/java/roomescape/reservation/domain/ReservationTest.java deleted file mode 100644 index 6f19375d..00000000 --- a/src/test/java/roomescape/reservation/domain/ReservationTest.java +++ /dev/null @@ -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 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) - ); - } -} diff --git a/src/test/java/roomescape/reservation/domain/ReservationTimeTest.java b/src/test/java/roomescape/reservation/domain/ReservationTimeTest.java deleted file mode 100644 index 17e9dfaa..00000000 --- a/src/test/java/roomescape/reservation/domain/ReservationTimeTest.java +++ /dev/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); - } -} diff --git a/src/test/java/roomescape/reservation/domain/repository/ReservationSearchSpecificationTest.java b/src/test/java/roomescape/reservation/domain/repository/ReservationSearchSpecificationTest.java deleted file mode 100644 index 56f33293..00000000 --- a/src/test/java/roomescape/reservation/domain/repository/ReservationSearchSpecificationTest.java +++ /dev/null @@ -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 spec = new ReservationSearchSpecification().sameThemeId(themeId).build(); - - // when - List found = reservationRepository.findAll(spec); - - // then - assertThat(found).containsExactly(reservation1, reservation2, reservation3); - } - - @Test - @DisplayName("동일한 회원의 예약을 찾는다.") - void searchByMemberId() { - // given - Long memberId = reservation1.getMember().getId(); - Specification spec = new ReservationSearchSpecification().sameMemberId(memberId).build(); - - // when - List found = reservationRepository.findAll(spec); - - // then - assertThat(found).containsExactly(reservation1, reservation2, reservation3); - } - - @Test - @DisplayName("동일한 시간의 예약을 찾는다.") - void searchByTimeId() { - // given - Long timeId = reservation1.getReservationTime().getId(); - Specification spec = new ReservationSearchSpecification().sameTimeId(timeId).build(); - - // when - List found = reservationRepository.findAll(spec); - - // then - assertThat(found).containsExactly(reservation1, reservation2, reservation3); - } - - @Test - @DisplayName("동일한 날짜의 예약을 찾는다.") - void searchByDate() { - // given - LocalDate date = reservation1.getDate(); - Specification spec = new ReservationSearchSpecification().sameDate(date).build(); - - // when - List found = reservationRepository.findAll(spec); - - // then - assertThat(found).containsExactly(reservation1); - } - - @Test - @DisplayName("확정 상태인 예약을 찾는다.") - void searchConfirmedReservation() { - // given - Specification spec = new ReservationSearchSpecification().confirmed().build(); - - // when - List found = reservationRepository.findAll(spec); - - // then - assertThat(found).containsExactly(reservation1, reservation2); - } - - @Test - @DisplayName("대기 중인 예약을 찾는다.") - void searchWaitingReservation() { - // given - Specification spec = new ReservationSearchSpecification().waiting().build(); - - // when - List found = reservationRepository.findAll(spec); - - // then - assertThat(found).containsExactly(reservation3); - } - - @Test - @DisplayName("특정 날짜 이후의 예약을 찾는다.") - void searchDateStartFrom() { - // given : 어제 이후의 예약을 조회하면, 모든 예약이 조회되어야 한다. - LocalDate date = LocalDate.now().minusDays(1L); - Specification spec = new ReservationSearchSpecification().dateStartFrom(date).build(); - - // when - List found = reservationRepository.findAll(spec); - - // then - assertThat(found).containsExactly(reservation1, reservation2, reservation3); - } - - @Test - @DisplayName("특정 날짜 이전의 예약을 찾는다.") - void searchDateEndAt() { - // given : 내일 이전의 예약을 조회하면, 모든 예약이 조회되어야 한다. - LocalDate date = LocalDate.now().plusDays(1L); - Specification spec = new ReservationSearchSpecification().dateEndAt(date).build(); - - // when - List found = reservationRepository.findAll(spec); - - // then - assertThat(found).containsExactly(reservation1, reservation2, reservation3); - } -} diff --git a/src/test/java/roomescape/reservation/infrastructure/persistence/ReservationRepositoryTest.kt b/src/test/java/roomescape/reservation/infrastructure/persistence/ReservationRepositoryTest.kt new file mode 100644 index 00000000..40a40923 --- /dev/null +++ b/src/test/java/roomescape/reservation/infrastructure/persistence/ReservationRepositoryTest.kt @@ -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 = 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 = 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) + } +} diff --git a/src/test/java/roomescape/reservation/infrastructure/persistence/ReservationSearchSpecificationTest.kt b/src/test/java/roomescape/reservation/infrastructure/persistence/ReservationSearchSpecificationTest.kt new file mode 100644 index 00000000..34f96d9f --- /dev/null +++ b/src/test/java/roomescape/reservation/infrastructure/persistence/ReservationSearchSpecificationTest.kt @@ -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 = reservationRepository.findAll(spec) + + assertSoftly(results) { + this shouldHaveSize 3 + this shouldContainExactly listOf(confirmedNow, confirmedNotPaidYesterday, waitingTomorrow) + } + } + + "동일한 회원의 예약을 조회한다" { + val spec = ReservationSearchSpecification() + .sameMemberId(member.id) + .build() + + val results: List = reservationRepository.findAll(spec) + + assertSoftly(results) { + this shouldHaveSize 3 + this shouldContainExactly listOf(confirmedNow, confirmedNotPaidYesterday, waitingTomorrow) + } + } + + "동일한 예약 시간의 예약을 조회한다" { + val spec = ReservationSearchSpecification() + .sameTimeId(reservationTime.id) + .build() + + val results: List = reservationRepository.findAll(spec) + + assertSoftly(results) { + this shouldHaveSize 3 + this shouldContainExactly listOf(confirmedNow, confirmedNotPaidYesterday, waitingTomorrow) + } + } + + "동일한 날짜의 예약을 조회한다" { + val spec = ReservationSearchSpecification() + .sameDate(LocalDate.now()) + .build() + + val results: List = reservationRepository.findAll(spec) + + assertSoftly(results) { + this shouldHaveSize 1 + this shouldContainExactly listOf(confirmedNow) + } + } + + "확정 상태인 예약을 조회한다" { + val spec = ReservationSearchSpecification() + .confirmed() + .build() + + val results: List = reservationRepository.findAll(spec) + + assertSoftly(results) { + this shouldHaveSize 2 + this shouldContainExactly listOf(confirmedNow, confirmedNotPaidYesterday) + } + } + + "대기 상태인 예약을 조회한다" { + val spec = ReservationSearchSpecification() + .waiting() + .build() + + val results: List = reservationRepository.findAll(spec) + + assertSoftly(results) { + this shouldHaveSize 1 + this shouldContainExactly listOf(waitingTomorrow) + } + } + + "예약 날짜가 오늘 이후인 예약을 조회한다" { + val spec = ReservationSearchSpecification() + .dateStartFrom(LocalDate.now()) + .build() + + val results: List = 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 = 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() + } + } +} diff --git a/src/test/java/roomescape/reservation/infrastructure/persistence/ReservationTimeRepositoryTest.kt b/src/test/java/roomescape/reservation/infrastructure/persistence/ReservationTimeRepositoryTest.kt new file mode 100644 index 00000000..ba7045a3 --- /dev/null +++ b/src/test/java/roomescape/reservation/infrastructure/persistence/ReservationTimeRepositoryTest.kt @@ -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 + } + } +}) diff --git a/src/test/java/roomescape/reservation/service/ReservationServiceTest.java b/src/test/java/roomescape/reservation/service/ReservationServiceTest.java deleted file mode 100644 index b85aa741..00000000 --- a/src/test/java/roomescape/reservation/service/ReservationServiceTest.java +++ /dev/null @@ -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); - } -} diff --git a/src/test/java/roomescape/reservation/service/ReservationTimeServiceTest.java b/src/test/java/roomescape/reservation/service/ReservationTimeServiceTest.java deleted file mode 100644 index 473f5698..00000000 --- a/src/test/java/roomescape/reservation/service/ReservationTimeServiceTest.java +++ /dev/null @@ -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); - } -} diff --git a/src/test/java/roomescape/reservation/service/ReservationWithPaymentServiceTest.java b/src/test/java/roomescape/reservation/service/ReservationWithPaymentServiceTest.java deleted file mode 100644 index 4a7e5cdc..00000000 --- a/src/test/java/roomescape/reservation/service/ReservationWithPaymentServiceTest.java +++ /dev/null @@ -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(); - } -} diff --git a/src/test/java/roomescape/reservation/web/ReservationControllerTest.kt b/src/test/java/roomescape/reservation/web/ReservationControllerTest.kt new file mode 100644 index 00000000..f4fbfffe --- /dev/null +++ b/src/test/java/roomescape/reservation/web/ReservationControllerTest.kt @@ -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> + 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> + 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> + 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> + 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> + 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> + 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> { + val reservations: MutableMap> = mutableMapOf() + val members: List = 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 + } +} diff --git a/src/test/java/roomescape/reservation/web/ReservationTimeControllerTest.kt b/src/test/java/roomescape/reservation/web/ReservationTimeControllerTest.kt new file mode 100644 index 00000000..71b4040a --- /dev/null +++ b/src/test/java/roomescape/reservation/web/ReservationTimeControllerTest.kt @@ -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 = 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 + } + } + } + } + } +} diff --git a/src/test/java/roomescape/theme/util/TestThemeCreateUtil.kt b/src/test/java/roomescape/theme/util/TestThemeCreateUtil.kt index 4111939a..0f05cdfa 100644 --- a/src/test/java/roomescape/theme/util/TestThemeCreateUtil.kt +++ b/src/test/java/roomescape/theme/util/TestThemeCreateUtil.kt @@ -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 diff --git a/src/test/java/roomescape/theme/web/MostReservedThemeAPITest.kt b/src/test/java/roomescape/theme/web/MostReservedThemeAPITest.kt index 8931f6a2..73b62fdf 100644 --- a/src/test/java/roomescape/theme/web/MostReservedThemeAPITest.kt +++ b/src/test/java/roomescape/theme/web/MostReservedThemeAPITest.kt @@ -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 { diff --git a/src/test/java/roomescape/theme/web/ThemeControllerTest.kt b/src/test/java/roomescape/theme/web/ThemeControllerTest.kt index af3ea616..46893438 100644 --- a/src/test/java/roomescape/theme/web/ThemeControllerTest.kt +++ b/src/test/java/roomescape/theme/web/ThemeControllerTest.kt @@ -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( diff --git a/src/test/java/roomescape/util/DatabaseCleaner.kt b/src/test/java/roomescape/util/DatabaseCleaner.kt new file mode 100644 index 00000000..17e83bf6 --- /dev/null +++ b/src/test/java/roomescape/util/DatabaseCleaner.kt @@ -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 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) + } +} diff --git a/src/test/java/roomescape/util/Fixtures.kt b/src/test/java/roomescape/util/Fixtures.kt index 5236ef28..99bb0ec4 100644 --- a/src/test/java/roomescape/util/Fixtures.kt +++ b/src/test/java/roomescape/util/Fixtures.kt @@ -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 ) diff --git a/src/test/java/roomescape/util/RoomescapeApiTest.kt b/src/test/java/roomescape/util/RoomescapeApiTest.kt index 803bf86e..e8a0088d 100644 --- a/src/test/java/roomescape/util/RoomescapeApiTest.kt +++ b/src/test/java/roomescape/util/RoomescapeApiTest.kt @@ -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 { diff --git a/src/test/resources/reservationData.sql b/src/test/resources/reservationData.sql deleted file mode 100644 index b5a4c521..00000000 --- a/src/test/resources/reservationData.sql +++ /dev/null @@ -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); diff --git a/src/test/resources/test_search_data.sql b/src/test/resources/test_search_data.sql deleted file mode 100644 index c288d560..00000000 --- a/src/test/resources/test_search_data.sql +++ /dev/null @@ -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'); diff --git a/src/test/resources/truncate.sql b/src/test/resources/truncate.sql deleted file mode 100644 index cd541e0e..00000000 --- a/src/test/resources/truncate.sql +++ /dev/null @@ -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;