-
Notifications
You must be signed in to change notification settings - Fork 0
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
The head ref may contain hidden characters: "105-feature-\uACB0\uC81C-\uC2B9\uC778-api"
feature: 결제 승인 api #110
Conversation
|
||
private val log = LoggerFactory.getLogger(PaymentFacadeService::class.java) | ||
|
||
fun succeedPayment(command: SucceedPaymentCommand) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OrderPayment
도 여기서 생성되는 게 맞을까요?? OrderPayment
에 수정이 있을 것 같아서 아직 구현하지 않으신 걸까요?? 😀
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
놓쳤습니다! 추가할게요. 여기서 생성되는 게 맞습니다.
There was a problem hiding this 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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
매의 눈!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
띠용!
@hgo641 홍고 이거 테스트 때문에 이렇게 해두셨던 건가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오.......옹? 아니요? 실수인듯? 헤헤
INVALID_CODE(BAD_REQUEST, "PF01", "지원하지 않는 결제 실패 코드입니다."), | ||
|
||
INVALID_ORDER_ID(BAD_REQUEST, "PF01", "지원하지 않는 결제 실패 코드입니다."), |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
범용적인 용어보다 구체적으로 표현하면 더 좋을 것 같아요.
logFailureCommand
같은..?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fun save(tossPayment: TossPayment): TossPayment { | ||
return paymentRepository.save(tossPayment) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
결제가 정상적으로 승인 될 때에도 orderStatus의 변화가 필요할 것 같습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
추가했습니다!
} catch (e: WebClientResponseException) { | ||
val errorResponse = objectMapper.readValue(e.responseBodyAsString, PaymentErrorResponseFromPG::class.java) | ||
throw PaymentException(PaymentExceptionType.from(errorResponse.code)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
api 요청에 대한 에러 핸들링 좋네요👍
fun from(orderNumber: String): OrderNumber { | ||
return OrderNumber(orderNumber) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OrderNumber
로의 변환에서는 검증은 따로 안해도 괜찮겠죠...?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OrderNumber 생성 규칙이 있으니, 그에 따라 정규식으로 검증하면 좋겠네요! 추가하겠습니다
ORDER_NOT_FOUND(NOT_FOUND, "O11", "존재하지 않는 주문입니다."), | ||
ORDER_PRICE_NOT_MATCH(BAD_REQUEST, "O10", "주문한 상품의 가격이 일치하지 않습니다."), | ||
PAYMENT_PRICE_NOT_MATCH(BAD_REQUEST, "O12", "주문한 상품 가격과 결제 금액이 일치하지 않습니다."), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
순서가 바꼈어요!
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", "주문한 상품 가격과 결제 금액이 일치하지 않습니다."), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
매의 눈 감사합니다 👍
val paramMap = mutableMapOf<String, Any?>().apply { | ||
put("code", failPaymentRequest.code) | ||
put("message", failPaymentRequest.message) | ||
put("orderId", failPaymentRequest.orderId) | ||
}.filterValues { it != null } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nullable할 필드 때문에 이렇게 사용하셨군요!
아래 처럼 바로 선언 해도 괜찮을 것 같아요! 어차피 새로운 map으로 반환해서 mutable로 안해도 될 것 같습니다.
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 } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아하! 변경할게요~ 👍
Test Results 39 files + 4 39 suites +4 15s ⏱️ -1s 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.
♻️ This comment has been updated with latest results. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
리뷰 반영 확인했습니다~!
OrderPayment와 함께 상태 변경까지👍
고생하셨습니다~!
There was a problem hiding this 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
status code를 200으로 하신 이유 궁금해요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Post 요청에 대한 body 를 구성해서 내려주기에 200 이 적절한 상태 코드라고 생각했습니다!
어색하다고 생각되는 부분이 있으실까요?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
문서를 봤는데, REJECT_CARD_COMPANY
의 경우 비밀번호 오류, 한도 초과, 포인트 부족 등이 원인이더군요... 😵💫
비밀번호 오류, 한도 초과같은거는 유저에게 안내를 해주면 좋을 것 같은데, 프론트 분들이 FailPaymentResponse의 커맨드 메시지를 보고 알아서 처리하시는 건가요??
There was a problem hiding this comment.
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}") | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
각 코드별로 예외를 던져서 ControllerAdvice에서 로깅을 하는 방법도 있을 것 같은데, 위 방식으로 구현하신 이유가 궁금해요!!
저희가 코드에서 발생한 에러가 아니라서 그런걸까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
만약 그렇다면 해당 API가 예외를 던질 때에만 정상 동작으로 간주하고, 예외를 던지지 않는 경우 코드에 오류가 있다고 판단해야 합니다.
이는 예외를 예외답지 않게 사용하는 방식이 라고 생각했습니다!
결제 실패에 대해 적절한 로직을 처리하고, 그에 대한 응답을 내려주는 것이 괜찮지 않을까요?
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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", "이미 처리된 결제 입니다"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ProductExceptionType에서 에러코드 P로 시작하는거 쓰고있는걸로 알아요!
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오... OauthApiClientConfig
도 나중에 여기로 옮기는 게 좋을까여
There was a problem hiding this comment.
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}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
여운이 남는 REJECT_CARD_COMPANY 로그메시지군요ㅋㅋ
There was a problem hiding this comment.
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}") |
There was a problem hiding this comment.
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사에서 결제를 중단했습니다." 로 로그를 찍기에 조금 애매한 것 같아요...!
"상점 계약 또는 카드사에서 문제가 발생했습니다." ...? 어렵네요 🥲
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- 상점 계약 -> 토스페이먼츠와 계약 기간이 끝난 이유 등으로 결제를 중단
- 원천사(카드사 등) 문제 -> 해당 문제로 토스페이먼츠에서 결제를 중단
PG사에서 결제를 중단했다
는 문장이 두 문제를 추상적으로 표현하고 있다고 생각했어요.
결제를 중단한 구체적인 이유는 message 로 따로 로그를 남기니, 이 정도도 충분하지 않을까요?
혹은 PG사 혹은 원천사에서 결제를 중단했습니다
는 어떠실까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
좋아요~~~
44ae47e
to
6436c48
Compare
📌 관련 이슈
📁 작업 설명
토스페이먼츠가 안내하는 결제 플로우는 그림과 같습니다. 이번에 작업한 내용은 사용자가 토스페이먼츠가 띄워준 결제창에 결제정보를 입력한 이후부터, 결제승인 결과를 응답하는 것까지 입니다.
결제 성공 시
결제 플로우를 펫쿠아 애플리케이션에 맞게 다시 그리면 위와 같습니다. 여기서 주황색 화살표가 이번 PR의 작업 부분입니다. 사용자가 입력한 결제정보가 유효하다면, 토스페이먼츠는 미리 입력한 successUrl 로 사용자를 리다이렉트 시킵니다. 그러면 client 는 해당 url 로 Post 요청을 보냅니다. 이때
accessToken
,paymentType
,orderId(orderNumber)
,paymentKey
,amount
를 포함합니다.서버에서는 입력받은 정보를 통해 검증을 수행합니다.
검증을 완료했다면 이후 토스페이먼츠 측에 결제 승인 요청을 보냅니다. 결제 승인 요청은 Facade 패턴을 적용해 트랜잭션 밖에서 실행될 수 있도록 했습니다.
결제 승인 요청을 성공했다면 응답을 통해 TossPayment 객체를 생성하고 저장합니다. Client 에게는 아무런 응답을 하지 않습니다.
결제 승인 요청을 실패했다면 해당 응답을 예외로 바꾸어, 예외를 응답합니다. 결제 승인 시 발생할 수 있는 에러는 다음과 같습니다.
결제 실패 시
다시 결제 플로우로 돌아와 사용자가 결제창에서 결제를 실패했을 시 플로우를 보겠습니다. 사용자가 결제를 취소하거나, 토스페이먼츠 계약기간이 끝난 것 등의 이유로 결제를 실패할 수 있습니다. 토스페이먼츠는 미리 입력한 failUrl 로 사용자를 리다이렉트 시킵니다. 그러면 client 는 해당 url 로 Post 요청을 보냅니다. 이때
accessToken
,code
,message
,orderId(OrderNumber)(nullable)
를 포함합니다.서버는 실패 code 에 따라 로직을 달리 수행합니다.
orderNumber(orderId) 를 함께 보내는
PAY_PROCESS_ABORTED
,REJECT_CARD_COMPANY
code의 경우 order를 조회해 주문을 취소합니다. 반면,PAY_PROCESS_CANCELED
는 orderNumber(orderId) 를 함께 보내지 않아 order를 조회할 수 없습니다. 따라서 주문을 취소하지 않습니다. 한편, 결제 실패에 대한 로그를 남깁니다.주문을 취소하는 등의 로직을 마친 후, code, message Client 에게 그대로 응답합니다.
참고사항