Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: 결제 승인 api #110

Merged
merged 26 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
81bffa4
chore: tossPayments 관련 설정 추가 서브모듈 업데이트
Combi153 Feb 24, 2024
590cb51
feat: Base64 인코딩 util 메서드 추가
Combi153 Feb 24, 2024
9cd02d8
feat: PaymentGatewayClient 추가
Combi153 Feb 24, 2024
a70daf1
chore: rebase 충돌 해결
Combi153 Feb 29, 2024
fc1f350
chore: rebase 충돌 해결
Combi153 Feb 29, 2024
7f711a9
feat: 예외 처리 로직 추가
Combi153 Feb 28, 2024
ad9c888
chore: rebase 충돌 해결
Combi153 Feb 29, 2024
d633576
refactor: PG사 api 호출 로직 트랜잭션 분리
Combi153 Feb 28, 2024
a6111dc
fix: 예외 처리 수정
Combi153 Feb 29, 2024
295d6e2
refactor: 결제 금액 검증 시 예외 종류 수정
Combi153 Feb 29, 2024
65f5734
chore: rebase 충돌 해결
Combi153 Feb 29, 2024
e316163
build: mockk 를 위한 설정 추가
Combi153 Feb 29, 2024
dbe491d
refactor: config 프로필 분리
Combi153 Feb 29, 2024
7253ed1
test: PaymentController 테스트 작성
Combi153 Feb 29, 2024
4bb8b7b
refactor: Order 결제 시 권한 검증하도록 수정
Combi153 Feb 29, 2024
e6492e9
refactor: 사용하지 않는 코드 제거
Combi153 Feb 29, 2024
49f246f
feat: 결제 실패 처리 api 개발
Combi153 Mar 1, 2024
10dce1c
feat: 결제 실패 로깅 추가
Combi153 Mar 1, 2024
90c0a37
refactor: 결제 실패 로깅 방법 수정 및 메서드, 클래스 이름 변경
Combi153 Mar 2, 2024
9094d1e
refactor: 로그 기능 메서드 이름 변경
Combi153 Mar 4, 2024
e7d7a59
feat: Order 정적 팩터리 메서드 검증 로직 추가
Combi153 Mar 4, 2024
9059ddb
refactor: 예외 이름 및 순서 변경
Combi153 Mar 4, 2024
d8c0f17
test: 테스트 params 생성 로직 변경
Combi153 Mar 5, 2024
b119917
refactor: 결제 승인 시 주문 상태를 변경하고 OrderPayment 저장하도록 수정
Combi153 Mar 5, 2024
67eef66
refactor: PaymentExceptionType 코드 수정
Combi153 Mar 6, 2024
6436c48
refactor: log 메시지 수정
Combi153 Mar 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend-submodule
7 changes: 7 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
}
10 changes: 6 additions & 4 deletions src/main/kotlin/com/petqua/application/order/OrderService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.petqua.application.order

import com.petqua.application.order.dto.SaveOrderCommand
import com.petqua.application.order.dto.SaveOrderResponse
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
Expand Down Expand Up @@ -33,6 +34,7 @@ class OrderService(
private val productOptionRepository: ProductOptionRepository,
private val shippingAddressRepository: ShippingAddressRepository,
private val storeRepository: StoreRepository,
private val paymentGatewayClient: PaymentGatewayClient,
) {

fun save(command: SaveOrderCommand): SaveOrderResponse {
Expand Down Expand Up @@ -68,8 +70,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
Expand Down Expand Up @@ -127,8 +129,8 @@ class OrderService(
return SaveOrderResponse(
orderId = orders.first().orderNumber.value,
orderName = orders.first().orderName.value,
successUrl = "successUrl",
failUrl = "failUrl",
successUrl = paymentGatewayClient.successUrl(),
failUrl = paymentGatewayClient.failUrl(),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,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,
Expand Down
75 changes: 75 additions & 0 deletions src/main/kotlin/com/petqua/application/payment/PaymentDtos.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.petqua.application.payment

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.ORDER_NUMBER_MISSING_EXCEPTION
import java.math.BigDecimal

data class SucceedPaymentCommand(
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,
): SucceedPaymentCommand {
return SucceedPaymentCommand(
memberId = memberId,
paymentType = TossPaymentType.from(paymentType),
orderNumber = OrderNumber.from(orderId),
paymentKey = paymentKey,
amount = amount,
)
}
}
}

data class FailPaymentCommand(
val memberId: Long,
val code: FailPaymentCode,
val message: String,
val orderNumber: String?,
) {

fun toOrderNumber(): OrderNumber {
return orderNumber?.let { OrderNumber.from(it) } ?: throw FailPaymentException(ORDER_NUMBER_MISSING_EXCEPTION)
}

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 FailPaymentResponse(
val code: FailPaymentCode,
val message: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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

@Service
class PaymentFacadeService(
private val paymentService: PaymentService,
private val paymentGatewayService: PaymentGatewayService,
) {

private val log = LoggerFactory.getLogger(PaymentFacadeService::class.java)

fun succeedPayment(command: SucceedPaymentCommand) {
Copy link
Contributor

@hgo641 hgo641 Mar 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OrderPayment도 여기서 생성되는 게 맞을까요?? OrderPayment에 수정이 있을 것 같아서 아직 구현하지 않으신 걸까요?? 😀

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

놓쳤습니다! 추가할게요. 여기서 생성되는 게 맞습니다.

paymentService.validateAmount(command)
val paymentResponse = paymentGatewayService.confirmPayment(command.toPaymentConfirmRequest())
paymentService.processPayment(paymentResponse.toPayment())
}

fun failPayment(command: FailPaymentCommand): FailPaymentResponse {
logFailPayment(command)
if (command.code != PAY_PROCESS_CANCELED) {
paymentService.cancelOrder(command.memberId, command.toOrderNumber())
}
return FailPaymentResponse(command.code, command.message)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://docs.tosspayments.com/resources/faq#%EC%97%90%EB%9F%AC-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85

문서를 봤는데, REJECT_CARD_COMPANY의 경우 비밀번호 오류, 한도 초과, 포인트 부족 등이 원인이더군요... 😵‍💫
비밀번호 오류, 한도 초과같은거는 유저에게 안내를 해주면 좋을 것 같은데, 프론트 분들이 FailPaymentResponse의 커맨드 메시지를 보고 알아서 처리하시는 건가요??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

자주 묻는 질문 부분은 본 적이 없었는데 덕분에 궁금증이 일부 해결됐어요!!!!!

말씀하신 안내는 프론트에서 메시지를 띄워주는 것으로 충분할 것 같습니다! 어차피 저희가 할 수 있는 게 없습니다

}

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}")
REJECT_CARD_COMPANY -> log.warn("카드사에서 결제를 거절했습니다. message: ${command.message}, OrderNumber: ${command.orderNumber}, MemberId: ${command.memberId}")
}
}
}
184 changes: 184 additions & 0 deletions src/main/kotlin/com/petqua/application/payment/PaymentGatewayDtos.kt
Original file line number Diff line number Diff line change
@@ -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<CancelResponseFromPG>?,
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<CashReceiptHistoryResponseFromPG>?,
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,
)
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading
Loading