refactor: 결제 확정 로직 변화에 따른 테스트 및 API 수정

- 기존의 결제 확정 API 및 테스트 제거(취소는 유지)
- PaymentWriter 제거
- 테스트 코드 반영
This commit is contained in:
이상진 2025-10-16 13:55:13 +09:00
parent c1eb1aa2b4
commit c3330e5652
8 changed files with 73 additions and 505 deletions

View File

@ -1,5 +1,6 @@
package com.sangdol.roomescape.payment.business package com.sangdol.roomescape.payment.business
import com.sangdol.common.persistence.IDGenerator
import com.sangdol.common.persistence.TransactionExecutionUtil import com.sangdol.common.persistence.TransactionExecutionUtil
import com.sangdol.roomescape.payment.business.domain.UserFacingPaymentErrorCode import com.sangdol.roomescape.payment.business.domain.UserFacingPaymentErrorCode
import com.sangdol.roomescape.payment.dto.* import com.sangdol.roomescape.payment.dto.*
@ -8,6 +9,7 @@ import com.sangdol.roomescape.payment.exception.PaymentErrorCode
import com.sangdol.roomescape.payment.exception.PaymentException import com.sangdol.roomescape.payment.exception.PaymentException
import com.sangdol.roomescape.payment.infrastructure.client.TosspayClient import com.sangdol.roomescape.payment.infrastructure.client.TosspayClient
import com.sangdol.roomescape.payment.infrastructure.persistence.* import com.sangdol.roomescape.payment.infrastructure.persistence.*
import com.sangdol.roomescape.payment.mapper.toEntity
import com.sangdol.roomescape.payment.mapper.toEvent import com.sangdol.roomescape.payment.mapper.toEvent
import com.sangdol.roomescape.payment.mapper.toResponse import com.sangdol.roomescape.payment.mapper.toResponse
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
@ -20,11 +22,11 @@ private val log: KLogger = KotlinLogging.logger {}
@Service @Service
class PaymentService( class PaymentService(
private val idGenerator: IDGenerator,
private val paymentClient: TosspayClient, private val paymentClient: TosspayClient,
private val paymentRepository: PaymentRepository, private val paymentRepository: PaymentRepository,
private val paymentDetailRepository: PaymentDetailRepository, private val paymentDetailRepository: PaymentDetailRepository,
private val canceledPaymentRepository: CanceledPaymentRepository, private val canceledPaymentRepository: CanceledPaymentRepository,
private val paymentWriter: PaymentWriter,
private val transactionExecutionUtil: TransactionExecutionUtil, private val transactionExecutionUtil: TransactionExecutionUtil,
private val eventPublisher: ApplicationEventPublisher private val eventPublisher: ApplicationEventPublisher
) { ) {
@ -60,19 +62,6 @@ class PaymentService(
} }
} }
fun savePayment(
reservationId: Long,
paymentGatewayResponse: PaymentGatewayResponse
): PaymentCreateResponse {
val payment: PaymentEntity = paymentWriter.createPayment(
reservationId = reservationId,
paymentGatewayResponse = paymentGatewayResponse
)
val detail: PaymentDetailEntity = paymentWriter.createDetail(paymentGatewayResponse, payment.id)
return PaymentCreateResponse(paymentId = payment.id, detailId = detail.id)
}
fun cancel(userId: Long, request: PaymentCancelRequest) { fun cancel(userId: Long, request: PaymentCancelRequest) {
val payment: PaymentEntity = findByReservationIdOrThrow(request.reservationId) val payment: PaymentEntity = findByReservationIdOrThrow(request.reservationId)
@ -83,12 +72,17 @@ class PaymentService(
) )
transactionExecutionUtil.withNewTransaction(isReadOnly = false) { transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
paymentWriter.cancel( val payment = findByReservationIdOrThrow(request.reservationId).apply { this.cancel() }
userId = userId,
payment = payment, clientCancelResponse.cancels.toEntity(
requestedAt = request.requestedAt, id = idGenerator.create(),
cancelResponse = clientCancelResponse paymentId = payment.id,
) cancelRequestedAt = request.requestedAt,
canceledBy = userId
).also {
canceledPaymentRepository.save(it)
log.debug { "[cancel] 결제 취소 정보 저장 완료: payment.id=${payment.id}" }
}
}.also { }.also {
log.info { "[cancel] 결제 취소 완료: paymentId=${payment.id}" } log.info { "[cancel] 결제 취소 완료: paymentId=${payment.id}" }
} }

View File

@ -1,80 +0,0 @@
package com.sangdol.roomescape.payment.business
import com.sangdol.common.persistence.IDGenerator
import com.sangdol.roomescape.payment.exception.PaymentErrorCode
import com.sangdol.roomescape.payment.exception.PaymentException
import com.sangdol.roomescape.payment.business.domain.PaymentMethod
import com.sangdol.roomescape.payment.dto.PaymentGatewayCancelResponse
import com.sangdol.roomescape.payment.dto.PaymentGatewayResponse
import com.sangdol.roomescape.payment.mapper.toCardDetailEntity
import com.sangdol.roomescape.payment.mapper.toEasypayPrepaidDetailEntity
import com.sangdol.roomescape.payment.mapper.toEntity
import com.sangdol.roomescape.payment.mapper.toTransferDetailEntity
import com.sangdol.roomescape.payment.infrastructure.persistence.*
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Component
import java.time.Instant
private val log: KLogger = KotlinLogging.logger {}
@Component
class PaymentWriter(
private val paymentRepository: PaymentRepository,
private val paymentDetailRepository: PaymentDetailRepository,
private val canceledPaymentRepository: CanceledPaymentRepository,
private val idGenerator: IDGenerator,
) {
fun createPayment(
reservationId: Long,
paymentGatewayResponse: PaymentGatewayResponse
): PaymentEntity {
log.info { "[PaymentWriterV2.createPayment] 결제 승인 및 결제 정보 저장 시작: reservationId=${reservationId}, paymentKey=${paymentGatewayResponse.paymentKey}" }
return paymentGatewayResponse.toEntity(id = idGenerator.create(), reservationId = reservationId).also {
paymentRepository.save(it)
log.info { "[PaymentWriterV2.createPayment] 결제 승인 및 결제 정보 저장 완료: reservationId=${reservationId}, payment.id=${it.id}" }
}
}
fun createDetail(
paymentGatewayResponse: PaymentGatewayResponse,
paymentId: Long,
): PaymentDetailEntity {
val method: PaymentMethod = paymentGatewayResponse.method
val id = idGenerator.create()
if (method == PaymentMethod.TRANSFER) {
return paymentDetailRepository.save(paymentGatewayResponse.toTransferDetailEntity(id, paymentId))
}
if (method == PaymentMethod.EASY_PAY && paymentGatewayResponse.card == null) {
return paymentDetailRepository.save(paymentGatewayResponse.toEasypayPrepaidDetailEntity(id, paymentId))
}
if (paymentGatewayResponse.card != null) {
return paymentDetailRepository.save(paymentGatewayResponse.toCardDetailEntity(id, paymentId))
}
throw PaymentException(PaymentErrorCode.NOT_SUPPORTED_PAYMENT_TYPE)
}
fun cancel(
userId: Long,
payment: PaymentEntity,
requestedAt: Instant,
cancelResponse: PaymentGatewayCancelResponse
): CanceledPaymentEntity {
log.debug { "[PaymentWriterV2.cancelPayment] 결제 취소 정보 저장 시작: payment.id=${payment.id}" }
paymentRepository.save(payment.apply { this.cancel() })
return cancelResponse.cancels.toEntity(
id = idGenerator.create(),
paymentId = payment.id,
cancelRequestedAt = requestedAt,
canceledBy = userId
).also {
canceledPaymentRepository.save(it)
log.debug { "[PaymentWriterV2.cancelPayment] 결제 취소 정보 저장 완료: payment.id=${payment.id}" }
}
}
}

View File

@ -2,11 +2,8 @@ package com.sangdol.roomescape.payment.docs
import com.sangdol.common.types.web.CommonApiResponse import com.sangdol.common.types.web.CommonApiResponse
import com.sangdol.roomescape.auth.web.support.User import com.sangdol.roomescape.auth.web.support.User
import com.sangdol.roomescape.auth.web.support.UserOnly
import com.sangdol.roomescape.common.types.CurrentUserContext import com.sangdol.roomescape.common.types.CurrentUserContext
import com.sangdol.roomescape.payment.dto.PaymentGatewayResponse
import com.sangdol.roomescape.payment.dto.PaymentCancelRequest import com.sangdol.roomescape.payment.dto.PaymentCancelRequest
import com.sangdol.roomescape.payment.dto.PaymentConfirmRequest
import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses import io.swagger.v3.oas.annotations.responses.ApiResponses
@ -16,13 +13,6 @@ import org.springframework.web.bind.annotation.RequestBody
interface PaymentAPI { interface PaymentAPI {
@UserOnly
@Operation(summary = "결제 승인")
@ApiResponses(ApiResponse(responseCode = "200", useReturnTypeSchema = true))
fun confirmPayment(
@Valid @RequestBody request: PaymentConfirmRequest
): ResponseEntity<CommonApiResponse<PaymentGatewayResponse>>
@Operation(summary = "결제 취소") @Operation(summary = "결제 취소")
@ApiResponses(ApiResponse(responseCode = "200", useReturnTypeSchema = true)) @ApiResponses(ApiResponse(responseCode = "200", useReturnTypeSchema = true))
fun cancelPayment( fun cancelPayment(

View File

@ -6,79 +6,9 @@ import com.sangdol.roomescape.payment.dto.CancelDetail
import com.sangdol.roomescape.payment.dto.PaymentGatewayResponse import com.sangdol.roomescape.payment.dto.PaymentGatewayResponse
import com.sangdol.roomescape.payment.exception.PaymentErrorCode import com.sangdol.roomescape.payment.exception.PaymentErrorCode
import com.sangdol.roomescape.payment.exception.PaymentException import com.sangdol.roomescape.payment.exception.PaymentException
import com.sangdol.roomescape.payment.infrastructure.persistence.* import com.sangdol.roomescape.payment.infrastructure.persistence.CanceledPaymentEntity
import java.time.Instant import java.time.Instant
fun PaymentGatewayResponse.toEntity(
id: Long,
reservationId: Long,
) = PaymentEntity(
id = id,
reservationId = reservationId,
paymentKey = this.paymentKey,
orderId = this.orderId,
totalAmount = this.totalAmount,
requestedAt = this.requestedAt.toInstant(),
approvedAt = this.approvedAt.toInstant(),
type = this.type,
method = this.method,
status = this.status,
)
fun PaymentGatewayResponse.toCardDetailEntity(id: Long, paymentId: Long): PaymentCardDetailEntity {
val cardDetail = this.card ?: throw PaymentException(PaymentErrorCode.PAYMENT_UNEXPECTED_ERROR)
return PaymentCardDetailEntity(
id = id,
paymentId = paymentId,
suppliedAmount = this.suppliedAmount,
vat = this.vat,
issuerCode = cardDetail.issuerCode,
cardType = cardDetail.cardType,
ownerType = cardDetail.ownerType,
amount = cardDetail.amount,
cardNumber = cardDetail.number,
approvalNumber = cardDetail.approveNo,
installmentPlanMonths = cardDetail.installmentPlanMonths,
isInterestFree = cardDetail.isInterestFree,
easypayProviderCode = this.easyPay?.provider,
easypayDiscountAmount = this.easyPay?.discountAmount,
)
}
fun PaymentGatewayResponse.toEasypayPrepaidDetailEntity(
id: Long,
paymentId: Long
): PaymentEasypayPrepaidDetailEntity {
val easyPayDetail = this.easyPay ?: throw PaymentException(PaymentErrorCode.PAYMENT_UNEXPECTED_ERROR)
return PaymentEasypayPrepaidDetailEntity(
id = id,
paymentId = paymentId,
suppliedAmount = this.suppliedAmount,
vat = this.vat,
easypayProviderCode = easyPayDetail.provider,
amount = easyPayDetail.amount,
discountAmount = easyPayDetail.discountAmount
)
}
fun PaymentGatewayResponse.toTransferDetailEntity(
id: Long,
paymentId: Long
): PaymentBankTransferDetailEntity {
val transferDetail = this.transfer ?: throw PaymentException(PaymentErrorCode.PAYMENT_UNEXPECTED_ERROR)
return PaymentBankTransferDetailEntity(
id = id,
paymentId = paymentId,
suppliedAmount = this.suppliedAmount,
vat = this.vat,
bankCode = transferDetail.bankCode,
settlementStatus = transferDetail.settlementStatus
)
}
fun CancelDetail.toEntity( fun CancelDetail.toEntity(
id: Long, id: Long,
paymentId: Long, paymentId: Long,
@ -115,7 +45,7 @@ fun PaymentGatewayResponse.toEvent(reservationId: Long): PaymentEvent {
} }
fun PaymentGatewayResponse.toDetail(): PaymentDetail { fun PaymentGatewayResponse.toDetail(): PaymentDetail {
return when(this.method) { return when (this.method) {
PaymentMethod.TRANSFER -> this.toBankTransferDetail() PaymentMethod.TRANSFER -> this.toBankTransferDetail()
PaymentMethod.CARD -> this.toCardDetail() PaymentMethod.CARD -> this.toCardDetail()
PaymentMethod.EASY_PAY -> { PaymentMethod.EASY_PAY -> {

View File

@ -6,27 +6,18 @@ import com.sangdol.roomescape.common.types.CurrentUserContext
import com.sangdol.roomescape.payment.business.PaymentService import com.sangdol.roomescape.payment.business.PaymentService
import com.sangdol.roomescape.payment.docs.PaymentAPI import com.sangdol.roomescape.payment.docs.PaymentAPI
import com.sangdol.roomescape.payment.dto.PaymentCancelRequest import com.sangdol.roomescape.payment.dto.PaymentCancelRequest
import com.sangdol.roomescape.payment.dto.PaymentConfirmRequest
import com.sangdol.roomescape.payment.dto.PaymentGatewayResponse
import jakarta.validation.Valid import jakarta.validation.Valid
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController @RestController
@RequestMapping("/payments") @RequestMapping("/payments")
class PaymentController( class PaymentController(
private val paymentService: PaymentService private val paymentService: PaymentService
) : PaymentAPI { ) : PaymentAPI {
@PostMapping("/confirm")
override fun confirmPayment(
@Valid @RequestBody request: PaymentConfirmRequest
): ResponseEntity<CommonApiResponse<PaymentGatewayResponse>> {
val response = paymentService.requestConfirm(request)
return ResponseEntity.ok(CommonApiResponse(response))
}
@PostMapping("/cancel") @PostMapping("/cancel")
override fun cancelPayment( override fun cancelPayment(
@User user: CurrentUserContext, @User user: CurrentUserContext,

View File

@ -4,22 +4,21 @@ import com.ninjasquad.springmockk.MockkBean
import com.sangdol.common.types.web.HttpStatus import com.sangdol.common.types.web.HttpStatus
import com.sangdol.roomescape.auth.exception.AuthErrorCode import com.sangdol.roomescape.auth.exception.AuthErrorCode
import com.sangdol.roomescape.payment.business.PaymentService import com.sangdol.roomescape.payment.business.PaymentService
import com.sangdol.roomescape.payment.business.domain.* import com.sangdol.roomescape.payment.business.domain.PaymentMethod
import com.sangdol.roomescape.payment.dto.* import com.sangdol.roomescape.payment.business.domain.PaymentStatus
import com.sangdol.roomescape.payment.exception.ExternalPaymentException import com.sangdol.roomescape.payment.dto.PaymentConfirmRequest
import com.sangdol.roomescape.payment.exception.PaymentErrorCode import com.sangdol.roomescape.payment.exception.PaymentErrorCode
import com.sangdol.roomescape.payment.infrastructure.client.TosspayClient import com.sangdol.roomescape.payment.infrastructure.client.TosspayClient
import com.sangdol.roomescape.payment.infrastructure.persistence.CanceledPaymentRepository import com.sangdol.roomescape.payment.infrastructure.persistence.CanceledPaymentRepository
import com.sangdol.roomescape.payment.infrastructure.persistence.PaymentDetailRepository
import com.sangdol.roomescape.payment.infrastructure.persistence.PaymentEntity
import com.sangdol.roomescape.payment.infrastructure.persistence.PaymentRepository import com.sangdol.roomescape.payment.infrastructure.persistence.PaymentRepository
import com.sangdol.roomescape.supports.FunSpecSpringbootTest import com.sangdol.roomescape.payment.mapper.toDetailEntity
import com.sangdol.roomescape.supports.PaymentFixture import com.sangdol.roomescape.payment.mapper.toEntity
import com.sangdol.roomescape.supports.runExceptionTest import com.sangdol.roomescape.payment.mapper.toEvent
import com.sangdol.roomescape.supports.runTest import com.sangdol.roomescape.supports.*
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import io.mockk.clearMocks
import io.mockk.every import io.mockk.every
import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.CoreMatchers.equalTo
import org.springframework.data.repository.findByIdOrNull import org.springframework.data.repository.findByIdOrNull
import org.springframework.http.HttpMethod import org.springframework.http.HttpMethod
@ -28,211 +27,10 @@ class PaymentAPITest(
private val tosspayClient: TosspayClient, private val tosspayClient: TosspayClient,
private val paymentService: PaymentService, private val paymentService: PaymentService,
private val paymentRepository: PaymentRepository, private val paymentRepository: PaymentRepository,
private val paymentDetailRepository: PaymentDetailRepository,
private val canceledPaymentRepository: CanceledPaymentRepository private val canceledPaymentRepository: CanceledPaymentRepository
) : FunSpecSpringbootTest() { ) : FunSpecSpringbootTest() {
init { init {
context("결제를 승인한다.") {
context("권한이 없으면 접근할 수 없다.") {
val endpoint = "/payments/confirm"
test("비회원") {
runExceptionTest(
method = HttpMethod.POST,
endpoint = endpoint,
expectedErrorCode = AuthErrorCode.TOKEN_NOT_FOUND
)
}
test("관리자") {
runExceptionTest(
token = testAuthUtil.defaultHqAdminLogin().second,
method = HttpMethod.POST,
endpoint = endpoint,
expectedErrorCode = AuthErrorCode.ACCESS_DENIED
)
}
}
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("결제 처리중 오류가 발생한다.") {
lateinit var token: String
val commonRequest = PaymentFixture.confirmRequest
beforeTest {
token = testAuthUtil.defaultUserLogin().second
}
afterTest {
clearMocks(tosspayClient)
}
test("예외 코드가 UserFacingPaymentErrorCode에 있으면 결제 실패 메시지를 같이 담는다.") {
val statusCode = HttpStatus.BAD_REQUEST.value()
val message = "거래금액 한도를 초과했습니다."
every {
tosspayClient.confirm(commonRequest.paymentKey, commonRequest.orderId, commonRequest.amount)
} throws ExternalPaymentException(
httpStatusCode = statusCode,
errorCode = UserFacingPaymentErrorCode.EXCEED_MAX_AMOUNT.name,
message = message
)
runTest(
token = token,
using = {
body(commonRequest)
},
on = {
post("/payments/confirm")
},
expect = {
statusCode(statusCode)
body("code", equalTo(PaymentErrorCode.PAYMENT_CLIENT_ERROR.errorCode))
body("message", containsString(message))
}
)
}
context("예외 코드가 UserFacingPaymentErrorCode에 없으면 Client의 상태 코드에 따라 다르게 처리한다.") {
mapOf(
HttpStatus.BAD_REQUEST.value() to PaymentErrorCode.PAYMENT_CLIENT_ERROR,
HttpStatus.INTERNAL_SERVER_ERROR.value() to PaymentErrorCode.PAYMENT_PROVIDER_ERROR
).forEach { (statusCode, expectedErrorCode) ->
test("statusCode=${statusCode}") {
val message = "잘못된 시크릿키 연동 정보 입니다."
every {
tosspayClient.confirm(commonRequest.paymentKey, commonRequest.orderId, commonRequest.amount)
} throws ExternalPaymentException(
httpStatusCode = statusCode,
errorCode = "INVALID_API_KEY",
message = message
)
runTest(
token = token,
using = {
body(commonRequest)
},
on = {
post("/payments/confirm")
},
expect = {
statusCode(statusCode)
body("code", equalTo(expectedErrorCode.errorCode))
body("message", equalTo(expectedErrorCode.message))
}
)
}
}
}
}
}
context("결제를 취소한다.") { context("결제를 취소한다.") {
context("권한이 없으면 접근할 수 없다.") { context("권한이 없으면 접근할 수 없다.") {
val endpoint = "/payments/cancel" val endpoint = "/payments/cancel"
@ -262,7 +60,7 @@ class PaymentAPITest(
val reservation = dummyInitializer.createConfirmReservation(user = user) val reservation = dummyInitializer.createConfirmReservation(user = user)
val confirmRequest = PaymentFixture.confirmRequest val confirmRequest = PaymentFixture.confirmRequest
val paymentCreateResponse = createPayment( val paymentEntity = createPayment(
request = confirmRequest, request = confirmRequest,
reservationId = reservation.id reservationId = reservation.id
) )
@ -289,10 +87,10 @@ class PaymentAPITest(
statusCode(HttpStatus.OK.value()) statusCode(HttpStatus.OK.value())
} }
).also { ).also {
val payment = paymentRepository.findByIdOrNull(paymentCreateResponse.paymentId) val payment = paymentRepository.findByIdOrNull(paymentEntity.id)
?: throw AssertionError("Unexpected Exception Occurred.") ?: throw AssertionError("Unexpected Exception Occurred.")
val canceledPayment = val canceledPayment =
canceledPaymentRepository.findByPaymentId(paymentCreateResponse.paymentId) canceledPaymentRepository.findByPaymentId(paymentEntity.id)
?: throw AssertionError("Unexpected Exception Occurred.") ?: throw AssertionError("Unexpected Exception Occurred.")
payment.status shouldBe PaymentStatus.CANCELED payment.status shouldBe PaymentStatus.CANCELED
@ -319,7 +117,7 @@ class PaymentAPITest(
private fun createPayment( private fun createPayment(
request: PaymentConfirmRequest, request: PaymentConfirmRequest,
reservationId: Long, reservationId: Long,
): PaymentCreateResponse { ): PaymentEntity {
every { every {
tosspayClient.confirm(request.paymentKey, request.orderId, request.amount) tosspayClient.confirm(request.paymentKey, request.orderId, request.amount)
} returns PaymentFixture.confirmResponse( } returns PaymentFixture.confirmResponse(
@ -331,49 +129,10 @@ class PaymentAPITest(
transferDetail = null, transferDetail = null,
) )
val paymentResponse = paymentService.requestConfirm(request) val paymentEvent = paymentService.requestConfirm(reservationId, request).toEvent(reservationId)
return paymentService.savePayment(reservationId, paymentResponse)
}
fun runConfirmTest( return paymentRepository.save(paymentEvent.toEntity(IDGenerator.create())).also {
cardDetail: CardDetailResponse? = null, paymentDetailRepository.save(paymentEvent.toDetailEntity(IDGenerator.create(), it.id))
easyPayDetail: EasyPayDetailResponse? = null,
transferDetail: TransferDetailResponse? = null,
paymentKey: String = "paymentKey",
amount: Int = 10000,
) {
val token = testAuthUtil.defaultUserLogin().second
val request = PaymentFixture.confirmRequest.copy(paymentKey = paymentKey, amount = amount)
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 = token,
using = {
body(request)
},
on = {
post("/payments/confirm")
},
expect = {
statusCode(HttpStatus.OK.value())
}
)
} }
} }

View File

@ -1,11 +1,11 @@
package com.sangdol.roomescape.supports package com.sangdol.roomescape.supports
import com.sangdol.roomescape.payment.business.PaymentWriter
import com.sangdol.roomescape.payment.business.domain.PaymentMethod import com.sangdol.roomescape.payment.business.domain.PaymentMethod
import com.sangdol.roomescape.payment.dto.* import com.sangdol.roomescape.payment.dto.*
import com.sangdol.roomescape.payment.infrastructure.persistence.CanceledPaymentEntity import com.sangdol.roomescape.payment.infrastructure.persistence.*
import com.sangdol.roomescape.payment.infrastructure.persistence.PaymentEntity import com.sangdol.roomescape.payment.mapper.toDetailEntity
import com.sangdol.roomescape.payment.infrastructure.persistence.PaymentRepository import com.sangdol.roomescape.payment.mapper.toEntity
import com.sangdol.roomescape.payment.mapper.toEvent
import com.sangdol.roomescape.payment.mapper.toResponse import com.sangdol.roomescape.payment.mapper.toResponse
import com.sangdol.roomescape.reservation.dto.PendingReservationCreateRequest import com.sangdol.roomescape.reservation.dto.PendingReservationCreateRequest
import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationEntity import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationEntity
@ -33,7 +33,8 @@ class DummyInitializer(
private val scheduleRepository: ScheduleRepository, private val scheduleRepository: ScheduleRepository,
private val reservationRepository: ReservationRepository, private val reservationRepository: ReservationRepository,
private val paymentRepository: PaymentRepository, private val paymentRepository: PaymentRepository,
private val paymentWriter: PaymentWriter private val paymentDetailRepository: PaymentDetailRepository,
private val canceledPaymentRepository: CanceledPaymentRepository
) { ) {
fun createStore( fun createStore(
@ -156,27 +157,6 @@ class DummyInitializer(
} }
} }
fun createExpiredOrCanceledReservation(
user: UserEntity,
status: ReservationStatus,
storeId: Long = IDGenerator.create(),
themeRequest: ThemeCreateRequest = ThemeFixture.createRequest,
scheduleRequest: ScheduleCreateRequest = ScheduleFixture.createRequest,
reservationRequest: PendingReservationCreateRequest = ReservationFixture.pendingCreateRequest,
): ReservationEntity {
return createPendingReservation(user, storeId, themeRequest, scheduleRequest, reservationRequest).apply {
this.status = status
}.also {
reservationRepository.save(it)
scheduleRepository.findByIdOrNull(it.scheduleId)?.let { schedule ->
schedule.status = ScheduleStatus.AVAILABLE
schedule.holdExpiredAt = null
scheduleRepository.save(schedule)
}
}
}
fun createPayment( fun createPayment(
reservationId: Long, reservationId: Long,
request: PaymentConfirmRequest = PaymentFixture.confirmRequest, request: PaymentConfirmRequest = PaymentFixture.confirmRequest,
@ -204,12 +184,10 @@ class DummyInitializer(
transferDetail = transferDetail transferDetail = transferDetail
) )
val payment = paymentWriter.createPayment( val paymentEvent = clientConfirmResponse.toEvent(reservationId)
reservationId = reservationId, val payment = paymentRepository.save(paymentEvent.toEntity(IDGenerator.create()))
paymentGatewayResponse = clientConfirmResponse
)
val detail = paymentWriter.createDetail(clientConfirmResponse, payment.id) val detail = paymentDetailRepository.save(paymentEvent.toDetailEntity(IDGenerator.create(), payment.id))
return payment.toResponse(detail = detail.toResponse(), cancel = null) return payment.toResponse(detail = detail.toResponse(), cancel = null)
} }
@ -227,11 +205,14 @@ class DummyInitializer(
cancelReason = cancelReason, cancelReason = cancelReason,
) )
return paymentWriter.cancel(
userId, return clientCancelResponse.cancels.toEntity(
payment, id = IDGenerator.create(),
requestedAt = Instant.now(), paymentId = payment.id,
clientCancelResponse cancelRequestedAt = Instant.now(),
) canceledBy = userId
).also {
canceledPaymentRepository.save(it)
}
} }
} }

View File

@ -1,7 +1,8 @@
package com.sangdol.roomescape.supports package com.sangdol.roomescape.supports
import com.sangdol.roomescape.admin.infrastructure.persistence.AdminRepository import com.sangdol.roomescape.admin.infrastructure.persistence.AdminRepository
import com.sangdol.roomescape.payment.business.PaymentWriter import com.sangdol.roomescape.payment.infrastructure.persistence.CanceledPaymentRepository
import com.sangdol.roomescape.payment.infrastructure.persistence.PaymentDetailRepository
import com.sangdol.roomescape.payment.infrastructure.persistence.PaymentRepository import com.sangdol.roomescape.payment.infrastructure.persistence.PaymentRepository
import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationRepository import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationRepository
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleRepository import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleRepository
@ -43,13 +44,7 @@ abstract class FunSpecSpringbootTest(
} }
}) { }) {
@Autowired @Autowired
private lateinit var userRepository: UserRepository lateinit var testAuthUtil: TestAuthUtil
@Autowired
private lateinit var adminRepository: AdminRepository
@Autowired
private lateinit var storeRepository: StoreRepository
@Autowired @Autowired
lateinit var dummyInitializer: DummyInitializer lateinit var dummyInitializer: DummyInitializer
@ -57,32 +52,40 @@ abstract class FunSpecSpringbootTest(
@LocalServerPort @LocalServerPort
var port: Int = 0 var port: Int = 0
lateinit var testAuthUtil: TestAuthUtil
override suspend fun beforeSpec(spec: Spec) { override suspend fun beforeSpec(spec: Spec) {
RestAssured.port = port RestAssured.port = port
testAuthUtil = TestAuthUtil(userRepository, adminRepository, storeRepository)
} }
} }
@TestConfiguration @TestConfiguration
class TestConfig { class TestConfig {
@Bean
fun testAuthUtil(
userRepository: UserRepository,
adminRepository: AdminRepository,
storeRepository: StoreRepository
): TestAuthUtil {
return TestAuthUtil(userRepository, adminRepository, storeRepository)
}
@Bean @Bean
fun dummyInitializer( fun dummyInitializer(
storeRepository: StoreRepository, storeRepository: StoreRepository,
themeRepository: ThemeRepository, themeRepository: ThemeRepository,
scheduleRepository: ScheduleRepository, scheduleRepository: ScheduleRepository,
reservationRepository: ReservationRepository, reservationRepository: ReservationRepository,
paymentWriter: PaymentWriter, paymentRepository: PaymentRepository,
paymentRepository: PaymentRepository paymentDetailRepository: PaymentDetailRepository,
canceledPaymentRepository: CanceledPaymentRepository
): DummyInitializer { ): DummyInitializer {
return DummyInitializer( return DummyInitializer(
themeRepository = themeRepository, themeRepository = themeRepository,
scheduleRepository = scheduleRepository, scheduleRepository = scheduleRepository,
reservationRepository = reservationRepository, reservationRepository = reservationRepository,
paymentWriter = paymentWriter,
paymentRepository = paymentRepository, paymentRepository = paymentRepository,
storeRepository = storeRepository storeRepository = storeRepository,
paymentDetailRepository = paymentDetailRepository,
canceledPaymentRepository = canceledPaymentRepository
) )
} }
} }