pricelees 5fe1427fc1 [#30] 코드 구조 개선 (#31)
<!-- 제목 양식 -->
<!-- [이슈번호] 작업 요약 (예시: [#10] Gitea 템플릿 생성) -->

## 📝 관련 이슈 및 PR

**PR과 관련된 이슈 번호**
- #30

##  작업 내용
<!-- 어떤 작업을 했는지 알려주세요! -->
- ReservationService를 읽기(Find) / 쓰기(Write) 서비스로 분리
- 모든 도메인에 repository를 사용하는 Finder, Writer, Validator 도입 -> ReservationService에 있는 조회, 검증, 쓰기 작업을 별도의 클래스로 분리하기 위함이었고, 이 과정에서 다른 도메인에도 도입함.

## 🧪 테스트
<!-- 어떤 테스트를 생각했고 진행했는지 알려주세요! -->
새로 추가된 기능 & 클래스는 모두 테스트 추가하였고, 작업 후 전체 테스트 완료

## 📚 참고 자료 및 기타
<!-- 참고한 자료, 또는 논의할 사항이 있다면 알려주세요! -->

Reviewed-on: #31
Co-authored-by: pricelees <priceelees@gmail.com>
Co-committed-by: pricelees <priceelees@gmail.com>
2025-08-06 10:16:08 +00:00

167 lines
6.1 KiB
Kotlin

package roomescape.payment.business
import io.kotest.assertions.assertSoftly
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.types.shouldBeInstanceOf
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import roomescape.payment.exception.PaymentErrorCode
import roomescape.payment.exception.PaymentException
import roomescape.payment.implement.PaymentFinder
import roomescape.payment.implement.PaymentWriter
import roomescape.payment.infrastructure.client.PaymentApproveResponse
import roomescape.payment.infrastructure.persistence.CanceledPaymentEntity
import roomescape.payment.web.PaymentCancelRequest
import roomescape.payment.web.PaymentCancelResponse
import roomescape.util.PaymentFixture
import roomescape.util.ReservationFixture
import java.time.LocalDate
import java.time.LocalTime
import java.time.OffsetDateTime
import java.time.ZoneOffset
class PaymentServiceTest : FunSpec({
val paymentFinder: PaymentFinder = mockk()
val paymentWriter: PaymentWriter = mockk()
val paymentService = PaymentService(paymentFinder, paymentWriter)
context("createPayment") {
val approvedPaymentInfo = PaymentApproveResponse(
paymentKey = "paymentKey",
orderId = "orderId",
totalAmount = 1000L,
approvedAt = OffsetDateTime.now(),
)
val reservation = ReservationFixture.create(id = 1L)
test("정상 응답") {
every {
paymentWriter.create(
paymentKey = approvedPaymentInfo.paymentKey,
orderId = approvedPaymentInfo.orderId,
totalAmount = approvedPaymentInfo.totalAmount,
approvedAt = approvedPaymentInfo.approvedAt,
reservation = reservation
)
} returns PaymentFixture.create(
id = 1L,
orderId = approvedPaymentInfo.orderId,
paymentKey = approvedPaymentInfo.paymentKey,
totalAmount = approvedPaymentInfo.totalAmount,
approvedAt = approvedPaymentInfo.approvedAt,
reservation = reservation
)
val response = paymentService.createPayment(approvedPaymentInfo, reservation)
assertSoftly(response) {
it.id shouldBe 1L
it.paymentKey shouldBe approvedPaymentInfo.paymentKey
it.reservationId shouldBe reservation.id
}
}
}
context("createCanceledPayment(canceledPaymentInfo)") {
val canceledPaymentInfo = PaymentCancelResponse(
cancelStatus = "normal",
cancelReason = "고객 요청",
cancelAmount = 1000L,
canceledAt = OffsetDateTime.now(),
)
val approvedAt = OffsetDateTime.now()
val paymentKey = "paymentKey"
test("CanceledPaymentEntity를 응답") {
every {
paymentWriter.createCanceled(
cancelReason = canceledPaymentInfo.cancelReason,
cancelAmount = canceledPaymentInfo.cancelAmount,
canceledAt = canceledPaymentInfo.canceledAt,
approvedAt = approvedAt,
paymentKey = paymentKey
)
} returns PaymentFixture.createCanceled(
id = 1L,
paymentKey = paymentKey,
cancelAmount = canceledPaymentInfo.cancelAmount
)
val response = paymentService.createCanceledPayment(canceledPaymentInfo, approvedAt, paymentKey)
response.shouldBeInstanceOf<CanceledPaymentEntity>()
response.paymentKey shouldBe paymentKey
}
}
context("createCanceledPayment(reservationId)") {
val reservationId = 1L
test("취소 사유를 '예약 취소'로 하여 PaymentCancelRequest를 응답") {
val payment = PaymentFixture.create(id = 1L, paymentKey = "paymentKey", totalAmount = 1000L)
every {
paymentFinder.findByReservationId(reservationId)
} returns payment
val cancelReasonSlot = slot<String>()
every {
paymentWriter.createCanceled(payment, capture(cancelReasonSlot), any())
} returns PaymentFixture.createCanceled(
id = 1L,
paymentKey = payment.paymentKey,
cancelAmount = payment.totalAmount
)
val response = paymentService.createCanceledPayment(reservationId)
response.shouldBeInstanceOf<PaymentCancelRequest>()
cancelReasonSlot.captured shouldBe "예약 취소"
}
test("결제 정보가 없으면 예외 응답") {
every {
paymentFinder.findByReservationId(reservationId)
} throws PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND)
shouldThrow<PaymentException> {
paymentService.createCanceledPayment(reservationId)
}.also {
it.errorCode shouldBe PaymentErrorCode.PAYMENT_NOT_FOUND
}
}
}
context("updateCanceledTime") {
val paymentKey = "paymentKey"
val canceledAt = OffsetDateTime.of(LocalDate.of(2025, 8, 5), LocalTime.of(10, 0), ZoneOffset.UTC)
test("정상 응답") {
val canceled = PaymentFixture.createCanceled(id = 1L)
every {
paymentFinder.findCanceledByKey(paymentKey)
} returns canceled
paymentService.updateCanceledTime(paymentKey, canceledAt)
canceled.canceledAt shouldBe canceledAt
}
test("결제 취소 정보가 없으면 예외 응답") {
every {
paymentFinder.findCanceledByKey(paymentKey)
} throws PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND)
shouldThrow<PaymentException> {
paymentService.updateCanceledTime(paymentKey, canceledAt)
}.also {
it.errorCode shouldBe PaymentErrorCode.PAYMENT_NOT_FOUND
}
}
}
})