From 81bffa443b5dbc66a5ddf4df0b9575ac821e5e33 Mon Sep 17 00:00:00 2001 From: Combi153 Date: Sat, 24 Feb 2024 12:13:54 +0900 Subject: [PATCH 01/26] =?UTF-8?q?chore:=20tossPayments=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80=20=EC=84=9C?= =?UTF-8?q?=EB=B8=8C=EB=AA=A8=EB=93=88=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend-submodule | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend-submodule b/backend-submodule index abd8358b..7da737b9 160000 --- a/backend-submodule +++ b/backend-submodule @@ -1 +1 @@ -Subproject commit abd8358b8b01ac497dbc295aa91aa9ca0340702a +Subproject commit 7da737b9efbaa8b3e79f2bf725e3660eebb173fd From 590cb518b87ec7cd66a892af26c42a9fb54c6995 Mon Sep 17 00:00:00 2001 From: Combi153 Date: Sat, 24 Feb 2024 12:14:52 +0900 Subject: [PATCH 02/26] =?UTF-8?q?feat:=20Base64=20=EC=9D=B8=EC=BD=94?= =?UTF-8?q?=EB=94=A9=20util=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/petqua/common/util/AuthUtils.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/main/kotlin/com/petqua/common/util/AuthUtils.kt diff --git a/src/main/kotlin/com/petqua/common/util/AuthUtils.kt b/src/main/kotlin/com/petqua/common/util/AuthUtils.kt new file mode 100644 index 00000000..d6d24bd6 --- /dev/null +++ b/src/main/kotlin/com/petqua/common/util/AuthUtils.kt @@ -0,0 +1,15 @@ +package com.petqua.common.util + +import java.util.Base64 + +private const val EMPTY_PASSWORD = "" + +object BasicAuthUtils { + fun encodeCredentialsWithColon( + userName: String, + password: String = EMPTY_PASSWORD, + ): String { + val credentials = "$userName:$password" + return Base64.getEncoder().encodeToString(credentials.toByteArray()) + } +} From 9cd02d8cd35332ed5e3146702df46ecb764969bb Mon Sep 17 00:00:00 2001 From: Combi153 Date: Sat, 24 Feb 2024 12:18:41 +0900 Subject: [PATCH 03/26] =?UTF-8?q?feat:=20PaymentGatewayClient=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/order/payment/PaymentDtos.kt | 179 ++++++++++++++++++ .../order/payment/PaymentGatewayClient.kt | 10 + .../order/payment/PaymentProperties.kt | 10 + .../order/payment/TossPaymentClient.kt | 26 +++ .../order/payment/TossPaymentsApiClient.kt | 21 ++ .../petqua/common/config/ApiClientConfig.kt | 23 +++ 6 files changed, 269 insertions(+) create mode 100644 src/main/kotlin/com/petqua/application/order/payment/PaymentDtos.kt create mode 100644 src/main/kotlin/com/petqua/application/order/payment/PaymentGatewayClient.kt create mode 100644 src/main/kotlin/com/petqua/application/order/payment/PaymentProperties.kt create mode 100644 src/main/kotlin/com/petqua/application/order/payment/TossPaymentClient.kt create mode 100644 src/main/kotlin/com/petqua/application/order/payment/TossPaymentsApiClient.kt create mode 100644 src/main/kotlin/com/petqua/common/config/ApiClientConfig.kt diff --git a/src/main/kotlin/com/petqua/application/order/payment/PaymentDtos.kt b/src/main/kotlin/com/petqua/application/order/payment/PaymentDtos.kt new file mode 100644 index 00000000..f279ee5f --- /dev/null +++ b/src/main/kotlin/com/petqua/application/order/payment/PaymentDtos.kt @@ -0,0 +1,179 @@ +package com.petqua.application.order.payment + +import com.petqua.domain.order.OrderName +import com.petqua.domain.order.OrderNumber +import com.petqua.domain.payment.tosspayment.TossPayment +import com.petqua.domain.payment.tosspayment.TossPaymentMethod +import com.petqua.domain.payment.tosspayment.TossPaymentStatus +import com.petqua.domain.payment.tosspayment.TossPaymentType +import java.math.BigDecimal + +data class PaymentConfirmRequestToPG( + val orderNumber: OrderNumber, + val paymentKey: String, + val amount: BigDecimal, +) + +data class PaymentResponseFromPG( + val version: String, + val paymentKey: String, + val type: String, + val orderId: String, + val orderName: String, + val mid: String, + val currency: String, + val method: String, + val totalAmount: BigDecimal, + val balanceAmount: BigDecimal, + val status: String, + val requestedAt: String, + val approvedAt: String, + val useEscrow: Boolean, + val lastTransactionKey: String?, + val suppliedAmount: String, + val vat: BigDecimal, + val cultureExpense: Boolean, + val taxFreeAmount: BigDecimal, + val taxExemptionAmount: BigDecimal, + val cancels: List?, + val isPartialCancelable: Boolean, + val card: CardResponseFromPG?, + val virtualAccount: VirtualAccountResponseFromPG?, + val secret: String?, + val mobilePhone: MobilePhoneResponseFromPG?, + val giftCertificate: GiftCertificateResponseFromPG?, + val transfer: TransferResponseFromPG?, + val receipt: ReceiptResponseFromPG?, + val checkout: CheckoutResponseFromPG?, + val easyPay: EasyPayResponseFromPG?, + val country: String, + val failure: FailureResponseFromPG?, + val cashReceipt: CashReceiptResponseFromPG?, + val cashReceipts: List?, + val discount: DiscountResponseFromPG?, +) { + fun toPayment(): TossPayment { + return TossPayment( + paymentKey = paymentKey, + orderNumber = OrderNumber.from(orderId), + orderName = OrderName(orderName), + method = TossPaymentMethod.valueOf(method), + totalAmount = totalAmount, + status = TossPaymentStatus.valueOf(status), + requestedAt = requestedAt, + approvedAt = approvedAt, + useEscrow = useEscrow, + type = TossPaymentType.valueOf(type), + ) + } +} + +data class CancelResponseFromPG( + val cancelAmount: BigDecimal, + val cancelReason: String, + val taxFreeAmount: BigDecimal, + val taxExemptionAmount: BigDecimal, + val refundableAmount: BigDecimal, + val easyPayDiscountAmount: BigDecimal, + val canceledAt: String, + val transactionKey: String, + val receiptKey: String?, +) + +data class CardResponseFromPG( + val amount: BigDecimal, + val issuerCode: String, + val acquirerCode: String?, + val number: String, + val installmentPlanMonths: Int, + val approveNo: String, + val useCardPoint: Boolean, + val cardType: String, + val ownerType: String, + val acquireStatus: String, + val isInterestFree: Boolean, + val interestPayer: String?, +) + +data class VirtualAccountResponseFromPG( + val accountType: String, + val accountNumber: String, + val bankCode: String, + val customerName: String, + val dueDate: String, + val refundStatus: String, + val expired: Boolean, + val settlementStatus: String, + val refundReceiveAccount: RefundReceiveAccountResponseFromPG?, // 결제창 띄운 시점부터 30분 동안만 조회 가능, 결제 승인 직후 응답 저장 필요 -> 고객의 계좌 정보를 저장해야 해? +) + +data class RefundReceiveAccountResponseFromPG( + val bankCode: String, + val accountNumber: String, + val holderName: String, +) + +data class MobilePhoneResponseFromPG( + val customerMobilePhone: String, + val settlementStatus: String, + val receiptUrl: String +) + +data class GiftCertificateResponseFromPG( + val approveNo: String, + val settlementStatus: String, +) + +data class TransferResponseFromPG( + val bankCode: String, + val settlementStatus: String, +) + +data class ReceiptResponseFromPG( + val url: String, +) + +data class CheckoutResponseFromPG( + val url: String, +) + +data class EasyPayResponseFromPG( + val provider: String, + val amount: BigDecimal, + val discountAmount: BigDecimal, +) + +data class FailureResponseFromPG( + val code: String, + val message: String, +) + +data class CashReceiptResponseFromPG( + val type: String, + val receiptKey: String, + val issueNumber: String, + val receiptUrl: String, + val amount: BigDecimal, + val taxFreeAmount: BigDecimal, +) + +data class CashReceiptHistoryResponseFromPG( + val receiptKey: String, + val orderId: String, + val orderName: String, + val type: String, + val issueNumber: String, + val receiptUrl: String, + val businessNumber: String, + val transactionType: String, + val amount: BigDecimal, + val taxFreeAmount: BigDecimal, + val issueStatus: String, + val failure: FailureResponseFromPG, + val customerIdentityNumber: String, + val requestedAt: String +) + +data class DiscountResponseFromPG( + val amount: BigDecimal, +) diff --git a/src/main/kotlin/com/petqua/application/order/payment/PaymentGatewayClient.kt b/src/main/kotlin/com/petqua/application/order/payment/PaymentGatewayClient.kt new file mode 100644 index 00000000..4ae8877b --- /dev/null +++ b/src/main/kotlin/com/petqua/application/order/payment/PaymentGatewayClient.kt @@ -0,0 +1,10 @@ +package com.petqua.application.order.payment + +interface PaymentGatewayClient { + + fun confirmPayment(paymentConfirmRequestToPG: PaymentConfirmRequestToPG): PaymentResponseFromPG + + fun successUrl(): String + + fun failUrl(): String +} diff --git a/src/main/kotlin/com/petqua/application/order/payment/PaymentProperties.kt b/src/main/kotlin/com/petqua/application/order/payment/PaymentProperties.kt new file mode 100644 index 00000000..56743f7b --- /dev/null +++ b/src/main/kotlin/com/petqua/application/order/payment/PaymentProperties.kt @@ -0,0 +1,10 @@ +package com.petqua.application.order.payment + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties(prefix = "payment.toss-payments") +data class PaymentProperties( + val secretKey: String, + val successUrl: String, + val failUrl: String, +) diff --git a/src/main/kotlin/com/petqua/application/order/payment/TossPaymentClient.kt b/src/main/kotlin/com/petqua/application/order/payment/TossPaymentClient.kt new file mode 100644 index 00000000..75f7bb3f --- /dev/null +++ b/src/main/kotlin/com/petqua/application/order/payment/TossPaymentClient.kt @@ -0,0 +1,26 @@ +package com.petqua.application.order.payment + +import com.petqua.common.util.BasicAuthUtils +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.stereotype.Component + +@EnableConfigurationProperties(PaymentProperties::class) +@Component +class TossPaymentClient( + private val tossPaymentsApiClient: TossPaymentsApiClient, + private val paymentProperties: PaymentProperties, +) : PaymentGatewayClient { + + override fun confirmPayment(paymentConfirmRequestToPG: PaymentConfirmRequestToPG): PaymentResponseFromPG { + val credentials = BasicAuthUtils.encodeCredentialsWithColon(paymentProperties.secretKey) + return tossPaymentsApiClient.confirmPayment(credentials, paymentConfirmRequestToPG) + } + + override fun successUrl(): String { + return paymentProperties.successUrl + } + + override fun failUrl(): String { + return paymentProperties.failUrl + } +} diff --git a/src/main/kotlin/com/petqua/application/order/payment/TossPaymentsApiClient.kt b/src/main/kotlin/com/petqua/application/order/payment/TossPaymentsApiClient.kt new file mode 100644 index 00000000..68bd20a5 --- /dev/null +++ b/src/main/kotlin/com/petqua/application/order/payment/TossPaymentsApiClient.kt @@ -0,0 +1,21 @@ +package com.petqua.application.order.payment + +import org.springframework.http.HttpHeaders.AUTHORIZATION +import org.springframework.http.MediaType.APPLICATION_JSON_VALUE +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestHeader +import org.springframework.web.service.annotation.HttpExchange +import org.springframework.web.service.annotation.PostExchange + +@HttpExchange(url = "https://api.tosspayments.com") +interface TossPaymentsApiClient { + + @PostExchange( + url = "/v1/payments/confirm", + contentType = APPLICATION_JSON_VALUE, + ) + fun confirmPayment( + @RequestHeader(name = AUTHORIZATION) credentials: String, + @RequestBody paymentConfirmRequestToPG: PaymentConfirmRequestToPG, + ): PaymentResponseFromPG +} diff --git a/src/main/kotlin/com/petqua/common/config/ApiClientConfig.kt b/src/main/kotlin/com/petqua/common/config/ApiClientConfig.kt new file mode 100644 index 00000000..dc19aa00 --- /dev/null +++ b/src/main/kotlin/com/petqua/common/config/ApiClientConfig.kt @@ -0,0 +1,23 @@ +package com.petqua.common.config + +import com.petqua.application.order.payment.TossPaymentsApiClient +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.reactive.function.client.support.WebClientAdapter +import org.springframework.web.service.invoker.HttpServiceProxyFactory + +@Configuration +class ApiClientConfig { + + @Bean + fun tossPaymentsApiClient(): TossPaymentsApiClient { + return createApiClient(TossPaymentsApiClient::class.java) + } + + private fun createApiClient(clazz: Class): T { + val webClient = WebClient.create() + val builder = HttpServiceProxyFactory.builderFor(WebClientAdapter.create(webClient)) + return builder.build().createClient(clazz) + } +} From a70daf1793ff9e4547c5c1ec29eb9cfe53f4b6ed Mon Sep 17 00:00:00 2001 From: Combi153 Date: Thu, 29 Feb 2024 16:37:18 +0900 Subject: [PATCH 04/26] =?UTF-8?q?chore:=20rebase=20=EC=B6=A9=EB=8F=8C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../petqua/common/config/DataInitializer.kt | 17 +++ .../kotlin/com/petqua/domain/order/Order.kt | 12 +- .../com/petqua/domain/order/OrderTest.kt | 34 +++++ .../com/petqua/test/fixture/OrderFixture.kt | 126 ++++++++++++++++++ 4 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 src/test/kotlin/com/petqua/domain/order/OrderTest.kt diff --git a/src/main/kotlin/com/petqua/common/config/DataInitializer.kt b/src/main/kotlin/com/petqua/common/config/DataInitializer.kt index b1b623ad..6dd80676 100644 --- a/src/main/kotlin/com/petqua/common/config/DataInitializer.kt +++ b/src/main/kotlin/com/petqua/common/config/DataInitializer.kt @@ -9,6 +9,8 @@ import com.petqua.domain.keyword.ProductKeyword import com.petqua.domain.keyword.ProductKeywordRepository import com.petqua.domain.member.Member import com.petqua.domain.member.MemberRepository +import com.petqua.domain.order.ShippingAddress +import com.petqua.domain.order.ShippingAddressRepository import com.petqua.domain.product.Product import com.petqua.domain.product.ProductRepository import com.petqua.domain.product.WishCount @@ -73,6 +75,7 @@ class DataInitializer( private val wishProductRepository: WishProductRepository, private val productKeywordRepository: ProductKeywordRepository, private val productDescriptionRepository: ProductDescriptionRepository, + private val shippingAddressRepository: ShippingAddressRepository, ) { @EventListener(ApplicationReadyEvent::class) @@ -87,6 +90,20 @@ class DataInitializer( // banner saveBanners() + val shippingAddress = shippingAddressRepository.save( + ShippingAddress( + memberId = member.id, + name = "집", + receiver = "홍길동", + phoneNumber = "010-1234-5678", + zipCode = 12345, + address = "서울시 강남구 역삼동 99번길", + detailAddress = "101동 101호", + isDefaultAddress = true, + ) + ) + + // others saveCommerceData(member.id) } diff --git a/src/main/kotlin/com/petqua/domain/order/Order.kt b/src/main/kotlin/com/petqua/domain/order/Order.kt index 070fff33..e8767564 100644 --- a/src/main/kotlin/com/petqua/domain/order/Order.kt +++ b/src/main/kotlin/com/petqua/domain/order/Order.kt @@ -1,6 +1,9 @@ package com.petqua.domain.order import com.petqua.common.domain.BaseEntity +import com.petqua.common.util.throwExceptionWhen +import com.petqua.exception.order.OrderException +import com.petqua.exception.order.OrderExceptionType.ORDER_PRICE_NOT_MATCH import jakarta.persistence.AttributeOverride import jakarta.persistence.Column import jakarta.persistence.Embedded @@ -45,4 +48,11 @@ class Order( @Column(nullable = false) val totalAmount: BigDecimal, -) : BaseEntity() +) : BaseEntity() { + + fun validateAmount(amount: BigDecimal) { + throwExceptionWhen(totalAmount != amount) { + throw OrderException(ORDER_PRICE_NOT_MATCH) + } + } +} diff --git a/src/test/kotlin/com/petqua/domain/order/OrderTest.kt b/src/test/kotlin/com/petqua/domain/order/OrderTest.kt new file mode 100644 index 00000000..3606b879 --- /dev/null +++ b/src/test/kotlin/com/petqua/domain/order/OrderTest.kt @@ -0,0 +1,34 @@ +package com.petqua.domain.order + +import com.petqua.exception.order.OrderException +import com.petqua.exception.order.OrderExceptionType.ORDER_PRICE_NOT_MATCH +import com.petqua.test.fixture.order +import io.kotest.assertions.throwables.shouldNotThrow +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import java.math.BigDecimal.ONE +import java.math.BigDecimal.TEN + +class OrderTest : StringSpec({ + + "결제 금액을 검증한다" { + val order = order( + totalAmount = TEN + ) + + shouldNotThrow { + order.validateAmount(TEN) + } + } + + "결제 금액을 검증할 때 금액이 다르다면 예외를 던진다" { + val order = order( + totalAmount = TEN + ) + + shouldThrow { + order.validateAmount(ONE) + }.exceptionType() shouldBe ORDER_PRICE_NOT_MATCH + } +}) diff --git a/src/test/kotlin/com/petqua/test/fixture/OrderFixture.kt b/src/test/kotlin/com/petqua/test/fixture/OrderFixture.kt index 432f9e2b..222e063f 100644 --- a/src/test/kotlin/com/petqua/test/fixture/OrderFixture.kt +++ b/src/test/kotlin/com/petqua/test/fixture/OrderFixture.kt @@ -5,12 +5,138 @@ import com.petqua.application.order.dto.SaveOrderCommand import com.petqua.common.util.setDefaultScale import com.petqua.domain.delivery.DeliveryMethod import com.petqua.domain.delivery.DeliveryMethod.COMMON +import com.petqua.domain.delivery.DeliveryMethod.SAFETY +import com.petqua.domain.order.Order +import com.petqua.domain.order.OrderName +import com.petqua.domain.order.OrderNumber +import com.petqua.domain.order.OrderProduct +import com.petqua.domain.order.OrderShippingAddress +import com.petqua.domain.order.OrderStatus +import com.petqua.domain.order.ShippingNumber import com.petqua.domain.product.option.Sex import com.petqua.domain.product.option.Sex.FEMALE import java.math.BigDecimal import java.math.BigDecimal.ONE import java.math.BigDecimal.ZERO +private const val DEFAULT_SCALE = 2 + +fun order( + id: Long = 0L, + memberId: Long = 0L, + orderNumber: OrderNumber = OrderNumber.from("202402211607026029E90DB030"), + orderName: OrderName = OrderName("상품1"), + receiver: String = "receiver", + phoneNumber: String = "010-1234-5678", + zipCode: Int = 12345, + address: String = "서울시 강남구 역삼동 99번길", + detailAddress: String = "101동 101호", + requestMessage: String? = null, + quantity: Int = 1, + originalPrice: BigDecimal = ONE, + discountRate: Int = 0, + discountPrice: BigDecimal = originalPrice, + deliveryFee: BigDecimal = 3000.toBigDecimal(), + shippingNumber: ShippingNumber = ShippingNumber(""), + orderPrice: BigDecimal = discountPrice, + productId: Long = 0L, + productName: String = "orderProduct", + thumbnailUrl: String = "image.url", + storeId: Long = 0L, + storeName: String = "storeName", + deliveryMethod: DeliveryMethod = SAFETY, + sex: Sex = FEMALE, + isAbleToCancel: Boolean = true, + status: OrderStatus = OrderStatus.ORDER_CREATED, + totalAmount: BigDecimal = orderPrice + deliveryFee, +): Order { + return Order( + id = id, + memberId = memberId, + orderNumber = orderNumber, + orderName = orderName, + orderShippingAddress = orderShippingAddress( + receiver = receiver, + phoneNumber = phoneNumber, + zipCode = zipCode, + address = address, + detailAddress = detailAddress, + requestMessage = requestMessage, + ), + orderProduct = orderProduct( + quantity = quantity, + originalPrice = originalPrice, + discountRate = discountRate, + discountPrice = discountPrice, + deliveryFee = deliveryFee, + shippingNumber = shippingNumber, + orderPrice = orderPrice, + productId = productId, + productName = productName, + thumbnailUrl = thumbnailUrl, + storeId = storeId, + storeName = storeName, + deliveryMethod = deliveryMethod, + sex = sex, + ), + isAbleToCancel = isAbleToCancel, + status = status, + totalAmount = totalAmount, + ) +} + +fun orderShippingAddress( + receiver: String = "receiver", + phoneNumber: String = "010-1234-5678", + zipCode: Int = 12345, + address: String = "서울시 강남구 역삼동 99번길", + detailAddress: String = "101동 101호", + requestMessage: String?, +): OrderShippingAddress { + return OrderShippingAddress( + receiver = receiver, + phoneNumber = phoneNumber, + zipCode = zipCode, + address = address, + detailAddress = detailAddress, + requestMessage = requestMessage, + ) +} + +fun orderProduct( + quantity: Int = 1, + originalPrice: BigDecimal = ONE, + discountRate: Int = 0, + discountPrice: BigDecimal = originalPrice, + deliveryFee: BigDecimal = 3000.toBigDecimal(), + shippingNumber: ShippingNumber = ShippingNumber(""), + orderPrice: BigDecimal = discountPrice, + productId: Long = 0L, + productName: String = "orderProduct", + thumbnailUrl: String = "image.url", + storeId: Long = 0L, + storeName: String = "storeName", + deliveryMethod: DeliveryMethod = SAFETY, + sex: Sex = FEMALE, +): OrderProduct { + return OrderProduct( + quantity = quantity, + originalPrice = originalPrice, + discountRate = discountRate, + discountPrice = discountPrice, + deliveryFee = deliveryFee, + shippingNumber = shippingNumber, + orderPrice = orderPrice, + productId = productId, + productName = productName, + thumbnailUrl = thumbnailUrl, + storeId = storeId, + storeName = storeName, + deliveryMethod = deliveryMethod, + sex = sex, + ) +} + fun saveOrderCommand( memberId: Long = 0L, shippingAddressId: Long = 0L, From fc1f35040537d6da803921f2e894a29abb32f67d Mon Sep 17 00:00:00 2001 From: Combi153 Date: Thu, 29 Feb 2024 16:38:36 +0900 Subject: [PATCH 05/26] =?UTF-8?q?chore:=20rebase=20=EC=B6=A9=EB=8F=8C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../petqua/application/order/OrderService.kt | 30 ++- .../petqua/application/order/dto/OrderDtos.kt | 173 +++++++++++++++++- .../com/petqua/domain/order/OrderNumber.kt | 4 + .../petqua/domain/order/OrderRepository.kt | 9 + .../domain/payment/tosspayment/TossPayment.kt | 6 +- .../tosspayment/TossPaymentRepository.kt | 6 + .../payment/tosspayment/TossPaymentType.kt | 12 ++ .../exception/order/OrderExceptionType.kt | 3 + .../presentation/order/OrderController.kt | 2 +- .../presentation/order/dto/OrderDtos.kt | 18 ++ src/test/resources/application.yml | 6 + 11 files changed, 257 insertions(+), 12 deletions(-) create mode 100644 src/main/kotlin/com/petqua/domain/payment/tosspayment/TossPaymentRepository.kt diff --git a/src/main/kotlin/com/petqua/application/order/OrderService.kt b/src/main/kotlin/com/petqua/application/order/OrderService.kt index 5ad16cf8..95575a1b 100644 --- a/src/main/kotlin/com/petqua/application/order/OrderService.kt +++ b/src/main/kotlin/com/petqua/application/order/OrderService.kt @@ -1,7 +1,9 @@ package com.petqua.application.order +import com.petqua.application.order.dto.PayOrderCommand import com.petqua.application.order.dto.SaveOrderCommand import com.petqua.application.order.dto.SaveOrderResponse +import com.petqua.application.order.payment.PaymentGatewayClient import com.petqua.common.domain.findByIdOrThrow import com.petqua.common.util.throwExceptionWhen import com.petqua.domain.order.Order @@ -12,10 +14,13 @@ import com.petqua.domain.order.OrderShippingAddress import com.petqua.domain.order.OrderStatus.ORDER_CREATED import com.petqua.domain.order.ShippingAddressRepository import com.petqua.domain.order.ShippingNumber +import com.petqua.domain.order.findByOrderNumberOrThrow +import com.petqua.domain.payment.tosspayment.TossPaymentRepository import com.petqua.domain.product.ProductRepository import com.petqua.domain.product.option.ProductOptionRepository import com.petqua.domain.store.StoreRepository import com.petqua.exception.order.OrderException +import com.petqua.exception.order.OrderExceptionType.ORDER_NOT_FOUND import com.petqua.exception.order.OrderExceptionType.ORDER_PRICE_NOT_MATCH import com.petqua.exception.order.OrderExceptionType.PRODUCT_NOT_FOUND import com.petqua.exception.order.ShippingAddressException @@ -33,6 +38,8 @@ class OrderService( private val productOptionRepository: ProductOptionRepository, private val shippingAddressRepository: ShippingAddressRepository, private val storeRepository: StoreRepository, + private val paymentRepository: TossPaymentRepository, + private val paymentGatewayClient: PaymentGatewayClient, ) { fun save(command: SaveOrderCommand): SaveOrderResponse { @@ -68,8 +75,8 @@ class OrderService( ?: throw ProductException(INVALID_PRODUCT_OPTION) throwExceptionWhen( - productCommand.orderPrice != (product.discountPrice + productOption.additionalPrice) * productCommand.quantity.toBigDecimal() - || productCommand.deliveryFee != product.getDeliveryFee(productCommand.deliveryMethod) + productCommand.orderPrice.setScale(2) != (product.discountPrice + productOption.additionalPrice) * productCommand.quantity.toBigDecimal() + || productCommand.deliveryFee.setScale(2) != product.getDeliveryFee(productCommand.deliveryMethod) ) { OrderException( ORDER_PRICE_NOT_MATCH @@ -127,8 +134,23 @@ class OrderService( return SaveOrderResponse( orderId = orders.first().orderNumber.value, orderName = orders.first().orderName.value, - successUrl = "successUrl", - failUrl = "failUrl", + successUrl = paymentGatewayClient.successUrl(), + failUrl = paymentGatewayClient.failUrl(), ) } + + fun payOrder(command: PayOrderCommand) { + // 총가격 검증, orderName 으로 찾기 + val order = orderRepository.findByOrderNumberOrThrow(command.orderNumber) { + OrderException(ORDER_NOT_FOUND) + } + + order.validateAmount(command.amount) + + // api 승인 요청 + val paymentResponse = paymentGatewayClient.confirmPayment(command.toPaymentConfirmRequest()) + val payment = paymentRepository.save(paymentResponse.toPayment()) + + // 응답 추가 + } } diff --git a/src/main/kotlin/com/petqua/application/order/dto/OrderDtos.kt b/src/main/kotlin/com/petqua/application/order/dto/OrderDtos.kt index 143634cf..c1cf4cb7 100644 --- a/src/main/kotlin/com/petqua/application/order/dto/OrderDtos.kt +++ b/src/main/kotlin/com/petqua/application/order/dto/OrderDtos.kt @@ -1,11 +1,15 @@ package com.petqua.application.order.dto +import com.petqua.application.order.payment.PaymentConfirmRequestToPG import com.petqua.domain.delivery.DeliveryMethod +import com.petqua.domain.order.OrderNumber import com.petqua.domain.order.OrderProduct import com.petqua.domain.order.ShippingNumber +import com.petqua.domain.payment.tosspayment.TossPaymentType import com.petqua.domain.product.Product import com.petqua.domain.product.option.ProductOption import com.petqua.domain.product.option.Sex +import io.swagger.v3.oas.annotations.media.Schema import java.math.BigDecimal data class SaveOrderCommand( @@ -44,12 +48,12 @@ data class OrderProductCommand( ): OrderProduct { return OrderProduct( quantity = quantity, - originalPrice = originalPrice, + originalPrice = originalPrice.setScale(2), discountRate = discountRate, - discountPrice = discountPrice, - deliveryFee = deliveryFee, + discountPrice = discountPrice.setScale(2), + deliveryFee = deliveryFee.setScale(2), shippingNumber = shippingNumber, - orderPrice = orderPrice, + orderPrice = orderPrice.setScale(2), productId = productId, productName = product.name, thumbnailUrl = product.thumbnailUrl, @@ -67,3 +71,164 @@ data class SaveOrderResponse( val successUrl: String, val failUrl: String, ) + +data class PayOrderCommand( + val paymentType: TossPaymentType, + val orderNumber: OrderNumber, + val paymentKey: String, + val amount: BigDecimal, +) { + fun toPaymentConfirmRequest(): PaymentConfirmRequestToPG { + return PaymentConfirmRequestToPG( + orderNumber = orderNumber, + paymentKey = paymentKey, + amount = amount + ) + } + + companion object { + fun of( + paymentType: String, + orderId: String, + paymentKey: String, + amount: BigDecimal + ): PayOrderCommand { + return PayOrderCommand( + paymentType = TossPaymentType.from(paymentType), + orderNumber = OrderNumber.from(orderId), + paymentKey = paymentKey, + amount = amount, + ) + } + } +} + +data class PayOrderResponse( + + val memberNickname: String, + + + @Schema( + description = "상품 id", + example = "1" + ) + val productId: Long, + + @Schema( + description = "상품 이름", + example = "알비노 풀레드 아시안 고정구피" + ) + val productName: String, + + @Schema( + description = "상품 판매점", + example = "S아쿠아" + ) + val storeName: String, + + @Schema( + description = "상품 썸네일 이미지", + example = "https://docs.petqua.co.kr/products/thumbnails/thumbnail1.jpeg" + ) + val thumbnailUrl: String, + + @Schema( + description = "할인 가격(판매 가격)", + example = "21000" + ) + val discountPrice: Int, + + val quantity: Int, + + @Schema( + description = "배송비", + example = "5000" + ) + val deliveryFee: BigDecimal?, +) + +data class OrderResponse( + @Schema( + description = "주문 번호", + example = "202402211607026029E90DB030" + ) + val orderNumber: String, + + @Schema( + description = "주문자", + example = "홍길동" + ) + val orderer: String, + + val shippingAddress: ShippigAddressResponse, + + @Schema( + description = "배송방법", + example = "안전배송" + ) + val deliveryMethod: String, + + @Schema( + description = "배송비", + example = "3000" + ) + val deliveryFee: Int, + + @Schema( + description = "주문자", + example = "홍길동" + ) + val paymentMethod: String, + + // (...) +) + +data class ShippigAddressResponse( + @Schema( + description = "배송지 id", + example = "1" + ) + val shippingAddressId: Long, + + @Schema( + description = "배송지 이름", + example = "집" + ) + val name: String, + + @Schema( + description = "받는 사람", + example = "홍길동" + ) + val receiver: String, + + @Schema( + description = "전화 번호", + example = "010-1234-1234" + ) + val phoneNumber: String, + + @Schema( + description = "우편 번호", + example = "12345" + ) + val zipCode: Int, + + @Schema( + description = "주소", + example = "서울특별시 강남구 역삼동 99번길" + ) + val address: String, + + @Schema( + description = "상세 주소", + example = "101동 101호" + ) + val detailAddress: String, + + @Schema( + description = "배송 메시지", + example = "부재 시 경비실에 맡겨주세요" + ) + val requestMessage: String?, +) diff --git a/src/main/kotlin/com/petqua/domain/order/OrderNumber.kt b/src/main/kotlin/com/petqua/domain/order/OrderNumber.kt index 0bd7b29b..b05992c4 100644 --- a/src/main/kotlin/com/petqua/domain/order/OrderNumber.kt +++ b/src/main/kotlin/com/petqua/domain/order/OrderNumber.kt @@ -19,5 +19,9 @@ data class OrderNumber( val uuid = UUID.randomUUID().toString().replace("-", "").substring(0, 12).uppercase() return OrderNumber(createdTime.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + uuid) } + + fun from(orderNumber: String): OrderNumber { + return OrderNumber(orderNumber) + } } } diff --git a/src/main/kotlin/com/petqua/domain/order/OrderRepository.kt b/src/main/kotlin/com/petqua/domain/order/OrderRepository.kt index 27632406..177722eb 100644 --- a/src/main/kotlin/com/petqua/domain/order/OrderRepository.kt +++ b/src/main/kotlin/com/petqua/domain/order/OrderRepository.kt @@ -2,5 +2,14 @@ package com.petqua.domain.order import org.springframework.data.jpa.repository.JpaRepository +fun OrderRepository.findByOrderNumberOrThrow( + orderNumber: OrderNumber, + exceptionSupplier: () -> Exception = { IllegalArgumentException("${Order::class.java.name} entity 를 찾을 수 없습니다.") } +): Order { + return findByOrderNumber(orderNumber) ?: throw exceptionSupplier() +} + interface OrderRepository : JpaRepository { + + fun findByOrderNumber(orderNumber: OrderNumber): Order? } diff --git a/src/main/kotlin/com/petqua/domain/payment/tosspayment/TossPayment.kt b/src/main/kotlin/com/petqua/domain/payment/tosspayment/TossPayment.kt index 6f37631e..d0d01f34 100644 --- a/src/main/kotlin/com/petqua/domain/payment/tosspayment/TossPayment.kt +++ b/src/main/kotlin/com/petqua/domain/payment/tosspayment/TossPayment.kt @@ -11,8 +11,8 @@ import jakarta.persistence.Enumerated import jakarta.persistence.GeneratedValue import jakarta.persistence.GenerationType.IDENTITY import jakarta.persistence.Id +import java.math.BigDecimal -// FIXME: 레퍼런스 첨부 https://docs.tosspayments.com/reference#%EA%B2%B0%EC%A0%9C @Entity class TossPayment( @Id @GeneratedValue(strategy = IDENTITY) @@ -34,7 +34,7 @@ class TossPayment( val method: TossPaymentMethod, @Column(nullable = false) - val totalAmount: String, + val totalAmount: BigDecimal, @Enumerated(STRING) @Column(nullable = false) @@ -47,7 +47,7 @@ class TossPayment( val approvedAt: String, @Column(nullable = false) - val useEscrow: String, // FIXME: 레퍼런스 첨부 https://docs.tosspayments.com/resources/glossary/escrow + val useEscrow: Boolean, @Enumerated(STRING) @Column(nullable = false) diff --git a/src/main/kotlin/com/petqua/domain/payment/tosspayment/TossPaymentRepository.kt b/src/main/kotlin/com/petqua/domain/payment/tosspayment/TossPaymentRepository.kt new file mode 100644 index 00000000..02196d2b --- /dev/null +++ b/src/main/kotlin/com/petqua/domain/payment/tosspayment/TossPaymentRepository.kt @@ -0,0 +1,6 @@ +package com.petqua.domain.payment.tosspayment + +import org.springframework.data.jpa.repository.JpaRepository + +interface TossPaymentRepository : JpaRepository { +} diff --git a/src/main/kotlin/com/petqua/domain/payment/tosspayment/TossPaymentType.kt b/src/main/kotlin/com/petqua/domain/payment/tosspayment/TossPaymentType.kt index 2311bd8e..31e17db9 100644 --- a/src/main/kotlin/com/petqua/domain/payment/tosspayment/TossPaymentType.kt +++ b/src/main/kotlin/com/petqua/domain/payment/tosspayment/TossPaymentType.kt @@ -1,5 +1,9 @@ package com.petqua.domain.payment.tosspayment +import com.petqua.exception.order.OrderException +import com.petqua.exception.order.OrderExceptionType.INVALID_PAYMENT_TYPE +import java.util.Locale + enum class TossPaymentType( private val description: String, ) { @@ -7,4 +11,12 @@ enum class TossPaymentType( NORMAL("일반 결제"), BILLING("자동 결제"), BRAND_PAY("브랜드 페이"), + ; + + companion object { + fun from(name: String): TossPaymentType { + return enumValues().find { it.name == name.uppercase(Locale.ENGLISH) } + ?: throw OrderException(INVALID_PAYMENT_TYPE) + } + } } diff --git a/src/main/kotlin/com/petqua/exception/order/OrderExceptionType.kt b/src/main/kotlin/com/petqua/exception/order/OrderExceptionType.kt index 1bfc022f..9f4e17a3 100644 --- a/src/main/kotlin/com/petqua/exception/order/OrderExceptionType.kt +++ b/src/main/kotlin/com/petqua/exception/order/OrderExceptionType.kt @@ -12,7 +12,10 @@ enum class OrderExceptionType( PRODUCT_NOT_FOUND(BAD_REQUEST, "O01", "주문한 상품이 존재하지 않습니다."), + ORDER_NOT_FOUND(BAD_REQUEST, "O11", "존재하지 않는 주문입니다."), ORDER_PRICE_NOT_MATCH(BAD_REQUEST, "O10", "주문한 상품의 가격이 일치하지 않습니다."), + + INVALID_PAYMENT_TYPE(BAD_REQUEST, "O20", "유효하지 않은 결제 방식입니다."), ; override fun httpStatus(): HttpStatus { diff --git a/src/main/kotlin/com/petqua/presentation/order/OrderController.kt b/src/main/kotlin/com/petqua/presentation/order/OrderController.kt index 81e235c3..48bcf378 100644 --- a/src/main/kotlin/com/petqua/presentation/order/OrderController.kt +++ b/src/main/kotlin/com/petqua/presentation/order/OrderController.kt @@ -16,8 +16,8 @@ import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController -@Tag(name = "Order", description = "주문 관련 API 명세") @SecurityRequirement(name = ACCESS_TOKEN_SECURITY_SCHEME_KEY) +@Tag(name = "Order", description = "주문 관련 API 명세") @RequestMapping("/orders") @RestController class OrderController( diff --git a/src/main/kotlin/com/petqua/presentation/order/dto/OrderDtos.kt b/src/main/kotlin/com/petqua/presentation/order/dto/OrderDtos.kt index 05fdf657..b663a31f 100644 --- a/src/main/kotlin/com/petqua/presentation/order/dto/OrderDtos.kt +++ b/src/main/kotlin/com/petqua/presentation/order/dto/OrderDtos.kt @@ -1,6 +1,7 @@ package com.petqua.presentation.order.dto import com.petqua.application.order.dto.OrderProductCommand +import com.petqua.application.order.dto.PayOrderCommand import com.petqua.application.order.dto.SaveOrderCommand import com.petqua.domain.delivery.DeliveryMethod import com.petqua.domain.product.option.Sex @@ -53,3 +54,20 @@ data class OrderProductRequest( ) } } + +data class PayOrderRequest( + val paymentType: String, + val orderId: String, + val paymentKey: String, + val amount: BigDecimal, +) { + + fun toCommand(): PayOrderCommand { + return PayOrderCommand.of( + paymentType = paymentType, + orderId = orderId, + paymentKey = paymentKey, + amount = amount, + ) + } +} diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 54beb9e2..351df777 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -34,3 +34,9 @@ logging: org.hibernate.orm.jdbc.bind: TRACE org.apache.coyote.http11: debug root: info + +payment: + toss-payments: + secret-key: test-secret-key # for test + success-url: ${base-url}/orders/payment/success + fail-url: ${base-url}/orders/payment/fail From 7f711a975292890a91abb43e9652f8fd30adfbff Mon Sep 17 00:00:00 2001 From: Combi153 Date: Wed, 28 Feb 2024 17:01:26 +0900 Subject: [PATCH 06/26] =?UTF-8?q?feat:=20=EC=98=88=EC=99=B8=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../petqua/application/order/OrderService.kt | 4 +- .../application/order/payment/PaymentDtos.kt | 5 ++ .../order/payment/TossPaymentClient.kt | 12 ++- .../exception/payment/PaymentException.kt | 13 +++ .../exception/payment/PaymentExceptionType.kt | 90 +++++++++++++++++++ 5 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/com/petqua/exception/payment/PaymentException.kt create mode 100644 src/main/kotlin/com/petqua/exception/payment/PaymentExceptionType.kt diff --git a/src/main/kotlin/com/petqua/application/order/OrderService.kt b/src/main/kotlin/com/petqua/application/order/OrderService.kt index 95575a1b..2adc0e24 100644 --- a/src/main/kotlin/com/petqua/application/order/OrderService.kt +++ b/src/main/kotlin/com/petqua/application/order/OrderService.kt @@ -149,8 +149,6 @@ class OrderService( // api 승인 요청 val paymentResponse = paymentGatewayClient.confirmPayment(command.toPaymentConfirmRequest()) - val payment = paymentRepository.save(paymentResponse.toPayment()) - - // 응답 추가 + paymentRepository.save(paymentResponse.toPayment()) } } diff --git a/src/main/kotlin/com/petqua/application/order/payment/PaymentDtos.kt b/src/main/kotlin/com/petqua/application/order/payment/PaymentDtos.kt index f279ee5f..74090501 100644 --- a/src/main/kotlin/com/petqua/application/order/payment/PaymentDtos.kt +++ b/src/main/kotlin/com/petqua/application/order/payment/PaymentDtos.kt @@ -177,3 +177,8 @@ data class CashReceiptHistoryResponseFromPG( data class DiscountResponseFromPG( val amount: BigDecimal, ) + +data class PaymentErrorResponseFromPG( + val code: String, + val message: String, +) diff --git a/src/main/kotlin/com/petqua/application/order/payment/TossPaymentClient.kt b/src/main/kotlin/com/petqua/application/order/payment/TossPaymentClient.kt index 75f7bb3f..bdc138b5 100644 --- a/src/main/kotlin/com/petqua/application/order/payment/TossPaymentClient.kt +++ b/src/main/kotlin/com/petqua/application/order/payment/TossPaymentClient.kt @@ -1,19 +1,29 @@ package com.petqua.application.order.payment +import com.fasterxml.jackson.databind.ObjectMapper import com.petqua.common.util.BasicAuthUtils +import com.petqua.exception.payment.PaymentException +import com.petqua.exception.payment.PaymentExceptionType import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.stereotype.Component +import org.springframework.web.client.RestClientResponseException @EnableConfigurationProperties(PaymentProperties::class) @Component class TossPaymentClient( private val tossPaymentsApiClient: TossPaymentsApiClient, private val paymentProperties: PaymentProperties, + private val objectMapper: ObjectMapper, ) : PaymentGatewayClient { override fun confirmPayment(paymentConfirmRequestToPG: PaymentConfirmRequestToPG): PaymentResponseFromPG { val credentials = BasicAuthUtils.encodeCredentialsWithColon(paymentProperties.secretKey) - return tossPaymentsApiClient.confirmPayment(credentials, paymentConfirmRequestToPG) + try { + return tossPaymentsApiClient.confirmPayment(credentials, paymentConfirmRequestToPG) + } catch (e: RestClientResponseException) { + val errorResponse = objectMapper.readValue(e.responseBodyAsString, PaymentErrorResponseFromPG::class.java) + throw PaymentException(PaymentExceptionType.from(errorResponse.code)) + } } override fun successUrl(): String { diff --git a/src/main/kotlin/com/petqua/exception/payment/PaymentException.kt b/src/main/kotlin/com/petqua/exception/payment/PaymentException.kt new file mode 100644 index 00000000..d2dc4eab --- /dev/null +++ b/src/main/kotlin/com/petqua/exception/payment/PaymentException.kt @@ -0,0 +1,13 @@ +package com.petqua.exception.payment + +import com.petqua.common.exception.BaseException +import com.petqua.common.exception.BaseExceptionType + +class PaymentException( + private val exceptionType: PaymentExceptionType, +) : BaseException() { + + override fun exceptionType(): BaseExceptionType { + return exceptionType + } +} diff --git a/src/main/kotlin/com/petqua/exception/payment/PaymentExceptionType.kt b/src/main/kotlin/com/petqua/exception/payment/PaymentExceptionType.kt new file mode 100644 index 00000000..573f1c7e --- /dev/null +++ b/src/main/kotlin/com/petqua/exception/payment/PaymentExceptionType.kt @@ -0,0 +1,90 @@ +package com.petqua.exception.payment + +import com.petqua.common.exception.BaseExceptionType +import org.springframework.http.HttpStatus +import org.springframework.http.HttpStatus.BAD_REQUEST +import org.springframework.http.HttpStatus.FORBIDDEN +import org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR +import org.springframework.http.HttpStatus.NOT_FOUND +import org.springframework.http.HttpStatus.UNAUTHORIZED +import java.util.Locale + +enum class PaymentExceptionType( + private val httpStatus: HttpStatus, + private val code: String, + private val errorMessage: String, +) : BaseExceptionType { + + ALREADY_PROCESSED_PAYMENT(BAD_REQUEST, "P01", "이미 처리된 결제 입니다"), + PROVIDER_ERROR(BAD_REQUEST, "P02", "일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요."), + EXCEED_MAX_CARD_INSTALLMENT_PLAN(BAD_REQUEST, "P03", "설정 가능한 최대 할부 개월 수를 초과했습니다."), + INVALID_REQUEST(BAD_REQUEST, "P04", "잘못된 요청입니다."), + NOT_ALLOWED_POINT_USE(BAD_REQUEST, "P05", "포인트 사용이 불가한 카드로 카드 포인트 결제에 실패했습니다."), + INVALID_API_KEY(BAD_REQUEST, "P06", "잘못된 시크릿키 연동 정보 입니다."), + INVALID_REJECT_CARD(BAD_REQUEST, "P07", "카드 사용이 거절되었습니다. 카드사 문의가 필요합니다."), + BELOW_MINIMUM_AMOUNT(BAD_REQUEST, "P08", "신용카드는 결제금액이 100원 이상, 계좌는 200원이상부터 결제가 가능합니다."), + INVALID_CARD_EXPIRATION(BAD_REQUEST, "P09", "카드 정보를 다시 확인해주세요. (유효기간)"), + INVALID_STOPPED_CARD(BAD_REQUEST, "P10", "정지된 카드 입니다."), + EXCEED_MAX_DAILY_PAYMENT_COUNT(BAD_REQUEST, "P11", "하루 결제 가능 횟수를 초과했습니다."), + NOT_SUPPORTED_INSTALLMENT_PLAN_CARD_OR_MERCHANT(BAD_REQUEST, "P12", "할부가 지원되지 않는 카드 또는 가맹점 입니다."), + INVALID_CARD_INSTALLMENT_PLAN(BAD_REQUEST, "P13", "할부 개월 정보가 잘못되었습니다."), + NOT_SUPPORTED_MONTHLY_INSTALLMENT_PLAN(BAD_REQUEST, "P14", "할부가 지원되지 않는 카드입니다."), + EXCEED_MAX_PAYMENT_AMOUNT(BAD_REQUEST, "P15", "하루 결제 가능 금액을 초과했습니다."), + NOT_FOUND_TERMINAL_ID(BAD_REQUEST, "P16", "단말기번호(Terminal Id)가 없습니다. 토스페이먼츠로 문의 바랍니다."), + INVALID_AUTHORIZE_AUTH(BAD_REQUEST, "P17", "유효하지 않은 인증 방식입니다."), + INVALID_CARD_LOST_OR_STOLEN(BAD_REQUEST, "P18", "분실 혹은 도난 카드입니다."), + RESTRICTED_TRANSFER_ACCOUNT(BAD_REQUEST, "P19", "계좌는 등록 후 12시간 뒤부터 결제할 수 있습니다. 관련 정책은 해당 은행으로 문의해주세요."), + INVALID_CARD_NUMBER(BAD_REQUEST, "P20", "카드번호를 다시 확인해주세요."), + INVALID_UNREGISTERED_SUBMALL(BAD_REQUEST, "P21", "등록되지 않은 서브몰입니다. 서브몰이 없는 가맹점이라면 안심클릭이나 ISP 결제가 필요합니다."), + NOT_REGISTERED_BUSINESS(BAD_REQUEST, "P22", "등록되지 않은 사업자 번호입니다."), + EXCEED_MAX_ONE_DAY_WITHDRAW_AMOUNT(BAD_REQUEST, "P23", "1일 출금 한도를 초과했습니다."), + EXCEED_MAX_ONE_TIME_WITHDRAW_AMOUNT(BAD_REQUEST, "P24", "1회 출금 한도를 초과했습니다."), + CARD_PROCESSING_ERROR(BAD_REQUEST, "P25", "카드사에서 오류가 발생했습니다."), + EXCEED_MAX_AMOUNT(BAD_REQUEST, "P26", "거래금액 한도를 초과했습니다."), + INVALID_ACCOUNT_INFO_RE_REGISTER(BAD_REQUEST, "P27", "유효하지 않은 계좌입니다. 계좌 재등록 후 시도해주세요."), + NOT_AVAILABLE_PAYMENT(BAD_REQUEST, "P28", "결제가 불가능한 시간대입니다."), + UNAPPROVED_ORDER_ID(BAD_REQUEST, "P29", "아직 승인되지 않은 주문번호입니다."), + + UNAUTHORIZED_KEY(UNAUTHORIZED, "P30", "인증되지 않은 시크릿 키 혹은 클라이언트 키 입니다."), + + REJECT_ACCOUNT_PAYMENT(FORBIDDEN, "P31", "잔액부족으로 결제에 실패했습니다."), + REJECT_CARD_PAYMENT(FORBIDDEN, "P32", "한도초과 혹은 잔액부족으로 결제에 실패했습니다."), + REJECT_CARD_COMPANY(FORBIDDEN, "P33", "결제 승인이 거절되었습니다."), + FORBIDDEN_REQUEST(FORBIDDEN, "P34", "허용되지 않은 요청입니다."), + REJECT_TOSSPAY_INVALID_ACCOUNT(FORBIDDEN, "P35", "선택하신 출금 계좌가 출금이체 등록이 되어 있지 않아요. 계좌를 다시 등록해 주세요."), + EXCEED_MAX_AUTH_COUNT(FORBIDDEN, "P36", "최대 인증 횟수를 초과했습니다. 카드사로 문의해주세요."), + EXCEED_MAX_ONE_DAY_AMOUNT(FORBIDDEN, "P37", "일일 한도를 초과했습니다."), + NOT_AVAILABLE_BANK(FORBIDDEN, "P38", "은행 서비스 시간이 아닙니다."), + INVALID_PASSWORD(FORBIDDEN, "P39", "결제 비밀번호가 일치하지 않습니다."), + INCORRECT_BASIC_AUTH_FORMAT(FORBIDDEN, "P40", "잘못된 요청입니다. ':' 를 포함해 인코딩해주세요."), + FDS_ERROR(FORBIDDEN, "P41", "[토스페이먼츠] 위험거래가 감지되어 결제가 제한됩니다.발송된 문자에 포함된 링크를 통해 본인인증 후 결제가 가능합니다.(고객센터: 1644-8051)"), + + NOT_FOUND_PAYMENT(NOT_FOUND, "P42", "존재하지 않는 결제 정보 입니다."), + NOT_FOUND_PAYMENT_SESSION(NOT_FOUND, "P43", "결제 시간이 만료되어 결제 진행 데이터가 존재하지 않습니다."), + + FAILED_PAYMENT_INTERNAL_SYSTEM_PROCESSING(INTERNAL_SERVER_ERROR, "P44", "결제가 완료되지 않았어요. 다시 시도해주세요."), + FAILED_INTERNAL_SYSTEM_PROCESSING(INTERNAL_SERVER_ERROR, "P45", "내부 시스템 처리 작업이 실패했습니다. 잠시 후 다시 시도해주세요."), + UNKNOWN_PAYMENT_ERROR(INTERNAL_SERVER_ERROR, "P46", "결제에 실패했어요. 같은 문제가 반복된다면 은행이나 카드사로 문의해주세요."), + + UNKNOWN_PAYMENT_EXCEPTION(INTERNAL_SERVER_ERROR, "P50", "지원하지 않는 결제 예외가 발생했습니다."), + ; + + override fun httpStatus(): HttpStatus { + return httpStatus + } + + override fun code(): String { + return code + } + + override fun errorMessage(): String { + return errorMessage + } + + companion object { + fun from(name: String): PaymentExceptionType { + return enumValues().find { it.name == name.uppercase(Locale.ENGLISH) } + ?: throw PaymentException(UNKNOWN_PAYMENT_EXCEPTION) + } + } +} From ad9c8888a9fb78a901b4bfa46c8513583c9eb302 Mon Sep 17 00:00:00 2001 From: Combi153 Date: Thu, 29 Feb 2024 16:40:24 +0900 Subject: [PATCH 07/26] =?UTF-8?q?chore:=20rebase=20=EC=B6=A9=EB=8F=8C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../petqua/application/order/OrderService.kt | 20 +----- .../petqua/application/order/dto/OrderDtos.kt | 4 +- .../order/payment/PaymentProperties.kt | 10 --- .../{order => }/payment/PaymentDtos.kt | 10 +-- .../application/payment/PaymentService.kt | 31 ++++++++ .../infra}/PaymentGatewayClient.kt | 5 +- .../infra}/TossPaymentClient.kt | 13 +++- .../infra}/TossPaymentsApiClient.kt | 4 +- .../petqua/common/config/ApiClientConfig.kt | 2 +- .../presentation/order/PaymentController.kt | 39 +++++++++++ .../application/auth/AuthFacadeServiceTest.kt | 9 ++- .../presentation/auth/AuthControllerTest.kt | 4 +- .../kotlin/com/petqua/test/ApiTestConfig.kt | 4 +- ...thTestConfig.kt => ApiClientTestConfig.kt} | 11 ++- .../test/fake/FakeTossPaymentsApiClient.kt | 70 +++++++++++++++++++ 15 files changed, 185 insertions(+), 51 deletions(-) delete mode 100644 src/main/kotlin/com/petqua/application/order/payment/PaymentProperties.kt rename src/main/kotlin/com/petqua/application/{order => }/payment/PaymentDtos.kt (95%) create mode 100644 src/main/kotlin/com/petqua/application/payment/PaymentService.kt rename src/main/kotlin/com/petqua/application/{order/payment => payment/infra}/PaymentGatewayClient.kt (53%) rename src/main/kotlin/com/petqua/application/{order/payment => payment/infra}/TossPaymentClient.kt (75%) rename src/main/kotlin/com/petqua/application/{order/payment => payment/infra}/TossPaymentsApiClient.kt (81%) create mode 100644 src/main/kotlin/com/petqua/presentation/order/PaymentController.kt rename src/test/kotlin/com/petqua/test/config/{OauthTestConfig.kt => ApiClientTestConfig.kt} (51%) create mode 100644 src/test/kotlin/com/petqua/test/fake/FakeTossPaymentsApiClient.kt diff --git a/src/main/kotlin/com/petqua/application/order/OrderService.kt b/src/main/kotlin/com/petqua/application/order/OrderService.kt index 2adc0e24..845592fa 100644 --- a/src/main/kotlin/com/petqua/application/order/OrderService.kt +++ b/src/main/kotlin/com/petqua/application/order/OrderService.kt @@ -1,9 +1,8 @@ package com.petqua.application.order -import com.petqua.application.order.dto.PayOrderCommand import com.petqua.application.order.dto.SaveOrderCommand import com.petqua.application.order.dto.SaveOrderResponse -import com.petqua.application.order.payment.PaymentGatewayClient +import com.petqua.application.payment.infra.PaymentGatewayClient import com.petqua.common.domain.findByIdOrThrow import com.petqua.common.util.throwExceptionWhen import com.petqua.domain.order.Order @@ -14,13 +13,10 @@ import com.petqua.domain.order.OrderShippingAddress import com.petqua.domain.order.OrderStatus.ORDER_CREATED import com.petqua.domain.order.ShippingAddressRepository import com.petqua.domain.order.ShippingNumber -import com.petqua.domain.order.findByOrderNumberOrThrow -import com.petqua.domain.payment.tosspayment.TossPaymentRepository import com.petqua.domain.product.ProductRepository import com.petqua.domain.product.option.ProductOptionRepository import com.petqua.domain.store.StoreRepository import com.petqua.exception.order.OrderException -import com.petqua.exception.order.OrderExceptionType.ORDER_NOT_FOUND import com.petqua.exception.order.OrderExceptionType.ORDER_PRICE_NOT_MATCH import com.petqua.exception.order.OrderExceptionType.PRODUCT_NOT_FOUND import com.petqua.exception.order.ShippingAddressException @@ -38,7 +34,6 @@ class OrderService( private val productOptionRepository: ProductOptionRepository, private val shippingAddressRepository: ShippingAddressRepository, private val storeRepository: StoreRepository, - private val paymentRepository: TossPaymentRepository, private val paymentGatewayClient: PaymentGatewayClient, ) { @@ -138,17 +133,4 @@ class OrderService( failUrl = paymentGatewayClient.failUrl(), ) } - - fun payOrder(command: PayOrderCommand) { - // 총가격 검증, orderName 으로 찾기 - val order = orderRepository.findByOrderNumberOrThrow(command.orderNumber) { - OrderException(ORDER_NOT_FOUND) - } - - order.validateAmount(command.amount) - - // api 승인 요청 - val paymentResponse = paymentGatewayClient.confirmPayment(command.toPaymentConfirmRequest()) - paymentRepository.save(paymentResponse.toPayment()) - } } diff --git a/src/main/kotlin/com/petqua/application/order/dto/OrderDtos.kt b/src/main/kotlin/com/petqua/application/order/dto/OrderDtos.kt index c1cf4cb7..88b5a41f 100644 --- a/src/main/kotlin/com/petqua/application/order/dto/OrderDtos.kt +++ b/src/main/kotlin/com/petqua/application/order/dto/OrderDtos.kt @@ -1,6 +1,6 @@ package com.petqua.application.order.dto -import com.petqua.application.order.payment.PaymentConfirmRequestToPG +import com.petqua.application.payment.PaymentConfirmRequestToPG import com.petqua.domain.delivery.DeliveryMethod import com.petqua.domain.order.OrderNumber import com.petqua.domain.order.OrderProduct @@ -91,7 +91,7 @@ data class PayOrderCommand( paymentType: String, orderId: String, paymentKey: String, - amount: BigDecimal + amount: BigDecimal, ): PayOrderCommand { return PayOrderCommand( paymentType = TossPaymentType.from(paymentType), diff --git a/src/main/kotlin/com/petqua/application/order/payment/PaymentProperties.kt b/src/main/kotlin/com/petqua/application/order/payment/PaymentProperties.kt deleted file mode 100644 index 56743f7b..00000000 --- a/src/main/kotlin/com/petqua/application/order/payment/PaymentProperties.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.petqua.application.order.payment - -import org.springframework.boot.context.properties.ConfigurationProperties - -@ConfigurationProperties(prefix = "payment.toss-payments") -data class PaymentProperties( - val secretKey: String, - val successUrl: String, - val failUrl: String, -) diff --git a/src/main/kotlin/com/petqua/application/order/payment/PaymentDtos.kt b/src/main/kotlin/com/petqua/application/payment/PaymentDtos.kt similarity index 95% rename from src/main/kotlin/com/petqua/application/order/payment/PaymentDtos.kt rename to src/main/kotlin/com/petqua/application/payment/PaymentDtos.kt index 74090501..91664721 100644 --- a/src/main/kotlin/com/petqua/application/order/payment/PaymentDtos.kt +++ b/src/main/kotlin/com/petqua/application/payment/PaymentDtos.kt @@ -1,4 +1,4 @@ -package com.petqua.application.order.payment +package com.petqua.application.payment import com.petqua.domain.order.OrderName import com.petqua.domain.order.OrderNumber @@ -30,7 +30,7 @@ data class PaymentResponseFromPG( val approvedAt: String, val useEscrow: Boolean, val lastTransactionKey: String?, - val suppliedAmount: String, + val suppliedAmount: BigDecimal, val vat: BigDecimal, val cultureExpense: Boolean, val taxFreeAmount: BigDecimal, @@ -104,7 +104,7 @@ data class VirtualAccountResponseFromPG( val refundStatus: String, val expired: Boolean, val settlementStatus: String, - val refundReceiveAccount: RefundReceiveAccountResponseFromPG?, // 결제창 띄운 시점부터 30분 동안만 조회 가능, 결제 승인 직후 응답 저장 필요 -> 고객의 계좌 정보를 저장해야 해? + val refundReceiveAccount: RefundReceiveAccountResponseFromPG?, // 결제창 띄운 시점부터 30분 동안만 조회 가능 ) data class RefundReceiveAccountResponseFromPG( @@ -116,7 +116,7 @@ data class RefundReceiveAccountResponseFromPG( data class MobilePhoneResponseFromPG( val customerMobilePhone: String, val settlementStatus: String, - val receiptUrl: String + val receiptUrl: String, ) data class GiftCertificateResponseFromPG( @@ -171,7 +171,7 @@ data class CashReceiptHistoryResponseFromPG( val issueStatus: String, val failure: FailureResponseFromPG, val customerIdentityNumber: String, - val requestedAt: String + val requestedAt: String, ) data class DiscountResponseFromPG( diff --git a/src/main/kotlin/com/petqua/application/payment/PaymentService.kt b/src/main/kotlin/com/petqua/application/payment/PaymentService.kt new file mode 100644 index 00000000..61eb2246 --- /dev/null +++ b/src/main/kotlin/com/petqua/application/payment/PaymentService.kt @@ -0,0 +1,31 @@ +package com.petqua.application.payment + +import com.petqua.application.order.dto.PayOrderCommand +import com.petqua.application.payment.infra.PaymentGatewayClient +import com.petqua.domain.order.OrderRepository +import com.petqua.domain.order.findByOrderNumberOrThrow +import com.petqua.domain.payment.tosspayment.TossPaymentRepository +import com.petqua.exception.order.OrderException +import com.petqua.exception.order.OrderExceptionType +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Transactional +@Service +class PaymentService( + private val orderRepository: OrderRepository, + private val paymentRepository: TossPaymentRepository, + private val paymentGatewayClient: PaymentGatewayClient, +) { + + fun payOrder(command: PayOrderCommand) { + val order = orderRepository.findByOrderNumberOrThrow(command.orderNumber) { + OrderException(OrderExceptionType.ORDER_NOT_FOUND) + } + + order.validateAmount(command.amount) + + val paymentResponse = paymentGatewayClient.confirmPayment(command.toPaymentConfirmRequest()) + paymentRepository.save(paymentResponse.toPayment()) + } +} diff --git a/src/main/kotlin/com/petqua/application/order/payment/PaymentGatewayClient.kt b/src/main/kotlin/com/petqua/application/payment/infra/PaymentGatewayClient.kt similarity index 53% rename from src/main/kotlin/com/petqua/application/order/payment/PaymentGatewayClient.kt rename to src/main/kotlin/com/petqua/application/payment/infra/PaymentGatewayClient.kt index 4ae8877b..91d40511 100644 --- a/src/main/kotlin/com/petqua/application/order/payment/PaymentGatewayClient.kt +++ b/src/main/kotlin/com/petqua/application/payment/infra/PaymentGatewayClient.kt @@ -1,4 +1,7 @@ -package com.petqua.application.order.payment +package com.petqua.application.payment.infra + +import com.petqua.application.payment.PaymentConfirmRequestToPG +import com.petqua.application.payment.PaymentResponseFromPG interface PaymentGatewayClient { diff --git a/src/main/kotlin/com/petqua/application/order/payment/TossPaymentClient.kt b/src/main/kotlin/com/petqua/application/payment/infra/TossPaymentClient.kt similarity index 75% rename from src/main/kotlin/com/petqua/application/order/payment/TossPaymentClient.kt rename to src/main/kotlin/com/petqua/application/payment/infra/TossPaymentClient.kt index bdc138b5..197d3473 100644 --- a/src/main/kotlin/com/petqua/application/order/payment/TossPaymentClient.kt +++ b/src/main/kotlin/com/petqua/application/payment/infra/TossPaymentClient.kt @@ -1,13 +1,24 @@ -package com.petqua.application.order.payment +package com.petqua.application.payment.infra import com.fasterxml.jackson.databind.ObjectMapper +import com.petqua.application.payment.PaymentConfirmRequestToPG +import com.petqua.application.payment.PaymentErrorResponseFromPG +import com.petqua.application.payment.PaymentResponseFromPG import com.petqua.common.util.BasicAuthUtils import com.petqua.exception.payment.PaymentException import com.petqua.exception.payment.PaymentExceptionType +import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.stereotype.Component import org.springframework.web.client.RestClientResponseException +@ConfigurationProperties(prefix = "payment.toss-payments") +data class PaymentProperties( + val secretKey: String, + val successUrl: String, + val failUrl: String, +) + @EnableConfigurationProperties(PaymentProperties::class) @Component class TossPaymentClient( diff --git a/src/main/kotlin/com/petqua/application/order/payment/TossPaymentsApiClient.kt b/src/main/kotlin/com/petqua/application/payment/infra/TossPaymentsApiClient.kt similarity index 81% rename from src/main/kotlin/com/petqua/application/order/payment/TossPaymentsApiClient.kt rename to src/main/kotlin/com/petqua/application/payment/infra/TossPaymentsApiClient.kt index 68bd20a5..cfe42152 100644 --- a/src/main/kotlin/com/petqua/application/order/payment/TossPaymentsApiClient.kt +++ b/src/main/kotlin/com/petqua/application/payment/infra/TossPaymentsApiClient.kt @@ -1,5 +1,7 @@ -package com.petqua.application.order.payment +package com.petqua.application.payment.infra +import com.petqua.application.payment.PaymentConfirmRequestToPG +import com.petqua.application.payment.PaymentResponseFromPG import org.springframework.http.HttpHeaders.AUTHORIZATION import org.springframework.http.MediaType.APPLICATION_JSON_VALUE import org.springframework.web.bind.annotation.RequestBody diff --git a/src/main/kotlin/com/petqua/common/config/ApiClientConfig.kt b/src/main/kotlin/com/petqua/common/config/ApiClientConfig.kt index dc19aa00..88ec4af4 100644 --- a/src/main/kotlin/com/petqua/common/config/ApiClientConfig.kt +++ b/src/main/kotlin/com/petqua/common/config/ApiClientConfig.kt @@ -1,6 +1,6 @@ package com.petqua.common.config -import com.petqua.application.order.payment.TossPaymentsApiClient +import com.petqua.application.payment.infra.TossPaymentsApiClient import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.web.reactive.function.client.WebClient diff --git a/src/main/kotlin/com/petqua/presentation/order/PaymentController.kt b/src/main/kotlin/com/petqua/presentation/order/PaymentController.kt new file mode 100644 index 00000000..4de39256 --- /dev/null +++ b/src/main/kotlin/com/petqua/presentation/order/PaymentController.kt @@ -0,0 +1,39 @@ +package com.petqua.presentation.order + +import com.petqua.application.payment.PaymentService +import com.petqua.common.config.ACCESS_TOKEN_SECURITY_SCHEME_KEY +import com.petqua.domain.auth.Auth +import com.petqua.domain.auth.LoginMember +import com.petqua.presentation.order.dto.PayOrderRequest +import io.swagger.v3.oas.annotations.security.SecurityRequirement +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.ResponseEntity +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 + +@SecurityRequirement(name = ACCESS_TOKEN_SECURITY_SCHEME_KEY) +@Tag(name = "Payment", description = "결제 관련 API 명세") +@RequestMapping("/orders/payment") +@RestController +class PaymentController( + private val paymentService: PaymentService, +) { + + @PostMapping("/success") + fun payOrder( + @Auth loginMember: LoginMember, + @RequestBody request: PayOrderRequest, + ): ResponseEntity { + paymentService.payOrder(request.toCommand()) + return ResponseEntity.ok().build() + } + + @PostMapping("/fail") + fun cancelOrderPayment( + @Auth loginMember: LoginMember, + ): ResponseEntity { + return ResponseEntity.ok().build() + } +} diff --git a/src/test/kotlin/com/petqua/application/auth/AuthFacadeServiceTest.kt b/src/test/kotlin/com/petqua/application/auth/AuthFacadeServiceTest.kt index 4bd7de7b..e2e05d0b 100644 --- a/src/test/kotlin/com/petqua/application/auth/AuthFacadeServiceTest.kt +++ b/src/test/kotlin/com/petqua/application/auth/AuthFacadeServiceTest.kt @@ -13,7 +13,7 @@ import com.petqua.exception.auth.AuthExceptionType import com.petqua.exception.member.MemberException import com.petqua.exception.member.MemberExceptionType.NOT_FOUND_MEMBER import com.petqua.test.DataCleaner -import com.petqua.test.config.OauthTestConfig +import com.petqua.test.config.ApiClientTestConfig import com.petqua.test.fixture.member import io.kotest.assertions.assertSoftly import io.kotest.assertions.throwables.shouldNotThrow @@ -25,12 +25,11 @@ import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE import org.springframework.context.annotation.Import import java.lang.System.currentTimeMillis -import java.time.LocalDateTime -import java.util.* +import java.util.Date @SpringBootTest(webEnvironment = NONE) -@Import(OauthTestConfig::class) -class AuthFacadeServiceTest( +@Import(ApiClientTestConfig::class) +class AuthServiceTest( private val authFacadeService: AuthFacadeService, private val refreshTokenRepository: RefreshTokenRepository, private val authTokenProvider: AuthTokenProvider, diff --git a/src/test/kotlin/com/petqua/presentation/auth/AuthControllerTest.kt b/src/test/kotlin/com/petqua/presentation/auth/AuthControllerTest.kt index d970bcf2..48ab35d6 100644 --- a/src/test/kotlin/com/petqua/presentation/auth/AuthControllerTest.kt +++ b/src/test/kotlin/com/petqua/presentation/auth/AuthControllerTest.kt @@ -9,7 +9,7 @@ import com.petqua.domain.auth.token.RefreshToken import com.petqua.domain.auth.token.RefreshTokenRepository import com.petqua.domain.member.MemberRepository import com.petqua.test.ApiTestConfig -import com.petqua.test.config.OauthTestConfig +import com.petqua.test.config.ApiClientTestConfig import com.petqua.test.fixture.member import io.kotest.assertions.assertSoftly import io.kotest.matchers.nulls.shouldNotBeNull @@ -22,7 +22,7 @@ import org.springframework.http.HttpStatus.NO_CONTENT import org.springframework.http.HttpStatus.OK import java.util.Date -@Import(OauthTestConfig::class) +@Import(ApiClientTestConfig::class) class AuthControllerTest( private val memberRepository: MemberRepository, private val refreshTokenRepository: RefreshTokenRepository, diff --git a/src/test/kotlin/com/petqua/test/ApiTestConfig.kt b/src/test/kotlin/com/petqua/test/ApiTestConfig.kt index e27e40b7..1e4cab63 100644 --- a/src/test/kotlin/com/petqua/test/ApiTestConfig.kt +++ b/src/test/kotlin/com/petqua/test/ApiTestConfig.kt @@ -1,7 +1,7 @@ package com.petqua.test import com.petqua.presentation.auth.AuthExtractor -import com.petqua.test.config.OauthTestConfig +import com.petqua.test.config.ApiClientTestConfig import io.kotest.core.spec.style.BehaviorSpec import io.restassured.RestAssured import io.restassured.module.kotlin.extensions.Extract @@ -20,7 +20,7 @@ data class AuthResponse( val accessToken: String, ) -@Import(OauthTestConfig::class) +@Import(ApiClientTestConfig::class) @SpringBootTest(webEnvironment = RANDOM_PORT) abstract class ApiTestConfig() : BehaviorSpec() { diff --git a/src/test/kotlin/com/petqua/test/config/OauthTestConfig.kt b/src/test/kotlin/com/petqua/test/config/ApiClientTestConfig.kt similarity index 51% rename from src/test/kotlin/com/petqua/test/config/OauthTestConfig.kt rename to src/test/kotlin/com/petqua/test/config/ApiClientTestConfig.kt index 7b79d216..8796da79 100644 --- a/src/test/kotlin/com/petqua/test/config/OauthTestConfig.kt +++ b/src/test/kotlin/com/petqua/test/config/ApiClientTestConfig.kt @@ -1,15 +1,22 @@ package com.petqua.test.config +import com.petqua.application.payment.infra.TossPaymentsApiClient +import com.petqua.domain.auth.FakeKakaoOauthApiClient import com.petqua.domain.auth.oauth.kakao.KakaoOauthApiClient -import com.petqua.test.fake.FakeKakaoOauthApiClient +import com.petqua.test.fake.FakeTossPaymentsApiClient import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean @TestConfiguration -class OauthTestConfig { +class ApiClientTestConfig { @Bean fun kakaoOauthApiClient(): KakaoOauthApiClient { return FakeKakaoOauthApiClient() } + + @Bean + fun tossPaymentsApiClient(): TossPaymentsApiClient { + return FakeTossPaymentsApiClient() + } } diff --git a/src/test/kotlin/com/petqua/test/fake/FakeTossPaymentsApiClient.kt b/src/test/kotlin/com/petqua/test/fake/FakeTossPaymentsApiClient.kt new file mode 100644 index 00000000..8f74de25 --- /dev/null +++ b/src/test/kotlin/com/petqua/test/fake/FakeTossPaymentsApiClient.kt @@ -0,0 +1,70 @@ +package com.petqua.test.fake + +import com.petqua.application.payment.CardResponseFromPG +import com.petqua.application.payment.PaymentConfirmRequestToPG +import com.petqua.application.payment.PaymentResponseFromPG +import com.petqua.application.payment.infra.TossPaymentsApiClient +import com.petqua.domain.payment.tosspayment.TossPaymentMethod +import com.petqua.domain.payment.tosspayment.TossPaymentStatus +import com.petqua.domain.payment.tosspayment.TossPaymentType +import java.math.BigDecimal + +class FakeTossPaymentsApiClient : TossPaymentsApiClient { + + override fun confirmPayment( + credentials: String, + paymentConfirmRequestToPG: PaymentConfirmRequestToPG, + ): PaymentResponseFromPG { + return PaymentResponseFromPG( + version = "version", + paymentKey = paymentConfirmRequestToPG.paymentKey, + type = TossPaymentType.NORMAL.name, + orderId = paymentConfirmRequestToPG.orderNumber.value, + orderName = "orderName", + mid = "mid", + currency = "currency", + method = TossPaymentMethod.CREDIT_CARD.name, + totalAmount = paymentConfirmRequestToPG.amount, + balanceAmount = paymentConfirmRequestToPG.amount, + status = TossPaymentStatus.DONE.name, + requestedAt = "2022-01-01T00:00:00+09:00", + approvedAt = "2022-01-01T00:00:00+09:00", + useEscrow = false, + lastTransactionKey = null, + suppliedAmount = paymentConfirmRequestToPG.amount, + vat = BigDecimal.ZERO, + cultureExpense = false, + taxFreeAmount = BigDecimal.ZERO, + taxExemptionAmount = BigDecimal.ZERO, + cancels = null, + isPartialCancelable = true, + card = CardResponseFromPG( + amount = paymentConfirmRequestToPG.amount, + issuerCode = "issuerCode", + acquirerCode = null, + number = "card_number", + installmentPlanMonths = 0, + approveNo = "approveNo", + useCardPoint = false, + cardType = "신용", + ownerType = "개인", + acquireStatus = "READY", + isInterestFree = false, + interestPayer = null, + ), + virtualAccount = null, + secret = null, + mobilePhone = null, + giftCertificate = null, + transfer = null, + receipt = null, + checkout = null, + easyPay = null, + country = "KR", + failure = null, + cashReceipt = null, + cashReceipts = null, + discount = null, + ) + } +} From d633576d7d7b5b0b7e88d62745d3fc2b01f5cdfe Mon Sep 17 00:00:00 2001 From: Combi153 Date: Wed, 28 Feb 2024 18:59:28 +0900 Subject: [PATCH 08/26] =?UTF-8?q?refactor:=20PG=EC=82=AC=20api=20=ED=98=B8?= =?UTF-8?q?=EC=B6=9C=20=EB=A1=9C=EC=A7=81=20=ED=8A=B8=EB=9E=9C=EC=9E=AD?= =?UTF-8?q?=EC=85=98=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../payment/PaymentFacadeService.kt | 17 +++++++++++++++ .../payment/PaymentGatewayService.kt | 14 +++++++++++++ .../application/payment/PaymentService.kt | 12 +++++------ .../presentation/order/dto/OrderDtos.kt | 18 ---------------- .../{order => payment}/PaymentController.kt | 9 ++++---- .../presentation/payment/PaymentDtos.kt | 21 +++++++++++++++++++ 6 files changed, 62 insertions(+), 29 deletions(-) create mode 100644 src/main/kotlin/com/petqua/application/payment/PaymentFacadeService.kt create mode 100644 src/main/kotlin/com/petqua/application/payment/PaymentGatewayService.kt rename src/main/kotlin/com/petqua/presentation/{order => payment}/PaymentController.kt (82%) create mode 100644 src/main/kotlin/com/petqua/presentation/payment/PaymentDtos.kt diff --git a/src/main/kotlin/com/petqua/application/payment/PaymentFacadeService.kt b/src/main/kotlin/com/petqua/application/payment/PaymentFacadeService.kt new file mode 100644 index 00000000..06f2c777 --- /dev/null +++ b/src/main/kotlin/com/petqua/application/payment/PaymentFacadeService.kt @@ -0,0 +1,17 @@ +package com.petqua.application.payment + +import com.petqua.application.order.dto.PayOrderCommand +import org.springframework.stereotype.Service + +@Service +class PaymentFacadeService( + private val paymentService: PaymentService, + private val paymentGatewayService: PaymentGatewayService, +) { + + fun payOrder(command: PayOrderCommand) { + paymentService.validateAmount(command) + val paymentResponse = paymentGatewayService.confirmPayment(command.toPaymentConfirmRequest()) + paymentService.save(paymentResponse.toPayment()) + } +} diff --git a/src/main/kotlin/com/petqua/application/payment/PaymentGatewayService.kt b/src/main/kotlin/com/petqua/application/payment/PaymentGatewayService.kt new file mode 100644 index 00000000..c246c0b7 --- /dev/null +++ b/src/main/kotlin/com/petqua/application/payment/PaymentGatewayService.kt @@ -0,0 +1,14 @@ +package com.petqua.application.payment + +import com.petqua.application.payment.infra.PaymentGatewayClient +import org.springframework.stereotype.Service + +@Service +class PaymentGatewayService( + private val paymentGatewayClient: PaymentGatewayClient, +) { + + fun confirmPayment(request: PaymentConfirmRequestToPG): PaymentResponseFromPG { + return paymentGatewayClient.confirmPayment(request) + } +} diff --git a/src/main/kotlin/com/petqua/application/payment/PaymentService.kt b/src/main/kotlin/com/petqua/application/payment/PaymentService.kt index 61eb2246..891de3c6 100644 --- a/src/main/kotlin/com/petqua/application/payment/PaymentService.kt +++ b/src/main/kotlin/com/petqua/application/payment/PaymentService.kt @@ -1,9 +1,9 @@ package com.petqua.application.payment import com.petqua.application.order.dto.PayOrderCommand -import com.petqua.application.payment.infra.PaymentGatewayClient import com.petqua.domain.order.OrderRepository import com.petqua.domain.order.findByOrderNumberOrThrow +import com.petqua.domain.payment.tosspayment.TossPayment import com.petqua.domain.payment.tosspayment.TossPaymentRepository import com.petqua.exception.order.OrderException import com.petqua.exception.order.OrderExceptionType @@ -15,17 +15,17 @@ import org.springframework.transaction.annotation.Transactional class PaymentService( private val orderRepository: OrderRepository, private val paymentRepository: TossPaymentRepository, - private val paymentGatewayClient: PaymentGatewayClient, ) { - fun payOrder(command: PayOrderCommand) { + @Transactional(readOnly = true) + fun validateAmount(command: PayOrderCommand) { val order = orderRepository.findByOrderNumberOrThrow(command.orderNumber) { OrderException(OrderExceptionType.ORDER_NOT_FOUND) } - order.validateAmount(command.amount) + } - val paymentResponse = paymentGatewayClient.confirmPayment(command.toPaymentConfirmRequest()) - paymentRepository.save(paymentResponse.toPayment()) + fun save(tossPayment: TossPayment): TossPayment { + return paymentRepository.save(tossPayment) } } diff --git a/src/main/kotlin/com/petqua/presentation/order/dto/OrderDtos.kt b/src/main/kotlin/com/petqua/presentation/order/dto/OrderDtos.kt index b663a31f..05fdf657 100644 --- a/src/main/kotlin/com/petqua/presentation/order/dto/OrderDtos.kt +++ b/src/main/kotlin/com/petqua/presentation/order/dto/OrderDtos.kt @@ -1,7 +1,6 @@ package com.petqua.presentation.order.dto import com.petqua.application.order.dto.OrderProductCommand -import com.petqua.application.order.dto.PayOrderCommand import com.petqua.application.order.dto.SaveOrderCommand import com.petqua.domain.delivery.DeliveryMethod import com.petqua.domain.product.option.Sex @@ -54,20 +53,3 @@ data class OrderProductRequest( ) } } - -data class PayOrderRequest( - val paymentType: String, - val orderId: String, - val paymentKey: String, - val amount: BigDecimal, -) { - - fun toCommand(): PayOrderCommand { - return PayOrderCommand.of( - paymentType = paymentType, - orderId = orderId, - paymentKey = paymentKey, - amount = amount, - ) - } -} diff --git a/src/main/kotlin/com/petqua/presentation/order/PaymentController.kt b/src/main/kotlin/com/petqua/presentation/payment/PaymentController.kt similarity index 82% rename from src/main/kotlin/com/petqua/presentation/order/PaymentController.kt rename to src/main/kotlin/com/petqua/presentation/payment/PaymentController.kt index 4de39256..8ca5515a 100644 --- a/src/main/kotlin/com/petqua/presentation/order/PaymentController.kt +++ b/src/main/kotlin/com/petqua/presentation/payment/PaymentController.kt @@ -1,10 +1,9 @@ -package com.petqua.presentation.order +package com.petqua.presentation.payment -import com.petqua.application.payment.PaymentService +import com.petqua.application.payment.PaymentFacadeService import com.petqua.common.config.ACCESS_TOKEN_SECURITY_SCHEME_KEY import com.petqua.domain.auth.Auth import com.petqua.domain.auth.LoginMember -import com.petqua.presentation.order.dto.PayOrderRequest import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.http.ResponseEntity @@ -18,7 +17,7 @@ import org.springframework.web.bind.annotation.RestController @RequestMapping("/orders/payment") @RestController class PaymentController( - private val paymentService: PaymentService, + private val paymentFacadeService: PaymentFacadeService, ) { @PostMapping("/success") @@ -26,7 +25,7 @@ class PaymentController( @Auth loginMember: LoginMember, @RequestBody request: PayOrderRequest, ): ResponseEntity { - paymentService.payOrder(request.toCommand()) + paymentFacadeService.payOrder(request.toCommand()) return ResponseEntity.ok().build() } diff --git a/src/main/kotlin/com/petqua/presentation/payment/PaymentDtos.kt b/src/main/kotlin/com/petqua/presentation/payment/PaymentDtos.kt new file mode 100644 index 00000000..8caf68c7 --- /dev/null +++ b/src/main/kotlin/com/petqua/presentation/payment/PaymentDtos.kt @@ -0,0 +1,21 @@ +package com.petqua.presentation.payment + +import com.petqua.application.order.dto.PayOrderCommand +import java.math.BigDecimal + +data class PayOrderRequest( + val paymentType: String, + val orderId: String, + val paymentKey: String, + val amount: BigDecimal, +) { + + fun toCommand(): PayOrderCommand { + return PayOrderCommand.of( + paymentType = paymentType, + orderId = orderId, + paymentKey = paymentKey, + amount = amount, + ) + } +} From a6111dcd3c722ba3f053357d20514ec792b34728 Mon Sep 17 00:00:00 2001 From: Combi153 Date: Thu, 29 Feb 2024 14:14:43 +0900 Subject: [PATCH 09/26] =?UTF-8?q?fix:=20=EC=98=88=EC=99=B8=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/petqua/application/payment/infra/TossPaymentClient.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/petqua/application/payment/infra/TossPaymentClient.kt b/src/main/kotlin/com/petqua/application/payment/infra/TossPaymentClient.kt index 197d3473..0c77a3af 100644 --- a/src/main/kotlin/com/petqua/application/payment/infra/TossPaymentClient.kt +++ b/src/main/kotlin/com/petqua/application/payment/infra/TossPaymentClient.kt @@ -10,7 +10,7 @@ import com.petqua.exception.payment.PaymentExceptionType import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.stereotype.Component -import org.springframework.web.client.RestClientResponseException +import org.springframework.web.reactive.function.client.WebClientResponseException @ConfigurationProperties(prefix = "payment.toss-payments") data class PaymentProperties( @@ -31,7 +31,7 @@ class TossPaymentClient( val credentials = BasicAuthUtils.encodeCredentialsWithColon(paymentProperties.secretKey) try { return tossPaymentsApiClient.confirmPayment(credentials, paymentConfirmRequestToPG) - } catch (e: RestClientResponseException) { + } catch (e: WebClientResponseException) { val errorResponse = objectMapper.readValue(e.responseBodyAsString, PaymentErrorResponseFromPG::class.java) throw PaymentException(PaymentExceptionType.from(errorResponse.code)) } From 295d6e22b877204434648fecf6c7346cb8e9e24e Mon Sep 17 00:00:00 2001 From: Combi153 Date: Thu, 29 Feb 2024 16:32:02 +0900 Subject: [PATCH 10/26] =?UTF-8?q?refactor:=20=EA=B2=B0=EC=A0=9C=20?= =?UTF-8?q?=EA=B8=88=EC=95=A1=20=EA=B2=80=EC=A6=9D=20=EC=8B=9C=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=A2=85=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/petqua/application/payment/PaymentService.kt | 6 +++--- src/main/kotlin/com/petqua/domain/order/Order.kt | 4 ++-- .../kotlin/com/petqua/exception/order/OrderExceptionType.kt | 1 + 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/com/petqua/application/payment/PaymentService.kt b/src/main/kotlin/com/petqua/application/payment/PaymentService.kt index 891de3c6..15a2f272 100644 --- a/src/main/kotlin/com/petqua/application/payment/PaymentService.kt +++ b/src/main/kotlin/com/petqua/application/payment/PaymentService.kt @@ -6,7 +6,7 @@ import com.petqua.domain.order.findByOrderNumberOrThrow import com.petqua.domain.payment.tosspayment.TossPayment import com.petqua.domain.payment.tosspayment.TossPaymentRepository import com.petqua.exception.order.OrderException -import com.petqua.exception.order.OrderExceptionType +import com.petqua.exception.order.OrderExceptionType.ORDER_NOT_FOUND import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -20,9 +20,9 @@ class PaymentService( @Transactional(readOnly = true) fun validateAmount(command: PayOrderCommand) { val order = orderRepository.findByOrderNumberOrThrow(command.orderNumber) { - OrderException(OrderExceptionType.ORDER_NOT_FOUND) + OrderException(ORDER_NOT_FOUND) } - order.validateAmount(command.amount) + order.validateAmount(command.amount.setScale(2)) } fun save(tossPayment: TossPayment): TossPayment { diff --git a/src/main/kotlin/com/petqua/domain/order/Order.kt b/src/main/kotlin/com/petqua/domain/order/Order.kt index e8767564..44380493 100644 --- a/src/main/kotlin/com/petqua/domain/order/Order.kt +++ b/src/main/kotlin/com/petqua/domain/order/Order.kt @@ -3,7 +3,7 @@ package com.petqua.domain.order import com.petqua.common.domain.BaseEntity import com.petqua.common.util.throwExceptionWhen import com.petqua.exception.order.OrderException -import com.petqua.exception.order.OrderExceptionType.ORDER_PRICE_NOT_MATCH +import com.petqua.exception.order.OrderExceptionType.PAYMENT_PRICE_NOT_MATCH import jakarta.persistence.AttributeOverride import jakarta.persistence.Column import jakarta.persistence.Embedded @@ -52,7 +52,7 @@ class Order( fun validateAmount(amount: BigDecimal) { throwExceptionWhen(totalAmount != amount) { - throw OrderException(ORDER_PRICE_NOT_MATCH) + throw OrderException(PAYMENT_PRICE_NOT_MATCH) } } } diff --git a/src/main/kotlin/com/petqua/exception/order/OrderExceptionType.kt b/src/main/kotlin/com/petqua/exception/order/OrderExceptionType.kt index 9f4e17a3..a7ebe934 100644 --- a/src/main/kotlin/com/petqua/exception/order/OrderExceptionType.kt +++ b/src/main/kotlin/com/petqua/exception/order/OrderExceptionType.kt @@ -14,6 +14,7 @@ enum class OrderExceptionType( ORDER_NOT_FOUND(BAD_REQUEST, "O11", "존재하지 않는 주문입니다."), ORDER_PRICE_NOT_MATCH(BAD_REQUEST, "O10", "주문한 상품의 가격이 일치하지 않습니다."), + PAYMENT_PRICE_NOT_MATCH(BAD_REQUEST, "P51", "주문한 상품 가격과 결제 금액이 일치하지 않습니다."), INVALID_PAYMENT_TYPE(BAD_REQUEST, "O20", "유효하지 않은 결제 방식입니다."), ; From 65f5734ce82f14e201cf76ad0ee283f87c7f01de Mon Sep 17 00:00:00 2001 From: Combi153 Date: Thu, 29 Feb 2024 16:41:04 +0900 Subject: [PATCH 11/26] =?UTF-8?q?chore:=20rebase=20=EC=B6=A9=EB=8F=8C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../petqua/common/config/ApiClientConfig.kt | 2 +- .../application/auth/AuthFacadeServiceTest.kt | 1 + .../payment/PaymentFacadeServiceTest.kt | 112 ++++++++++++++++++ .../com/petqua/domain/order/OrderTest.kt | 4 +- .../petqua/test/config/ApiClientTestConfig.kt | 2 +- .../petqua/test/fixture/PaymentFixtures.kt | 85 +++++++++++++ 6 files changed, 202 insertions(+), 4 deletions(-) create mode 100644 src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt create mode 100644 src/test/kotlin/com/petqua/test/fixture/PaymentFixtures.kt diff --git a/src/main/kotlin/com/petqua/common/config/ApiClientConfig.kt b/src/main/kotlin/com/petqua/common/config/ApiClientConfig.kt index 88ec4af4..d9452b3b 100644 --- a/src/main/kotlin/com/petqua/common/config/ApiClientConfig.kt +++ b/src/main/kotlin/com/petqua/common/config/ApiClientConfig.kt @@ -11,7 +11,7 @@ import org.springframework.web.service.invoker.HttpServiceProxyFactory class ApiClientConfig { @Bean - fun tossPaymentsApiClient(): TossPaymentsApiClient { + fun TossPaymentsApiClient(): TossPaymentsApiClient { return createApiClient(TossPaymentsApiClient::class.java) } diff --git a/src/test/kotlin/com/petqua/application/auth/AuthFacadeServiceTest.kt b/src/test/kotlin/com/petqua/application/auth/AuthFacadeServiceTest.kt index e2e05d0b..62d1398d 100644 --- a/src/test/kotlin/com/petqua/application/auth/AuthFacadeServiceTest.kt +++ b/src/test/kotlin/com/petqua/application/auth/AuthFacadeServiceTest.kt @@ -25,6 +25,7 @@ import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE import org.springframework.context.annotation.Import import java.lang.System.currentTimeMillis +import java.time.LocalDateTime import java.util.Date @SpringBootTest(webEnvironment = NONE) diff --git a/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt b/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt new file mode 100644 index 00000000..9943cbc0 --- /dev/null +++ b/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt @@ -0,0 +1,112 @@ +package com.petqua.application.payment + +import com.petqua.domain.order.OrderNumber +import com.petqua.domain.order.OrderRepository +import com.petqua.domain.payment.tosspayment.TossPaymentRepository +import com.petqua.exception.order.OrderException +import com.petqua.exception.order.OrderExceptionType.ORDER_NOT_FOUND +import com.petqua.exception.order.OrderExceptionType.PAYMENT_PRICE_NOT_MATCH +import com.petqua.test.DataCleaner +import com.petqua.test.config.ApiClientTestConfig +import com.petqua.test.fake.FakeTossPaymentsApiClient +import com.petqua.test.fixture.order +import com.petqua.test.fixture.payOrderCommand +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.annotation.Import +import java.math.BigDecimal.ONE + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) +@Import(ApiClientTestConfig::class) +class PaymentFacadeServiceTest( + private val paymentFacadeService: PaymentFacadeService, + private val orderRepository: OrderRepository, + private val paymentRepository: TossPaymentRepository, + private val dataCleaner: DataCleaner, + + private val fakeTossPaymentsApiClient: FakeTossPaymentsApiClient, +) : BehaviorSpec({ + + Given("결제를 요청할 때") { + val order = orderRepository.save( + order( + orderNumber = OrderNumber.from("orderNumber"), + totalAmount = ONE + ) + ) + +// When("유효한 요쳥이면") { +// every { +// fakeTossPaymentsApiClient.confirmPayment(any(String::class), any(PaymentConfirmRequestToPG::class)) +// } returns paymentResponseFromPG( +// paymentKey = "paymentKey", +// orderNumber = order.orderNumber, +// totalAmount = order.totalAmount +// ) +// +// Then("PG사에 결제 승인 요청을 보낸다") { +// verify(exactly = 1) { +// fakeTossPaymentsApiClient.confirmPayment(any(String::class), any(PaymentConfirmRequestToPG::class)) +// } +// } +// } + + When("결제 승인에 성공하면") { + paymentFacadeService.payOrder( + command = payOrderCommand( + orderNumber = order.orderNumber, + amount = order.totalAmount, + ) + ) + + Then("Payment 객체를 생성한다") { + val payments = paymentRepository.findAll() + + assertSoftly { + payments.size shouldBe 1 + val payment = payments[0] + + payment.orderNumber shouldBe order.orderNumber + payment.totalAmount shouldBe order.totalAmount.setScale(2) + } + } + } + + When("존재하지 않는 주문이면") { + val orderNumber = OrderNumber.from("wrongOrderNumber") + + Then("예외를 던진다") { + shouldThrow { + paymentFacadeService.payOrder( + command = payOrderCommand( + orderNumber = orderNumber, + amount = order.totalAmount, + ) + ) + }.exceptionType() shouldBe ORDER_NOT_FOUND + } + } + + When("결제 금액이 다르면") { + val amount = order.totalAmount + ONE + + Then("예외를 던진다") { + shouldThrow { + paymentFacadeService.payOrder( + command = payOrderCommand( + orderNumber = order.orderNumber, + amount = amount, + ) + ) + }.exceptionType() shouldBe PAYMENT_PRICE_NOT_MATCH + } + } + } + + afterContainer { + dataCleaner.clean() + } +}) diff --git a/src/test/kotlin/com/petqua/domain/order/OrderTest.kt b/src/test/kotlin/com/petqua/domain/order/OrderTest.kt index 3606b879..7ab93a94 100644 --- a/src/test/kotlin/com/petqua/domain/order/OrderTest.kt +++ b/src/test/kotlin/com/petqua/domain/order/OrderTest.kt @@ -1,7 +1,7 @@ package com.petqua.domain.order import com.petqua.exception.order.OrderException -import com.petqua.exception.order.OrderExceptionType.ORDER_PRICE_NOT_MATCH +import com.petqua.exception.order.OrderExceptionType.PAYMENT_PRICE_NOT_MATCH import com.petqua.test.fixture.order import io.kotest.assertions.throwables.shouldNotThrow import io.kotest.assertions.throwables.shouldThrow @@ -29,6 +29,6 @@ class OrderTest : StringSpec({ shouldThrow { order.validateAmount(ONE) - }.exceptionType() shouldBe ORDER_PRICE_NOT_MATCH + }.exceptionType() shouldBe PAYMENT_PRICE_NOT_MATCH } }) diff --git a/src/test/kotlin/com/petqua/test/config/ApiClientTestConfig.kt b/src/test/kotlin/com/petqua/test/config/ApiClientTestConfig.kt index 8796da79..85786e93 100644 --- a/src/test/kotlin/com/petqua/test/config/ApiClientTestConfig.kt +++ b/src/test/kotlin/com/petqua/test/config/ApiClientTestConfig.kt @@ -1,8 +1,8 @@ package com.petqua.test.config import com.petqua.application.payment.infra.TossPaymentsApiClient -import com.petqua.domain.auth.FakeKakaoOauthApiClient import com.petqua.domain.auth.oauth.kakao.KakaoOauthApiClient +import com.petqua.test.fake.FakeKakaoOauthApiClient import com.petqua.test.fake.FakeTossPaymentsApiClient import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean diff --git a/src/test/kotlin/com/petqua/test/fixture/PaymentFixtures.kt b/src/test/kotlin/com/petqua/test/fixture/PaymentFixtures.kt new file mode 100644 index 00000000..c85f4079 --- /dev/null +++ b/src/test/kotlin/com/petqua/test/fixture/PaymentFixtures.kt @@ -0,0 +1,85 @@ +package com.petqua.test.fixture + +import com.petqua.application.order.dto.PayOrderCommand +import com.petqua.application.payment.CardResponseFromPG +import com.petqua.application.payment.PaymentResponseFromPG +import com.petqua.domain.order.OrderNumber +import com.petqua.domain.payment.tosspayment.TossPaymentMethod.CREDIT_CARD +import com.petqua.domain.payment.tosspayment.TossPaymentStatus.DONE +import com.petqua.domain.payment.tosspayment.TossPaymentType +import com.petqua.domain.payment.tosspayment.TossPaymentType.NORMAL +import java.math.BigDecimal +import java.math.BigDecimal.ONE +import java.math.BigDecimal.ZERO + +fun payOrderCommand( + paymentType: TossPaymentType = NORMAL, + orderNumber: OrderNumber = OrderNumber.from("OrderNumber"), + paymentKey: String = "paymentKey", + amount: BigDecimal = ZERO, +): PayOrderCommand { + return PayOrderCommand( + paymentType = paymentType, + orderNumber = orderNumber, + paymentKey = paymentKey, + amount = amount, + ) +} + +fun paymentResponseFromPG( + paymentKey: String = "paymentKey", + orderNumber: OrderNumber = OrderNumber.from("OrderNumber"), + totalAmount: BigDecimal = ONE, +): PaymentResponseFromPG { + return PaymentResponseFromPG( + version = "version", + paymentKey = paymentKey, + type = NORMAL.name, + orderId = orderNumber.value, + orderName = "orderName", + mid = "mid", + currency = "currency", + method = CREDIT_CARD.name, + totalAmount = totalAmount, + balanceAmount = totalAmount, + status = DONE.name, + requestedAt = "2022-01-01T00:00:00+09:00", + approvedAt = "2022-01-01T00:00:00+09:00", + useEscrow = false, + lastTransactionKey = null, + suppliedAmount = totalAmount, + vat = ZERO, + cultureExpense = false, + taxFreeAmount = ZERO, + taxExemptionAmount = ZERO, + cancels = null, + isPartialCancelable = true, + card = CardResponseFromPG( + amount = totalAmount, + issuerCode = "issuerCode", + acquirerCode = null, + number = "card_number", + installmentPlanMonths = 0, + approveNo = "approveNo", + useCardPoint = false, + cardType = "신용", + ownerType = "개인", + acquireStatus = "READY", + isInterestFree = false, + interestPayer = null, + ), + virtualAccount = null, + secret = null, + mobilePhone = null, + giftCertificate = null, + transfer = null, + receipt = null, + checkout = null, + easyPay = null, + country = "KR", + failure = null, + cashReceipt = null, + cashReceipts = null, + discount = null, + ) +} From e316163faca47d1344e569eb7bc81f92d3877cd5 Mon Sep 17 00:00:00 2001 From: Combi153 Date: Thu, 29 Feb 2024 17:12:08 +0900 Subject: [PATCH 12/26] =?UTF-8?q?build:=20mockk=20=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 참고: https://mockk.io/doc/md/jdk16-access-exceptions.html --- build.gradle.kts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index 1baa4b1b..92663e97 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -106,3 +106,10 @@ tasks.register("copySecret", Copy::class) { include("application*.yml") into("./src/main/resources/") } + +tasks.test { + jvmArgs( + "--add-opens", "java.base/java.time=ALL-UNNAMED", + "--add-opens", "java.base/java.lang.reflect=ALL-UNNAMED" + ) +} From dbe491d32b510fd1c4d272c635be0799a4cfdf55 Mon Sep 17 00:00:00 2001 From: Combi153 Date: Thu, 29 Feb 2024 22:54:12 +0900 Subject: [PATCH 13/26] =?UTF-8?q?refactor:=20config=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../petqua/common/config/ApiClientConfig.kt | 4 +- .../domain/auth/oauth/OauthApiClientConfig.kt | 4 +- .../application/auth/AuthFacadeServiceTest.kt | 3 - .../order/ShippingAddressServiceTest.kt | 3 +- .../payment/PaymentFacadeServiceTest.kt | 69 +++++++++++++------ .../product/WishProductServiceTest.kt | 2 +- .../product/category/CategoryServiceTest.kt | 2 +- .../review/ProductReviewServiceTest.kt | 2 +- .../presentation/auth/AuthControllerTest.kt | 3 - .../petqua/test/config/ApiClientTestConfig.kt | 6 +- 10 files changed, 62 insertions(+), 36 deletions(-) diff --git a/src/main/kotlin/com/petqua/common/config/ApiClientConfig.kt b/src/main/kotlin/com/petqua/common/config/ApiClientConfig.kt index d9452b3b..9613fbfb 100644 --- a/src/main/kotlin/com/petqua/common/config/ApiClientConfig.kt +++ b/src/main/kotlin/com/petqua/common/config/ApiClientConfig.kt @@ -3,15 +3,17 @@ package com.petqua.common.config import com.petqua.application.payment.infra.TossPaymentsApiClient import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Profile import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.reactive.function.client.support.WebClientAdapter import org.springframework.web.service.invoker.HttpServiceProxyFactory @Configuration +@Profile("!test") class ApiClientConfig { @Bean - fun TossPaymentsApiClient(): TossPaymentsApiClient { + fun tossPaymentsApiClient(): TossPaymentsApiClient { return createApiClient(TossPaymentsApiClient::class.java) } diff --git a/src/main/kotlin/com/petqua/domain/auth/oauth/OauthApiClientConfig.kt b/src/main/kotlin/com/petqua/domain/auth/oauth/OauthApiClientConfig.kt index 878f6f89..70158876 100644 --- a/src/main/kotlin/com/petqua/domain/auth/oauth/OauthApiClientConfig.kt +++ b/src/main/kotlin/com/petqua/domain/auth/oauth/OauthApiClientConfig.kt @@ -3,15 +3,17 @@ package com.petqua.domain.auth.oauth import com.petqua.domain.auth.oauth.kakao.KakaoOauthApiClient import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Profile import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.reactive.function.client.support.WebClientAdapter import org.springframework.web.service.invoker.HttpServiceProxyFactory @Configuration +@Profile("!test") class OauthApiClientConfig { @Bean - fun KakaoOauthApiClient(): KakaoOauthApiClient { + fun kakaoOauthApiClient(): KakaoOauthApiClient { return HttpServiceProxyFactory.builderFor( WebClientAdapter.create(WebClient.create()) ).build().createClient(KakaoOauthApiClient::class.java) diff --git a/src/test/kotlin/com/petqua/application/auth/AuthFacadeServiceTest.kt b/src/test/kotlin/com/petqua/application/auth/AuthFacadeServiceTest.kt index 62d1398d..fdc6e1e7 100644 --- a/src/test/kotlin/com/petqua/application/auth/AuthFacadeServiceTest.kt +++ b/src/test/kotlin/com/petqua/application/auth/AuthFacadeServiceTest.kt @@ -13,7 +13,6 @@ import com.petqua.exception.auth.AuthExceptionType import com.petqua.exception.member.MemberException import com.petqua.exception.member.MemberExceptionType.NOT_FOUND_MEMBER import com.petqua.test.DataCleaner -import com.petqua.test.config.ApiClientTestConfig import com.petqua.test.fixture.member import io.kotest.assertions.assertSoftly import io.kotest.assertions.throwables.shouldNotThrow @@ -23,13 +22,11 @@ import io.kotest.matchers.shouldBe import io.mockk.verify import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE -import org.springframework.context.annotation.Import import java.lang.System.currentTimeMillis import java.time.LocalDateTime import java.util.Date @SpringBootTest(webEnvironment = NONE) -@Import(ApiClientTestConfig::class) class AuthServiceTest( private val authFacadeService: AuthFacadeService, private val refreshTokenRepository: RefreshTokenRepository, diff --git a/src/test/kotlin/com/petqua/application/order/ShippingAddressServiceTest.kt b/src/test/kotlin/com/petqua/application/order/ShippingAddressServiceTest.kt index 047cbb29..6dc2dabb 100644 --- a/src/test/kotlin/com/petqua/application/order/ShippingAddressServiceTest.kt +++ b/src/test/kotlin/com/petqua/application/order/ShippingAddressServiceTest.kt @@ -20,9 +20,10 @@ import io.kotest.matchers.nulls.shouldBeNull import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE import kotlin.Long.Companion.MIN_VALUE -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) +@SpringBootTest(webEnvironment = NONE) class ShippingAddressServiceTest( private val shippingAddressService: ShippingAddressService, private val shippingAddressRepository: ShippingAddressRepository, diff --git a/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt b/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt index 9943cbc0..2a6821bc 100644 --- a/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt +++ b/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt @@ -1,33 +1,36 @@ package com.petqua.application.payment +import com.ninjasquad.springmockk.SpykBean +import com.petqua.application.payment.infra.TossPaymentsApiClient import com.petqua.domain.order.OrderNumber import com.petqua.domain.order.OrderRepository import com.petqua.domain.payment.tosspayment.TossPaymentRepository import com.petqua.exception.order.OrderException import com.petqua.exception.order.OrderExceptionType.ORDER_NOT_FOUND import com.petqua.exception.order.OrderExceptionType.PAYMENT_PRICE_NOT_MATCH +import com.petqua.exception.payment.PaymentException +import com.petqua.exception.payment.PaymentExceptionType import com.petqua.test.DataCleaner -import com.petqua.test.config.ApiClientTestConfig -import com.petqua.test.fake.FakeTossPaymentsApiClient import com.petqua.test.fixture.order import com.petqua.test.fixture.payOrderCommand import io.kotest.assertions.assertSoftly import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.verify import org.springframework.boot.test.context.SpringBootTest -import org.springframework.context.annotation.Import +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE +import org.springframework.web.reactive.function.client.WebClientResponseException import java.math.BigDecimal.ONE -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) -@Import(ApiClientTestConfig::class) +@SpringBootTest(webEnvironment = NONE) class PaymentFacadeServiceTest( private val paymentFacadeService: PaymentFacadeService, private val orderRepository: OrderRepository, private val paymentRepository: TossPaymentRepository, private val dataCleaner: DataCleaner, - - private val fakeTossPaymentsApiClient: FakeTossPaymentsApiClient, + @SpykBean private val tossPaymentsApiClient: TossPaymentsApiClient, ) : BehaviorSpec({ Given("결제를 요청할 때") { @@ -38,21 +41,20 @@ class PaymentFacadeServiceTest( ) ) -// When("유효한 요쳥이면") { -// every { -// fakeTossPaymentsApiClient.confirmPayment(any(String::class), any(PaymentConfirmRequestToPG::class)) -// } returns paymentResponseFromPG( -// paymentKey = "paymentKey", -// orderNumber = order.orderNumber, -// totalAmount = order.totalAmount -// ) -// -// Then("PG사에 결제 승인 요청을 보낸다") { -// verify(exactly = 1) { -// fakeTossPaymentsApiClient.confirmPayment(any(String::class), any(PaymentConfirmRequestToPG::class)) -// } -// } -// } + When("유효한 요쳥이면") { + paymentFacadeService.payOrder( + command = payOrderCommand( + orderNumber = order.orderNumber, + amount = order.totalAmount, + ) + ) + + Then("PG사에 결제 승인 요청을 보낸다") { + verify(exactly = 1) { + tossPaymentsApiClient.confirmPayment(any(String::class), any(PaymentConfirmRequestToPG::class)) + } + } + } When("결제 승인에 성공하면") { paymentFacadeService.payOrder( @@ -104,6 +106,29 @@ class PaymentFacadeServiceTest( }.exceptionType() shouldBe PAYMENT_PRICE_NOT_MATCH } } + + When("PG사와 통신할 때 예외가 발생하면") { + every { + tossPaymentsApiClient.confirmPayment(any(String::class), any(PaymentConfirmRequestToPG::class)) + } throws WebClientResponseException( + 404, + "UNAUTHORIZED", + null, + "{\"code\":\"UNAUTHORIZED_KEY\",\"message\":\"인증되지 않은 시크릿 키 혹은 클라이언트 키 입니다.\",\"data\":null}".toByteArray(), + null + ) + + Then("예외를 던진다") { + shouldThrow { + paymentFacadeService.payOrder( + command = payOrderCommand( + orderNumber = order.orderNumber, + amount = order.totalAmount, + ) + ) + }.exceptionType() shouldBe PaymentExceptionType.UNAUTHORIZED_KEY + } + } } afterContainer { diff --git a/src/test/kotlin/com/petqua/application/product/WishProductServiceTest.kt b/src/test/kotlin/com/petqua/application/product/WishProductServiceTest.kt index 19560022..4a00bbe5 100644 --- a/src/test/kotlin/com/petqua/application/product/WishProductServiceTest.kt +++ b/src/test/kotlin/com/petqua/application/product/WishProductServiceTest.kt @@ -168,7 +168,7 @@ class WishProductServiceTest( private fun saveProducts( productRepository: ProductRepository, - store: Store + store: Store, ): Triple { val product1 = productRepository.save( product( diff --git a/src/test/kotlin/com/petqua/application/product/category/CategoryServiceTest.kt b/src/test/kotlin/com/petqua/application/product/category/CategoryServiceTest.kt index 29a4e67f..4865f57e 100644 --- a/src/test/kotlin/com/petqua/application/product/category/CategoryServiceTest.kt +++ b/src/test/kotlin/com/petqua/application/product/category/CategoryServiceTest.kt @@ -23,9 +23,9 @@ import io.kotest.assertions.assertSoftly import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.collections.shouldContainExactly import io.kotest.matchers.shouldBe -import java.math.BigDecimal import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE +import java.math.BigDecimal @SpringBootTest(webEnvironment = NONE) class CategoryServiceTest( diff --git a/src/test/kotlin/com/petqua/application/product/review/ProductReviewServiceTest.kt b/src/test/kotlin/com/petqua/application/product/review/ProductReviewServiceTest.kt index bd8e273f..fd4412d6 100644 --- a/src/test/kotlin/com/petqua/application/product/review/ProductReviewServiceTest.kt +++ b/src/test/kotlin/com/petqua/application/product/review/ProductReviewServiceTest.kt @@ -29,9 +29,9 @@ import io.kotest.matchers.collections.shouldBeSortedWith import io.kotest.matchers.ints.shouldBeGreaterThanOrEqual import io.kotest.matchers.nulls.shouldBeNull import io.kotest.matchers.shouldBe -import java.math.BigDecimal import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE +import java.math.BigDecimal @SpringBootTest(webEnvironment = NONE) class ProductReviewServiceTest( diff --git a/src/test/kotlin/com/petqua/presentation/auth/AuthControllerTest.kt b/src/test/kotlin/com/petqua/presentation/auth/AuthControllerTest.kt index 48ab35d6..48788064 100644 --- a/src/test/kotlin/com/petqua/presentation/auth/AuthControllerTest.kt +++ b/src/test/kotlin/com/petqua/presentation/auth/AuthControllerTest.kt @@ -9,20 +9,17 @@ import com.petqua.domain.auth.token.RefreshToken import com.petqua.domain.auth.token.RefreshTokenRepository import com.petqua.domain.member.MemberRepository import com.petqua.test.ApiTestConfig -import com.petqua.test.config.ApiClientTestConfig import com.petqua.test.fixture.member import io.kotest.assertions.assertSoftly import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.mockk.verify -import org.springframework.context.annotation.Import import org.springframework.http.HttpHeaders.AUTHORIZATION import org.springframework.http.HttpHeaders.SET_COOKIE import org.springframework.http.HttpStatus.NO_CONTENT import org.springframework.http.HttpStatus.OK import java.util.Date -@Import(ApiClientTestConfig::class) class AuthControllerTest( private val memberRepository: MemberRepository, private val refreshTokenRepository: RefreshTokenRepository, diff --git a/src/test/kotlin/com/petqua/test/config/ApiClientTestConfig.kt b/src/test/kotlin/com/petqua/test/config/ApiClientTestConfig.kt index 85786e93..94f53bed 100644 --- a/src/test/kotlin/com/petqua/test/config/ApiClientTestConfig.kt +++ b/src/test/kotlin/com/petqua/test/config/ApiClientTestConfig.kt @@ -4,10 +4,12 @@ import com.petqua.application.payment.infra.TossPaymentsApiClient import com.petqua.domain.auth.oauth.kakao.KakaoOauthApiClient import com.petqua.test.fake.FakeKakaoOauthApiClient import com.petqua.test.fake.FakeTossPaymentsApiClient -import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Profile -@TestConfiguration +@Configuration +@Profile("test") class ApiClientTestConfig { @Bean From 7253ed137356adf9f727737cd6eb8dd4d14c5496 Mon Sep 17 00:00:00 2001 From: Combi153 Date: Thu, 29 Feb 2024 23:33:18 +0900 Subject: [PATCH 14/26] =?UTF-8?q?test:=20PaymentController=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/payment/PaymentController.kt | 2 +- .../payment/PaymentFacadeServiceTest.kt | 8 +- .../payment/PaymentControllerSteps.kt | 26 ++++ .../payment/PaymentControllerTest.kt | 143 ++++++++++++++++++ .../petqua/test/fixture/PaymentFixtures.kt | 67 ++------ 5 files changed, 184 insertions(+), 62 deletions(-) create mode 100644 src/test/kotlin/com/petqua/presentation/payment/PaymentControllerSteps.kt create mode 100644 src/test/kotlin/com/petqua/presentation/payment/PaymentControllerTest.kt diff --git a/src/main/kotlin/com/petqua/presentation/payment/PaymentController.kt b/src/main/kotlin/com/petqua/presentation/payment/PaymentController.kt index 8ca5515a..607a4c17 100644 --- a/src/main/kotlin/com/petqua/presentation/payment/PaymentController.kt +++ b/src/main/kotlin/com/petqua/presentation/payment/PaymentController.kt @@ -26,7 +26,7 @@ class PaymentController( @RequestBody request: PayOrderRequest, ): ResponseEntity { paymentFacadeService.payOrder(request.toCommand()) - return ResponseEntity.ok().build() + return ResponseEntity.noContent().build() } @PostMapping("/fail") diff --git a/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt b/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt index 2a6821bc..2cff4ce2 100644 --- a/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt +++ b/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt @@ -9,7 +9,7 @@ import com.petqua.exception.order.OrderException import com.petqua.exception.order.OrderExceptionType.ORDER_NOT_FOUND import com.petqua.exception.order.OrderExceptionType.PAYMENT_PRICE_NOT_MATCH import com.petqua.exception.payment.PaymentException -import com.petqua.exception.payment.PaymentExceptionType +import com.petqua.exception.payment.PaymentExceptionType.UNAUTHORIZED_KEY import com.petqua.test.DataCleaner import com.petqua.test.fixture.order import com.petqua.test.fixture.payOrderCommand @@ -33,7 +33,7 @@ class PaymentFacadeServiceTest( @SpykBean private val tossPaymentsApiClient: TossPaymentsApiClient, ) : BehaviorSpec({ - Given("결제를 요청할 때") { + Given("주문을 결제할 때") { val order = orderRepository.save( order( orderNumber = OrderNumber.from("orderNumber"), @@ -92,7 +92,7 @@ class PaymentFacadeServiceTest( } } - When("결제 금액이 다르면") { + When("주문의 총가격과 결제 금액이 다르면") { val amount = order.totalAmount + ONE Then("예외를 던진다") { @@ -126,7 +126,7 @@ class PaymentFacadeServiceTest( amount = order.totalAmount, ) ) - }.exceptionType() shouldBe PaymentExceptionType.UNAUTHORIZED_KEY + }.exceptionType() shouldBe UNAUTHORIZED_KEY } } } diff --git a/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerSteps.kt b/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerSteps.kt new file mode 100644 index 00000000..96a1f296 --- /dev/null +++ b/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerSteps.kt @@ -0,0 +1,26 @@ +package com.petqua.presentation.payment + +import io.restassured.module.kotlin.extensions.Extract +import io.restassured.module.kotlin.extensions.Given +import io.restassured.module.kotlin.extensions.Then +import io.restassured.module.kotlin.extensions.When +import io.restassured.response.Response +import org.springframework.http.MediaType.APPLICATION_JSON_VALUE + +fun requestPayOrder( + accessToken: String, + payOrderRequest: PayOrderRequest, +): Response { + return Given { + log().all() + auth().preemptive().oauth2(accessToken) + contentType(APPLICATION_JSON_VALUE) + body(payOrderRequest) + } When { + post("/orders/payment/success") + } Then { + log().all() + } Extract { + response() + } +} diff --git a/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerTest.kt b/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerTest.kt new file mode 100644 index 00000000..5e053005 --- /dev/null +++ b/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerTest.kt @@ -0,0 +1,143 @@ +package com.petqua.presentation.payment + +import com.ninjasquad.springmockk.SpykBean +import com.petqua.application.payment.PaymentConfirmRequestToPG +import com.petqua.application.payment.infra.TossPaymentsApiClient +import com.petqua.common.exception.ExceptionResponse +import com.petqua.domain.order.OrderNumber +import com.petqua.domain.order.OrderRepository +import com.petqua.domain.payment.tosspayment.TossPaymentRepository +import com.petqua.exception.order.OrderExceptionType.ORDER_NOT_FOUND +import com.petqua.exception.order.OrderExceptionType.PAYMENT_PRICE_NOT_MATCH +import com.petqua.exception.payment.PaymentExceptionType.UNAUTHORIZED_KEY +import com.petqua.test.ApiTestConfig +import com.petqua.test.fixture.order +import com.petqua.test.fixture.payOrderRequest +import io.kotest.assertions.assertSoftly +import io.kotest.matchers.shouldBe +import io.mockk.every +import org.springframework.http.HttpStatus.BAD_REQUEST +import org.springframework.http.HttpStatus.NO_CONTENT +import org.springframework.http.HttpStatus.UNAUTHORIZED +import org.springframework.web.reactive.function.client.WebClientResponseException +import java.math.BigDecimal.ONE + +class PaymentControllerTest( + private val orderRepository: OrderRepository, + private val paymentRepository: TossPaymentRepository, + @SpykBean private val tossPaymentsApiClient: TossPaymentsApiClient, +) : ApiTestConfig() { + + init { + + Given("주문을 결제할 때") { + val accessToken = signInAsMember().accessToken + val memberId = getMemberIdByAccessToken(accessToken) + + val order = orderRepository.save( + order( + memberId = memberId, + orderNumber = OrderNumber.from("orderNumber"), + totalAmount = ONE + ) + ) + + When("유효한 요청이면") { + val response = requestPayOrder( + accessToken = accessToken, + payOrderRequest = payOrderRequest( + orderId = order.orderNumber.value, + amount = order.totalAmount + ) + ) + + Then("NO CONTENT 를 응답한다") { + response.statusCode shouldBe NO_CONTENT.value() + } + + Then("Payment 객체를 저장한다") { + val payments = paymentRepository.findAll() + + assertSoftly { + payments.size shouldBe 1 + val payment = payments[0] + + payment.orderNumber shouldBe order.orderNumber + payment.totalAmount shouldBe order.totalAmount.setScale(2) + } + } + } + + When("존재하지 않는 주문이면") { + val orderNumber = "wrongOrderNumber" + + val response = requestPayOrder( + accessToken = accessToken, + payOrderRequest = payOrderRequest( + orderId = orderNumber, + amount = order.totalAmount + ) + ) + + Then("예외를 응답한다") { + val errorResponse = response.`as`(ExceptionResponse::class.java) + + assertSoftly(response) { + statusCode shouldBe BAD_REQUEST.value() + errorResponse.message shouldBe ORDER_NOT_FOUND.errorMessage() + } + } + } + + When("주문의 총가격과 결제 금액이 다르면") { + val wrongAmount = order.totalAmount + ONE + + val response = requestPayOrder( + accessToken = accessToken, + payOrderRequest = payOrderRequest( + orderId = order.orderNumber.value, + amount = wrongAmount + ) + ) + + Then("예외를 응답한다") { + val errorResponse = response.`as`(ExceptionResponse::class.java) + + assertSoftly(response) { + statusCode shouldBe BAD_REQUEST.value() + errorResponse.message shouldBe PAYMENT_PRICE_NOT_MATCH.errorMessage() + } + } + } + + When("PG사와 통신할 때 예외가 발생하면") { + every { + tossPaymentsApiClient.confirmPayment(any(String::class), any(PaymentConfirmRequestToPG::class)) + } throws WebClientResponseException( + 404, + "UNAUTHORIZED", + null, + "{\"code\":\"UNAUTHORIZED_KEY\",\"message\":\"인증되지 않은 시크릿 키 혹은 클라이언트 키 입니다.\",\"data\":null}".toByteArray(), + null + ) + + val response = requestPayOrder( + accessToken = accessToken, + payOrderRequest = payOrderRequest( + orderId = order.orderNumber.value, + amount = order.totalAmount + ) + ) + + Then("예외를 응답한다") { + val errorResponse = response.`as`(ExceptionResponse::class.java) + + assertSoftly(response) { + statusCode shouldBe UNAUTHORIZED.value() + errorResponse.message shouldBe UNAUTHORIZED_KEY.errorMessage() + } + } + } + } + } +} diff --git a/src/test/kotlin/com/petqua/test/fixture/PaymentFixtures.kt b/src/test/kotlin/com/petqua/test/fixture/PaymentFixtures.kt index c85f4079..79c518b0 100644 --- a/src/test/kotlin/com/petqua/test/fixture/PaymentFixtures.kt +++ b/src/test/kotlin/com/petqua/test/fixture/PaymentFixtures.kt @@ -1,13 +1,10 @@ package com.petqua.test.fixture import com.petqua.application.order.dto.PayOrderCommand -import com.petqua.application.payment.CardResponseFromPG -import com.petqua.application.payment.PaymentResponseFromPG import com.petqua.domain.order.OrderNumber -import com.petqua.domain.payment.tosspayment.TossPaymentMethod.CREDIT_CARD -import com.petqua.domain.payment.tosspayment.TossPaymentStatus.DONE import com.petqua.domain.payment.tosspayment.TossPaymentType import com.petqua.domain.payment.tosspayment.TossPaymentType.NORMAL +import com.petqua.presentation.payment.PayOrderRequest import java.math.BigDecimal import java.math.BigDecimal.ONE import java.math.BigDecimal.ZERO @@ -26,60 +23,16 @@ fun payOrderCommand( ) } -fun paymentResponseFromPG( +fun payOrderRequest( + paymentType: String = NORMAL.name, + orderId: String = "orderId", paymentKey: String = "paymentKey", - orderNumber: OrderNumber = OrderNumber.from("OrderNumber"), - totalAmount: BigDecimal = ONE, -): PaymentResponseFromPG { - return PaymentResponseFromPG( - version = "version", + amount: BigDecimal = ONE, +): PayOrderRequest { + return PayOrderRequest( + paymentType = paymentType, + orderId = orderId, paymentKey = paymentKey, - type = NORMAL.name, - orderId = orderNumber.value, - orderName = "orderName", - mid = "mid", - currency = "currency", - method = CREDIT_CARD.name, - totalAmount = totalAmount, - balanceAmount = totalAmount, - status = DONE.name, - requestedAt = "2022-01-01T00:00:00+09:00", - approvedAt = "2022-01-01T00:00:00+09:00", - useEscrow = false, - lastTransactionKey = null, - suppliedAmount = totalAmount, - vat = ZERO, - cultureExpense = false, - taxFreeAmount = ZERO, - taxExemptionAmount = ZERO, - cancels = null, - isPartialCancelable = true, - card = CardResponseFromPG( - amount = totalAmount, - issuerCode = "issuerCode", - acquirerCode = null, - number = "card_number", - installmentPlanMonths = 0, - approveNo = "approveNo", - useCardPoint = false, - cardType = "신용", - ownerType = "개인", - acquireStatus = "READY", - isInterestFree = false, - interestPayer = null, - ), - virtualAccount = null, - secret = null, - mobilePhone = null, - giftCertificate = null, - transfer = null, - receipt = null, - checkout = null, - easyPay = null, - country = "KR", - failure = null, - cashReceipt = null, - cashReceipts = null, - discount = null, + amount = amount, ) } From 4bb8b7bb48bfee705bcb373294c3baf32d0af9da Mon Sep 17 00:00:00 2001 From: Combi153 Date: Thu, 29 Feb 2024 23:49:06 +0900 Subject: [PATCH 15/26] =?UTF-8?q?refactor:=20Order=20=EA=B2=B0=EC=A0=9C=20?= =?UTF-8?q?=EC=8B=9C=20=EA=B6=8C=ED=95=9C=20=EA=B2=80=EC=A6=9D=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../petqua/application/order/dto/OrderDtos.kt | 3 +++ .../application/payment/PaymentService.kt | 1 + .../kotlin/com/petqua/domain/order/Order.kt | 7 +++++ .../exception/order/OrderExceptionType.kt | 5 +++- .../presentation/payment/PaymentController.kt | 2 +- .../presentation/payment/PaymentDtos.kt | 3 ++- .../payment/PaymentFacadeServiceTest.kt | 27 +++++++++++++++++++ .../com/petqua/domain/order/OrderTest.kt | 22 +++++++++++++++ .../payment/PaymentControllerTest.kt | 23 ++++++++++++++++ .../petqua/test/fixture/PaymentFixtures.kt | 2 ++ 10 files changed, 92 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/com/petqua/application/order/dto/OrderDtos.kt b/src/main/kotlin/com/petqua/application/order/dto/OrderDtos.kt index 88b5a41f..bccd3e9c 100644 --- a/src/main/kotlin/com/petqua/application/order/dto/OrderDtos.kt +++ b/src/main/kotlin/com/petqua/application/order/dto/OrderDtos.kt @@ -73,6 +73,7 @@ data class SaveOrderResponse( ) data class PayOrderCommand( + val memberId: Long, val paymentType: TossPaymentType, val orderNumber: OrderNumber, val paymentKey: String, @@ -88,12 +89,14 @@ data class PayOrderCommand( companion object { fun of( + memberId: Long, paymentType: String, orderId: String, paymentKey: String, amount: BigDecimal, ): PayOrderCommand { return PayOrderCommand( + memberId = memberId, paymentType = TossPaymentType.from(paymentType), orderNumber = OrderNumber.from(orderId), paymentKey = paymentKey, diff --git a/src/main/kotlin/com/petqua/application/payment/PaymentService.kt b/src/main/kotlin/com/petqua/application/payment/PaymentService.kt index 15a2f272..dc0ba762 100644 --- a/src/main/kotlin/com/petqua/application/payment/PaymentService.kt +++ b/src/main/kotlin/com/petqua/application/payment/PaymentService.kt @@ -22,6 +22,7 @@ class PaymentService( val order = orderRepository.findByOrderNumberOrThrow(command.orderNumber) { OrderException(ORDER_NOT_FOUND) } + order.validateOwner(command.memberId) order.validateAmount(command.amount.setScale(2)) } diff --git a/src/main/kotlin/com/petqua/domain/order/Order.kt b/src/main/kotlin/com/petqua/domain/order/Order.kt index 44380493..26080256 100644 --- a/src/main/kotlin/com/petqua/domain/order/Order.kt +++ b/src/main/kotlin/com/petqua/domain/order/Order.kt @@ -3,6 +3,7 @@ package com.petqua.domain.order import com.petqua.common.domain.BaseEntity import com.petqua.common.util.throwExceptionWhen import com.petqua.exception.order.OrderException +import com.petqua.exception.order.OrderExceptionType.FORBIDDEN_ORDER import com.petqua.exception.order.OrderExceptionType.PAYMENT_PRICE_NOT_MATCH import jakarta.persistence.AttributeOverride import jakarta.persistence.Column @@ -55,4 +56,10 @@ class Order( throw OrderException(PAYMENT_PRICE_NOT_MATCH) } } + + fun validateOwner(memberId: Long) { + throwExceptionWhen(this.memberId != memberId) { + throw OrderException(FORBIDDEN_ORDER) + } + } } diff --git a/src/main/kotlin/com/petqua/exception/order/OrderExceptionType.kt b/src/main/kotlin/com/petqua/exception/order/OrderExceptionType.kt index a7ebe934..5d53a10a 100644 --- a/src/main/kotlin/com/petqua/exception/order/OrderExceptionType.kt +++ b/src/main/kotlin/com/petqua/exception/order/OrderExceptionType.kt @@ -3,6 +3,7 @@ package com.petqua.exception.order import com.petqua.common.exception.BaseExceptionType import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus.BAD_REQUEST +import org.springframework.http.HttpStatus.FORBIDDEN enum class OrderExceptionType( private val httpStatus: HttpStatus, @@ -14,9 +15,11 @@ enum class OrderExceptionType( ORDER_NOT_FOUND(BAD_REQUEST, "O11", "존재하지 않는 주문입니다."), ORDER_PRICE_NOT_MATCH(BAD_REQUEST, "O10", "주문한 상품의 가격이 일치하지 않습니다."), - PAYMENT_PRICE_NOT_MATCH(BAD_REQUEST, "P51", "주문한 상품 가격과 결제 금액이 일치하지 않습니다."), + PAYMENT_PRICE_NOT_MATCH(BAD_REQUEST, "O12", "주문한 상품 가격과 결제 금액이 일치하지 않습니다."), INVALID_PAYMENT_TYPE(BAD_REQUEST, "O20", "유효하지 않은 결제 방식입니다."), + + FORBIDDEN_ORDER(FORBIDDEN, "O30", "해당 주문에 대한 권한이 없습니다."), ; override fun httpStatus(): HttpStatus { diff --git a/src/main/kotlin/com/petqua/presentation/payment/PaymentController.kt b/src/main/kotlin/com/petqua/presentation/payment/PaymentController.kt index 607a4c17..01485cab 100644 --- a/src/main/kotlin/com/petqua/presentation/payment/PaymentController.kt +++ b/src/main/kotlin/com/petqua/presentation/payment/PaymentController.kt @@ -25,7 +25,7 @@ class PaymentController( @Auth loginMember: LoginMember, @RequestBody request: PayOrderRequest, ): ResponseEntity { - paymentFacadeService.payOrder(request.toCommand()) + paymentFacadeService.payOrder(request.toCommand(loginMember.memberId)) return ResponseEntity.noContent().build() } diff --git a/src/main/kotlin/com/petqua/presentation/payment/PaymentDtos.kt b/src/main/kotlin/com/petqua/presentation/payment/PaymentDtos.kt index 8caf68c7..6622429f 100644 --- a/src/main/kotlin/com/petqua/presentation/payment/PaymentDtos.kt +++ b/src/main/kotlin/com/petqua/presentation/payment/PaymentDtos.kt @@ -10,8 +10,9 @@ data class PayOrderRequest( val amount: BigDecimal, ) { - fun toCommand(): PayOrderCommand { + fun toCommand(memberId: Long): PayOrderCommand { return PayOrderCommand.of( + memberId = memberId, paymentType = paymentType, orderId = orderId, paymentKey = paymentKey, diff --git a/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt b/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt index 2cff4ce2..8d42f4e1 100644 --- a/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt +++ b/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt @@ -2,15 +2,18 @@ package com.petqua.application.payment import com.ninjasquad.springmockk.SpykBean import com.petqua.application.payment.infra.TossPaymentsApiClient +import com.petqua.domain.member.MemberRepository import com.petqua.domain.order.OrderNumber import com.petqua.domain.order.OrderRepository import com.petqua.domain.payment.tosspayment.TossPaymentRepository import com.petqua.exception.order.OrderException +import com.petqua.exception.order.OrderExceptionType import com.petqua.exception.order.OrderExceptionType.ORDER_NOT_FOUND import com.petqua.exception.order.OrderExceptionType.PAYMENT_PRICE_NOT_MATCH import com.petqua.exception.payment.PaymentException import com.petqua.exception.payment.PaymentExceptionType.UNAUTHORIZED_KEY import com.petqua.test.DataCleaner +import com.petqua.test.fixture.member import com.petqua.test.fixture.order import com.petqua.test.fixture.payOrderCommand import io.kotest.assertions.assertSoftly @@ -27,6 +30,7 @@ import java.math.BigDecimal.ONE @SpringBootTest(webEnvironment = NONE) class PaymentFacadeServiceTest( private val paymentFacadeService: PaymentFacadeService, + private val memberRepository: MemberRepository, private val orderRepository: OrderRepository, private val paymentRepository: TossPaymentRepository, private val dataCleaner: DataCleaner, @@ -34,8 +38,10 @@ class PaymentFacadeServiceTest( ) : BehaviorSpec({ Given("주문을 결제할 때") { + val member = memberRepository.save(member()) val order = orderRepository.save( order( + memberId = member.id, orderNumber = OrderNumber.from("orderNumber"), totalAmount = ONE ) @@ -44,6 +50,7 @@ class PaymentFacadeServiceTest( When("유효한 요쳥이면") { paymentFacadeService.payOrder( command = payOrderCommand( + memberId = order.memberId, orderNumber = order.orderNumber, amount = order.totalAmount, ) @@ -59,6 +66,7 @@ class PaymentFacadeServiceTest( When("결제 승인에 성공하면") { paymentFacadeService.payOrder( command = payOrderCommand( + memberId = order.memberId, orderNumber = order.orderNumber, amount = order.totalAmount, ) @@ -84,6 +92,7 @@ class PaymentFacadeServiceTest( shouldThrow { paymentFacadeService.payOrder( command = payOrderCommand( + memberId = order.memberId, orderNumber = orderNumber, amount = order.totalAmount, ) @@ -92,6 +101,22 @@ class PaymentFacadeServiceTest( } } + When("권한이 없는 회원이면") { + val memberId = Long.MIN_VALUE + + Then("예외를 던진다") { + shouldThrow { + paymentFacadeService.payOrder( + command = payOrderCommand( + memberId = memberId, + orderNumber = order.orderNumber, + amount = order.totalAmount, + ) + ) + }.exceptionType() shouldBe OrderExceptionType.FORBIDDEN_ORDER + } + } + When("주문의 총가격과 결제 금액이 다르면") { val amount = order.totalAmount + ONE @@ -99,6 +124,7 @@ class PaymentFacadeServiceTest( shouldThrow { paymentFacadeService.payOrder( command = payOrderCommand( + memberId = order.memberId, orderNumber = order.orderNumber, amount = amount, ) @@ -122,6 +148,7 @@ class PaymentFacadeServiceTest( shouldThrow { paymentFacadeService.payOrder( command = payOrderCommand( + memberId = order.memberId, orderNumber = order.orderNumber, amount = order.totalAmount, ) diff --git a/src/test/kotlin/com/petqua/domain/order/OrderTest.kt b/src/test/kotlin/com/petqua/domain/order/OrderTest.kt index 7ab93a94..f26161b1 100644 --- a/src/test/kotlin/com/petqua/domain/order/OrderTest.kt +++ b/src/test/kotlin/com/petqua/domain/order/OrderTest.kt @@ -1,6 +1,7 @@ package com.petqua.domain.order import com.petqua.exception.order.OrderException +import com.petqua.exception.order.OrderExceptionType.FORBIDDEN_ORDER import com.petqua.exception.order.OrderExceptionType.PAYMENT_PRICE_NOT_MATCH import com.petqua.test.fixture.order import io.kotest.assertions.throwables.shouldNotThrow @@ -9,6 +10,7 @@ import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.shouldBe import java.math.BigDecimal.ONE import java.math.BigDecimal.TEN +import kotlin.Long.Companion.MIN_VALUE class OrderTest : StringSpec({ @@ -31,4 +33,24 @@ class OrderTest : StringSpec({ order.validateAmount(ONE) }.exceptionType() shouldBe PAYMENT_PRICE_NOT_MATCH } + + "소유자를 검증한다" { + val order = order( + memberId = 1L + ) + + shouldNotThrow { + order.validateOwner(1L) + } + } + + "소유자를 검증할 때 회원 Id가 다르다면 예외를 던진다" { + val order = order( + memberId = 1L + ) + + shouldThrow { + order.validateOwner(MIN_VALUE) + }.exceptionType() shouldBe FORBIDDEN_ORDER + } }) diff --git a/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerTest.kt b/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerTest.kt index 5e053005..c2093cd5 100644 --- a/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerTest.kt +++ b/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerTest.kt @@ -7,6 +7,7 @@ import com.petqua.common.exception.ExceptionResponse import com.petqua.domain.order.OrderNumber import com.petqua.domain.order.OrderRepository import com.petqua.domain.payment.tosspayment.TossPaymentRepository +import com.petqua.exception.order.OrderExceptionType.FORBIDDEN_ORDER import com.petqua.exception.order.OrderExceptionType.ORDER_NOT_FOUND import com.petqua.exception.order.OrderExceptionType.PAYMENT_PRICE_NOT_MATCH import com.petqua.exception.payment.PaymentExceptionType.UNAUTHORIZED_KEY @@ -17,6 +18,7 @@ import io.kotest.assertions.assertSoftly import io.kotest.matchers.shouldBe import io.mockk.every import org.springframework.http.HttpStatus.BAD_REQUEST +import org.springframework.http.HttpStatus.FORBIDDEN import org.springframework.http.HttpStatus.NO_CONTENT import org.springframework.http.HttpStatus.UNAUTHORIZED import org.springframework.web.reactive.function.client.WebClientResponseException @@ -89,6 +91,27 @@ class PaymentControllerTest( } } + When("권한이 없는 회원의 요청이면") { + val otherAccessToken = signInAsMember().accessToken + + val response = requestPayOrder( + accessToken = otherAccessToken, + payOrderRequest = payOrderRequest( + orderId = order.orderNumber.value, + amount = order.totalAmount + ) + ) + + Then("예외를 응답한다") { + val errorResponse = response.`as`(ExceptionResponse::class.java) + + assertSoftly(response) { + statusCode shouldBe FORBIDDEN.value() + errorResponse.message shouldBe FORBIDDEN_ORDER.errorMessage() + } + } + } + When("주문의 총가격과 결제 금액이 다르면") { val wrongAmount = order.totalAmount + ONE diff --git a/src/test/kotlin/com/petqua/test/fixture/PaymentFixtures.kt b/src/test/kotlin/com/petqua/test/fixture/PaymentFixtures.kt index 79c518b0..bf7c5480 100644 --- a/src/test/kotlin/com/petqua/test/fixture/PaymentFixtures.kt +++ b/src/test/kotlin/com/petqua/test/fixture/PaymentFixtures.kt @@ -10,12 +10,14 @@ import java.math.BigDecimal.ONE import java.math.BigDecimal.ZERO fun payOrderCommand( + memberId: Long = 0L, paymentType: TossPaymentType = NORMAL, orderNumber: OrderNumber = OrderNumber.from("OrderNumber"), paymentKey: String = "paymentKey", amount: BigDecimal = ZERO, ): PayOrderCommand { return PayOrderCommand( + memberId = memberId, paymentType = paymentType, orderNumber = orderNumber, paymentKey = paymentKey, From e6492e90056639e043943f77e134d8745ac63fce Mon Sep 17 00:00:00 2001 From: Combi153 Date: Thu, 29 Feb 2024 23:50:44 +0900 Subject: [PATCH 16/26] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=BD=94=EB=93=9C=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../petqua/application/order/dto/OrderDtos.kt | 131 ------------------ .../product/WishProductServiceTest.kt | 2 +- 2 files changed, 1 insertion(+), 132 deletions(-) diff --git a/src/main/kotlin/com/petqua/application/order/dto/OrderDtos.kt b/src/main/kotlin/com/petqua/application/order/dto/OrderDtos.kt index bccd3e9c..bf015ac8 100644 --- a/src/main/kotlin/com/petqua/application/order/dto/OrderDtos.kt +++ b/src/main/kotlin/com/petqua/application/order/dto/OrderDtos.kt @@ -9,7 +9,6 @@ import com.petqua.domain.payment.tosspayment.TossPaymentType import com.petqua.domain.product.Product import com.petqua.domain.product.option.ProductOption import com.petqua.domain.product.option.Sex -import io.swagger.v3.oas.annotations.media.Schema import java.math.BigDecimal data class SaveOrderCommand( @@ -105,133 +104,3 @@ data class PayOrderCommand( } } } - -data class PayOrderResponse( - - val memberNickname: String, - - - @Schema( - description = "상품 id", - example = "1" - ) - val productId: Long, - - @Schema( - description = "상품 이름", - example = "알비노 풀레드 아시안 고정구피" - ) - val productName: String, - - @Schema( - description = "상품 판매점", - example = "S아쿠아" - ) - val storeName: String, - - @Schema( - description = "상품 썸네일 이미지", - example = "https://docs.petqua.co.kr/products/thumbnails/thumbnail1.jpeg" - ) - val thumbnailUrl: String, - - @Schema( - description = "할인 가격(판매 가격)", - example = "21000" - ) - val discountPrice: Int, - - val quantity: Int, - - @Schema( - description = "배송비", - example = "5000" - ) - val deliveryFee: BigDecimal?, -) - -data class OrderResponse( - @Schema( - description = "주문 번호", - example = "202402211607026029E90DB030" - ) - val orderNumber: String, - - @Schema( - description = "주문자", - example = "홍길동" - ) - val orderer: String, - - val shippingAddress: ShippigAddressResponse, - - @Schema( - description = "배송방법", - example = "안전배송" - ) - val deliveryMethod: String, - - @Schema( - description = "배송비", - example = "3000" - ) - val deliveryFee: Int, - - @Schema( - description = "주문자", - example = "홍길동" - ) - val paymentMethod: String, - - // (...) -) - -data class ShippigAddressResponse( - @Schema( - description = "배송지 id", - example = "1" - ) - val shippingAddressId: Long, - - @Schema( - description = "배송지 이름", - example = "집" - ) - val name: String, - - @Schema( - description = "받는 사람", - example = "홍길동" - ) - val receiver: String, - - @Schema( - description = "전화 번호", - example = "010-1234-1234" - ) - val phoneNumber: String, - - @Schema( - description = "우편 번호", - example = "12345" - ) - val zipCode: Int, - - @Schema( - description = "주소", - example = "서울특별시 강남구 역삼동 99번길" - ) - val address: String, - - @Schema( - description = "상세 주소", - example = "101동 101호" - ) - val detailAddress: String, - - @Schema( - description = "배송 메시지", - example = "부재 시 경비실에 맡겨주세요" - ) - val requestMessage: String?, -) diff --git a/src/test/kotlin/com/petqua/application/product/WishProductServiceTest.kt b/src/test/kotlin/com/petqua/application/product/WishProductServiceTest.kt index 4a00bbe5..93fb9076 100644 --- a/src/test/kotlin/com/petqua/application/product/WishProductServiceTest.kt +++ b/src/test/kotlin/com/petqua/application/product/WishProductServiceTest.kt @@ -73,7 +73,7 @@ class WishProductServiceTest( } Given("찜 상품 수정시") { - val product = productRepository.save(product()) + productRepository.save(product()) val member = memberRepository.save(member()) When("상품이 존재하지 않으면") { From 49f246fd0cc1a444bfa1bd21263cd92ea755dfee Mon Sep 17 00:00:00 2001 From: Combi153 Date: Fri, 1 Mar 2024 19:38:58 +0900 Subject: [PATCH 17/26] =?UTF-8?q?feat:=20=EA=B2=B0=EC=A0=9C=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=20=EC=B2=98=EB=A6=AC=20api=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../petqua/application/order/dto/OrderDtos.kt | 37 --- .../petqua/application/payment/PaymentDtos.kt | 217 +++++------------- .../payment/PaymentFacadeService.kt | 9 +- .../application/payment/PaymentGatewayDtos.kt | 184 +++++++++++++++ .../application/payment/PaymentService.kt | 10 +- .../kotlin/com/petqua/domain/order/Order.kt | 10 +- .../exception/order/OrderExceptionType.kt | 4 +- .../exception/payment/FailPaymentCode.kt | 19 ++ .../exception/payment/FailPaymentException.kt | 13 ++ .../payment/FailPaymentExceptionType.kt | 29 +++ .../presentation/payment/PaymentController.kt | 12 +- .../presentation/payment/PaymentDtos.kt | 19 +- .../payment/PaymentFacadeServiceTest.kt | 154 ++++++++++++- .../payment/PaymentControllerSteps.kt | 32 ++- .../payment/PaymentControllerTest.kt | 164 ++++++++++++- .../petqua/test/fixture/PaymentFixtures.kt | 17 +- 16 files changed, 712 insertions(+), 218 deletions(-) create mode 100644 src/main/kotlin/com/petqua/application/payment/PaymentGatewayDtos.kt create mode 100644 src/main/kotlin/com/petqua/exception/payment/FailPaymentCode.kt create mode 100644 src/main/kotlin/com/petqua/exception/payment/FailPaymentException.kt create mode 100644 src/main/kotlin/com/petqua/exception/payment/FailPaymentExceptionType.kt diff --git a/src/main/kotlin/com/petqua/application/order/dto/OrderDtos.kt b/src/main/kotlin/com/petqua/application/order/dto/OrderDtos.kt index bf015ac8..f432a8da 100644 --- a/src/main/kotlin/com/petqua/application/order/dto/OrderDtos.kt +++ b/src/main/kotlin/com/petqua/application/order/dto/OrderDtos.kt @@ -1,11 +1,8 @@ package com.petqua.application.order.dto -import com.petqua.application.payment.PaymentConfirmRequestToPG import com.petqua.domain.delivery.DeliveryMethod -import com.petqua.domain.order.OrderNumber import com.petqua.domain.order.OrderProduct import com.petqua.domain.order.ShippingNumber -import com.petqua.domain.payment.tosspayment.TossPaymentType import com.petqua.domain.product.Product import com.petqua.domain.product.option.ProductOption import com.petqua.domain.product.option.Sex @@ -70,37 +67,3 @@ data class SaveOrderResponse( val successUrl: String, val failUrl: String, ) - -data class PayOrderCommand( - val memberId: Long, - val paymentType: TossPaymentType, - val orderNumber: OrderNumber, - val paymentKey: String, - val amount: BigDecimal, -) { - fun toPaymentConfirmRequest(): PaymentConfirmRequestToPG { - return PaymentConfirmRequestToPG( - orderNumber = orderNumber, - paymentKey = paymentKey, - amount = amount - ) - } - - companion object { - fun of( - memberId: Long, - paymentType: String, - orderId: String, - paymentKey: String, - amount: BigDecimal, - ): PayOrderCommand { - return PayOrderCommand( - memberId = memberId, - paymentType = TossPaymentType.from(paymentType), - orderNumber = OrderNumber.from(orderId), - paymentKey = paymentKey, - amount = amount, - ) - } - } -} diff --git a/src/main/kotlin/com/petqua/application/payment/PaymentDtos.kt b/src/main/kotlin/com/petqua/application/payment/PaymentDtos.kt index 91664721..413e4765 100644 --- a/src/main/kotlin/com/petqua/application/payment/PaymentDtos.kt +++ b/src/main/kotlin/com/petqua/application/payment/PaymentDtos.kt @@ -1,184 +1,75 @@ package com.petqua.application.payment -import com.petqua.domain.order.OrderName import com.petqua.domain.order.OrderNumber -import com.petqua.domain.payment.tosspayment.TossPayment -import com.petqua.domain.payment.tosspayment.TossPaymentMethod -import com.petqua.domain.payment.tosspayment.TossPaymentStatus import com.petqua.domain.payment.tosspayment.TossPaymentType +import com.petqua.exception.payment.FailPaymentCode +import com.petqua.exception.payment.FailPaymentException +import com.petqua.exception.payment.FailPaymentExceptionType.INVALID_ORDER_ID import java.math.BigDecimal -data class PaymentConfirmRequestToPG( +data class PayOrderCommand( + val memberId: Long, + val paymentType: TossPaymentType, val orderNumber: OrderNumber, val paymentKey: String, val amount: BigDecimal, -) - -data class PaymentResponseFromPG( - val version: String, - val paymentKey: String, - val type: String, - val orderId: String, - val orderName: String, - val mid: String, - val currency: String, - val method: String, - val totalAmount: BigDecimal, - val balanceAmount: BigDecimal, - val status: String, - val requestedAt: String, - val approvedAt: String, - val useEscrow: Boolean, - val lastTransactionKey: String?, - val suppliedAmount: BigDecimal, - val vat: BigDecimal, - val cultureExpense: Boolean, - val taxFreeAmount: BigDecimal, - val taxExemptionAmount: BigDecimal, - val cancels: List?, - val isPartialCancelable: Boolean, - val card: CardResponseFromPG?, - val virtualAccount: VirtualAccountResponseFromPG?, - val secret: String?, - val mobilePhone: MobilePhoneResponseFromPG?, - val giftCertificate: GiftCertificateResponseFromPG?, - val transfer: TransferResponseFromPG?, - val receipt: ReceiptResponseFromPG?, - val checkout: CheckoutResponseFromPG?, - val easyPay: EasyPayResponseFromPG?, - val country: String, - val failure: FailureResponseFromPG?, - val cashReceipt: CashReceiptResponseFromPG?, - val cashReceipts: List?, - val discount: DiscountResponseFromPG?, ) { - fun toPayment(): TossPayment { - return TossPayment( + fun toPaymentConfirmRequest(): PaymentConfirmRequestToPG { + return PaymentConfirmRequestToPG( + orderNumber = orderNumber, paymentKey = paymentKey, - orderNumber = OrderNumber.from(orderId), - orderName = OrderName(orderName), - method = TossPaymentMethod.valueOf(method), - totalAmount = totalAmount, - status = TossPaymentStatus.valueOf(status), - requestedAt = requestedAt, - approvedAt = approvedAt, - useEscrow = useEscrow, - type = TossPaymentType.valueOf(type), + amount = amount ) } -} - -data class CancelResponseFromPG( - val cancelAmount: BigDecimal, - val cancelReason: String, - val taxFreeAmount: BigDecimal, - val taxExemptionAmount: BigDecimal, - val refundableAmount: BigDecimal, - val easyPayDiscountAmount: BigDecimal, - val canceledAt: String, - val transactionKey: String, - val receiptKey: String?, -) - -data class CardResponseFromPG( - val amount: BigDecimal, - val issuerCode: String, - val acquirerCode: String?, - val number: String, - val installmentPlanMonths: Int, - val approveNo: String, - val useCardPoint: Boolean, - val cardType: String, - val ownerType: String, - val acquireStatus: String, - val isInterestFree: Boolean, - val interestPayer: String?, -) - -data class VirtualAccountResponseFromPG( - val accountType: String, - val accountNumber: String, - val bankCode: String, - val customerName: String, - val dueDate: String, - val refundStatus: String, - val expired: Boolean, - val settlementStatus: String, - val refundReceiveAccount: RefundReceiveAccountResponseFromPG?, // 결제창 띄운 시점부터 30분 동안만 조회 가능 -) - -data class RefundReceiveAccountResponseFromPG( - val bankCode: String, - val accountNumber: String, - val holderName: String, -) - -data class MobilePhoneResponseFromPG( - val customerMobilePhone: String, - val settlementStatus: String, - val receiptUrl: String, -) -data class GiftCertificateResponseFromPG( - val approveNo: String, - val settlementStatus: String, -) - -data class TransferResponseFromPG( - val bankCode: String, - val settlementStatus: String, -) - -data class ReceiptResponseFromPG( - val url: String, -) - -data class CheckoutResponseFromPG( - val url: String, -) - -data class EasyPayResponseFromPG( - val provider: String, - val amount: BigDecimal, - val discountAmount: BigDecimal, -) + companion object { + fun of( + memberId: Long, + paymentType: String, + orderId: String, + paymentKey: String, + amount: BigDecimal, + ): PayOrderCommand { + return PayOrderCommand( + memberId = memberId, + paymentType = TossPaymentType.from(paymentType), + orderNumber = OrderNumber.from(orderId), + paymentKey = paymentKey, + amount = amount, + ) + } + } +} -data class FailureResponseFromPG( - val code: String, +data class FailPaymentCommand( + val memberId: Long, + val code: FailPaymentCode, val message: String, -) - -data class CashReceiptResponseFromPG( - val type: String, - val receiptKey: String, - val issueNumber: String, - val receiptUrl: String, - val amount: BigDecimal, - val taxFreeAmount: BigDecimal, -) + val orderNumber: String?, +) { -data class CashReceiptHistoryResponseFromPG( - val receiptKey: String, - val orderId: String, - val orderName: String, - val type: String, - val issueNumber: String, - val receiptUrl: String, - val businessNumber: String, - val transactionType: String, - val amount: BigDecimal, - val taxFreeAmount: BigDecimal, - val issueStatus: String, - val failure: FailureResponseFromPG, - val customerIdentityNumber: String, - val requestedAt: String, -) + fun toOrderNumber(): OrderNumber { + return orderNumber?.let { OrderNumber.from(it) } ?: throw FailPaymentException(INVALID_ORDER_ID) + } -data class DiscountResponseFromPG( - val amount: BigDecimal, -) + companion object { + fun of( + memberId: Long, + code: String, + message: String, + orderId: String?, + ): FailPaymentCommand { + return FailPaymentCommand( + memberId = memberId, + code = FailPaymentCode.from(code), + message = message, + orderNumber = orderId, + ) + } + } +} -data class PaymentErrorResponseFromPG( - val code: String, +data class FailPaymentResponse( + val code: FailPaymentCode, val message: String, ) diff --git a/src/main/kotlin/com/petqua/application/payment/PaymentFacadeService.kt b/src/main/kotlin/com/petqua/application/payment/PaymentFacadeService.kt index 06f2c777..a7b2c1c0 100644 --- a/src/main/kotlin/com/petqua/application/payment/PaymentFacadeService.kt +++ b/src/main/kotlin/com/petqua/application/payment/PaymentFacadeService.kt @@ -1,6 +1,6 @@ package com.petqua.application.payment -import com.petqua.application.order.dto.PayOrderCommand +import com.petqua.exception.payment.FailPaymentCode.PAY_PROCESS_CANCELED import org.springframework.stereotype.Service @Service @@ -14,4 +14,11 @@ class PaymentFacadeService( val paymentResponse = paymentGatewayService.confirmPayment(command.toPaymentConfirmRequest()) paymentService.save(paymentResponse.toPayment()) } + + fun failPayment(command: FailPaymentCommand): FailPaymentResponse { + if (command.code != PAY_PROCESS_CANCELED) { + paymentService.cancelOrder(command.memberId, command.toOrderNumber()) + } + return FailPaymentResponse(command.code, command.message) + } } diff --git a/src/main/kotlin/com/petqua/application/payment/PaymentGatewayDtos.kt b/src/main/kotlin/com/petqua/application/payment/PaymentGatewayDtos.kt new file mode 100644 index 00000000..91664721 --- /dev/null +++ b/src/main/kotlin/com/petqua/application/payment/PaymentGatewayDtos.kt @@ -0,0 +1,184 @@ +package com.petqua.application.payment + +import com.petqua.domain.order.OrderName +import com.petqua.domain.order.OrderNumber +import com.petqua.domain.payment.tosspayment.TossPayment +import com.petqua.domain.payment.tosspayment.TossPaymentMethod +import com.petqua.domain.payment.tosspayment.TossPaymentStatus +import com.petqua.domain.payment.tosspayment.TossPaymentType +import java.math.BigDecimal + +data class PaymentConfirmRequestToPG( + val orderNumber: OrderNumber, + val paymentKey: String, + val amount: BigDecimal, +) + +data class PaymentResponseFromPG( + val version: String, + val paymentKey: String, + val type: String, + val orderId: String, + val orderName: String, + val mid: String, + val currency: String, + val method: String, + val totalAmount: BigDecimal, + val balanceAmount: BigDecimal, + val status: String, + val requestedAt: String, + val approvedAt: String, + val useEscrow: Boolean, + val lastTransactionKey: String?, + val suppliedAmount: BigDecimal, + val vat: BigDecimal, + val cultureExpense: Boolean, + val taxFreeAmount: BigDecimal, + val taxExemptionAmount: BigDecimal, + val cancels: List?, + val isPartialCancelable: Boolean, + val card: CardResponseFromPG?, + val virtualAccount: VirtualAccountResponseFromPG?, + val secret: String?, + val mobilePhone: MobilePhoneResponseFromPG?, + val giftCertificate: GiftCertificateResponseFromPG?, + val transfer: TransferResponseFromPG?, + val receipt: ReceiptResponseFromPG?, + val checkout: CheckoutResponseFromPG?, + val easyPay: EasyPayResponseFromPG?, + val country: String, + val failure: FailureResponseFromPG?, + val cashReceipt: CashReceiptResponseFromPG?, + val cashReceipts: List?, + val discount: DiscountResponseFromPG?, +) { + fun toPayment(): TossPayment { + return TossPayment( + paymentKey = paymentKey, + orderNumber = OrderNumber.from(orderId), + orderName = OrderName(orderName), + method = TossPaymentMethod.valueOf(method), + totalAmount = totalAmount, + status = TossPaymentStatus.valueOf(status), + requestedAt = requestedAt, + approvedAt = approvedAt, + useEscrow = useEscrow, + type = TossPaymentType.valueOf(type), + ) + } +} + +data class CancelResponseFromPG( + val cancelAmount: BigDecimal, + val cancelReason: String, + val taxFreeAmount: BigDecimal, + val taxExemptionAmount: BigDecimal, + val refundableAmount: BigDecimal, + val easyPayDiscountAmount: BigDecimal, + val canceledAt: String, + val transactionKey: String, + val receiptKey: String?, +) + +data class CardResponseFromPG( + val amount: BigDecimal, + val issuerCode: String, + val acquirerCode: String?, + val number: String, + val installmentPlanMonths: Int, + val approveNo: String, + val useCardPoint: Boolean, + val cardType: String, + val ownerType: String, + val acquireStatus: String, + val isInterestFree: Boolean, + val interestPayer: String?, +) + +data class VirtualAccountResponseFromPG( + val accountType: String, + val accountNumber: String, + val bankCode: String, + val customerName: String, + val dueDate: String, + val refundStatus: String, + val expired: Boolean, + val settlementStatus: String, + val refundReceiveAccount: RefundReceiveAccountResponseFromPG?, // 결제창 띄운 시점부터 30분 동안만 조회 가능 +) + +data class RefundReceiveAccountResponseFromPG( + val bankCode: String, + val accountNumber: String, + val holderName: String, +) + +data class MobilePhoneResponseFromPG( + val customerMobilePhone: String, + val settlementStatus: String, + val receiptUrl: String, +) + +data class GiftCertificateResponseFromPG( + val approveNo: String, + val settlementStatus: String, +) + +data class TransferResponseFromPG( + val bankCode: String, + val settlementStatus: String, +) + +data class ReceiptResponseFromPG( + val url: String, +) + +data class CheckoutResponseFromPG( + val url: String, +) + +data class EasyPayResponseFromPG( + val provider: String, + val amount: BigDecimal, + val discountAmount: BigDecimal, +) + +data class FailureResponseFromPG( + val code: String, + val message: String, +) + +data class CashReceiptResponseFromPG( + val type: String, + val receiptKey: String, + val issueNumber: String, + val receiptUrl: String, + val amount: BigDecimal, + val taxFreeAmount: BigDecimal, +) + +data class CashReceiptHistoryResponseFromPG( + val receiptKey: String, + val orderId: String, + val orderName: String, + val type: String, + val issueNumber: String, + val receiptUrl: String, + val businessNumber: String, + val transactionType: String, + val amount: BigDecimal, + val taxFreeAmount: BigDecimal, + val issueStatus: String, + val failure: FailureResponseFromPG, + val customerIdentityNumber: String, + val requestedAt: String, +) + +data class DiscountResponseFromPG( + val amount: BigDecimal, +) + +data class PaymentErrorResponseFromPG( + val code: String, + val message: String, +) diff --git a/src/main/kotlin/com/petqua/application/payment/PaymentService.kt b/src/main/kotlin/com/petqua/application/payment/PaymentService.kt index dc0ba762..a3c7ebcb 100644 --- a/src/main/kotlin/com/petqua/application/payment/PaymentService.kt +++ b/src/main/kotlin/com/petqua/application/payment/PaymentService.kt @@ -1,6 +1,6 @@ package com.petqua.application.payment -import com.petqua.application.order.dto.PayOrderCommand +import com.petqua.domain.order.OrderNumber import com.petqua.domain.order.OrderRepository import com.petqua.domain.order.findByOrderNumberOrThrow import com.petqua.domain.payment.tosspayment.TossPayment @@ -29,4 +29,12 @@ class PaymentService( fun save(tossPayment: TossPayment): TossPayment { return paymentRepository.save(tossPayment) } + + fun cancelOrder(memberId: Long, orderNumber: OrderNumber) { + val order = orderRepository.findByOrderNumberOrThrow(orderNumber) { + OrderException(ORDER_NOT_FOUND) + } + order.validateOwner(memberId) + order.cancel() + } } diff --git a/src/main/kotlin/com/petqua/domain/order/Order.kt b/src/main/kotlin/com/petqua/domain/order/Order.kt index 26080256..ce6219fb 100644 --- a/src/main/kotlin/com/petqua/domain/order/Order.kt +++ b/src/main/kotlin/com/petqua/domain/order/Order.kt @@ -45,7 +45,7 @@ class Order( @Enumerated(STRING) @Column(nullable = false) - val status: OrderStatus, + var status: OrderStatus, @Column(nullable = false) val totalAmount: BigDecimal, @@ -62,4 +62,12 @@ class Order( throw OrderException(FORBIDDEN_ORDER) } } + + fun cancel() { + // TODO isAbleToCancel 사용 + // throwExceptionWhen(!isAbleToCancel) { + // OrderException(ORDER_NOT_FOUND) + // } + status = OrderStatus.CANCELED + } } diff --git a/src/main/kotlin/com/petqua/exception/order/OrderExceptionType.kt b/src/main/kotlin/com/petqua/exception/order/OrderExceptionType.kt index 5d53a10a..46608f8e 100644 --- a/src/main/kotlin/com/petqua/exception/order/OrderExceptionType.kt +++ b/src/main/kotlin/com/petqua/exception/order/OrderExceptionType.kt @@ -4,6 +4,7 @@ import com.petqua.common.exception.BaseExceptionType import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus.BAD_REQUEST import org.springframework.http.HttpStatus.FORBIDDEN +import org.springframework.http.HttpStatus.NOT_FOUND enum class OrderExceptionType( private val httpStatus: HttpStatus, @@ -13,13 +14,14 @@ enum class OrderExceptionType( PRODUCT_NOT_FOUND(BAD_REQUEST, "O01", "주문한 상품이 존재하지 않습니다."), - ORDER_NOT_FOUND(BAD_REQUEST, "O11", "존재하지 않는 주문입니다."), + ORDER_NOT_FOUND(NOT_FOUND, "O11", "존재하지 않는 주문입니다."), ORDER_PRICE_NOT_MATCH(BAD_REQUEST, "O10", "주문한 상품의 가격이 일치하지 않습니다."), PAYMENT_PRICE_NOT_MATCH(BAD_REQUEST, "O12", "주문한 상품 가격과 결제 금액이 일치하지 않습니다."), INVALID_PAYMENT_TYPE(BAD_REQUEST, "O20", "유효하지 않은 결제 방식입니다."), FORBIDDEN_ORDER(FORBIDDEN, "O30", "해당 주문에 대한 권한이 없습니다."), + ORDER_CAN_NOT_CANCEL(BAD_REQUEST, "O31", "취소할 수 없는 주문입니다."), ; override fun httpStatus(): HttpStatus { diff --git a/src/main/kotlin/com/petqua/exception/payment/FailPaymentCode.kt b/src/main/kotlin/com/petqua/exception/payment/FailPaymentCode.kt new file mode 100644 index 00000000..97ee169b --- /dev/null +++ b/src/main/kotlin/com/petqua/exception/payment/FailPaymentCode.kt @@ -0,0 +1,19 @@ +package com.petqua.exception.payment + +import com.petqua.exception.payment.FailPaymentExceptionType.INVALID_CODE +import java.util.Locale + +enum class FailPaymentCode { + + PAY_PROCESS_CANCELED, + PAY_PROCESS_ABORTED, + REJECT_CARD_COMPANY, + ; + + companion object { + fun from(name: String): FailPaymentCode { + return enumValues().find { it.name == name.uppercase(Locale.ENGLISH) } + ?: throw FailPaymentException(INVALID_CODE) + } + } +} diff --git a/src/main/kotlin/com/petqua/exception/payment/FailPaymentException.kt b/src/main/kotlin/com/petqua/exception/payment/FailPaymentException.kt new file mode 100644 index 00000000..e538957f --- /dev/null +++ b/src/main/kotlin/com/petqua/exception/payment/FailPaymentException.kt @@ -0,0 +1,13 @@ +package com.petqua.exception.payment + +import com.petqua.common.exception.BaseException +import com.petqua.common.exception.BaseExceptionType + +class FailPaymentException( + private val exceptionType: FailPaymentExceptionType, +) : BaseException() { + + override fun exceptionType(): BaseExceptionType { + return exceptionType + } +} diff --git a/src/main/kotlin/com/petqua/exception/payment/FailPaymentExceptionType.kt b/src/main/kotlin/com/petqua/exception/payment/FailPaymentExceptionType.kt new file mode 100644 index 00000000..91411eb1 --- /dev/null +++ b/src/main/kotlin/com/petqua/exception/payment/FailPaymentExceptionType.kt @@ -0,0 +1,29 @@ +package com.petqua.exception.payment + +import com.petqua.common.exception.BaseExceptionType +import org.springframework.http.HttpStatus +import org.springframework.http.HttpStatus.BAD_REQUEST + +enum class FailPaymentExceptionType( + private val httpStatus: HttpStatus, + private val code: String, + private val errorMessage: String, +) : BaseExceptionType { + + INVALID_CODE(BAD_REQUEST, "PF01", "지원하지 않는 결제 실패 코드입니다."), + + INVALID_ORDER_ID(BAD_REQUEST, "PF01", "지원하지 않는 결제 실패 코드입니다."), + ; + + override fun httpStatus(): HttpStatus { + return httpStatus + } + + override fun code(): String { + return code + } + + override fun errorMessage(): String { + return errorMessage + } +} diff --git a/src/main/kotlin/com/petqua/presentation/payment/PaymentController.kt b/src/main/kotlin/com/petqua/presentation/payment/PaymentController.kt index 01485cab..a35c8b45 100644 --- a/src/main/kotlin/com/petqua/presentation/payment/PaymentController.kt +++ b/src/main/kotlin/com/petqua/presentation/payment/PaymentController.kt @@ -1,5 +1,6 @@ package com.petqua.presentation.payment +import com.petqua.application.payment.FailPaymentResponse import com.petqua.application.payment.PaymentFacadeService import com.petqua.common.config.ACCESS_TOKEN_SECURITY_SCHEME_KEY import com.petqua.domain.auth.Auth @@ -8,7 +9,6 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.http.ResponseEntity 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 @@ -23,16 +23,18 @@ class PaymentController( @PostMapping("/success") fun payOrder( @Auth loginMember: LoginMember, - @RequestBody request: PayOrderRequest, + request: PayOrderRequest, ): ResponseEntity { paymentFacadeService.payOrder(request.toCommand(loginMember.memberId)) return ResponseEntity.noContent().build() } @PostMapping("/fail") - fun cancelOrderPayment( + fun failPayment( @Auth loginMember: LoginMember, - ): ResponseEntity { - return ResponseEntity.ok().build() + request: FailPaymentRequest, + ): ResponseEntity { + val response = paymentFacadeService.failPayment(request.toCommand(loginMember.memberId)) + return ResponseEntity.ok(response) } } diff --git a/src/main/kotlin/com/petqua/presentation/payment/PaymentDtos.kt b/src/main/kotlin/com/petqua/presentation/payment/PaymentDtos.kt index 6622429f..97f59f95 100644 --- a/src/main/kotlin/com/petqua/presentation/payment/PaymentDtos.kt +++ b/src/main/kotlin/com/petqua/presentation/payment/PaymentDtos.kt @@ -1,6 +1,7 @@ package com.petqua.presentation.payment -import com.petqua.application.order.dto.PayOrderCommand +import com.petqua.application.payment.FailPaymentCommand +import com.petqua.application.payment.PayOrderCommand import java.math.BigDecimal data class PayOrderRequest( @@ -20,3 +21,19 @@ data class PayOrderRequest( ) } } + +data class FailPaymentRequest( + val code: String, + val message: String, + val orderId: String?, +) { + + fun toCommand(memberId: Long): FailPaymentCommand { + return FailPaymentCommand.of( + memberId = memberId, + code = code, + message = message, + orderId = orderId, + ) + } +} diff --git a/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt b/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt index 8d42f4e1..c4f15062 100644 --- a/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt +++ b/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt @@ -2,17 +2,26 @@ package com.petqua.application.payment import com.ninjasquad.springmockk.SpykBean import com.petqua.application.payment.infra.TossPaymentsApiClient +import com.petqua.common.domain.findByIdOrThrow import com.petqua.domain.member.MemberRepository import com.petqua.domain.order.OrderNumber import com.petqua.domain.order.OrderRepository +import com.petqua.domain.order.OrderStatus.CANCELED +import com.petqua.domain.order.OrderStatus.ORDER_CREATED import com.petqua.domain.payment.tosspayment.TossPaymentRepository import com.petqua.exception.order.OrderException -import com.petqua.exception.order.OrderExceptionType +import com.petqua.exception.order.OrderExceptionType.FORBIDDEN_ORDER import com.petqua.exception.order.OrderExceptionType.ORDER_NOT_FOUND import com.petqua.exception.order.OrderExceptionType.PAYMENT_PRICE_NOT_MATCH +import com.petqua.exception.payment.FailPaymentCode.PAY_PROCESS_ABORTED +import com.petqua.exception.payment.FailPaymentCode.PAY_PROCESS_CANCELED +import com.petqua.exception.payment.FailPaymentException +import com.petqua.exception.payment.FailPaymentExceptionType.INVALID_CODE +import com.petqua.exception.payment.FailPaymentExceptionType.INVALID_ORDER_ID import com.petqua.exception.payment.PaymentException import com.petqua.exception.payment.PaymentExceptionType.UNAUTHORIZED_KEY import com.petqua.test.DataCleaner +import com.petqua.test.fixture.failPaymentCommand import com.petqua.test.fixture.member import com.petqua.test.fixture.order import com.petqua.test.fixture.payOrderCommand @@ -26,6 +35,7 @@ import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE import org.springframework.web.reactive.function.client.WebClientResponseException import java.math.BigDecimal.ONE +import kotlin.Long.Companion.MIN_VALUE @SpringBootTest(webEnvironment = NONE) class PaymentFacadeServiceTest( @@ -102,7 +112,7 @@ class PaymentFacadeServiceTest( } When("권한이 없는 회원이면") { - val memberId = Long.MIN_VALUE + val memberId = MIN_VALUE Then("예외를 던진다") { shouldThrow { @@ -113,7 +123,7 @@ class PaymentFacadeServiceTest( amount = order.totalAmount, ) ) - }.exceptionType() shouldBe OrderExceptionType.FORBIDDEN_ORDER + }.exceptionType() shouldBe FORBIDDEN_ORDER } } @@ -158,6 +168,144 @@ class PaymentFacadeServiceTest( } } + Given("사용자가 취소한 결제 실패를 처리할 때") { + val member = memberRepository.save(member()) + val order = orderRepository.save( + order( + memberId = member.id, + orderNumber = OrderNumber.from("orderNumber"), + totalAmount = ONE + ) + ) + + When("유효한 실패 내역이 입력되면") { + val response = paymentFacadeService.failPayment( + failPaymentCommand( + memberId = member.id, + code = PAY_PROCESS_CANCELED.name, + message = "사용자가 결제를 취소했습니다.", + orderNumber = null + ) + ) + + Then("실패 내역을 응답한다") { + response shouldBe FailPaymentResponse( + code = PAY_PROCESS_CANCELED, + message = "사용자가 결제를 취소했습니다.", + ) + } + + Then("주문을 취소하지 않는다") { + val updatedOrder = orderRepository.findByIdOrThrow(order.id) + + updatedOrder.status shouldBe ORDER_CREATED + } + } + + When("유효하지 않은 실패 코드가 입력되면") { + val code = "INVALID_CODE" + + Then("예외를 던진다") { + shouldThrow { + paymentFacadeService.failPayment( + failPaymentCommand( + memberId = member.id, + code = code, + message = "사용자가 결제를 취소했습니다.", + orderNumber = null + ) + ) + }.exceptionType() shouldBe INVALID_CODE + } + } + } + + Given("사용자가 취소하지 않은 결제 실패를 처리할 때") { + val member = memberRepository.save(member()) + val order = orderRepository.save( + order( + memberId = member.id, + orderNumber = OrderNumber.from("orderNumber"), + totalAmount = ONE + ) + ) + + When("유효한 실패 내역이 입력되면") { + val response = paymentFacadeService.failPayment( + failPaymentCommand( + memberId = member.id, + code = PAY_PROCESS_ABORTED.name, + message = "시스템 오류로 결제가 실패했습니다.", + orderNumber = order.orderNumber.value, + ) + ) + + Then("해당 내용을 응답한다") { + response shouldBe FailPaymentResponse( + code = PAY_PROCESS_ABORTED, + message = "시스템 오류로 결제가 실패했습니다.", + ) + } + + Then("주문을 취소한다") { + val updatedOrder = orderRepository.findByIdOrThrow(order.id) + + updatedOrder.status shouldBe CANCELED + } + } + + When("주문번호가 입력되지 않으면") { + val orderNumber = null + + Then("예외를 던진다") { + shouldThrow { + paymentFacadeService.failPayment( + failPaymentCommand( + memberId = member.id, + code = PAY_PROCESS_ABORTED.name, + message = "시스템 오류로 결제가 실패했습니다.", + orderNumber = orderNumber, + ) + ) + }.exceptionType() shouldBe INVALID_ORDER_ID + } + } + + When("주문에 대한 권한이 없다면") { + val memberId = MIN_VALUE + + Then("예외를 던진다") { + shouldThrow { + paymentFacadeService.failPayment( + failPaymentCommand( + memberId = memberId, + code = PAY_PROCESS_ABORTED.name, + message = "시스템 오류로 결제가 실패했습니다.", + orderNumber = order.orderNumber.value, + ) + ) + }.exceptionType() shouldBe FORBIDDEN_ORDER + } + } + + When("존재하지 않는 주문이면") { + val orderNumber = "wrongOrderNumber" + + Then("예외를 던진다") { + shouldThrow { + paymentFacadeService.failPayment( + failPaymentCommand( + memberId = member.id, + code = PAY_PROCESS_ABORTED.name, + message = "시스템 오류로 결제가 실패했습니다.", + orderNumber = orderNumber, + ) + ) + }.exceptionType() shouldBe ORDER_NOT_FOUND + } + } + } + afterContainer { dataCleaner.clean() } diff --git a/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerSteps.kt b/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerSteps.kt index 96a1f296..d0a42a48 100644 --- a/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerSteps.kt +++ b/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerSteps.kt @@ -5,7 +5,6 @@ import io.restassured.module.kotlin.extensions.Given import io.restassured.module.kotlin.extensions.Then import io.restassured.module.kotlin.extensions.When import io.restassured.response.Response -import org.springframework.http.MediaType.APPLICATION_JSON_VALUE fun requestPayOrder( accessToken: String, @@ -14,8 +13,12 @@ fun requestPayOrder( return Given { log().all() auth().preemptive().oauth2(accessToken) - contentType(APPLICATION_JSON_VALUE) - body(payOrderRequest) + params( + "paymentType", payOrderRequest.paymentType, + "orderId", payOrderRequest.orderId, + "paymentKey", payOrderRequest.paymentKey, + "amount", payOrderRequest.amount + ) } When { post("/orders/payment/success") } Then { @@ -24,3 +27,26 @@ fun requestPayOrder( response() } } + +fun requestFailPayment( + accessToken: String, + failPaymentRequest: FailPaymentRequest, +): Response { + val paramMap = mutableMapOf().apply { + put("code", failPaymentRequest.code) + put("message", failPaymentRequest.message) + put("orderId", failPaymentRequest.orderId) + }.filterValues { it != null } + + return Given { + log().all() + auth().preemptive().oauth2(accessToken) + params(paramMap) + } When { + post("/orders/payment/fail") + } Then { + log().all() + } Extract { + response() + } +} diff --git a/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerTest.kt b/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerTest.kt index c2093cd5..6870676e 100644 --- a/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerTest.kt +++ b/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerTest.kt @@ -1,6 +1,7 @@ package com.petqua.presentation.payment import com.ninjasquad.springmockk.SpykBean +import com.petqua.application.payment.FailPaymentResponse import com.petqua.application.payment.PaymentConfirmRequestToPG import com.petqua.application.payment.infra.TossPaymentsApiClient import com.petqua.common.exception.ExceptionResponse @@ -10,16 +11,23 @@ import com.petqua.domain.payment.tosspayment.TossPaymentRepository import com.petqua.exception.order.OrderExceptionType.FORBIDDEN_ORDER import com.petqua.exception.order.OrderExceptionType.ORDER_NOT_FOUND import com.petqua.exception.order.OrderExceptionType.PAYMENT_PRICE_NOT_MATCH +import com.petqua.exception.payment.FailPaymentCode.PAY_PROCESS_ABORTED +import com.petqua.exception.payment.FailPaymentCode.PAY_PROCESS_CANCELED +import com.petqua.exception.payment.FailPaymentExceptionType.INVALID_CODE +import com.petqua.exception.payment.FailPaymentExceptionType.INVALID_ORDER_ID import com.petqua.exception.payment.PaymentExceptionType.UNAUTHORIZED_KEY import com.petqua.test.ApiTestConfig import com.petqua.test.fixture.order import com.petqua.test.fixture.payOrderRequest import io.kotest.assertions.assertSoftly import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe import io.mockk.every import org.springframework.http.HttpStatus.BAD_REQUEST import org.springframework.http.HttpStatus.FORBIDDEN +import org.springframework.http.HttpStatus.NOT_FOUND import org.springframework.http.HttpStatus.NO_CONTENT +import org.springframework.http.HttpStatus.OK import org.springframework.http.HttpStatus.UNAUTHORIZED import org.springframework.web.reactive.function.client.WebClientResponseException import java.math.BigDecimal.ONE @@ -85,7 +93,7 @@ class PaymentControllerTest( val errorResponse = response.`as`(ExceptionResponse::class.java) assertSoftly(response) { - statusCode shouldBe BAD_REQUEST.value() + statusCode shouldBe NOT_FOUND.value() errorResponse.message shouldBe ORDER_NOT_FOUND.errorMessage() } } @@ -162,5 +170,159 @@ class PaymentControllerTest( } } } + + Given("사용자가 취소한 결제 실패를 처리할 때") { + val accessToken = signInAsMember().accessToken + val memberId = getMemberIdByAccessToken(accessToken) + + orderRepository.save( + order( + memberId = memberId, + orderNumber = OrderNumber.from("orderNumber"), + totalAmount = ONE + ) + ) + + When("유효한 실패 내역이 입력되면") { + val response = requestFailPayment( + accessToken = accessToken, + failPaymentRequest = FailPaymentRequest( + code = PAY_PROCESS_CANCELED.name, + message = "사용자가 결제를 취소했습니다.", + orderId = null + ) + ) + + Then("해당 내용을 응답한다") { + val failPaymentResponse = response.`as`(FailPaymentResponse::class.java) + + assertSoftly(failPaymentResponse) { + response.statusCode shouldBe OK.value() + code shouldBe PAY_PROCESS_CANCELED + message shouldNotBe null + } + } + } + + When("유효하지 않은 실패 코드가 입력되면") { + val response = requestFailPayment( + accessToken = accessToken, + failPaymentRequest = FailPaymentRequest( + code = "INVALID_CODE", + message = "사용자가 결제를 취소했습니다.", + orderId = null + ) + ) + + Then("예외를 응답한다") { + val exceptionResponse = response.`as`(ExceptionResponse::class.java) + + assertSoftly(exceptionResponse) { + response.statusCode shouldBe BAD_REQUEST.value() + exceptionResponse.message shouldBe INVALID_CODE.errorMessage() + } + } + } + } + + Given("사용자가 취소하지 않은 결제 실패를 처리할 때") { + val accessToken = signInAsMember().accessToken + val memberId = getMemberIdByAccessToken(accessToken) + + val order = orderRepository.save( + order( + memberId = memberId, + orderNumber = OrderNumber.from("orderNumber"), + totalAmount = ONE + ) + ) + + When("유효한 실패 내역이 입력되면") { + val response = requestFailPayment( + accessToken = accessToken, + failPaymentRequest = FailPaymentRequest( + code = PAY_PROCESS_ABORTED.name, + message = "시스템 오류로 결제가 실패했습니다.", + orderId = order.orderNumber.value, + ) + ) + + Then("해당 내용을 응답한다") { + val failPaymentResponse = response.`as`(FailPaymentResponse::class.java) + + assertSoftly(failPaymentResponse) { + response.statusCode shouldBe OK.value() + code shouldBe PAY_PROCESS_ABORTED + message shouldNotBe null + } + } + } + + When("주문번호가 입력되지 않으면") { + val orderId = null + + val response = requestFailPayment( + accessToken = accessToken, + failPaymentRequest = FailPaymentRequest( + code = PAY_PROCESS_ABORTED.name, + message = "시스템 오류로 결제가 실패했습니다.", + orderId = orderId, + ) + ) + + Then("예외를 응답한다") { + val exceptionResponse = response.`as`(ExceptionResponse::class.java) + + assertSoftly(exceptionResponse) { + response.statusCode shouldBe BAD_REQUEST.value() + exceptionResponse.message shouldBe INVALID_ORDER_ID.errorMessage() + } + } + } + + When("주문에 대한 권한이 없다면") { + val otherAccessToken = signInAsMember().accessToken + + val response = requestFailPayment( + accessToken = otherAccessToken, + failPaymentRequest = FailPaymentRequest( + code = PAY_PROCESS_ABORTED.name, + message = "시스템 오류로 결제가 실패했습니다.", + orderId = order.orderNumber.value, + ) + ) + + Then("예외를 응답한다") { + val exceptionResponse = response.`as`(ExceptionResponse::class.java) + + assertSoftly(exceptionResponse) { + response.statusCode shouldBe FORBIDDEN.value() + exceptionResponse.message shouldBe FORBIDDEN_ORDER.errorMessage() + } + } + } + + When("존재하지 않는 주문이면") { + val orderId = "wrongOrderId" + + val response = requestFailPayment( + accessToken = accessToken, + failPaymentRequest = FailPaymentRequest( + code = PAY_PROCESS_ABORTED.name, + message = "시스템 오류로 결제가 실패했습니다.", + orderId = orderId, + ) + ) + + Then("예외를 응답한다") { + val exceptionResponse = response.`as`(ExceptionResponse::class.java) + + assertSoftly(exceptionResponse) { + response.statusCode shouldBe NOT_FOUND.value() + exceptionResponse.message shouldBe ORDER_NOT_FOUND.errorMessage() + } + } + } + } } } diff --git a/src/test/kotlin/com/petqua/test/fixture/PaymentFixtures.kt b/src/test/kotlin/com/petqua/test/fixture/PaymentFixtures.kt index bf7c5480..1f75135d 100644 --- a/src/test/kotlin/com/petqua/test/fixture/PaymentFixtures.kt +++ b/src/test/kotlin/com/petqua/test/fixture/PaymentFixtures.kt @@ -1,6 +1,7 @@ package com.petqua.test.fixture -import com.petqua.application.order.dto.PayOrderCommand +import com.petqua.application.payment.FailPaymentCommand +import com.petqua.application.payment.PayOrderCommand import com.petqua.domain.order.OrderNumber import com.petqua.domain.payment.tosspayment.TossPaymentType import com.petqua.domain.payment.tosspayment.TossPaymentType.NORMAL @@ -38,3 +39,17 @@ fun payOrderRequest( amount = amount, ) } + +fun failPaymentCommand( + memberId: Long, + code: String, + message: String, + orderNumber: String?, +): FailPaymentCommand { + return FailPaymentCommand.of( + memberId = memberId, + code = code, + message = message, + orderId = orderNumber, + ) +} From 10dce1c070ea2ad53e9714368bca146c7c4eb150 Mon Sep 17 00:00:00 2001 From: Combi153 Date: Fri, 1 Mar 2024 19:46:44 +0900 Subject: [PATCH 18/26] =?UTF-8?q?feat:=20=EA=B2=B0=EC=A0=9C=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=20=EB=A1=9C=EA=B9=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../petqua/application/payment/PaymentFacadeService.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/kotlin/com/petqua/application/payment/PaymentFacadeService.kt b/src/main/kotlin/com/petqua/application/payment/PaymentFacadeService.kt index a7b2c1c0..7491884d 100644 --- a/src/main/kotlin/com/petqua/application/payment/PaymentFacadeService.kt +++ b/src/main/kotlin/com/petqua/application/payment/PaymentFacadeService.kt @@ -1,6 +1,8 @@ package com.petqua.application.payment +import com.petqua.exception.payment.FailPaymentCode.PAY_PROCESS_ABORTED import com.petqua.exception.payment.FailPaymentCode.PAY_PROCESS_CANCELED +import org.slf4j.LoggerFactory import org.springframework.stereotype.Service @Service @@ -9,6 +11,8 @@ class PaymentFacadeService( private val paymentGatewayService: PaymentGatewayService, ) { + private val log = LoggerFactory.getLogger(PaymentFacadeService::class.java) + fun payOrder(command: PayOrderCommand) { paymentService.validateAmount(command) val paymentResponse = paymentGatewayService.confirmPayment(command.toPaymentConfirmRequest()) @@ -16,6 +20,10 @@ class PaymentFacadeService( } fun failPayment(command: FailPaymentCommand): FailPaymentResponse { + if (command.code == PAY_PROCESS_ABORTED) { + log.error("PG사에서 결제가 중단되었습니다. message: ${command.message}, OrderNumber: ${command.orderNumber}, MemberId: ${command.memberId}") + } + if (command.code != PAY_PROCESS_CANCELED) { paymentService.cancelOrder(command.memberId, command.toOrderNumber()) } From 90c0a376511df73467728958a2deebaf2d8e4d66 Mon Sep 17 00:00:00 2001 From: Combi153 Date: Sat, 2 Mar 2024 09:46:29 +0900 Subject: [PATCH 19/26] =?UTF-8?q?refactor:=20=EA=B2=B0=EC=A0=9C=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=20=EB=A1=9C=EA=B9=85=20=EB=B0=A9=EB=B2=95=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EB=A9=94=EC=84=9C=EB=93=9C,?= =?UTF-8?q?=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=9D=B4=EB=A6=84=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../petqua/application/payment/PaymentDtos.kt | 6 ++-- .../payment/PaymentFacadeService.kt | 16 +++++++---- .../application/payment/PaymentService.kt | 2 +- .../presentation/payment/PaymentController.kt | 6 ++-- .../presentation/payment/PaymentDtos.kt | 8 +++--- .../payment/PaymentFacadeServiceTest.kt | 28 +++++++++---------- .../payment/PaymentControllerSteps.kt | 12 ++++---- .../payment/PaymentControllerTest.kt | 24 ++++++++-------- .../petqua/test/fixture/PaymentFixtures.kt | 16 +++++------ 9 files changed, 62 insertions(+), 56 deletions(-) diff --git a/src/main/kotlin/com/petqua/application/payment/PaymentDtos.kt b/src/main/kotlin/com/petqua/application/payment/PaymentDtos.kt index 413e4765..e992ca99 100644 --- a/src/main/kotlin/com/petqua/application/payment/PaymentDtos.kt +++ b/src/main/kotlin/com/petqua/application/payment/PaymentDtos.kt @@ -7,7 +7,7 @@ import com.petqua.exception.payment.FailPaymentException import com.petqua.exception.payment.FailPaymentExceptionType.INVALID_ORDER_ID import java.math.BigDecimal -data class PayOrderCommand( +data class SucceedPaymentCommand( val memberId: Long, val paymentType: TossPaymentType, val orderNumber: OrderNumber, @@ -29,8 +29,8 @@ data class PayOrderCommand( orderId: String, paymentKey: String, amount: BigDecimal, - ): PayOrderCommand { - return PayOrderCommand( + ): SucceedPaymentCommand { + return SucceedPaymentCommand( memberId = memberId, paymentType = TossPaymentType.from(paymentType), orderNumber = OrderNumber.from(orderId), diff --git a/src/main/kotlin/com/petqua/application/payment/PaymentFacadeService.kt b/src/main/kotlin/com/petqua/application/payment/PaymentFacadeService.kt index 7491884d..a12c2c58 100644 --- a/src/main/kotlin/com/petqua/application/payment/PaymentFacadeService.kt +++ b/src/main/kotlin/com/petqua/application/payment/PaymentFacadeService.kt @@ -2,6 +2,7 @@ package com.petqua.application.payment import com.petqua.exception.payment.FailPaymentCode.PAY_PROCESS_ABORTED import com.petqua.exception.payment.FailPaymentCode.PAY_PROCESS_CANCELED +import com.petqua.exception.payment.FailPaymentCode.REJECT_CARD_COMPANY import org.slf4j.LoggerFactory import org.springframework.stereotype.Service @@ -13,20 +14,25 @@ class PaymentFacadeService( private val log = LoggerFactory.getLogger(PaymentFacadeService::class.java) - fun payOrder(command: PayOrderCommand) { + fun succeedPayment(command: SucceedPaymentCommand) { paymentService.validateAmount(command) val paymentResponse = paymentGatewayService.confirmPayment(command.toPaymentConfirmRequest()) paymentService.save(paymentResponse.toPayment()) } fun failPayment(command: FailPaymentCommand): FailPaymentResponse { - if (command.code == PAY_PROCESS_ABORTED) { - log.error("PG사에서 결제가 중단되었습니다. message: ${command.message}, OrderNumber: ${command.orderNumber}, MemberId: ${command.memberId}") - } - + log(command) if (command.code != PAY_PROCESS_CANCELED) { paymentService.cancelOrder(command.memberId, command.toOrderNumber()) } return FailPaymentResponse(command.code, command.message) } + + private fun log(command: FailPaymentCommand) { + when (command.code) { + PAY_PROCESS_ABORTED -> log.error("PG사에서 결제를 중단했습니다. message: ${command.message}, OrderNumber: ${command.orderNumber}, MemberId: ${command.memberId}") + PAY_PROCESS_CANCELED -> log.warn("사용자가 결제를 중단했습니다. message: ${command.message}, OrderNumber: ${command.orderNumber}, MemberId: ${command.memberId}") + REJECT_CARD_COMPANY -> log.warn("카드사에서 결제를 거절했습니다.. message: ${command.message}, OrderNumber: ${command.orderNumber}, MemberId: ${command.memberId}") + } + } } diff --git a/src/main/kotlin/com/petqua/application/payment/PaymentService.kt b/src/main/kotlin/com/petqua/application/payment/PaymentService.kt index a3c7ebcb..06405100 100644 --- a/src/main/kotlin/com/petqua/application/payment/PaymentService.kt +++ b/src/main/kotlin/com/petqua/application/payment/PaymentService.kt @@ -18,7 +18,7 @@ class PaymentService( ) { @Transactional(readOnly = true) - fun validateAmount(command: PayOrderCommand) { + fun validateAmount(command: SucceedPaymentCommand) { val order = orderRepository.findByOrderNumberOrThrow(command.orderNumber) { OrderException(ORDER_NOT_FOUND) } diff --git a/src/main/kotlin/com/petqua/presentation/payment/PaymentController.kt b/src/main/kotlin/com/petqua/presentation/payment/PaymentController.kt index a35c8b45..3fc96d64 100644 --- a/src/main/kotlin/com/petqua/presentation/payment/PaymentController.kt +++ b/src/main/kotlin/com/petqua/presentation/payment/PaymentController.kt @@ -21,11 +21,11 @@ class PaymentController( ) { @PostMapping("/success") - fun payOrder( + fun succeedPayment( @Auth loginMember: LoginMember, - request: PayOrderRequest, + request: SucceedPaymentRequest, ): ResponseEntity { - paymentFacadeService.payOrder(request.toCommand(loginMember.memberId)) + paymentFacadeService.succeedPayment(request.toCommand(loginMember.memberId)) return ResponseEntity.noContent().build() } diff --git a/src/main/kotlin/com/petqua/presentation/payment/PaymentDtos.kt b/src/main/kotlin/com/petqua/presentation/payment/PaymentDtos.kt index 97f59f95..a8319578 100644 --- a/src/main/kotlin/com/petqua/presentation/payment/PaymentDtos.kt +++ b/src/main/kotlin/com/petqua/presentation/payment/PaymentDtos.kt @@ -1,18 +1,18 @@ package com.petqua.presentation.payment import com.petqua.application.payment.FailPaymentCommand -import com.petqua.application.payment.PayOrderCommand +import com.petqua.application.payment.SucceedPaymentCommand import java.math.BigDecimal -data class PayOrderRequest( +data class SucceedPaymentRequest( val paymentType: String, val orderId: String, val paymentKey: String, val amount: BigDecimal, ) { - fun toCommand(memberId: Long): PayOrderCommand { - return PayOrderCommand.of( + fun toCommand(memberId: Long): SucceedPaymentCommand { + return SucceedPaymentCommand.of( memberId = memberId, paymentType = paymentType, orderId = orderId, diff --git a/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt b/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt index c4f15062..96d3a562 100644 --- a/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt +++ b/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt @@ -24,7 +24,7 @@ import com.petqua.test.DataCleaner import com.petqua.test.fixture.failPaymentCommand import com.petqua.test.fixture.member import com.petqua.test.fixture.order -import com.petqua.test.fixture.payOrderCommand +import com.petqua.test.fixture.succeedPaymentCommand import io.kotest.assertions.assertSoftly import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.BehaviorSpec @@ -47,7 +47,7 @@ class PaymentFacadeServiceTest( @SpykBean private val tossPaymentsApiClient: TossPaymentsApiClient, ) : BehaviorSpec({ - Given("주문을 결제할 때") { + Given("결제 성공을 처리할 때") { val member = memberRepository.save(member()) val order = orderRepository.save( order( @@ -58,8 +58,8 @@ class PaymentFacadeServiceTest( ) When("유효한 요쳥이면") { - paymentFacadeService.payOrder( - command = payOrderCommand( + paymentFacadeService.succeedPayment( + command = succeedPaymentCommand( memberId = order.memberId, orderNumber = order.orderNumber, amount = order.totalAmount, @@ -74,8 +74,8 @@ class PaymentFacadeServiceTest( } When("결제 승인에 성공하면") { - paymentFacadeService.payOrder( - command = payOrderCommand( + paymentFacadeService.succeedPayment( + command = succeedPaymentCommand( memberId = order.memberId, orderNumber = order.orderNumber, amount = order.totalAmount, @@ -100,8 +100,8 @@ class PaymentFacadeServiceTest( Then("예외를 던진다") { shouldThrow { - paymentFacadeService.payOrder( - command = payOrderCommand( + paymentFacadeService.succeedPayment( + command = succeedPaymentCommand( memberId = order.memberId, orderNumber = orderNumber, amount = order.totalAmount, @@ -116,8 +116,8 @@ class PaymentFacadeServiceTest( Then("예외를 던진다") { shouldThrow { - paymentFacadeService.payOrder( - command = payOrderCommand( + paymentFacadeService.succeedPayment( + command = succeedPaymentCommand( memberId = memberId, orderNumber = order.orderNumber, amount = order.totalAmount, @@ -132,8 +132,8 @@ class PaymentFacadeServiceTest( Then("예외를 던진다") { shouldThrow { - paymentFacadeService.payOrder( - command = payOrderCommand( + paymentFacadeService.succeedPayment( + command = succeedPaymentCommand( memberId = order.memberId, orderNumber = order.orderNumber, amount = amount, @@ -156,8 +156,8 @@ class PaymentFacadeServiceTest( Then("예외를 던진다") { shouldThrow { - paymentFacadeService.payOrder( - command = payOrderCommand( + paymentFacadeService.succeedPayment( + command = succeedPaymentCommand( memberId = order.memberId, orderNumber = order.orderNumber, amount = order.totalAmount, diff --git a/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerSteps.kt b/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerSteps.kt index d0a42a48..1da2f9fb 100644 --- a/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerSteps.kt +++ b/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerSteps.kt @@ -6,18 +6,18 @@ import io.restassured.module.kotlin.extensions.Then import io.restassured.module.kotlin.extensions.When import io.restassured.response.Response -fun requestPayOrder( +fun requestSucceedPayment( accessToken: String, - payOrderRequest: PayOrderRequest, + succeedPaymentRequest: SucceedPaymentRequest, ): Response { return Given { log().all() auth().preemptive().oauth2(accessToken) params( - "paymentType", payOrderRequest.paymentType, - "orderId", payOrderRequest.orderId, - "paymentKey", payOrderRequest.paymentKey, - "amount", payOrderRequest.amount + "paymentType", succeedPaymentRequest.paymentType, + "orderId", succeedPaymentRequest.orderId, + "paymentKey", succeedPaymentRequest.paymentKey, + "amount", succeedPaymentRequest.amount ) } When { post("/orders/payment/success") diff --git a/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerTest.kt b/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerTest.kt index 6870676e..711efb24 100644 --- a/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerTest.kt +++ b/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerTest.kt @@ -18,7 +18,7 @@ import com.petqua.exception.payment.FailPaymentExceptionType.INVALID_ORDER_ID import com.petqua.exception.payment.PaymentExceptionType.UNAUTHORIZED_KEY import com.petqua.test.ApiTestConfig import com.petqua.test.fixture.order -import com.petqua.test.fixture.payOrderRequest +import com.petqua.test.fixture.succeedPaymentRequest import io.kotest.assertions.assertSoftly import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe @@ -40,7 +40,7 @@ class PaymentControllerTest( init { - Given("주문을 결제할 때") { + Given("결제 성공을 처리할 때") { val accessToken = signInAsMember().accessToken val memberId = getMemberIdByAccessToken(accessToken) @@ -53,9 +53,9 @@ class PaymentControllerTest( ) When("유효한 요청이면") { - val response = requestPayOrder( + val response = requestSucceedPayment( accessToken = accessToken, - payOrderRequest = payOrderRequest( + succeedPaymentRequest = succeedPaymentRequest( orderId = order.orderNumber.value, amount = order.totalAmount ) @@ -81,9 +81,9 @@ class PaymentControllerTest( When("존재하지 않는 주문이면") { val orderNumber = "wrongOrderNumber" - val response = requestPayOrder( + val response = requestSucceedPayment( accessToken = accessToken, - payOrderRequest = payOrderRequest( + succeedPaymentRequest = succeedPaymentRequest( orderId = orderNumber, amount = order.totalAmount ) @@ -102,9 +102,9 @@ class PaymentControllerTest( When("권한이 없는 회원의 요청이면") { val otherAccessToken = signInAsMember().accessToken - val response = requestPayOrder( + val response = requestSucceedPayment( accessToken = otherAccessToken, - payOrderRequest = payOrderRequest( + succeedPaymentRequest = succeedPaymentRequest( orderId = order.orderNumber.value, amount = order.totalAmount ) @@ -123,9 +123,9 @@ class PaymentControllerTest( When("주문의 총가격과 결제 금액이 다르면") { val wrongAmount = order.totalAmount + ONE - val response = requestPayOrder( + val response = requestSucceedPayment( accessToken = accessToken, - payOrderRequest = payOrderRequest( + succeedPaymentRequest = succeedPaymentRequest( orderId = order.orderNumber.value, amount = wrongAmount ) @@ -152,9 +152,9 @@ class PaymentControllerTest( null ) - val response = requestPayOrder( + val response = requestSucceedPayment( accessToken = accessToken, - payOrderRequest = payOrderRequest( + succeedPaymentRequest = succeedPaymentRequest( orderId = order.orderNumber.value, amount = order.totalAmount ) diff --git a/src/test/kotlin/com/petqua/test/fixture/PaymentFixtures.kt b/src/test/kotlin/com/petqua/test/fixture/PaymentFixtures.kt index 1f75135d..c9bd1e6b 100644 --- a/src/test/kotlin/com/petqua/test/fixture/PaymentFixtures.kt +++ b/src/test/kotlin/com/petqua/test/fixture/PaymentFixtures.kt @@ -1,23 +1,23 @@ package com.petqua.test.fixture import com.petqua.application.payment.FailPaymentCommand -import com.petqua.application.payment.PayOrderCommand +import com.petqua.application.payment.SucceedPaymentCommand import com.petqua.domain.order.OrderNumber import com.petqua.domain.payment.tosspayment.TossPaymentType import com.petqua.domain.payment.tosspayment.TossPaymentType.NORMAL -import com.petqua.presentation.payment.PayOrderRequest +import com.petqua.presentation.payment.SucceedPaymentRequest import java.math.BigDecimal import java.math.BigDecimal.ONE import java.math.BigDecimal.ZERO -fun payOrderCommand( +fun succeedPaymentCommand( memberId: Long = 0L, paymentType: TossPaymentType = NORMAL, orderNumber: OrderNumber = OrderNumber.from("OrderNumber"), paymentKey: String = "paymentKey", amount: BigDecimal = ZERO, -): PayOrderCommand { - return PayOrderCommand( +): SucceedPaymentCommand { + return SucceedPaymentCommand( memberId = memberId, paymentType = paymentType, orderNumber = orderNumber, @@ -26,13 +26,13 @@ fun payOrderCommand( ) } -fun payOrderRequest( +fun succeedPaymentRequest( paymentType: String = NORMAL.name, orderId: String = "orderId", paymentKey: String = "paymentKey", amount: BigDecimal = ONE, -): PayOrderRequest { - return PayOrderRequest( +): SucceedPaymentRequest { + return SucceedPaymentRequest( paymentType = paymentType, orderId = orderId, paymentKey = paymentKey, From 9094d1e96e840de28c91fd23cb9ba62cd7f99695 Mon Sep 17 00:00:00 2001 From: Combi153 Date: Mon, 4 Mar 2024 15:58:07 +0900 Subject: [PATCH 20/26] =?UTF-8?q?refactor:=20=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/petqua/application/payment/PaymentFacadeService.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/petqua/application/payment/PaymentFacadeService.kt b/src/main/kotlin/com/petqua/application/payment/PaymentFacadeService.kt index a12c2c58..8ac36666 100644 --- a/src/main/kotlin/com/petqua/application/payment/PaymentFacadeService.kt +++ b/src/main/kotlin/com/petqua/application/payment/PaymentFacadeService.kt @@ -21,14 +21,14 @@ class PaymentFacadeService( } fun failPayment(command: FailPaymentCommand): FailPaymentResponse { - log(command) + logFailPayment(command) if (command.code != PAY_PROCESS_CANCELED) { paymentService.cancelOrder(command.memberId, command.toOrderNumber()) } return FailPaymentResponse(command.code, command.message) } - private fun log(command: FailPaymentCommand) { + private fun logFailPayment(command: FailPaymentCommand) { when (command.code) { PAY_PROCESS_ABORTED -> log.error("PG사에서 결제를 중단했습니다. message: ${command.message}, OrderNumber: ${command.orderNumber}, MemberId: ${command.memberId}") PAY_PROCESS_CANCELED -> log.warn("사용자가 결제를 중단했습니다. message: ${command.message}, OrderNumber: ${command.orderNumber}, MemberId: ${command.memberId}") From e7d7a59b66eea207a168bc5bdb7105a288d792ab Mon Sep 17 00:00:00 2001 From: Combi153 Date: Mon, 4 Mar 2024 16:47:50 +0900 Subject: [PATCH 21/26] =?UTF-8?q?feat:=20Order=20=EC=A0=95=EC=A0=81=20?= =?UTF-8?q?=ED=8C=A9=ED=84=B0=EB=A6=AC=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/petqua/domain/order/OrderNumber.kt | 9 +++++++ .../payment/PaymentFacadeServiceTest.kt | 8 +++--- .../petqua/domain/order/OrderNumberTest.kt | 27 +++++++++++++++++++ .../payment/PaymentControllerTest.kt | 6 ++--- .../com/petqua/test/fixture/OrderFixture.kt | 2 +- .../petqua/test/fixture/PaymentFixtures.kt | 2 +- 6 files changed, 45 insertions(+), 9 deletions(-) create mode 100644 src/test/kotlin/com/petqua/domain/order/OrderNumberTest.kt diff --git a/src/main/kotlin/com/petqua/domain/order/OrderNumber.kt b/src/main/kotlin/com/petqua/domain/order/OrderNumber.kt index b05992c4..ba2cd95a 100644 --- a/src/main/kotlin/com/petqua/domain/order/OrderNumber.kt +++ b/src/main/kotlin/com/petqua/domain/order/OrderNumber.kt @@ -1,11 +1,15 @@ package com.petqua.domain.order +import com.petqua.common.util.throwExceptionWhen +import com.petqua.exception.order.OrderException +import com.petqua.exception.order.OrderExceptionType.ORDER_NOT_FOUND import jakarta.persistence.Column import jakarta.persistence.Embeddable import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter import java.util.UUID +import java.util.regex.Pattern @Embeddable data class OrderNumber( @@ -14,6 +18,8 @@ data class OrderNumber( ) { companion object { + private val orderNumberPattern = Pattern.compile("^\\d{14}[A-Z0-9]{12}\$") // 숫자 14자 + 숫자 or 대문자 12자 + fun generate(): OrderNumber { // 202402211607026029E90DB030 val createdTime = Instant.now().atZone(ZoneId.of("Asia/Seoul")).toLocalDateTime() val uuid = UUID.randomUUID().toString().replace("-", "").substring(0, 12).uppercase() @@ -21,6 +27,9 @@ data class OrderNumber( } fun from(orderNumber: String): OrderNumber { + throwExceptionWhen(!orderNumberPattern.matcher(orderNumber).matches()) { + OrderException(ORDER_NOT_FOUND) + } return OrderNumber(orderNumber) } } diff --git a/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt b/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt index 96d3a562..f5ef9064 100644 --- a/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt +++ b/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt @@ -52,7 +52,7 @@ class PaymentFacadeServiceTest( val order = orderRepository.save( order( memberId = member.id, - orderNumber = OrderNumber.from("orderNumber"), + orderNumber = OrderNumber.from("202402211607020ORDERNUMBER"), totalAmount = ONE ) ) @@ -96,7 +96,7 @@ class PaymentFacadeServiceTest( } When("존재하지 않는 주문이면") { - val orderNumber = OrderNumber.from("wrongOrderNumber") + val orderNumber = OrderNumber.from("20240221160702ORDERNUMBER0") Then("예외를 던진다") { shouldThrow { @@ -173,7 +173,7 @@ class PaymentFacadeServiceTest( val order = orderRepository.save( order( memberId = member.id, - orderNumber = OrderNumber.from("orderNumber"), + orderNumber = OrderNumber.from("202402211607020ORDERNUMBER"), totalAmount = ONE ) ) @@ -225,7 +225,7 @@ class PaymentFacadeServiceTest( val order = orderRepository.save( order( memberId = member.id, - orderNumber = OrderNumber.from("orderNumber"), + orderNumber = OrderNumber.from("202402211607020ORDERNUMBER"), totalAmount = ONE ) ) diff --git a/src/test/kotlin/com/petqua/domain/order/OrderNumberTest.kt b/src/test/kotlin/com/petqua/domain/order/OrderNumberTest.kt new file mode 100644 index 00000000..df44f385 --- /dev/null +++ b/src/test/kotlin/com/petqua/domain/order/OrderNumberTest.kt @@ -0,0 +1,27 @@ +package com.petqua.domain.order + +import com.petqua.exception.order.OrderException +import com.petqua.exception.order.OrderExceptionType.ORDER_NOT_FOUND +import io.kotest.assertions.throwables.shouldNotThrow +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe + +class OrderNumberTest : StringSpec({ + + "주문번호 생성 시 주문번호 형식에 맞는지 검증한다" { + val orderNumber = OrderNumber.generate().value + + shouldNotThrow { + OrderNumber.from(orderNumber) + } + } + + "주문번호 생성 시 주문번호 형식에 맞지 않다면 예외를 던진다" { + val invalidOrderNumber = "2024030416391INVALID" + + shouldThrow { + OrderNumber.from(invalidOrderNumber) + }.exceptionType() shouldBe ORDER_NOT_FOUND + } +}) diff --git a/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerTest.kt b/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerTest.kt index 711efb24..1e72e2d8 100644 --- a/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerTest.kt +++ b/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerTest.kt @@ -47,7 +47,7 @@ class PaymentControllerTest( val order = orderRepository.save( order( memberId = memberId, - orderNumber = OrderNumber.from("orderNumber"), + orderNumber = OrderNumber.from("202402211607020ORDERNUMBER"), totalAmount = ONE ) ) @@ -178,7 +178,7 @@ class PaymentControllerTest( orderRepository.save( order( memberId = memberId, - orderNumber = OrderNumber.from("orderNumber"), + orderNumber = OrderNumber.from("202402211607020ORDERNUMBER"), totalAmount = ONE ) ) @@ -232,7 +232,7 @@ class PaymentControllerTest( val order = orderRepository.save( order( memberId = memberId, - orderNumber = OrderNumber.from("orderNumber"), + orderNumber = OrderNumber.from("202402211607020ORDERNUMBER"), totalAmount = ONE ) ) diff --git a/src/test/kotlin/com/petqua/test/fixture/OrderFixture.kt b/src/test/kotlin/com/petqua/test/fixture/OrderFixture.kt index 222e063f..59be3de4 100644 --- a/src/test/kotlin/com/petqua/test/fixture/OrderFixture.kt +++ b/src/test/kotlin/com/petqua/test/fixture/OrderFixture.kt @@ -24,7 +24,7 @@ private const val DEFAULT_SCALE = 2 fun order( id: Long = 0L, memberId: Long = 0L, - orderNumber: OrderNumber = OrderNumber.from("202402211607026029E90DB030"), + orderNumber: OrderNumber = OrderNumber.from("202402211607020ORDERNUMBER"), orderName: OrderName = OrderName("상품1"), receiver: String = "receiver", phoneNumber: String = "010-1234-5678", diff --git a/src/test/kotlin/com/petqua/test/fixture/PaymentFixtures.kt b/src/test/kotlin/com/petqua/test/fixture/PaymentFixtures.kt index c9bd1e6b..53ecbca4 100644 --- a/src/test/kotlin/com/petqua/test/fixture/PaymentFixtures.kt +++ b/src/test/kotlin/com/petqua/test/fixture/PaymentFixtures.kt @@ -13,7 +13,7 @@ import java.math.BigDecimal.ZERO fun succeedPaymentCommand( memberId: Long = 0L, paymentType: TossPaymentType = NORMAL, - orderNumber: OrderNumber = OrderNumber.from("OrderNumber"), + orderNumber: OrderNumber = OrderNumber.from("202402211607020ORDERNUMBER"), paymentKey: String = "paymentKey", amount: BigDecimal = ZERO, ): SucceedPaymentCommand { From 9059ddb8c3b4d873a2cae9ad3a4dae32452108f1 Mon Sep 17 00:00:00 2001 From: Combi153 Date: Mon, 4 Mar 2024 16:56:16 +0900 Subject: [PATCH 22/26] =?UTF-8?q?refactor:=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=B0=8F=20=EC=88=9C=EC=84=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/com/petqua/application/payment/PaymentDtos.kt | 4 ++-- .../kotlin/com/petqua/exception/order/OrderExceptionType.kt | 2 +- .../com/petqua/exception/payment/FailPaymentExceptionType.kt | 3 +-- .../petqua/application/payment/PaymentFacadeServiceTest.kt | 4 ++-- .../com/petqua/presentation/payment/PaymentControllerTest.kt | 4 ++-- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/com/petqua/application/payment/PaymentDtos.kt b/src/main/kotlin/com/petqua/application/payment/PaymentDtos.kt index e992ca99..05af9714 100644 --- a/src/main/kotlin/com/petqua/application/payment/PaymentDtos.kt +++ b/src/main/kotlin/com/petqua/application/payment/PaymentDtos.kt @@ -4,7 +4,7 @@ import com.petqua.domain.order.OrderNumber import com.petqua.domain.payment.tosspayment.TossPaymentType import com.petqua.exception.payment.FailPaymentCode import com.petqua.exception.payment.FailPaymentException -import com.petqua.exception.payment.FailPaymentExceptionType.INVALID_ORDER_ID +import com.petqua.exception.payment.FailPaymentExceptionType.ORDER_NUMBER_MISSING_EXCEPTION import java.math.BigDecimal data class SucceedPaymentCommand( @@ -49,7 +49,7 @@ data class FailPaymentCommand( ) { fun toOrderNumber(): OrderNumber { - return orderNumber?.let { OrderNumber.from(it) } ?: throw FailPaymentException(INVALID_ORDER_ID) + return orderNumber?.let { OrderNumber.from(it) } ?: throw FailPaymentException(ORDER_NUMBER_MISSING_EXCEPTION) } companion object { diff --git a/src/main/kotlin/com/petqua/exception/order/OrderExceptionType.kt b/src/main/kotlin/com/petqua/exception/order/OrderExceptionType.kt index 46608f8e..0ece16fe 100644 --- a/src/main/kotlin/com/petqua/exception/order/OrderExceptionType.kt +++ b/src/main/kotlin/com/petqua/exception/order/OrderExceptionType.kt @@ -14,8 +14,8 @@ enum class OrderExceptionType( PRODUCT_NOT_FOUND(BAD_REQUEST, "O01", "주문한 상품이 존재하지 않습니다."), - ORDER_NOT_FOUND(NOT_FOUND, "O11", "존재하지 않는 주문입니다."), ORDER_PRICE_NOT_MATCH(BAD_REQUEST, "O10", "주문한 상품의 가격이 일치하지 않습니다."), + ORDER_NOT_FOUND(NOT_FOUND, "O11", "존재하지 않는 주문입니다."), PAYMENT_PRICE_NOT_MATCH(BAD_REQUEST, "O12", "주문한 상품 가격과 결제 금액이 일치하지 않습니다."), INVALID_PAYMENT_TYPE(BAD_REQUEST, "O20", "유효하지 않은 결제 방식입니다."), diff --git a/src/main/kotlin/com/petqua/exception/payment/FailPaymentExceptionType.kt b/src/main/kotlin/com/petqua/exception/payment/FailPaymentExceptionType.kt index 91411eb1..8fcf15a1 100644 --- a/src/main/kotlin/com/petqua/exception/payment/FailPaymentExceptionType.kt +++ b/src/main/kotlin/com/petqua/exception/payment/FailPaymentExceptionType.kt @@ -11,8 +11,7 @@ enum class FailPaymentExceptionType( ) : BaseExceptionType { INVALID_CODE(BAD_REQUEST, "PF01", "지원하지 않는 결제 실패 코드입니다."), - - INVALID_ORDER_ID(BAD_REQUEST, "PF01", "지원하지 않는 결제 실패 코드입니다."), + ORDER_NUMBER_MISSING_EXCEPTION(BAD_REQUEST, "PF02", "주문번호가 입력되지 않았습니다."), ; override fun httpStatus(): HttpStatus { diff --git a/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt b/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt index f5ef9064..1b68f410 100644 --- a/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt +++ b/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt @@ -17,7 +17,7 @@ import com.petqua.exception.payment.FailPaymentCode.PAY_PROCESS_ABORTED import com.petqua.exception.payment.FailPaymentCode.PAY_PROCESS_CANCELED import com.petqua.exception.payment.FailPaymentException import com.petqua.exception.payment.FailPaymentExceptionType.INVALID_CODE -import com.petqua.exception.payment.FailPaymentExceptionType.INVALID_ORDER_ID +import com.petqua.exception.payment.FailPaymentExceptionType.ORDER_NUMBER_MISSING_EXCEPTION import com.petqua.exception.payment.PaymentException import com.petqua.exception.payment.PaymentExceptionType.UNAUTHORIZED_KEY import com.petqua.test.DataCleaner @@ -267,7 +267,7 @@ class PaymentFacadeServiceTest( orderNumber = orderNumber, ) ) - }.exceptionType() shouldBe INVALID_ORDER_ID + }.exceptionType() shouldBe ORDER_NUMBER_MISSING_EXCEPTION } } diff --git a/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerTest.kt b/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerTest.kt index 1e72e2d8..4dc140ad 100644 --- a/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerTest.kt +++ b/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerTest.kt @@ -14,7 +14,7 @@ import com.petqua.exception.order.OrderExceptionType.PAYMENT_PRICE_NOT_MATCH import com.petqua.exception.payment.FailPaymentCode.PAY_PROCESS_ABORTED import com.petqua.exception.payment.FailPaymentCode.PAY_PROCESS_CANCELED import com.petqua.exception.payment.FailPaymentExceptionType.INVALID_CODE -import com.petqua.exception.payment.FailPaymentExceptionType.INVALID_ORDER_ID +import com.petqua.exception.payment.FailPaymentExceptionType.ORDER_NUMBER_MISSING_EXCEPTION import com.petqua.exception.payment.PaymentExceptionType.UNAUTHORIZED_KEY import com.petqua.test.ApiTestConfig import com.petqua.test.fixture.order @@ -275,7 +275,7 @@ class PaymentControllerTest( assertSoftly(exceptionResponse) { response.statusCode shouldBe BAD_REQUEST.value() - exceptionResponse.message shouldBe INVALID_ORDER_ID.errorMessage() + exceptionResponse.message shouldBe ORDER_NUMBER_MISSING_EXCEPTION.errorMessage() } } } From d8c0f1711bf1d1d24a5e413e37b76f8d815b40aa Mon Sep 17 00:00:00 2001 From: Combi153 Date: Tue, 5 Mar 2024 11:35:53 +0900 Subject: [PATCH 23/26] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20par?= =?UTF-8?q?ams=20=EC=83=9D=EC=84=B1=20=EB=A1=9C=EC=A7=81=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../payment/PaymentControllerSteps.kt | 10 +++---- .../product/ProductControllerSteps.kt | 26 +++++++++---------- .../category/CategoryControllerSteps.kt | 22 ++++++++-------- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerSteps.kt b/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerSteps.kt index 1da2f9fb..c47eba30 100644 --- a/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerSteps.kt +++ b/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerSteps.kt @@ -32,11 +32,11 @@ fun requestFailPayment( accessToken: String, failPaymentRequest: FailPaymentRequest, ): Response { - val paramMap = mutableMapOf().apply { - put("code", failPaymentRequest.code) - put("message", failPaymentRequest.message) - put("orderId", failPaymentRequest.orderId) - }.filterValues { it != null } + val paramMap = mapOf( + "code" to failPaymentRequest.code, + "message" to failPaymentRequest.message, + "orderId" to failPaymentRequest.orderId, + ).filterValues { it != null } return Given { log().all() diff --git a/src/test/kotlin/com/petqua/presentation/product/ProductControllerSteps.kt b/src/test/kotlin/com/petqua/presentation/product/ProductControllerSteps.kt index b551966a..08d2e57c 100644 --- a/src/test/kotlin/com/petqua/presentation/product/ProductControllerSteps.kt +++ b/src/test/kotlin/com/petqua/presentation/product/ProductControllerSteps.kt @@ -14,7 +14,7 @@ import io.restassured.response.Response fun requestReadProductById( productId: Long, - accessToken: String? = null + accessToken: String? = null, ): Response { return Given { log().all() @@ -59,10 +59,10 @@ fun requestReadProductKeyword( limit: Int = PAGING_LIMIT_CEILING, ): Response { return Given { - val paramMap = mutableMapOf().apply { - put("word", word) - put("limit", limit) - }.filterValues { it != null } + val paramMap = mapOf( + "word" to word, + "limit" to limit, + ).filterValues { it != null } log().all() params(paramMap) @@ -81,15 +81,15 @@ fun requestReadProductBySearch( sorter: String = Sorter.NONE.name, lastViewedId: Long = DEFAULT_LAST_VIEWED_ID, limit: Int = PAGING_LIMIT_CEILING, - accessToken: String? = null + accessToken: String? = null, ): Response { - val paramMap = mutableMapOf().apply { - put("word", word) - put("deliveryMethod", deliveryMethod.name) - put("sorter", sorter) - put("lastViewedId", lastViewedId) - put("limit", limit) - }.filterValues { it != null } + val paramMap = mapOf( + "word" to word, + "deliveryMethod" to deliveryMethod.name, + "sorter" to sorter, + "lastViewedId" to lastViewedId, + "limit" to limit, + ).filterValues { it != null } return Given { log().all() diff --git a/src/test/kotlin/com/petqua/presentation/product/category/CategoryControllerSteps.kt b/src/test/kotlin/com/petqua/presentation/product/category/CategoryControllerSteps.kt index 483d3f01..c49c70db 100644 --- a/src/test/kotlin/com/petqua/presentation/product/category/CategoryControllerSteps.kt +++ b/src/test/kotlin/com/petqua/presentation/product/category/CategoryControllerSteps.kt @@ -12,9 +12,9 @@ import io.restassured.response.Response fun requestReadSpecies( family: String? = null, ): Response { - val paramMap = mutableMapOf().apply { - put("family", family) - }.filterValues { it != null } + val paramMap = mapOf( + "family" to family + ).filterValues { it != null } return Given { log().all() @@ -37,14 +37,14 @@ fun requestReadProducts( limit: Int? = null, accessToken: String? = null, ): Response { - val paramMap = mutableMapOf().apply { - put("family", family) - put("species", species.takeIf { it.isNotEmpty() }) - put("deliveryMethod", deliveryMethod.name) - put("sorter", sorter) - put("lastViewedId", lastViewedId) - put("limit", limit) - }.filterValues { it != null } + val paramMap = mapOf( + "family" to family, + "species" to species.takeIf { it.isNotEmpty() }, + "deliveryMethod" to deliveryMethod.name, + "sorter" to sorter, + "lastViewedId" to lastViewedId, + "limit" to limit, + ).filterValues { it != null } return Given { log().all() From b1199176c8c0927fa1040bc80bde226baf453fb2 Mon Sep 17 00:00:00 2001 From: Combi153 Date: Tue, 5 Mar 2024 12:13:29 +0900 Subject: [PATCH 24/26] =?UTF-8?q?refactor:=20=EA=B2=B0=EC=A0=9C=20?= =?UTF-8?q?=EC=8A=B9=EC=9D=B8=20=EC=8B=9C=20=EC=A3=BC=EB=AC=B8=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EB=A5=BC=20=EB=B3=80=EA=B2=BD=ED=95=98=EA=B3=A0=20Ord?= =?UTF-8?q?erPayment=20=EC=A0=80=EC=9E=A5=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../payment/PaymentFacadeService.kt | 2 +- .../application/payment/PaymentService.kt | 18 ++++++- .../kotlin/com/petqua/domain/order/Order.kt | 8 +++ .../domain/order/OrderPaymentRepository.kt | 5 ++ .../com/petqua/domain/order/OrderStatus.kt | 5 ++ .../exception/order/OrderExceptionType.kt | 2 + .../payment/PaymentFacadeServiceTest.kt | 49 ++++++++++++++++- .../com/petqua/domain/order/OrderTest.kt | 23 ++++++++ .../payment/PaymentControllerTest.kt | 53 +++++++++++++++++++ 9 files changed, 161 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/com/petqua/domain/order/OrderPaymentRepository.kt diff --git a/src/main/kotlin/com/petqua/application/payment/PaymentFacadeService.kt b/src/main/kotlin/com/petqua/application/payment/PaymentFacadeService.kt index 8ac36666..3f4f5386 100644 --- a/src/main/kotlin/com/petqua/application/payment/PaymentFacadeService.kt +++ b/src/main/kotlin/com/petqua/application/payment/PaymentFacadeService.kt @@ -17,7 +17,7 @@ class PaymentFacadeService( fun succeedPayment(command: SucceedPaymentCommand) { paymentService.validateAmount(command) val paymentResponse = paymentGatewayService.confirmPayment(command.toPaymentConfirmRequest()) - paymentService.save(paymentResponse.toPayment()) + paymentService.processPayment(paymentResponse.toPayment()) } fun failPayment(command: FailPaymentCommand): FailPaymentResponse { diff --git a/src/main/kotlin/com/petqua/application/payment/PaymentService.kt b/src/main/kotlin/com/petqua/application/payment/PaymentService.kt index 06405100..d3cf53ee 100644 --- a/src/main/kotlin/com/petqua/application/payment/PaymentService.kt +++ b/src/main/kotlin/com/petqua/application/payment/PaymentService.kt @@ -1,6 +1,8 @@ package com.petqua.application.payment import com.petqua.domain.order.OrderNumber +import com.petqua.domain.order.OrderPayment +import com.petqua.domain.order.OrderPaymentRepository import com.petqua.domain.order.OrderRepository import com.petqua.domain.order.findByOrderNumberOrThrow import com.petqua.domain.payment.tosspayment.TossPayment @@ -14,6 +16,7 @@ import org.springframework.transaction.annotation.Transactional @Service class PaymentService( private val orderRepository: OrderRepository, + private val orderPaymentRepository: OrderPaymentRepository, private val paymentRepository: TossPaymentRepository, ) { @@ -26,8 +29,19 @@ class PaymentService( order.validateAmount(command.amount.setScale(2)) } - fun save(tossPayment: TossPayment): TossPayment { - return paymentRepository.save(tossPayment) + fun processPayment(tossPayment: TossPayment): OrderPayment { + val order = orderRepository.findByOrderNumberOrThrow(tossPayment.orderNumber) { + OrderException(ORDER_NOT_FOUND) + } + order.pay() + + val payment = paymentRepository.save(tossPayment) + return orderPaymentRepository.save( + OrderPayment( + orderId = order.id, + tossPaymentId = payment.id + ) + ) } fun cancelOrder(memberId: Long, orderNumber: OrderNumber) { diff --git a/src/main/kotlin/com/petqua/domain/order/Order.kt b/src/main/kotlin/com/petqua/domain/order/Order.kt index ce6219fb..41da0e72 100644 --- a/src/main/kotlin/com/petqua/domain/order/Order.kt +++ b/src/main/kotlin/com/petqua/domain/order/Order.kt @@ -4,6 +4,7 @@ import com.petqua.common.domain.BaseEntity import com.petqua.common.util.throwExceptionWhen import com.petqua.exception.order.OrderException import com.petqua.exception.order.OrderExceptionType.FORBIDDEN_ORDER +import com.petqua.exception.order.OrderExceptionType.ORDER_CAN_NOT_PAY import com.petqua.exception.order.OrderExceptionType.PAYMENT_PRICE_NOT_MATCH import jakarta.persistence.AttributeOverride import jakarta.persistence.Column @@ -70,4 +71,11 @@ class Order( // } status = OrderStatus.CANCELED } + + fun pay() { + throwExceptionWhen(!status.isAbleToPay()) { + throw OrderException(ORDER_CAN_NOT_PAY) + } + status = OrderStatus.PAYMENT_CONFIRMED + } } diff --git a/src/main/kotlin/com/petqua/domain/order/OrderPaymentRepository.kt b/src/main/kotlin/com/petqua/domain/order/OrderPaymentRepository.kt new file mode 100644 index 00000000..14ba41bb --- /dev/null +++ b/src/main/kotlin/com/petqua/domain/order/OrderPaymentRepository.kt @@ -0,0 +1,5 @@ +package com.petqua.domain.order + +import org.springframework.data.jpa.repository.JpaRepository + +interface OrderPaymentRepository : JpaRepository diff --git a/src/main/kotlin/com/petqua/domain/order/OrderStatus.kt b/src/main/kotlin/com/petqua/domain/order/OrderStatus.kt index e88bf161..589f8162 100644 --- a/src/main/kotlin/com/petqua/domain/order/OrderStatus.kt +++ b/src/main/kotlin/com/petqua/domain/order/OrderStatus.kt @@ -18,4 +18,9 @@ enum class OrderStatus( EXCHANGE_COMPLETED("교환 완료"), REFUND_REQUESTED("환불 요청"), REFUND_COMPLETED("환불 완료"), + ; + + fun isAbleToPay(): Boolean { + return this == ORDER_CREATED + } } diff --git a/src/main/kotlin/com/petqua/exception/order/OrderExceptionType.kt b/src/main/kotlin/com/petqua/exception/order/OrderExceptionType.kt index 0ece16fe..2ad58b67 100644 --- a/src/main/kotlin/com/petqua/exception/order/OrderExceptionType.kt +++ b/src/main/kotlin/com/petqua/exception/order/OrderExceptionType.kt @@ -21,7 +21,9 @@ enum class OrderExceptionType( INVALID_PAYMENT_TYPE(BAD_REQUEST, "O20", "유효하지 않은 결제 방식입니다."), FORBIDDEN_ORDER(FORBIDDEN, "O30", "해당 주문에 대한 권한이 없습니다."), + ORDER_CAN_NOT_CANCEL(BAD_REQUEST, "O31", "취소할 수 없는 주문입니다."), + ORDER_CAN_NOT_PAY(BAD_REQUEST, "O32", "결제할 수 없는 주문입니다."), ; override fun httpStatus(): HttpStatus { diff --git a/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt b/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt index 1b68f410..6c76b922 100644 --- a/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt +++ b/src/test/kotlin/com/petqua/application/payment/PaymentFacadeServiceTest.kt @@ -5,12 +5,15 @@ import com.petqua.application.payment.infra.TossPaymentsApiClient import com.petqua.common.domain.findByIdOrThrow import com.petqua.domain.member.MemberRepository import com.petqua.domain.order.OrderNumber +import com.petqua.domain.order.OrderPaymentRepository import com.petqua.domain.order.OrderRepository import com.petqua.domain.order.OrderStatus.CANCELED import com.petqua.domain.order.OrderStatus.ORDER_CREATED +import com.petqua.domain.order.OrderStatus.PAYMENT_CONFIRMED import com.petqua.domain.payment.tosspayment.TossPaymentRepository import com.petqua.exception.order.OrderException import com.petqua.exception.order.OrderExceptionType.FORBIDDEN_ORDER +import com.petqua.exception.order.OrderExceptionType.ORDER_CAN_NOT_PAY import com.petqua.exception.order.OrderExceptionType.ORDER_NOT_FOUND import com.petqua.exception.order.OrderExceptionType.PAYMENT_PRICE_NOT_MATCH import com.petqua.exception.payment.FailPaymentCode.PAY_PROCESS_ABORTED @@ -43,6 +46,7 @@ class PaymentFacadeServiceTest( private val memberRepository: MemberRepository, private val orderRepository: OrderRepository, private val paymentRepository: TossPaymentRepository, + private val orderPaymentRepository: OrderPaymentRepository, private val dataCleaner: DataCleaner, @SpykBean private val tossPaymentsApiClient: TossPaymentsApiClient, ) : BehaviorSpec({ @@ -82,7 +86,7 @@ class PaymentFacadeServiceTest( ) ) - Then("Payment 객체를 생성한다") { + Then("TossPayment 객체를 생성한다") { val payments = paymentRepository.findAll() assertSoftly { @@ -93,6 +97,26 @@ class PaymentFacadeServiceTest( payment.totalAmount shouldBe order.totalAmount.setScale(2) } } + + Then("Order의 상태를 변경한다") { + val updatedOrder = orderRepository.findByIdOrThrow(order.id) + + assertSoftly { + updatedOrder.status shouldBe PAYMENT_CONFIRMED + } + } + + Then("OrderPayment 객체를 생성한다") { + val orderPayments = orderPaymentRepository.findAll() + + assertSoftly { + orderPayments.size shouldBe 1 + val orderPayment = orderPayments[0] + + orderPayment.orderId shouldBe order.id + orderPayment.tossPaymentId shouldBe paymentRepository.findAll()[0].id + } + } } When("존재하지 않는 주문이면") { @@ -111,6 +135,29 @@ class PaymentFacadeServiceTest( } } + When("결제할 수 없는 주문이면") { + val invalidOrder = orderRepository.save( + order( + memberId = member.id, + orderNumber = OrderNumber.from("202402211607021ORDERNUMBER"), + totalAmount = ONE, + status = PAYMENT_CONFIRMED, + ) + ) + + Then("예외를 던진다") { + shouldThrow { + paymentFacadeService.succeedPayment( + command = succeedPaymentCommand( + memberId = invalidOrder.memberId, + orderNumber = invalidOrder.orderNumber, + amount = invalidOrder.totalAmount, + ) + ) + }.exceptionType() shouldBe ORDER_CAN_NOT_PAY + } + } + When("권한이 없는 회원이면") { val memberId = MIN_VALUE diff --git a/src/test/kotlin/com/petqua/domain/order/OrderTest.kt b/src/test/kotlin/com/petqua/domain/order/OrderTest.kt index f26161b1..0cb7c964 100644 --- a/src/test/kotlin/com/petqua/domain/order/OrderTest.kt +++ b/src/test/kotlin/com/petqua/domain/order/OrderTest.kt @@ -1,7 +1,10 @@ package com.petqua.domain.order +import com.petqua.domain.order.OrderStatus.CANCELED +import com.petqua.domain.order.OrderStatus.ORDER_CREATED import com.petqua.exception.order.OrderException import com.petqua.exception.order.OrderExceptionType.FORBIDDEN_ORDER +import com.petqua.exception.order.OrderExceptionType.ORDER_CAN_NOT_PAY import com.petqua.exception.order.OrderExceptionType.PAYMENT_PRICE_NOT_MATCH import com.petqua.test.fixture.order import io.kotest.assertions.throwables.shouldNotThrow @@ -53,4 +56,24 @@ class OrderTest : StringSpec({ order.validateOwner(MIN_VALUE) }.exceptionType() shouldBe FORBIDDEN_ORDER } + + "결제 처리를 한다" { + val order = order( + status = ORDER_CREATED + ) + + shouldNotThrow { + order.pay() + } + } + + "결제 처리 시 결제를 할 수 없다면 예외를 던진다" { + val order = order( + status = CANCELED + ) + + shouldThrow { + order.pay() + }.exceptionType() shouldBe ORDER_CAN_NOT_PAY + } }) diff --git a/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerTest.kt b/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerTest.kt index 4dc140ad..4bbd1220 100644 --- a/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerTest.kt +++ b/src/test/kotlin/com/petqua/presentation/payment/PaymentControllerTest.kt @@ -4,11 +4,15 @@ import com.ninjasquad.springmockk.SpykBean import com.petqua.application.payment.FailPaymentResponse import com.petqua.application.payment.PaymentConfirmRequestToPG import com.petqua.application.payment.infra.TossPaymentsApiClient +import com.petqua.common.domain.findByIdOrThrow import com.petqua.common.exception.ExceptionResponse import com.petqua.domain.order.OrderNumber +import com.petqua.domain.order.OrderPaymentRepository import com.petqua.domain.order.OrderRepository +import com.petqua.domain.order.OrderStatus import com.petqua.domain.payment.tosspayment.TossPaymentRepository import com.petqua.exception.order.OrderExceptionType.FORBIDDEN_ORDER +import com.petqua.exception.order.OrderExceptionType.ORDER_CAN_NOT_PAY import com.petqua.exception.order.OrderExceptionType.ORDER_NOT_FOUND import com.petqua.exception.order.OrderExceptionType.PAYMENT_PRICE_NOT_MATCH import com.petqua.exception.payment.FailPaymentCode.PAY_PROCESS_ABORTED @@ -35,6 +39,7 @@ import java.math.BigDecimal.ONE class PaymentControllerTest( private val orderRepository: OrderRepository, private val paymentRepository: TossPaymentRepository, + private val orderPaymentRepository: OrderPaymentRepository, @SpykBean private val tossPaymentsApiClient: TossPaymentsApiClient, ) : ApiTestConfig() { @@ -76,6 +81,26 @@ class PaymentControllerTest( payment.totalAmount shouldBe order.totalAmount.setScale(2) } } + + Then("Order의 상태를 변경한다") { + val updatedOrder = orderRepository.findByIdOrThrow(order.id) + + assertSoftly { + updatedOrder.status shouldBe OrderStatus.PAYMENT_CONFIRMED + } + } + + Then("OrderPayment 객체를 저장한다") { + val orderPayments = orderPaymentRepository.findAll() + + assertSoftly { + orderPayments.size shouldBe 1 + val payment = orderPayments[0] + + payment.orderId shouldBe order.id + payment.tossPaymentId shouldBe paymentRepository.findAll()[0].id + } + } } When("존재하지 않는 주문이면") { @@ -99,6 +124,34 @@ class PaymentControllerTest( } } + When("결제할 수 없는 주문이면") { + val invalidOrder = orderRepository.save( + order( + memberId = memberId, + orderNumber = OrderNumber.from("202402211607021ORDERNUMBER"), + totalAmount = ONE, + status = OrderStatus.PAYMENT_CONFIRMED, + ) + ) + + val response = requestSucceedPayment( + accessToken = accessToken, + succeedPaymentRequest = succeedPaymentRequest( + orderId = invalidOrder.orderNumber.value, + amount = invalidOrder.totalAmount + ) + ) + + Then("예외를 응답한다") { + val errorResponse = response.`as`(ExceptionResponse::class.java) + + assertSoftly(response) { + statusCode shouldBe BAD_REQUEST.value() + errorResponse.message shouldBe ORDER_CAN_NOT_PAY.errorMessage() + } + } + } + When("권한이 없는 회원의 요청이면") { val otherAccessToken = signInAsMember().accessToken From 67eef66419f2c3eecf8966915de99f163d7fa483 Mon Sep 17 00:00:00 2001 From: Combi153 Date: Wed, 6 Mar 2024 15:22:01 +0900 Subject: [PATCH 25/26] =?UTF-8?q?refactor:=20PaymentExceptionType=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/payment/PaymentExceptionType.kt | 94 +++++++++---------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/src/main/kotlin/com/petqua/exception/payment/PaymentExceptionType.kt b/src/main/kotlin/com/petqua/exception/payment/PaymentExceptionType.kt index 573f1c7e..39ca777e 100644 --- a/src/main/kotlin/com/petqua/exception/payment/PaymentExceptionType.kt +++ b/src/main/kotlin/com/petqua/exception/payment/PaymentExceptionType.kt @@ -15,58 +15,58 @@ enum class PaymentExceptionType( private val errorMessage: String, ) : BaseExceptionType { - ALREADY_PROCESSED_PAYMENT(BAD_REQUEST, "P01", "이미 처리된 결제 입니다"), - PROVIDER_ERROR(BAD_REQUEST, "P02", "일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요."), - EXCEED_MAX_CARD_INSTALLMENT_PLAN(BAD_REQUEST, "P03", "설정 가능한 최대 할부 개월 수를 초과했습니다."), - INVALID_REQUEST(BAD_REQUEST, "P04", "잘못된 요청입니다."), - NOT_ALLOWED_POINT_USE(BAD_REQUEST, "P05", "포인트 사용이 불가한 카드로 카드 포인트 결제에 실패했습니다."), - INVALID_API_KEY(BAD_REQUEST, "P06", "잘못된 시크릿키 연동 정보 입니다."), - INVALID_REJECT_CARD(BAD_REQUEST, "P07", "카드 사용이 거절되었습니다. 카드사 문의가 필요합니다."), - BELOW_MINIMUM_AMOUNT(BAD_REQUEST, "P08", "신용카드는 결제금액이 100원 이상, 계좌는 200원이상부터 결제가 가능합니다."), - INVALID_CARD_EXPIRATION(BAD_REQUEST, "P09", "카드 정보를 다시 확인해주세요. (유효기간)"), - INVALID_STOPPED_CARD(BAD_REQUEST, "P10", "정지된 카드 입니다."), - EXCEED_MAX_DAILY_PAYMENT_COUNT(BAD_REQUEST, "P11", "하루 결제 가능 횟수를 초과했습니다."), - NOT_SUPPORTED_INSTALLMENT_PLAN_CARD_OR_MERCHANT(BAD_REQUEST, "P12", "할부가 지원되지 않는 카드 또는 가맹점 입니다."), - INVALID_CARD_INSTALLMENT_PLAN(BAD_REQUEST, "P13", "할부 개월 정보가 잘못되었습니다."), - NOT_SUPPORTED_MONTHLY_INSTALLMENT_PLAN(BAD_REQUEST, "P14", "할부가 지원되지 않는 카드입니다."), - EXCEED_MAX_PAYMENT_AMOUNT(BAD_REQUEST, "P15", "하루 결제 가능 금액을 초과했습니다."), - NOT_FOUND_TERMINAL_ID(BAD_REQUEST, "P16", "단말기번호(Terminal Id)가 없습니다. 토스페이먼츠로 문의 바랍니다."), - INVALID_AUTHORIZE_AUTH(BAD_REQUEST, "P17", "유효하지 않은 인증 방식입니다."), - INVALID_CARD_LOST_OR_STOLEN(BAD_REQUEST, "P18", "분실 혹은 도난 카드입니다."), - RESTRICTED_TRANSFER_ACCOUNT(BAD_REQUEST, "P19", "계좌는 등록 후 12시간 뒤부터 결제할 수 있습니다. 관련 정책은 해당 은행으로 문의해주세요."), - INVALID_CARD_NUMBER(BAD_REQUEST, "P20", "카드번호를 다시 확인해주세요."), - INVALID_UNREGISTERED_SUBMALL(BAD_REQUEST, "P21", "등록되지 않은 서브몰입니다. 서브몰이 없는 가맹점이라면 안심클릭이나 ISP 결제가 필요합니다."), - NOT_REGISTERED_BUSINESS(BAD_REQUEST, "P22", "등록되지 않은 사업자 번호입니다."), - EXCEED_MAX_ONE_DAY_WITHDRAW_AMOUNT(BAD_REQUEST, "P23", "1일 출금 한도를 초과했습니다."), - EXCEED_MAX_ONE_TIME_WITHDRAW_AMOUNT(BAD_REQUEST, "P24", "1회 출금 한도를 초과했습니다."), - CARD_PROCESSING_ERROR(BAD_REQUEST, "P25", "카드사에서 오류가 발생했습니다."), - EXCEED_MAX_AMOUNT(BAD_REQUEST, "P26", "거래금액 한도를 초과했습니다."), - INVALID_ACCOUNT_INFO_RE_REGISTER(BAD_REQUEST, "P27", "유효하지 않은 계좌입니다. 계좌 재등록 후 시도해주세요."), - NOT_AVAILABLE_PAYMENT(BAD_REQUEST, "P28", "결제가 불가능한 시간대입니다."), - UNAPPROVED_ORDER_ID(BAD_REQUEST, "P29", "아직 승인되지 않은 주문번호입니다."), + ALREADY_PROCESSED_PAYMENT(BAD_REQUEST, "PA01", "이미 처리된 결제 입니다"), + PROVIDER_ERROR(BAD_REQUEST, "PA02", "일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요."), + EXCEED_MAX_CARD_INSTALLMENT_PLAN(BAD_REQUEST, "PA03", "설정 가능한 최대 할부 개월 수를 초과했습니다."), + INVALID_REQUEST(BAD_REQUEST, "PA04", "잘못된 요청입니다."), + NOT_ALLOWED_POINT_USE(BAD_REQUEST, "PA05", "포인트 사용이 불가한 카드로 카드 포인트 결제에 실패했습니다."), + INVALID_API_KEY(BAD_REQUEST, "PA06", "잘못된 시크릿키 연동 정보 입니다."), + INVALID_REJECT_CARD(BAD_REQUEST, "PA07", "카드 사용이 거절되었습니다. 카드사 문의가 필요합니다."), + BELOW_MINIMUM_AMOUNT(BAD_REQUEST, "PA08", "신용카드는 결제금액이 100원 이상, 계좌는 200원이상부터 결제가 가능합니다."), + INVALID_CARD_EXPIRATION(BAD_REQUEST, "PA09", "카드 정보를 다시 확인해주세요. (유효기간)"), + INVALID_STOPPED_CARD(BAD_REQUEST, "PA10", "정지된 카드 입니다."), + EXCEED_MAX_DAILY_PAYMENT_COUNT(BAD_REQUEST, "PA11", "하루 결제 가능 횟수를 초과했습니다."), + NOT_SUPPORTED_INSTALLMENT_PLAN_CARD_OR_MERCHANT(BAD_REQUEST, "PA12", "할부가 지원되지 않는 카드 또는 가맹점 입니다."), + INVALID_CARD_INSTALLMENT_PLAN(BAD_REQUEST, "PA13", "할부 개월 정보가 잘못되었습니다."), + NOT_SUPPORTED_MONTHLY_INSTALLMENT_PLAN(BAD_REQUEST, "PA14", "할부가 지원되지 않는 카드입니다."), + EXCEED_MAX_PAYMENT_AMOUNT(BAD_REQUEST, "PA15", "하루 결제 가능 금액을 초과했습니다."), + NOT_FOUND_TERMINAL_ID(BAD_REQUEST, "PA16", "단말기번호(Terminal Id)가 없습니다. 토스페이먼츠로 문의 바랍니다."), + INVALID_AUTHORIZE_AUTH(BAD_REQUEST, "PA17", "유효하지 않은 인증 방식입니다."), + INVALID_CARD_LOST_OR_STOLEN(BAD_REQUEST, "PA18", "분실 혹은 도난 카드입니다."), + RESTRICTED_TRANSFER_ACCOUNT(BAD_REQUEST, "PA19", "계좌는 등록 후 12시간 뒤부터 결제할 수 있습니다. 관련 정책은 해당 은행으로 문의해주세요."), + INVALID_CARD_NUMBER(BAD_REQUEST, "PA20", "카드번호를 다시 확인해주세요."), + INVALID_UNREGISTERED_SUBMALL(BAD_REQUEST, "PA21", "등록되지 않은 서브몰입니다. 서브몰이 없는 가맹점이라면 안심클릭이나 ISP 결제가 필요합니다."), + NOT_REGISTERED_BUSINESS(BAD_REQUEST, "PA22", "등록되지 않은 사업자 번호입니다."), + EXCEED_MAX_ONE_DAY_WITHDRAW_AMOUNT(BAD_REQUEST, "PA23", "1일 출금 한도를 초과했습니다."), + EXCEED_MAX_ONE_TIME_WITHDRAW_AMOUNT(BAD_REQUEST, "PA24", "1회 출금 한도를 초과했습니다."), + CARD_PROCESSING_ERROR(BAD_REQUEST, "PA25", "카드사에서 오류가 발생했습니다."), + EXCEED_MAX_AMOUNT(BAD_REQUEST, "PA26", "거래금액 한도를 초과했습니다."), + INVALID_ACCOUNT_INFO_RE_REGISTER(BAD_REQUEST, "PA27", "유효하지 않은 계좌입니다. 계좌 재등록 후 시도해주세요."), + NOT_AVAILABLE_PAYMENT(BAD_REQUEST, "PA28", "결제가 불가능한 시간대입니다."), + UNAPPROVED_ORDER_ID(BAD_REQUEST, "PA29", "아직 승인되지 않은 주문번호입니다."), - UNAUTHORIZED_KEY(UNAUTHORIZED, "P30", "인증되지 않은 시크릿 키 혹은 클라이언트 키 입니다."), + UNAUTHORIZED_KEY(UNAUTHORIZED, "PA30", "인증되지 않은 시크릿 키 혹은 클라이언트 키 입니다."), - REJECT_ACCOUNT_PAYMENT(FORBIDDEN, "P31", "잔액부족으로 결제에 실패했습니다."), - REJECT_CARD_PAYMENT(FORBIDDEN, "P32", "한도초과 혹은 잔액부족으로 결제에 실패했습니다."), - REJECT_CARD_COMPANY(FORBIDDEN, "P33", "결제 승인이 거절되었습니다."), - FORBIDDEN_REQUEST(FORBIDDEN, "P34", "허용되지 않은 요청입니다."), - REJECT_TOSSPAY_INVALID_ACCOUNT(FORBIDDEN, "P35", "선택하신 출금 계좌가 출금이체 등록이 되어 있지 않아요. 계좌를 다시 등록해 주세요."), - EXCEED_MAX_AUTH_COUNT(FORBIDDEN, "P36", "최대 인증 횟수를 초과했습니다. 카드사로 문의해주세요."), - EXCEED_MAX_ONE_DAY_AMOUNT(FORBIDDEN, "P37", "일일 한도를 초과했습니다."), - NOT_AVAILABLE_BANK(FORBIDDEN, "P38", "은행 서비스 시간이 아닙니다."), - INVALID_PASSWORD(FORBIDDEN, "P39", "결제 비밀번호가 일치하지 않습니다."), - INCORRECT_BASIC_AUTH_FORMAT(FORBIDDEN, "P40", "잘못된 요청입니다. ':' 를 포함해 인코딩해주세요."), - FDS_ERROR(FORBIDDEN, "P41", "[토스페이먼츠] 위험거래가 감지되어 결제가 제한됩니다.발송된 문자에 포함된 링크를 통해 본인인증 후 결제가 가능합니다.(고객센터: 1644-8051)"), + REJECT_ACCOUNT_PAYMENT(FORBIDDEN, "PA31", "잔액부족으로 결제에 실패했습니다."), + REJECT_CARD_PAYMENT(FORBIDDEN, "PA32", "한도초과 혹은 잔액부족으로 결제에 실패했습니다."), + REJECT_CARD_COMPANY(FORBIDDEN, "PA33", "결제 승인이 거절되었습니다."), + FORBIDDEN_REQUEST(FORBIDDEN, "PA34", "허용되지 않은 요청입니다."), + REJECT_TOSSPAY_INVALID_ACCOUNT(FORBIDDEN, "PA35", "선택하신 출금 계좌가 출금이체 등록이 되어 있지 않아요. 계좌를 다시 등록해 주세요."), + EXCEED_MAX_AUTH_COUNT(FORBIDDEN, "PA36", "최대 인증 횟수를 초과했습니다. 카드사로 문의해주세요."), + EXCEED_MAX_ONE_DAY_AMOUNT(FORBIDDEN, "PA37", "일일 한도를 초과했습니다."), + NOT_AVAILABLE_BANK(FORBIDDEN, "PA38", "은행 서비스 시간이 아닙니다."), + INVALID_PASSWORD(FORBIDDEN, "PA39", "결제 비밀번호가 일치하지 않습니다."), + INCORRECT_BASIC_AUTH_FORMAT(FORBIDDEN, "PA40", "잘못된 요청입니다. ':' 를 포함해 인코딩해주세요."), + FDS_ERROR(FORBIDDEN, "PA41", "[토스페이먼츠] 위험거래가 감지되어 결제가 제한됩니다.발송된 문자에 포함된 링크를 통해 본인인증 후 결제가 가능합니다.(고객센터: 1644-8051)"), - NOT_FOUND_PAYMENT(NOT_FOUND, "P42", "존재하지 않는 결제 정보 입니다."), - NOT_FOUND_PAYMENT_SESSION(NOT_FOUND, "P43", "결제 시간이 만료되어 결제 진행 데이터가 존재하지 않습니다."), + NOT_FOUND_PAYMENT(NOT_FOUND, "PA42", "존재하지 않는 결제 정보 입니다."), + NOT_FOUND_PAYMENT_SESSION(NOT_FOUND, "PA43", "결제 시간이 만료되어 결제 진행 데이터가 존재하지 않습니다."), - FAILED_PAYMENT_INTERNAL_SYSTEM_PROCESSING(INTERNAL_SERVER_ERROR, "P44", "결제가 완료되지 않았어요. 다시 시도해주세요."), - FAILED_INTERNAL_SYSTEM_PROCESSING(INTERNAL_SERVER_ERROR, "P45", "내부 시스템 처리 작업이 실패했습니다. 잠시 후 다시 시도해주세요."), - UNKNOWN_PAYMENT_ERROR(INTERNAL_SERVER_ERROR, "P46", "결제에 실패했어요. 같은 문제가 반복된다면 은행이나 카드사로 문의해주세요."), + FAILED_PAYMENT_INTERNAL_SYSTEM_PROCESSING(INTERNAL_SERVER_ERROR, "PA44", "결제가 완료되지 않았어요. 다시 시도해주세요."), + FAILED_INTERNAL_SYSTEM_PROCESSING(INTERNAL_SERVER_ERROR, "PA45", "내부 시스템 처리 작업이 실패했습니다. 잠시 후 다시 시도해주세요."), + UNKNOWN_PAYMENT_ERROR(INTERNAL_SERVER_ERROR, "PA46", "결제에 실패했어요. 같은 문제가 반복된다면 은행이나 카드사로 문의해주세요."), - UNKNOWN_PAYMENT_EXCEPTION(INTERNAL_SERVER_ERROR, "P50", "지원하지 않는 결제 예외가 발생했습니다."), + UNKNOWN_PAYMENT_EXCEPTION(INTERNAL_SERVER_ERROR, "PA50", "지원하지 않는 결제 예외가 발생했습니다."), ; override fun httpStatus(): HttpStatus { From 6436c48640d95596f6666ce1be5139f4e7d8dccc Mon Sep 17 00:00:00 2001 From: Combi153 Date: Wed, 6 Mar 2024 15:36:28 +0900 Subject: [PATCH 26/26] =?UTF-8?q?refactor:=20log=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/petqua/application/payment/PaymentFacadeService.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/petqua/application/payment/PaymentFacadeService.kt b/src/main/kotlin/com/petqua/application/payment/PaymentFacadeService.kt index 3f4f5386..f948b34f 100644 --- a/src/main/kotlin/com/petqua/application/payment/PaymentFacadeService.kt +++ b/src/main/kotlin/com/petqua/application/payment/PaymentFacadeService.kt @@ -30,9 +30,9 @@ class PaymentFacadeService( private fun logFailPayment(command: FailPaymentCommand) { when (command.code) { - PAY_PROCESS_ABORTED -> log.error("PG사에서 결제를 중단했습니다. message: ${command.message}, OrderNumber: ${command.orderNumber}, MemberId: ${command.memberId}") + PAY_PROCESS_ABORTED -> log.error("PG사 혹은 원천사에서 결제를 중단했습니다. message: ${command.message}, OrderNumber: ${command.orderNumber}, MemberId: ${command.memberId}") PAY_PROCESS_CANCELED -> log.warn("사용자가 결제를 중단했습니다. message: ${command.message}, OrderNumber: ${command.orderNumber}, MemberId: ${command.memberId}") - REJECT_CARD_COMPANY -> log.warn("카드사에서 결제를 거절했습니다.. message: ${command.message}, OrderNumber: ${command.orderNumber}, MemberId: ${command.memberId}") + REJECT_CARD_COMPANY -> log.warn("카드사에서 결제를 거절했습니다. message: ${command.message}, OrderNumber: ${command.orderNumber}, MemberId: ${command.memberId}") } } }