[#41] 예약 스키마 재정의 #42

Merged
pricelees merged 41 commits from refactor/#41 into main 2025-09-09 00:43:39 +00:00
2 changed files with 461 additions and 0 deletions
Showing only changes of commit b847e59d6f - Show all commits

View File

@ -0,0 +1,361 @@
package roomescape.payment
import com.ninjasquad.springmockk.MockkBean
import io.kotest.matchers.shouldBe
import io.mockk.every
import org.hamcrest.CoreMatchers.equalTo
import org.springframework.data.repository.findByIdOrNull
import org.springframework.http.HttpStatus
import roomescape.payment.business.PaymentService
import roomescape.payment.exception.PaymentErrorCode
import roomescape.payment.infrastructure.client.CardDetail
import roomescape.payment.infrastructure.client.EasyPayDetail
import roomescape.payment.infrastructure.client.TosspayClient
import roomescape.payment.infrastructure.client.TransferDetail
import roomescape.payment.infrastructure.common.*
import roomescape.payment.infrastructure.persistence.*
import roomescape.payment.web.PaymentConfirmRequest
import roomescape.payment.web.PaymentCreateResponse
import roomescape.reservation.infrastructure.persistence.ReservationEntity
import roomescape.util.FunSpecSpringbootTest
import roomescape.util.PaymentFixture
import roomescape.util.runTest
class PaymentAPITest(
@MockkBean
private val tosspayClient: TosspayClient,
private val paymentService: PaymentService,
private val paymentRepository: PaymentRepository,
private val paymentDetailRepository: PaymentDetailRepository,
private val canceledPaymentRepository: CanceledPaymentRepository
) : FunSpecSpringbootTest() {
init {
context("결제를 승인한다.") {
val amount = 100_000
context("간편결제 + 카드로 ${amount}원을 결제한다.") {
context("일시불") {
test("토스페이 + 토스뱅크카드(신용)") {
runConfirmTest(
amount = amount,
cardDetail = PaymentFixture.cardDetail(
amount = amount,
issuerCode = CardIssuerCode.TOSS_BANK,
cardType = CardType.CREDIT,
),
easyPayDetail = PaymentFixture.easypayDetail(
amount = 0,
provider = EasyPayCompanyCode.TOSSPAY
)
)
}
test("삼성페이 + 삼성카드(법인)") {
runConfirmTest(
amount = amount,
cardDetail = PaymentFixture.cardDetail(
amount = amount,
issuerCode = CardIssuerCode.SAMSUNG,
cardType = CardType.CREDIT,
ownerType = CardOwnerType.CORPORATE
),
easyPayDetail = PaymentFixture.easypayDetail(
amount = 0,
provider = EasyPayCompanyCode.SAMSUNGPAY
)
)
}
}
context("할부") {
val installmentPlanMonths = 12
test("네이버페이 + 신한카드 / 12개월") {
runConfirmTest(
amount = amount,
cardDetail = PaymentFixture.cardDetail(
amount = amount,
issuerCode = CardIssuerCode.SHINHAN,
installmentPlanMonths = installmentPlanMonths
),
easyPayDetail = PaymentFixture.easypayDetail(
amount = 0,
provider = EasyPayCompanyCode.NAVERPAY
)
)
}
}
context("간편결제사 포인트 일부 사용") {
val point = (amount * 0.1).toInt()
test("토스페이 + 국민카드(체크) / 일시불 / $point 포인트 사용") {
runConfirmTest(
amount = amount,
cardDetail = PaymentFixture.cardDetail(
amount = (amount - point),
issuerCode = CardIssuerCode.KOOKMIN,
cardType = CardType.CHECK
),
easyPayDetail = PaymentFixture.easypayDetail(
amount = 0,
provider = EasyPayCompanyCode.TOSSPAY,
discountAmount = point
)
)
}
}
}
context("간편결제사의 선불 충전금액으로 ${amount}원을 결제한다.") {
test("토스페이 + 토스페이머니 / 전액") {
runConfirmTest(
easyPayDetail = PaymentFixture.easypayDetail(
amount = amount,
provider = EasyPayCompanyCode.TOSSPAY
)
)
}
val point = (amount * 0.05).toInt()
test("카카오페이 + 카카오페이머니 / $point 사용") {
runConfirmTest(
easyPayDetail = PaymentFixture.easypayDetail(
amount = (amount - point),
provider = EasyPayCompanyCode.KAKAOPAY,
discountAmount = point
)
)
}
}
context("계좌이체로 결제한다.") {
test("토스뱅크") {
runConfirmTest(
transferDetail = PaymentFixture.transferDetail(bankCode = BankCode.TOSS_BANK)
)
}
}
context("지원하지 않는 결제수단으로 요청시 실패한다.") {
val supportedMethod = listOf(
PaymentMethod.CARD,
PaymentMethod.EASY_PAY,
PaymentMethod.TRANSFER,
)
PaymentMethod.entries.filter { it !in supportedMethod }.forEach {
test("결제 수단: ${it.koreanName}") {
val reservation = dummyInitializer.createConfirmReservation(
adminToken = loginUtil.loginAsAdmin(),
reserverToken = loginUtil.loginAsUser()
)
val request = PaymentFixture.confirmRequest
every {
tosspayClient.confirm(request.paymentKey, request.orderId, request.amount)
} returns PaymentFixture.confirmResponse(
paymentKey = request.paymentKey,
amount = request.amount,
method = it,
cardDetail = null,
easyPayDetail = null,
transferDetail = null,
)
runTest(
token = loginUtil.loginAsUser(),
using = {
body(PaymentFixture.confirmRequest)
},
on = {
post("/payments?reservationId=${reservation.id}")
},
expect = {
statusCode(HttpStatus.BAD_REQUEST.value())
body("code", equalTo(PaymentErrorCode.NOT_SUPPORTED_PAYMENT_TYPE.errorCode))
}
)
}
}
}
}
context("결제를 취소한다.") {
test("정상 취소") {
val token = loginUtil.loginAsAdmin()
val confirmRequest = PaymentFixture.confirmRequest
val reservation = dummyInitializer.createConfirmReservation(
adminToken = token,
reserverToken = token
)
val paymentCreateResponse = createPayment(
request = confirmRequest,
reservationId = reservation.id
)
every {
tosspayClient.cancel(
confirmRequest.paymentKey,
confirmRequest.amount,
cancelReason = "cancelReason"
)
} returns PaymentFixture.cancelResponse(confirmRequest.amount)
runTest(
token = token,
using = {
val cancelRequest = PaymentFixture.cancelRequest.copy(
reservationId = reservation.id
)
body(cancelRequest)
},
on = {
post("/payments/cancel")
},
expect = {
statusCode(HttpStatus.OK.value())
}
).also {
val payment = paymentRepository.findByIdOrNull(paymentCreateResponse.paymentId)
?: throw AssertionError("Unexpected Exception Occurred.")
val canceledPayment =
canceledPaymentRepository.findByPaymentId(paymentCreateResponse.paymentId)
?: throw AssertionError("Unexpected Exception Occurred.")
payment.status shouldBe PaymentStatus.CANCELED
canceledPayment.paymentId shouldBe payment.id
canceledPayment.cancelAmount shouldBe payment.totalAmount
}
}
test("예약에 대한 결제 정보가 없으면 실패한다.") {
val token = loginUtil.loginAsAdmin()
val reservation = dummyInitializer.createConfirmReservation(
adminToken = token,
reserverToken = token,
)
runTest(
token = token,
using = {
body(PaymentFixture.cancelRequest.copy(reservationId = reservation.id))
},
on = {
post("/payments/cancel")
},
expect = {
statusCode(HttpStatus.NOT_FOUND.value())
body("code", equalTo(PaymentErrorCode.PAYMENT_NOT_FOUND.errorCode))
}
)
}
}
}
private fun createPayment(
request: PaymentConfirmRequest,
reservationId: Long,
): PaymentCreateResponse {
every {
tosspayClient.confirm(request.paymentKey, request.orderId, request.amount)
} returns PaymentFixture.confirmResponse(
request.paymentKey,
request.amount,
method = PaymentMethod.CARD,
cardDetail = PaymentFixture.cardDetail(request.amount),
easyPayDetail = null,
transferDetail = null,
)
return paymentService.confirm(reservationId, request)
}
fun runConfirmTest(
cardDetail: CardDetail? = null,
easyPayDetail: EasyPayDetail? = null,
transferDetail: TransferDetail? = null,
paymentKey: String = "paymentKey",
amount: Int = 10000,
) {
val request = PaymentFixture.confirmRequest.copy(paymentKey = paymentKey, amount = amount)
val reservation: ReservationEntity = dummyInitializer.createPendingReservation(
adminToken = loginUtil.loginAsAdmin(),
reserverToken = loginUtil.loginAsUser(),
)
val method = if (easyPayDetail != null) {
PaymentMethod.EASY_PAY
} else if (cardDetail != null) {
PaymentMethod.CARD
} else if (transferDetail != null) {
PaymentMethod.TRANSFER
} else {
throw AssertionError("결제타입 확인 필요.")
}
val clientResponse = PaymentFixture.confirmResponse(
paymentKey, amount, method, cardDetail, easyPayDetail, transferDetail
)
every {
tosspayClient.confirm(request.paymentKey, request.orderId, request.amount)
} returns clientResponse
runTest(
token = loginUtil.loginAsUser(),
using = {
body(request)
},
on = {
post("/payments?reservationId=${reservation.id}")
},
expect = {
statusCode(HttpStatus.OK.value())
}
).also {
val createdPayment = paymentRepository.findByIdOrNull(it.extract().path("data.paymentId"))
?: throw AssertionError("Unexpected Exception Occurred.")
val createdPaymentDetail =
paymentDetailRepository.findByIdOrNull(it.extract().path("data.detailId"))
?: throw AssertionError("Unexpected Exception Occurred.")
createdPayment.status shouldBe clientResponse.status
createdPayment.method shouldBe clientResponse.method
createdPayment.reservationId shouldBe reservation.id
when (createdPaymentDetail) {
is PaymentCardDetailEntity -> {
createdPaymentDetail.issuerCode shouldBe clientResponse.card!!.issuerCode
createdPaymentDetail.cardType shouldBe clientResponse.card.cardType
createdPaymentDetail.cardNumber shouldBe clientResponse.card.number
createdPaymentDetail.suppliedAmount shouldBe clientResponse.suppliedAmount
createdPaymentDetail.vat shouldBe clientResponse.vat
createdPaymentDetail.amount shouldBe (clientResponse.totalAmount - (clientResponse.easyPay?.discountAmount
?: 0))
clientResponse.easyPay?.let { easypay ->
createdPaymentDetail.easypayProviderCode shouldBe easypay.provider
createdPaymentDetail.easypayDiscountAmount shouldBe easypay.discountAmount
}
}
is PaymentBankTransferDetailEntity -> {
createdPaymentDetail.suppliedAmount shouldBe clientResponse.suppliedAmount
createdPaymentDetail.vat shouldBe clientResponse.vat
createdPaymentDetail.bankCode shouldBe clientResponse.transfer!!.bankCode
createdPaymentDetail.settlementStatus shouldBe clientResponse.transfer.settlementStatus
}
is PaymentEasypayPrepaidDetailEntity -> {
createdPaymentDetail.suppliedAmount shouldBe clientResponse.suppliedAmount
createdPaymentDetail.vat shouldBe clientResponse.vat
createdPaymentDetail.easypayProviderCode shouldBe clientResponse.easyPay!!.provider
createdPaymentDetail.amount shouldBe clientResponse.easyPay.amount
createdPaymentDetail.discountAmount shouldBe clientResponse.easyPay.discountAmount
}
}
}
}
}

View File

@ -3,11 +3,17 @@ package roomescape.util
import com.github.f4b6a3.tsid.TsidFactory
import roomescape.member.infrastructure.persistence.MemberEntity
import roomescape.member.infrastructure.persistence.Role
import roomescape.payment.infrastructure.client.*
import roomescape.payment.infrastructure.common.*
import roomescape.payment.web.PaymentCancelRequest
import roomescape.payment.web.PaymentConfirmRequest
import roomescape.reservation.web.PendingReservationCreateRequest
import roomescape.schedule.web.ScheduleCreateRequest
import roomescape.theme.infrastructure.persistence.Difficulty
import roomescape.theme.web.ThemeCreateRequest
import java.time.LocalDate
import java.time.LocalTime
import java.time.OffsetDateTime
const val INVALID_PK: Long = 9999L
val tsidFactory = TsidFactory(0)
@ -53,3 +59,97 @@ object ScheduleFixture {
themeId = 1L
)
}
object PaymentFixture {
val confirmRequest: PaymentConfirmRequest = PaymentConfirmRequest(
paymentKey = "paymentKey",
orderId = "orderId",
amount = 10000,
paymentType = PaymentType.NORMAL
)
val cancelRequest: PaymentCancelRequest = PaymentCancelRequest(
reservationId = 1L,
cancelReason = "cancelReason",
)
fun cardDetail(
amount: Int,
issuerCode: CardIssuerCode = CardIssuerCode.SHINHAN,
cardType: CardType = CardType.CREDIT,
ownerType: CardOwnerType = CardOwnerType.PERSONAL,
installmentPlanMonths: Int = 0,
): CardDetail = CardDetail(
issuerCode = issuerCode,
number = "429335*********",
amount = amount,
cardType = cardType,
ownerType = ownerType,
isInterestFree = false,
approveNo = "1828382",
installmentPlanMonths = installmentPlanMonths
)
fun easypayDetail(
amount: Int,
provider: EasyPayCompanyCode = EasyPayCompanyCode.TOSSPAY,
discountAmount: Int = 0
): EasyPayDetail = EasyPayDetail(provider, amount, discountAmount)
fun transferDetail(
bankCode: BankCode = BankCode.SHINHAN,
settlementStatus: String = "COMPLETED"
): TransferDetail = TransferDetail(
bankCode = bankCode,
settlementStatus = settlementStatus
)
fun confirmResponse(
paymentKey: String,
amount: Int,
method: PaymentMethod,
cardDetail: CardDetail?,
easyPayDetail: EasyPayDetail?,
transferDetail: TransferDetail?
) = PaymentClientConfirmResponse(
paymentKey = paymentKey,
status = PaymentStatus.DONE,
totalAmount = amount,
vat = (amount * 0.1).toInt(),
suppliedAmount = (amount * 0.9).toInt(),
method = method,
card = cardDetail,
easyPay = easyPayDetail,
transfer = transferDetail,
requestedAt = OffsetDateTime.now(),
approvedAt = OffsetDateTime.now().plusSeconds(5)
)
fun cancelResponse(
amount: Int,
cardDiscountAmount: Int = 0,
transferDiscountAmount: Int = 0,
easypayDiscountAmount: Int = 0,
cancelReason: String = "cancelReason"
) = PaymentClientCancelResponse(
status = PaymentStatus.CANCELED,
cancels = CancelDetail(
cancelAmount = amount,
cardDiscountAmount = cardDiscountAmount,
transferDiscountAmount = transferDiscountAmount,
easyPayDiscountAmount = easypayDiscountAmount,
canceledAt = OffsetDateTime.now().plusSeconds(5),
cancelReason = cancelReason
),
)
}
object ReservationFixture {
val pendingCreateRequest: PendingReservationCreateRequest = PendingReservationCreateRequest(
scheduleId = 1L,
reserverName = "Wilbur Stuart",
reserverContact = "wilbur@example.com",
participantCount = 5,
requirement = "Hello, Nice to meet you!"
)
}