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

feature: 결제 승인 api #110

merged 26 commits into from
Mar 6, 2024

Conversation

Combi153
Copy link
Contributor

@Combi153 Combi153 commented Mar 2, 2024

📌 관련 이슈

📁 작업 설명

image

토스페이먼츠가 안내하는 결제 플로우는 그림과 같습니다. 이번에 작업한 내용은 사용자가 토스페이먼츠가 띄워준 결제창에 결제정보를 입력한 이후부터, 결제승인 결과를 응답하는 것까지 입니다.

결제 성공 시

image

결제 플로우를 펫쿠아 애플리케이션에 맞게 다시 그리면 위와 같습니다. 여기서 주황색 화살표가 이번 PR의 작업 부분입니다. 사용자가 입력한 결제정보가 유효하다면, 토스페이먼츠는 미리 입력한 successUrl 로 사용자를 리다이렉트 시킵니다. 그러면 client 는 해당 url 로 Post 요청을 보냅니다. 이때 accessToken, paymentType, orderId(orderNumber), paymentKey, amount 를 포함합니다.

서버에서는 입력받은 정보를 통해 검증을 수행합니다.

  • 회원 여부 검증
  • 회원의 주문이 맞는지 검증(권한 검증)
  • 금액 검증

검증을 완료했다면 이후 토스페이먼츠 측에 결제 승인 요청을 보냅니다. 결제 승인 요청은 Facade 패턴을 적용해 트랜잭션 밖에서 실행될 수 있도록 했습니다.

결제 승인 요청을 성공했다면 응답을 통해 TossPayment 객체를 생성하고 저장합니다. Client 에게는 아무런 응답을 하지 않습니다.
image

결제 승인 요청을 실패했다면 해당 응답을 예외로 바꾸어, 예외를 응답합니다. 결제 승인 시 발생할 수 있는 에러는 다음과 같습니다.

@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)
        try {
            return tossPaymentsApiClient.confirmPayment(credentials, paymentConfirmRequestToPG)
        } catch (e: WebClientResponseException) {
            val errorResponse = objectMapper.readValue(e.responseBodyAsString, PaymentErrorResponseFromPG::class.java)
            throw PaymentException(PaymentExceptionType.from(errorResponse.code))
        }
    }
}

결제 실패 시

image

다시 결제 플로우로 돌아와 사용자가 결제창에서 결제를 실패했을 시 플로우를 보겠습니다. 사용자가 결제를 취소하거나, 토스페이먼츠 계약기간이 끝난 것 등의 이유로 결제를 실패할 수 있습니다. 토스페이먼츠는 미리 입력한 failUrl 로 사용자를 리다이렉트 시킵니다. 그러면 client 는 해당 url 로 Post 요청을 보냅니다. 이때 accessToken, code, message, orderId(OrderNumber)(nullable) 를 포함합니다.

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

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

    // (...)

    fun failPayment(command: FailPaymentCommand): FailPaymentResponse {
        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}")
        }
    }
}

서버는 실패 code 에 따라 로직을 달리 수행합니다.
image
orderNumber(orderId) 를 함께 보내는 PAY_PROCESS_ABORTED, REJECT_CARD_COMPANY code의 경우 order를 조회해 주문을 취소합니다. 반면, PAY_PROCESS_CANCELED 는 orderNumber(orderId) 를 함께 보내지 않아 order를 조회할 수 없습니다. 따라서 주문을 취소하지 않습니다. 한편, 결제 실패에 대한 로그를 남깁니다.

주문을 취소하는 등의 로직을 마친 후, code, message Client 에게 그대로 응답합니다.

참고사항

  • 외부 API 로 주고받는 request, response DTO에 대해 FromXX, ToXX 등의 suffix 를 두면 어떨까요. controller 에서 사용되는 dto와 구분하기 위해서요!
  • response 각 필드에 대한 설명은 공식 문서 에 자세히 나와있습니다.
  • Order 의 status 를 어떻게 활용할지는 아직 모르겠습니다.
  • 이렇게 구현하는게 맞는지 모르겠습니다...! 적극적인 리뷰 부탁드리겠습니다!!!!

@Combi153 Combi153 self-assigned this Mar 2, 2024

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.

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

Copy link
Member

@TaeyeonRoyce TaeyeonRoyce left a comment

Choose a reason for hiding this comment

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

결제 복잡한데 잘 구현해주셨군요!
직관적인 설명 덕분에 코드 읽는데 어려움이 없었습니다..!!👍
몇가지 코멘트 남겨두었어요.

그리고 외부 API로 주고 받는 DTO에 RequestToXxx, ResponseFromXxx 이렇게 사용하는거 좋아요! API 통신에 사용 되는 것 같으면서 저희 client와 사용하는 dto와는 구분되어서 좋네요.

class OauthApiClientConfig {

@Bean
fun KakaoOauthApiClient(): KakaoOauthApiClient {
fun kakaoOauthApiClient(): KakaoOauthApiClient {
Copy link
Member

Choose a reason for hiding this comment

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

매의 눈!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

띠용!
@hgo641 홍고 이거 테스트 때문에 이렇게 해두셨던 건가요?

Copy link
Contributor

Choose a reason for hiding this comment

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

오.......옹? 아니요? 실수인듯? 헤헤

Comment on lines 13 to 15
INVALID_CODE(BAD_REQUEST, "PF01", "지원하지 않는 결제 실패 코드입니다."),

INVALID_ORDER_ID(BAD_REQUEST, "PF01", "지원하지 않는 결제 실패 코드입니다."),
Copy link
Member

Choose a reason for hiding this comment

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

INVALID_ORDER_ID 코드랑 메세지 변경이 필요합니다!

return FailPaymentResponse(command.code, command.message)
}

private fun log(command: FailPaymentCommand) {
Copy link
Member

Choose a reason for hiding this comment

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

범용적인 용어보다 구체적으로 표현하면 더 좋을 것 같아요.
logFailureCommand 같은..?

Copy link
Contributor Author

@Combi153 Combi153 Mar 4, 2024

Choose a reason for hiding this comment

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

메서드 이름은 수정하겠습니다!

이 부분을 로그로 남기는 게 좋을지 어떨지 여쭤보고 싶었어요! 다들 어떻게 생각하시나요? 남긴다면 어떤 level 로 남길지도요.

저는 log 로 남겨야 한다고 생각했습니다. PG사에서 결제를 중단한 게 다른 실패보다 훨씬 더 큰 사항이라 생각해서 error 로 두었습니다. 하지만 각각 어떤 상황에서 실패하는지 구체적으로 모르겠어서 확신은 없는 상태입니다.

image image

https://docs.tosspayments.com/reference/error-codes#failurl%EB%A1%9C-%EC%A0%84%EB%8B%AC%EB%90%98%EB%8A%94-%EC%97%90%EB%9F%AC

Comment on lines 29 to 45
fun save(tossPayment: TossPayment): TossPayment {
return paymentRepository.save(tossPayment)
}
Copy link
Member

Choose a reason for hiding this comment

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

결제가 정상적으로 승인 될 때에도 orderStatus의 변화가 필요할 것 같습니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

추가했습니다!

Comment on lines +34 to +37
} catch (e: WebClientResponseException) {
val errorResponse = objectMapper.readValue(e.responseBodyAsString, PaymentErrorResponseFromPG::class.java)
throw PaymentException(PaymentExceptionType.from(errorResponse.code))
}
Copy link
Member

Choose a reason for hiding this comment

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

api 요청에 대한 에러 핸들링 좋네요👍

Comment on lines 23 to 34
fun from(orderNumber: String): OrderNumber {
return OrderNumber(orderNumber)
}
Copy link
Member

Choose a reason for hiding this comment

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

OrderNumber로의 변환에서는 검증은 따로 안해도 괜찮겠죠...?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OrderNumber 생성 규칙이 있으니, 그에 따라 정규식으로 검증하면 좋겠네요! 추가하겠습니다

Comment on lines 17 to 19
ORDER_NOT_FOUND(NOT_FOUND, "O11", "존재하지 않는 주문입니다."),
ORDER_PRICE_NOT_MATCH(BAD_REQUEST, "O10", "주문한 상품의 가격이 일치하지 않습니다."),
PAYMENT_PRICE_NOT_MATCH(BAD_REQUEST, "O12", "주문한 상품 가격과 결제 금액이 일치하지 않습니다."),
Copy link
Member

Choose a reason for hiding this comment

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

순서가 바꼈어요!

Suggested change
ORDER_NOT_FOUND(NOT_FOUND, "O11", "존재하지 않는 주문입니다."),
ORDER_PRICE_NOT_MATCH(BAD_REQUEST, "O10", "주문한 상품의 가격이 일치하지 않습니다."),
PAYMENT_PRICE_NOT_MATCH(BAD_REQUEST, "O12", "주문한 상품 가격과 결제 금액이 일치하지 않습니다."),
ORDER_PRICE_NOT_MATCH(BAD_REQUEST, "O10", "주문한 상품의 가격이 일치하지 않습니다."),
ORDER_NOT_FOUND(NOT_FOUND, "O11", "존재하지 않는 주문입니다."),
PAYMENT_PRICE_NOT_MATCH(BAD_REQUEST, "O12", "주문한 상품 가격과 결제 금액이 일치하지 않습니다."),

Copy link
Contributor Author

Choose a reason for hiding this comment

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

매의 눈 감사합니다 👍

Comment on lines 35 to 39
val paramMap = mutableMapOf<String, Any?>().apply {
put("code", failPaymentRequest.code)
put("message", failPaymentRequest.message)
put("orderId", failPaymentRequest.orderId)
}.filterValues { it != null }
Copy link
Member

Choose a reason for hiding this comment

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

nullable할 필드 때문에 이렇게 사용하셨군요!
아래 처럼 바로 선언 해도 괜찮을 것 같아요! 어차피 새로운 map으로 반환해서 mutable로 안해도 될 것 같습니다.

Suggested change
val paramMap = mutableMapOf<String, Any?>().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 }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

아하! 변경할게요~ 👍

Copy link

github-actions bot commented Mar 5, 2024

Test Results

 39 files  + 4   39 suites  +4   15s ⏱️ -1s
193 tests +23  193 ✅ +23  0 💤 ±0  0 ❌ ±0 
274 runs  +40  274 ✅ +40  0 💤 ±0  0 ❌ ±0 

Results for commit 6436c48. ± Comparison against base commit 0f9cfe9.

This pull request removes 5 and adds 28 tests. Note that renamed tests count towards both.
com.petqua.application.auth.AuthFacadeServiceTest ‑ Then: 멤버의 인증 토큰을 발급한다
com.petqua.application.auth.AuthFacadeServiceTest ‑ Then: 발급한 refreshToken을 저장한다
com.petqua.application.auth.AuthFacadeServiceTest ‑ Then: 예외가 발생한다
com.petqua.application.auth.AuthFacadeServiceTest ‑ Then: 입력한 회원의 정보를 삭제한다
com.petqua.application.auth.AuthFacadeServiceTest ‑ Then: 토큰 정보를 갱신한다
com.petqua.application.auth.AuthServiceTest ‑ Then: 멤버의 인증 토큰을 발급한다
com.petqua.application.auth.AuthServiceTest ‑ Then: 발급한 refreshToken을 저장한다
com.petqua.application.auth.AuthServiceTest ‑ Then: 예외가 발생한다
com.petqua.application.auth.AuthServiceTest ‑ Then: 입력한 회원의 정보를 삭제한다
com.petqua.application.auth.AuthServiceTest ‑ Then: 토큰 정보를 갱신한다
com.petqua.application.payment.PaymentFacadeServiceTest ‑ Then: OrderPayment 객체를 생성한다
com.petqua.application.payment.PaymentFacadeServiceTest ‑ Then: Order의 상태를 변경한다
com.petqua.application.payment.PaymentFacadeServiceTest ‑ Then: PG사에 결제 승인 요청을 보낸다
com.petqua.application.payment.PaymentFacadeServiceTest ‑ Then: TossPayment 객체를 생성한다
com.petqua.application.payment.PaymentFacadeServiceTest ‑ Then: 실패 내역을 응답한다
…

♻️ This comment has been updated with latest results.

Copy link
Member

@TaeyeonRoyce TaeyeonRoyce left a comment

Choose a reason for hiding this comment

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

리뷰 반영 확인했습니다~!
OrderPayment와 함께 상태 변경까지👍

고생하셨습니다~!

Copy link
Contributor

@hgo641 hgo641 left a comment

Choose a reason for hiding this comment

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

결제 서비스 구조 너무 좋은데요??? 👍 최고에요
리뷰가 늦어서 죄송합니다.......🙇‍♀️
질문만 몇 개 남기고 어프루브합니다...ㅎㅎ!

request: FailPaymentRequest,
): ResponseEntity<FailPaymentResponse> {
val response = paymentFacadeService.failPayment(request.toCommand(loginMember.memberId))
return ResponseEntity.ok(response)
Copy link
Contributor

Choose a reason for hiding this comment

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

status code를 200으로 하신 이유 궁금해요!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Post 요청에 대한 body 를 구성해서 내려주기에 200 이 적절한 상태 코드라고 생각했습니다!

어색하다고 생각되는 부분이 있으실까요?

Copy link
Contributor

@hgo641 hgo641 Mar 6, 2024

Choose a reason for hiding this comment

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

결제 실패에 대한 응답이니까 200이 적절할지 400번대가 적절할지 혼자서 고민이었습니다ㅋㅋ
일단 실패니까...? 예외랑 같은 결이지 않나 생각했는데, 사용자 결제 취소같은 것도 있으니까... 200이 맞는 것........같습니다?!
그냥 ... 후추의 생각이 궁금했어요... 🥴

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.

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

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

Comment on lines 31 to 37
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}")
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

각 코드별로 예외를 던져서 ControllerAdvice에서 로깅을 하는 방법도 있을 것 같은데, 위 방식으로 구현하신 이유가 궁금해요!!
저희가 코드에서 발생한 에러가 아니라서 그런걸까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

만약 그렇다면 해당 API가 예외를 던질 때에만 정상 동작으로 간주하고, 예외를 던지지 않는 경우 코드에 오류가 있다고 판단해야 합니다.

이는 예외를 예외답지 않게 사용하는 방식이 라고 생각했습니다!

결제 실패에 대해 적절한 로직을 처리하고, 그에 대한 응답을 내려주는 것이 괜찮지 않을까요?

Copy link
Contributor

Choose a reason for hiding this comment

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

ㅎㅎ... 저는 결제 실패에 대한 처리를 하는 것을 PG사에서 결제 수행 중 발생한 예외를 처리하는 거랑 같다고 생각했어요...
근데 앞 코멘트에서 말한 것 처럼... 지금처럼 올바른 응답을 던져주는 게 더 맞는 것 같네요!

private val objectMapper: ObjectMapper,
) : PaymentGatewayClient {

override fun confirmPayment(paymentConfirmRequestToPG: PaymentConfirmRequestToPG): PaymentResponseFromPG {
Copy link
Contributor

Choose a reason for hiding this comment

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

저도 DTO에 FromXX, ToXX suffix 있는거 좋아요~~

private val errorMessage: String,
) : BaseExceptionType {

ALREADY_PROCESSED_PAYMENT(BAD_REQUEST, "P01", "이미 처리된 결제 입니다"),
Copy link
Contributor

Choose a reason for hiding this comment

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

ProductExceptionType에서 에러코드 P로 시작하는거 쓰고있는걸로 알아요!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

헉 감사합니다! PA 로 바꿀게요 👍


@Configuration
@Profile("!test")
class ApiClientConfig {
Copy link
Contributor

Choose a reason for hiding this comment

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

오... OauthApiClientConfig 도 나중에 여기로 옮기는 게 좋을까여

Copy link
Contributor Author

Choose a reason for hiding this comment

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

옮겨도 괜찮을 것 같아요!

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}")
Copy link
Contributor

@hgo641 hgo641 Mar 5, 2024

Choose a reason for hiding this comment

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

여운이 남는 REJECT_CARD_COMPANY 로그메시지군요ㅋㅋ

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}")
Copy link
Contributor

Choose a reason for hiding this comment

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

상점 계약 또는 원천사 문제가 있을 때 PAY_PROCESS_ABORTED 에러가 발생합니다.
인증실패 등 원천사 에러 메시지를 받았다면, PG와 관련없는 에러입니다. 원천사에 문의해주세요.

"PG사에서 결제를 중단했습니다." 로 로그를 찍기에 조금 애매한 것 같아요...!
"상점 계약 또는 카드사에서 문제가 발생했습니다." ...? 어렵네요 🥲

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  • 상점 계약 -> 토스페이먼츠와 계약 기간이 끝난 이유 등으로 결제를 중단
  • 원천사(카드사 등) 문제 -> 해당 문제로 토스페이먼츠에서 결제를 중단

PG사에서 결제를 중단했다는 문장이 두 문제를 추상적으로 표현하고 있다고 생각했어요.

결제를 중단한 구체적인 이유는 message 로 따로 로그를 남기니, 이 정도도 충분하지 않을까요?

혹은 PG사 혹은 원천사에서 결제를 중단했습니다는 어떠실까요?

Copy link
Contributor

Choose a reason for hiding this comment

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

좋아요~~~

@Combi153 Combi153 force-pushed the 105-feature-결제-승인-api branch from 44ae47e to 6436c48 Compare March 6, 2024 06:37
@Combi153 Combi153 merged commit 87dea2c into develop Mar 6, 2024
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

feature: 결제 승인 API
3 participants