[#44] 매장 기능 도입 #45

Merged
pricelees merged 116 commits from feat/#44 into main 2025-09-20 03:15:06 +00:00
Showing only changes of commit be18775271 - Show all commits

View File

@ -1,6 +1,7 @@
package roomescape.reservation
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import org.hamcrest.CoreMatchers.equalTo
import org.springframework.data.repository.findByIdOrNull
import org.springframework.http.HttpMethod
@ -9,7 +10,6 @@ import roomescape.auth.exception.AuthErrorCode
import roomescape.common.config.next
import roomescape.common.exception.CommonErrorCode
import roomescape.common.util.DateUtils
import roomescape.user.infrastructure.persistence.UserEntity
import roomescape.payment.infrastructure.common.BankCode
import roomescape.payment.infrastructure.common.CardIssuerCode
import roomescape.payment.infrastructure.common.EasyPayCompanyCode
@ -21,6 +21,7 @@ import roomescape.reservation.infrastructure.persistence.ReservationRepository
import roomescape.reservation.infrastructure.persistence.ReservationStatus
import roomescape.reservation.web.MostReservedThemeIdListResponse
import roomescape.reservation.web.ReservationCancelRequest
import roomescape.reservation.web.ReservationSummaryResponse
import roomescape.schedule.infrastructure.persistence.ScheduleEntity
import roomescape.schedule.infrastructure.persistence.ScheduleRepository
import roomescape.schedule.infrastructure.persistence.ScheduleStatus
@ -28,6 +29,7 @@ import roomescape.supports.*
import roomescape.theme.infrastructure.persistence.ThemeEntity
import roomescape.theme.infrastructure.persistence.ThemeRepository
import roomescape.theme.web.toEntity
import roomescape.user.infrastructure.persistence.UserEntity
import java.time.LocalDate
import java.time.LocalTime
@ -55,7 +57,7 @@ class ReservationApiTest(
test("관리자") {
runExceptionTest(
token = testAuthUtil.defaultHqAdminLogin(),
token = testAuthUtil.defaultHqAdminLogin().second,
method = HttpMethod.POST,
endpoint = endpoint,
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
@ -66,13 +68,12 @@ class ReservationApiTest(
test("정상 생성") {
val schedule: ScheduleEntity = dummyInitializer.createSchedule(
adminToken = testAuthUtil.defaultHqAdminLogin(),
request = ScheduleFixture.createRequest,
status = ScheduleStatus.HOLD
)
runTest(
token = testAuthUtil.defaultUserLogin(),
token = testAuthUtil.defaultUserLogin().second,
using = {
body(commonRequest.copy(scheduleId = schedule.id))
},
@ -84,8 +85,7 @@ class ReservationApiTest(
}
).also {
val reservation: ReservationEntity =
reservationRepository.findByIdOrNull(it.extract().path("data.id"))
?: throw AssertionError("Unexpected Exception Occurred.")
reservationRepository.findByIdOrNull(it.extract().path("data.id"))!!
reservation.status shouldBe ReservationStatus.PENDING
reservation.scheduleId shouldBe schedule.id
@ -94,14 +94,10 @@ class ReservationApiTest(
}
test("예약을 생성할 때 해당 일정이 ${ScheduleStatus.HOLD} 상태가 아니면 실패한다.") {
val schedule: ScheduleEntity = dummyInitializer.createSchedule(
adminToken = testAuthUtil.defaultHqAdminLogin(),
request = ScheduleFixture.createRequest,
status = ScheduleStatus.AVAILABLE
)
val schedule: ScheduleEntity = dummyInitializer.createSchedule(status = ScheduleStatus.AVAILABLE)
runTest(
token = testAuthUtil.defaultUserLogin(),
token = testAuthUtil.defaultUserLogin().second,
using = {
body(commonRequest.copy(scheduleId = schedule.id))
},
@ -116,22 +112,16 @@ class ReservationApiTest(
}
test("예약 인원이 테마의 최소 인원보다 작거나 최대 인원보다 많으면 실패한다.") {
val adminToken = testAuthUtil.defaultHqAdminLogin()
val theme: ThemeEntity = dummyInitializer.createTheme(
adminToken = adminToken,
request = ThemeFixture.createRequest
)
val theme: ThemeEntity = dummyInitializer.createTheme()
val schedule: ScheduleEntity = dummyInitializer.createSchedule(
adminToken = adminToken,
request = ScheduleFixture.createRequest.copy(themeId = theme.id),
status = ScheduleStatus.HOLD
)
val userToken = testAuthUtil.defaultUserLogin()
val token = testAuthUtil.defaultUserLogin().second
runExceptionTest(
token = userToken,
token = token,
method = HttpMethod.POST,
endpoint = endpoint,
requestBody = commonRequest.copy(
@ -142,7 +132,7 @@ class ReservationApiTest(
)
runExceptionTest(
token = userToken,
token = token,
method = HttpMethod.POST,
endpoint = endpoint,
requestBody = commonRequest.copy(
@ -156,7 +146,7 @@ class ReservationApiTest(
context("필수 입력값이 입력되지 않으면 실패한다.") {
test("예약자명") {
runExceptionTest(
token = testAuthUtil.defaultUserLogin(),
token = testAuthUtil.defaultUserLogin().second,
method = HttpMethod.POST,
endpoint = endpoint,
requestBody = commonRequest.copy(reserverName = ""),
@ -166,7 +156,7 @@ class ReservationApiTest(
test("예약자 연락처") {
runExceptionTest(
token = testAuthUtil.defaultUserLogin(),
token = testAuthUtil.defaultUserLogin().second,
method = HttpMethod.POST,
endpoint = endpoint,
requestBody = commonRequest.copy(reserverContact = ""),
@ -190,7 +180,7 @@ class ReservationApiTest(
test("관리자") {
runExceptionTest(
token = testAuthUtil.defaultHqAdminLogin(),
token = testAuthUtil.defaultHqAdminLogin().second,
method = HttpMethod.POST,
endpoint = endpoint,
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
@ -199,15 +189,11 @@ class ReservationApiTest(
}
test("정상 응답") {
val userToken = testAuthUtil.defaultUserLogin()
val reservation: ReservationEntity = dummyInitializer.createPendingReservation(
adminToken = testAuthUtil.defaultHqAdminLogin(),
reserverToken = userToken,
)
val (user, token) = testAuthUtil.defaultUserLogin()
val reservation: ReservationEntity = dummyInitializer.createPendingReservation(user = user)
runTest(
token = userToken,
token = token,
on = {
post("/reservations/${reservation.id}/confirm")
},
@ -215,11 +201,8 @@ class ReservationApiTest(
statusCode(HttpStatus.OK.value())
}
).also {
val updatedReservation = reservationRepository.findByIdOrNull(reservation.id)
?: throw AssertionError("Unexpected Exception Occurred.")
val updatedSchedule = scheduleRepository.findByIdOrNull(updatedReservation.scheduleId)
?: throw AssertionError("Unexpected Exception Occurred.")
val updatedReservation = reservationRepository.findByIdOrNull(reservation.id)!!
val updatedSchedule = scheduleRepository.findByIdOrNull(updatedReservation.scheduleId)!!
updatedSchedule.status shouldBe ScheduleStatus.RESERVED
updatedReservation.status shouldBe ReservationStatus.CONFIRMED
@ -228,7 +211,7 @@ class ReservationApiTest(
test("예약이 없으면 실패한다.") {
runExceptionTest(
token = testAuthUtil.defaultUserLogin(),
token = testAuthUtil.defaultUserLogin().second,
method = HttpMethod.POST,
endpoint = "/reservations/$INVALID_PK/confirm",
expectedErrorCode = ReservationErrorCode.RESERVATION_NOT_FOUND
@ -250,7 +233,7 @@ class ReservationApiTest(
test("관리자") {
runExceptionTest(
token = testAuthUtil.defaultHqAdminLogin(),
token = testAuthUtil.defaultHqAdminLogin().second,
method = HttpMethod.POST,
endpoint = endpoint,
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
@ -259,15 +242,12 @@ class ReservationApiTest(
}
test("정상 응답") {
val userToken = testAuthUtil.defaultUserLogin()
val (user, token) = testAuthUtil.defaultUserLogin()
val reservation: ReservationEntity = dummyInitializer.createConfirmReservation(
adminToken = testAuthUtil.defaultHqAdminLogin(),
reserverToken = userToken,
)
val reservation: ReservationEntity = dummyInitializer.createConfirmReservation(user = user)
runTest(
token = userToken,
token = token,
using = {
body(ReservationCancelRequest(cancelReason = "test"))
},
@ -278,10 +258,8 @@ class ReservationApiTest(
statusCode(HttpStatus.OK.value())
}
).also {
val updatedReservation = reservationRepository.findByIdOrNull(reservation.id)
?: throw AssertionError("Unexpected Exception Occurred.")
val updatedSchedule = scheduleRepository.findByIdOrNull(updatedReservation.scheduleId)
?: throw AssertionError("Unexpected Exception Occurred.")
val updatedReservation = reservationRepository.findByIdOrNull(reservation.id)!!
val updatedSchedule = scheduleRepository.findByIdOrNull(updatedReservation.scheduleId)!!
updatedReservation.status shouldBe ReservationStatus.CANCELED
updatedSchedule.status shouldBe ScheduleStatus.AVAILABLE
@ -291,7 +269,7 @@ class ReservationApiTest(
test("예약이 없으면 실패한다.") {
runExceptionTest(
token = testAuthUtil.defaultUserLogin(),
token = testAuthUtil.defaultUserLogin().second,
method = HttpMethod.POST,
endpoint = "/reservations/$INVALID_PK/cancel",
requestBody = ReservationCancelRequest(cancelReason = "test"),
@ -300,13 +278,13 @@ class ReservationApiTest(
}
test("다른 회원의 예약을 취소할 수 없다.") {
val reservation: ReservationEntity = dummyInitializer.createConfirmReservation(
adminToken = testAuthUtil.defaultHqAdminLogin(),
reserverToken = testAuthUtil.defaultUserLogin(),
)
val (user, token) = testAuthUtil.defaultUserLogin()
val otherUserToken =
val reservation: ReservationEntity = dummyInitializer.createConfirmReservation(user = user)
val (otherUser, otherUserToken) =
testAuthUtil.userLogin(UserFixture.createUser(email = "test@test.com", phone = "01011111111"))
.also { it.first.email shouldNotBe user.email }
runExceptionTest(
token = otherUserToken,
@ -332,7 +310,7 @@ class ReservationApiTest(
test("관리자") {
runExceptionTest(
token = testAuthUtil.defaultHqAdminLogin(),
token = testAuthUtil.defaultHqAdminLogin().second,
method = HttpMethod.POST,
endpoint = endpoint,
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
@ -341,32 +319,22 @@ class ReservationApiTest(
}
test("정상 응답") {
val userToken = testAuthUtil.defaultUserLogin()
val adminToken = testAuthUtil.defaultHqAdminLogin()
val (user, userToken) = testAuthUtil.defaultUserLogin()
for (i in 1..3) {
dummyInitializer.createConfirmReservation(
adminToken = adminToken,
reserverToken = userToken,
themeRequest = ThemeFixture.createRequest.copy(name = "theme-$i"),
scheduleRequest = ScheduleFixture.createRequest.copy(
date = LocalDate.now().plusDays(i.toLong()),
time = LocalTime.now().plusHours(i.toLong())
initialize("테스트를 위한 3개의 ${ReservationStatus.CONFIRMED} 예약과 1개의 ${ReservationStatus.PENDING} 예약 생성") {
(1..3).forEach { i ->
dummyInitializer.createConfirmReservation(
user = user,
scheduleRequest = ScheduleFixture.createRequest.copy(
date = LocalDate.now().plusDays(i.toLong()),
time = LocalTime.now().plusHours(i.toLong())
)
)
)
}.also {
dummyInitializer.createPendingReservation(user = user)
}
}
// PENDING 예약은 조회되지 않음.
dummyInitializer.createPendingReservation(
adminToken = adminToken,
reserverToken = userToken,
themeRequest = ThemeFixture.createRequest.copy(name = "theme-$4"),
scheduleRequest = ScheduleFixture.createRequest.copy(
date = LocalDate.now().plusDays(1),
time = LocalTime.now()
)
)
runTest(
token = userToken,
on = {
@ -380,7 +348,13 @@ class ReservationApiTest(
propsNameIfList = "reservations"
)
}
)
).also { response ->
ResponseParser.parseListResponse<ReservationSummaryResponse>(
response.extract().path("data.reservations")
).forEach {
it.status shouldBe ReservationStatus.CONFIRMED
}
}
}
}
@ -398,7 +372,7 @@ class ReservationApiTest(
test("관리자") {
runExceptionTest(
token = testAuthUtil.defaultHqAdminLogin(),
token = testAuthUtil.defaultHqAdminLogin().second,
method = HttpMethod.POST,
endpoint = endpoint,
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
@ -412,10 +386,8 @@ class ReservationApiTest(
lateinit var reservation: ReservationEntity
beforeTest {
reservation = dummyInitializer.createConfirmReservation(
adminToken = testAuthUtil.defaultHqAdminLogin(),
reserverToken = testAuthUtil.defaultUserLogin(),
)
val user = testAuthUtil.defaultUserLogin().first
reservation = dummyInitializer.createConfirmReservation(user = user)
}
test("카드 결제") {
@ -547,7 +519,7 @@ class ReservationApiTest(
test("예약이 없으면 실패한다.") {
runExceptionTest(
token = testAuthUtil.defaultUserLogin(),
token = testAuthUtil.defaultUserLogin().second,
method = HttpMethod.GET,
endpoint = "/reservations/$INVALID_PK/detail",
expectedErrorCode = ReservationErrorCode.RESERVATION_NOT_FOUND
@ -555,13 +527,12 @@ class ReservationApiTest(
}
test("예약은 있지만, 결제 정보를 찾을 수 없으면 null로 지정한다.") {
val reservation = dummyInitializer.createConfirmReservation(
adminToken = testAuthUtil.defaultHqAdminLogin(),
reserverToken = testAuthUtil.defaultUserLogin(),
)
val (user, token) = testAuthUtil.defaultUserLogin()
val reservation = dummyInitializer.createConfirmReservation(user = user)
runTest(
token = testAuthUtil.defaultUserLogin(),
token = token,
on = {
get("/reservations/${reservation.id}/detail")
},
@ -573,10 +544,8 @@ class ReservationApiTest(
}
test("예약과 결제는 있지만, 결제 세부 내역이 없으면 세부 내역만 null로 지정한다..") {
val reservation = dummyInitializer.createConfirmReservation(
adminToken = testAuthUtil.defaultHqAdminLogin(),
reserverToken = testAuthUtil.defaultUserLogin(),
)
val (user, token) = testAuthUtil.defaultUserLogin()
val reservation = dummyInitializer.createConfirmReservation(user = user)
dummyInitializer.createPayment(
reservationId = reservation.id,
@ -586,7 +555,7 @@ class ReservationApiTest(
}
runTest(
token = testAuthUtil.defaultUserLogin(),
token = token,
on = {
get("/reservations/${reservation.id}/detail")
},
@ -621,7 +590,7 @@ class ReservationApiTest(
reservation: ReservationEntity
): LinkedHashMap<String, Any> {
return runTest(
token = testAuthUtil.defaultUserLogin(),
token = testAuthUtil.defaultUserLogin().second,
on = {
get("/reservations/${reservation.id}/detail")
},
@ -639,87 +608,71 @@ class ReservationApiTest(
val user: UserEntity = testAuthUtil.defaultUser()
val themeIds: List<Long> = (1..5).map {
themeRepository.save(
ThemeFixture.createRequest.copy(name = "theme-$it").toEntity(id = tsidFactory.next())
).id
themeRepository.save(ThemeFixture.createRequest.copy().toEntity(id = tsidFactory.next())).id
}
val store = dummyInitializer.createStore()
// 첫 번째 테마: 유효한 2개 예약
(1L..2L).forEach {
createScheduleAndReservation(
date = DateUtils.getSundayOfPreviousWeek(LocalDate.now()).plusDays(it),
themeId = themeIds[0],
userId = user.id,
dummyInitializer.createConfirmReservation(
user = user,
storeId = store.id,
scheduleRequest = ScheduleFixture.createRequest.copy(
date = DateUtils.getSundayOfPreviousWeek(LocalDate.now()).plusDays(it),
themeId = themeIds[0],
)
)
}
// 두 번째 테마: 유효한 1개 예약
createScheduleAndReservation(
date = DateUtils.getSundayOfPreviousWeek(LocalDate.now()),
themeId = themeIds[1],
userId = user.id,
dummyInitializer.createConfirmReservation(
user = user,
storeId = store.id,
scheduleRequest = ScheduleFixture.createRequest.copy(
date = DateUtils.getSundayOfPreviousWeek(LocalDate.now()),
themeId = themeIds[1],
)
)
// 세 번째 테마: 유효한 3개 예약
(1L..3L).forEach {
createScheduleAndReservation(
date = DateUtils.getSundayOfPreviousWeek(LocalDate.now()).plusDays(it),
themeId = themeIds[2],
userId = user.id,
dummyInitializer.createConfirmReservation(
user = user,
storeId = store.id,
scheduleRequest = ScheduleFixture.createRequest.copy(
date = DateUtils.getSundayOfPreviousWeek(LocalDate.now()).plusDays(it),
themeId = themeIds[2],
)
)
}
// 네 번째 테마: Pending 상태인 3개 예약 -> 집계되지 않음.
(1L..3L).forEach {
createScheduleAndReservation(
date = DateUtils.getSundayOfPreviousWeek(LocalDate.now()).plusDays(it),
themeId = themeIds[3],
userId = user.id,
isPending = true
dummyInitializer.createPendingReservation(
user = user,
storeId = store.id,
scheduleRequest = ScheduleFixture.createRequest.copy(
date = DateUtils.getSundayOfPreviousWeek(LocalDate.now()).plusDays(it),
themeId = themeIds[3],
)
)
}
// 다섯 번째 테마: 이번주의 확정 예약 -> 집계되지 않음.
(1L..3L).forEach { i ->
val thisMonday = DateUtils.getSundayOfPreviousWeek(LocalDate.now()).plusDays(8)
createScheduleAndReservation(
date = thisMonday.plusDays(i),
themeId = themeIds[4],
userId = user.id,
dummyInitializer.createConfirmReservation(
user = user,
storeId = store.id,
scheduleRequest = ScheduleFixture.createRequest.copy(
date = thisMonday.plusDays(i),
themeId = themeIds[4],
)
)
}
// 조회 예상 결과: 세번째, 첫번째, 두번째 테마 순서
return MostReservedThemeIdListResponse(listOf(themeIds[2], themeIds[0], themeIds[1]))
}
private fun createScheduleAndReservation(
date: LocalDate,
themeId: Long,
userId: Long,
isPending: Boolean = false
) {
val schedule = ScheduleEntity(
id = tsidFactory.next(),
date = date,
time = LocalTime.now(),
themeId = themeId,
status = if (isPending) ScheduleStatus.HOLD else ScheduleStatus.RESERVED
).also {
scheduleRepository.save(it)
}
ReservationEntity(
id = tsidFactory.next(),
userId = userId,
scheduleId = schedule.id,
reserverName = "이상돌",
reserverContact = "01012345678",
participantCount = 4,
requirement = "잘부탁드려요!",
status = if (isPending) ReservationStatus.PENDING else ReservationStatus.CONFIRMED,
).also {
reservationRepository.save(it)
}
}
}