generated from pricelees/issue-pr-template
[#56] 예약 & 결제 프로세스 및 패키지 구조 재정의 #57
@ -0,0 +1,293 @@
|
|||||||
|
package com.sangdol.roomescape.order
|
||||||
|
|
||||||
|
import com.ninjasquad.springmockk.SpykBean
|
||||||
|
import com.sangdol.common.utils.KoreaDate
|
||||||
|
import com.sangdol.common.utils.KoreaTime
|
||||||
|
import com.sangdol.roomescape.auth.exception.AuthErrorCode
|
||||||
|
import com.sangdol.roomescape.order.exception.OrderErrorCode
|
||||||
|
import com.sangdol.roomescape.order.infrastructure.persistence.AttemptResult
|
||||||
|
import com.sangdol.roomescape.order.infrastructure.persistence.PaymentAttemptEntity
|
||||||
|
import com.sangdol.roomescape.order.infrastructure.persistence.PaymentAttemptRepository
|
||||||
|
import com.sangdol.roomescape.order.infrastructure.persistence.PostOrderTaskRepository
|
||||||
|
import com.sangdol.roomescape.payment.business.PaymentService
|
||||||
|
import com.sangdol.roomescape.payment.business.domain.PaymentMethod
|
||||||
|
import com.sangdol.roomescape.payment.dto.PaymentConfirmRequest
|
||||||
|
import com.sangdol.roomescape.payment.dto.PaymentGatewayResponse
|
||||||
|
import com.sangdol.roomescape.payment.exception.PaymentErrorCode
|
||||||
|
import com.sangdol.roomescape.payment.exception.PaymentException
|
||||||
|
import com.sangdol.roomescape.payment.infrastructure.persistence.PaymentDetailRepository
|
||||||
|
import com.sangdol.roomescape.payment.infrastructure.persistence.PaymentRepository
|
||||||
|
import com.sangdol.roomescape.reservation.exception.ReservationErrorCode
|
||||||
|
import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationRepository
|
||||||
|
import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationStatus
|
||||||
|
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleRepository
|
||||||
|
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus
|
||||||
|
import com.sangdol.roomescape.supports.*
|
||||||
|
import com.sangdol.roomescape.user.infrastructure.persistence.UserEntity
|
||||||
|
import io.kotest.assertions.assertSoftly
|
||||||
|
import io.kotest.matchers.booleans.shouldBeTrue
|
||||||
|
import io.kotest.matchers.nulls.shouldNotBeNull
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import io.mockk.every
|
||||||
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
|
import org.springframework.http.HttpMethod
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
|
|
||||||
|
class OrderApiTest(
|
||||||
|
@SpykBean private val paymentService: PaymentService,
|
||||||
|
private val paymentAttemptRepository: PaymentAttemptRepository,
|
||||||
|
private val reservationRepository: ReservationRepository,
|
||||||
|
private val postOrderTaskRepository: PostOrderTaskRepository,
|
||||||
|
private val scheduleRepository: ScheduleRepository,
|
||||||
|
private val paymentRepository: PaymentRepository,
|
||||||
|
private val paymentDetailRepository: PaymentDetailRepository
|
||||||
|
) : FunSpecSpringbootTest() {
|
||||||
|
|
||||||
|
val paymentRequest: PaymentConfirmRequest = PaymentFixture.confirmRequest
|
||||||
|
val expectedPaymentResponse: PaymentGatewayResponse = PaymentFixture.confirmResponse(
|
||||||
|
paymentKey = paymentRequest.paymentKey,
|
||||||
|
orderId = paymentRequest.orderId,
|
||||||
|
amount = paymentRequest.amount,
|
||||||
|
method = PaymentMethod.CARD
|
||||||
|
)
|
||||||
|
|
||||||
|
init {
|
||||||
|
context("결제 및 예약을 확정한다.") {
|
||||||
|
lateinit var user: UserEntity
|
||||||
|
lateinit var token: String
|
||||||
|
|
||||||
|
beforeTest {
|
||||||
|
val loginResult = testAuthUtil.defaultUserLogin()
|
||||||
|
user = loginResult.first
|
||||||
|
token = loginResult.second
|
||||||
|
}
|
||||||
|
|
||||||
|
context("권한이 없으면 접근할 수 없다.") {
|
||||||
|
test("비회원") {
|
||||||
|
runExceptionTest(
|
||||||
|
method = HttpMethod.POST,
|
||||||
|
endpoint = "/orders/${INVALID_PK}/confirm",
|
||||||
|
expectedErrorCode = AuthErrorCode.TOKEN_NOT_FOUND
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
test("관리자") {
|
||||||
|
runExceptionTest(
|
||||||
|
token = testAuthUtil.defaultHqAdminLogin().second,
|
||||||
|
method = HttpMethod.POST,
|
||||||
|
endpoint = "/orders/${INVALID_PK}/confirm",
|
||||||
|
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("정상 응답") {
|
||||||
|
val reservation = dummyInitializer.createPendingReservation(user)
|
||||||
|
|
||||||
|
every {
|
||||||
|
paymentService.requestConfirm(paymentRequest)
|
||||||
|
} returns expectedPaymentResponse
|
||||||
|
|
||||||
|
runTest(
|
||||||
|
token = token,
|
||||||
|
using = {
|
||||||
|
body(paymentRequest)
|
||||||
|
},
|
||||||
|
on = {
|
||||||
|
post("/orders/${reservation.id}/confirm")
|
||||||
|
},
|
||||||
|
expect = {
|
||||||
|
statusCode(HttpStatus.OK.value())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assertSoftly(scheduleRepository.findByIdOrNull(reservation.scheduleId)!!) {
|
||||||
|
this.status shouldBe ScheduleStatus.RESERVED
|
||||||
|
this.holdExpiredAt shouldBe null
|
||||||
|
}
|
||||||
|
reservationRepository.findByIdOrNull(reservation.id)!!.status shouldBe ReservationStatus.CONFIRMED
|
||||||
|
|
||||||
|
assertSoftly(paymentRepository.findByReservationId(reservation.id)) {
|
||||||
|
this.shouldNotBeNull()
|
||||||
|
this.status shouldBe expectedPaymentResponse.status
|
||||||
|
|
||||||
|
paymentDetailRepository.findByPaymentId(this.id)!!.shouldNotBeNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
paymentAttemptRepository.isSuccessAttemptExists(reservation.id).shouldBeTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
context("검증 과정에서의 실패 응답") {
|
||||||
|
test("예약이 없으면 실패한다.") {
|
||||||
|
runExceptionTest(
|
||||||
|
token = token,
|
||||||
|
method = HttpMethod.POST,
|
||||||
|
endpoint = "/orders/${INVALID_PK}/confirm",
|
||||||
|
requestBody = paymentRequest,
|
||||||
|
expectedErrorCode = ReservationErrorCode.RESERVATION_NOT_FOUND
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
test("이미 결제가 완료된 예약이면 실패한다.") {
|
||||||
|
val reservation = dummyInitializer.createPendingReservation(user)
|
||||||
|
|
||||||
|
paymentAttemptRepository.save(PaymentAttemptEntity(
|
||||||
|
id = IDGenerator.create(),
|
||||||
|
reservationId = reservation.id,
|
||||||
|
result = AttemptResult.SUCCESS
|
||||||
|
))
|
||||||
|
|
||||||
|
runExceptionTest(
|
||||||
|
token = token,
|
||||||
|
method = HttpMethod.POST,
|
||||||
|
endpoint = "/orders/${reservation.id}/confirm",
|
||||||
|
requestBody = paymentRequest,
|
||||||
|
expectedErrorCode = OrderErrorCode.NOT_CONFIRMABLE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
test("이미 확정된 예약이면 실패한다.") {
|
||||||
|
val reservation = dummyInitializer.createConfirmReservation(user)
|
||||||
|
|
||||||
|
runExceptionTest(
|
||||||
|
token = token,
|
||||||
|
method = HttpMethod.POST,
|
||||||
|
endpoint = "/orders/${reservation.id}/confirm",
|
||||||
|
requestBody = paymentRequest,
|
||||||
|
expectedErrorCode = OrderErrorCode.NOT_CONFIRMABLE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
test("만료된 예약이면 실패한다.") {
|
||||||
|
val reservation = dummyInitializer.createExpiredOrCanceledReservation(user, ReservationStatus.EXPIRED)
|
||||||
|
|
||||||
|
runExceptionTest(
|
||||||
|
token = token,
|
||||||
|
method = HttpMethod.POST,
|
||||||
|
endpoint = "/orders/${reservation.id}/confirm",
|
||||||
|
requestBody = paymentRequest,
|
||||||
|
expectedErrorCode = OrderErrorCode.NOT_CONFIRMABLE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
test("취소된 예약이면 실패한다.") {
|
||||||
|
val reservation = dummyInitializer.createExpiredOrCanceledReservation(user, ReservationStatus.CANCELED)
|
||||||
|
|
||||||
|
runExceptionTest(
|
||||||
|
token = token,
|
||||||
|
method = HttpMethod.POST,
|
||||||
|
endpoint = "/orders/${reservation.id}/confirm",
|
||||||
|
requestBody = paymentRequest,
|
||||||
|
expectedErrorCode = OrderErrorCode.NOT_CONFIRMABLE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
test("일정이 HOLD 상태가 아니라면 실패한다.") {
|
||||||
|
val schedule = dummyInitializer.createSchedule(status = ScheduleStatus.AVAILABLE)
|
||||||
|
val reservation = dummyInitializer.createPendingReservation(
|
||||||
|
user = user,
|
||||||
|
reservationRequest = ReservationFixture.pendingCreateRequest.copy(scheduleId = schedule.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
runExceptionTest(
|
||||||
|
token = token,
|
||||||
|
method = HttpMethod.POST,
|
||||||
|
endpoint = "/orders/${reservation.id}/confirm",
|
||||||
|
requestBody = paymentRequest,
|
||||||
|
expectedErrorCode = OrderErrorCode.NOT_CONFIRMABLE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
test("일정의 시작 시간이 현재 시간 이전이면 실패한다.") {
|
||||||
|
val schedule = dummyInitializer.createSchedule(
|
||||||
|
status = ScheduleStatus.HOLD,
|
||||||
|
request = ScheduleFixture.createRequest.copy(
|
||||||
|
date = KoreaDate.today(),
|
||||||
|
time = KoreaTime.now()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val reservation = dummyInitializer.createPendingReservation(
|
||||||
|
user = user,
|
||||||
|
reservationRequest = ReservationFixture.pendingCreateRequest.copy(scheduleId = schedule.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
runExceptionTest(
|
||||||
|
token = token,
|
||||||
|
method = HttpMethod.POST,
|
||||||
|
endpoint = "/orders/${reservation.id}/confirm",
|
||||||
|
requestBody = paymentRequest,
|
||||||
|
expectedErrorCode = OrderErrorCode.NOT_CONFIRMABLE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("결제 과정에서의 실패 응답.") {
|
||||||
|
test("결제에 실패해도 검증을 통과하면 예약을 ${ReservationStatus.PAYMENT_IN_PROGRESS} 상태로 바꾸고, 결제 시도 이력을 기록한다.") {
|
||||||
|
val reservation = dummyInitializer.createPendingReservation(user)
|
||||||
|
|
||||||
|
every {
|
||||||
|
paymentService.requestConfirm(paymentRequest)
|
||||||
|
} throws PaymentException(PaymentErrorCode.PAYMENT_CLIENT_ERROR)
|
||||||
|
|
||||||
|
runExceptionTest(
|
||||||
|
token = token,
|
||||||
|
method = HttpMethod.POST,
|
||||||
|
endpoint = "/orders/${reservation.id}/confirm",
|
||||||
|
requestBody = paymentRequest,
|
||||||
|
expectedErrorCode = PaymentErrorCode.PAYMENT_CLIENT_ERROR
|
||||||
|
).also {
|
||||||
|
it.extract().path<Long>("trial") shouldBe 0
|
||||||
|
}
|
||||||
|
|
||||||
|
assertSoftly(reservationRepository.findByIdOrNull(reservation.id)!!) {
|
||||||
|
this.status shouldBe ReservationStatus.PAYMENT_IN_PROGRESS
|
||||||
|
}
|
||||||
|
|
||||||
|
val paymentAttempt = paymentAttemptRepository.findAll().first { it.reservationId == reservation.id }
|
||||||
|
assertSoftly(paymentAttempt) {
|
||||||
|
it.shouldNotBeNull()
|
||||||
|
it.result shouldBe AttemptResult.FAILED
|
||||||
|
it.errorCode shouldBe PaymentErrorCode.PAYMENT_CLIENT_ERROR.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("결제 성공 이후 실패 응답.") {
|
||||||
|
test("결제 이력 저장 과정에서 예외가 발생하면 해당 작업을 저장하며, 사용자는 정상 응답을 받는다.") {
|
||||||
|
val reservation = dummyInitializer.createPendingReservation(user)
|
||||||
|
|
||||||
|
every {
|
||||||
|
paymentService.requestConfirm(paymentRequest)
|
||||||
|
} returns expectedPaymentResponse
|
||||||
|
|
||||||
|
every {
|
||||||
|
paymentService.savePayment(reservation.id, expectedPaymentResponse)
|
||||||
|
} throws RuntimeException("결제 저장 실패!")
|
||||||
|
|
||||||
|
runTest(
|
||||||
|
token = token,
|
||||||
|
using = {
|
||||||
|
body(paymentRequest)
|
||||||
|
},
|
||||||
|
on = {
|
||||||
|
post("/orders/${reservation.id}/confirm")
|
||||||
|
},
|
||||||
|
expect = {
|
||||||
|
statusCode(HttpStatus.OK.value())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
paymentAttemptRepository.isSuccessAttemptExists(reservation.id).shouldBeTrue()
|
||||||
|
|
||||||
|
val postOrderTask = postOrderTaskRepository.findAll().first { it.reservationId == reservation.id }
|
||||||
|
assertSoftly(postOrderTask) {
|
||||||
|
it.shouldNotBeNull()
|
||||||
|
it.paymentKey shouldBe paymentRequest.paymentKey
|
||||||
|
it.trial shouldBe 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user