Skip to content

Commit

Permalink
feat: 결제 로직 리팩토링 (#194)
Browse files Browse the repository at this point in the history
* feat: 결제 로직 추가

- AccessToken, PortoneService에서 의존성 제거
- Portone Access Token Redis에 저장 (30분)
- Payment DAO 리팩토링
- Pending 2개 이상 생성 방지

* refactor: remove function in swagger
  • Loading branch information
seogwoojin authored Nov 20, 2024
1 parent 9a19605 commit 67264bd
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class SecurityConfig(
"/meeting/actuator/health/**",
"/api/user/isDuplicatedKakaoTalkId",
"/api/payment/refund/match",
"/api/payment/portone-webhook",
"/api/payment/webhook",
"/api/auth/reissue",
"/api/verification/send-email",
"/api/verification/verify-email"
Expand Down
20 changes: 7 additions & 13 deletions src/main/kotlin/uoslife/servermeeting/payment/dao/PaymentDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package uoslife.servermeeting.payment.dao

import com.querydsl.jpa.impl.JPAQueryFactory
import org.springframework.stereotype.Repository
import uoslife.servermeeting.meetingteam.entity.QMeetingTeam.meetingTeam
import uoslife.servermeeting.meetingteam.entity.QUserTeam.userTeam
import uoslife.servermeeting.meetingteam.entity.enums.TeamType
import uoslife.servermeeting.payment.entity.Payment
import uoslife.servermeeting.payment.entity.QPayment.payment
Expand All @@ -13,31 +11,28 @@ import uoslife.servermeeting.user.entity.User

@Repository
class PaymentDao(private val queryFactory: JPAQueryFactory) {
fun getPaymentWithUserAndTeamType(user: User, teamType: TeamType): List<Payment>? {
fun getAllPaymentWithUserAndTeamType(requestUser: User, teamType: TeamType): List<Payment>? {
return when (teamType) {
TeamType.SINGLE ->
queryFactory
.selectFrom(payment)
.join(meetingTeam)
.join(userTeam)
.on(userTeam.user.eq(user))
.where(meetingTeam.type.eq(TeamType.SINGLE))
.join(user)
.where(payment.teamType.eq(TeamType.SINGLE))
.where(user.eq(requestUser))
.fetch()
TeamType.TRIPLE ->
queryFactory
.selectFrom(payment)
.join(meetingTeam)
.join(userTeam)
.on(userTeam.user.eq(user))
.where(meetingTeam.type.eq(TeamType.TRIPLE))
.join(user)
.where(payment.teamType.eq(TeamType.TRIPLE))
.where(user.eq(requestUser))
.fetch()
}
}

fun getSuccessPaymentFromUserIdAndTeamType(userId: Long, teamType: TeamType): Payment? {
return queryFactory
.selectFrom(payment)
.join(meetingTeam)
.join(user)
.on(user.id.eq(userId))
.where(payment.teamType.eq(teamType))
Expand All @@ -47,7 +42,6 @@ class PaymentDao(private val queryFactory: JPAQueryFactory) {
fun getPendingPaymentFromUserIdAndTeamType(userId: Long, teamType: TeamType): Payment? {
return queryFactory
.selectFrom(payment)
.join(meetingTeam)
.join(user)
.on(user.id.eq(userId))
.where(payment.teamType.eq(teamType))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import uoslife.servermeeting.payment.entity.enums.PaymentStatus
class PaymentResponseDto {
companion object {
private const val PAYMENT_SUCCESS = "paid"
private const val PAYMENT_FAILED = "cancelled"
}
data class PaymentRequestResponse(
@Schema(description = "결제 상품 고유 번호") @field:NotBlank var merchantUid: String,
Expand Down Expand Up @@ -44,6 +45,12 @@ class PaymentResponseDto {
val merchant_uid: String?,
@Schema(description = "Portone 결제 여부", example = PAYMENT_SUCCESS) var status: String?,
) {
@Schema(hidden = true)
fun isCancelled(): Boolean {
return status == PAYMENT_FAILED
}

@Schema(hidden = true)
fun isSuccess(): Boolean {
return status == PAYMENT_SUCCESS
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package uoslife.servermeeting.payment.service.impl

import java.time.Duration
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.stereotype.Service
import uoslife.servermeeting.global.auth.exception.ExternalApiFailedException
Expand All @@ -11,19 +12,22 @@ import uoslife.servermeeting.payment.dto.response.PortOneResponseDto
@Service
class PortOneAPIService(
private val paymentClient: PaymentClient,
private val redisTemplate: RedisTemplate<String, String>
private val redisTemplate: RedisTemplate<String, String>,
@Value("\${portone.api.imp.key}") private val impKey: String,
@Value("\${portone.api.imp.secret}") private val impSecret: String,
) {
companion object {
const val ACCESS_TOKEN_KEY = "PORTONE_ACCESS_KEY"
const val SUCCESS_CODE = 0
const val TOKEN_VALID_TIME = 1700L // 포트원 1800초 보다 축소
}
fun getAccessToken(impKey: String, impSecret: String): String {
private fun getAccessToken(): String {
val accessKey = redisTemplate.opsForValue().get(ACCESS_TOKEN_KEY)
if (accessKey != null) {
return accessKey
}
val accessTokenResponse = requestAccessToken(impKey, impSecret)

val accessTokenResponse = requestAccessToken()
checkResponseCode(accessTokenResponse)
val newAccessToken = accessTokenResponse.response!!.access_token
redisTemplate
Expand All @@ -32,37 +36,29 @@ class PortOneAPIService(
return newAccessToken
}

fun requestAccessToken(
impKey: String,
impSecret: String
): PortOneResponseDto.AccessTokenResponse {
private fun requestAccessToken(): PortOneResponseDto.AccessTokenResponse {
return paymentClient.getAccessToken(
PortOneRequestDto.AccessTokenRequest(imp_key = impKey, imp_secret = impSecret)
)
}

fun checkPayment(
accessToken: String,
impUid: String
): PortOneResponseDto.SingleHistoryResponse {
fun checkPayment(impUid: String): PortOneResponseDto.SingleHistoryResponse {
val accessToken = getAccessToken()
return paymentClient.checkPayment(accessToken = accessToken, impUid = impUid)
}

fun refundPayment(
accessToken: String,
impUid: String?,
price: Number?
): PortOneResponseDto.RefundResponse {
fun refundPayment(impUid: String?, price: Number?): PortOneResponseDto.RefundResponse {
val accessToken = getAccessToken()
return paymentClient.refundPayment(
accessToken,
PortOneRequestDto.RefundRequest(imp_uid = impUid, amount = price)
)
}

fun findPaymentByMID(
accessToken: String,
merchantUid: String,
): PortOneResponseDto.SingleHistoryResponse {
val accessToken = getAccessToken()
return paymentClient.findPaymentByMID(accessToken = accessToken, merchantUid = merchantUid)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@ class PortOneService(
private val portOneAPIService: PortOneAPIService,
@Value("\${portone.api.price.single}") private val singlePrice: Int,
@Value("\${portone.api.price.triple}") private val triplePrice: Int,
@Value("\${portone.api.imp.key}") private val impKey: String,
@Value("\${portone.api.imp.secret}") private val impSecret: String,
) : PaymentService {
companion object {
private val logger = LoggerFactory.getLogger(PaymentService::class.java)
}

// 결제 정보를 생성할 수 있는 유일한 로직
@Transactional
override fun requestPayment(
userId: Long,
Expand All @@ -65,7 +65,8 @@ class PortOneService(
val phoneNumber = user.phoneNumber ?: throw PhoneNumberNotFoundException()

checkUserIsLeader(userTeam)
checkSuccessPayment(user, teamType)
checkSuccessPayment(userId, teamType)
checkPendingPayment(userId, teamType) // todo : DB 접속 너무 많음

val payment =
Payment.createPayment(
Expand Down Expand Up @@ -95,13 +96,19 @@ class PortOneService(
)
}

private fun checkPendingPayment(userId: Long, teamType: TeamType) {
paymentDao.getPendingPaymentFromUserIdAndTeamType(userId, teamType)?.let {
throw UserAlreadyHavePaymentException()
}
}

private fun checkUserIsLeader(userTeam: UserTeam) {
if (!userTeam.isLeader) throw OnlyTeamLeaderCanCreatePaymentException()
}

private fun checkSuccessPayment(user: User, teamType: TeamType) {
paymentDao.getPaymentWithUserAndTeamType(user, teamType)?.forEach {
if (it.isSuccess()) throw UserAlreadyHavePaymentException()
private fun checkSuccessPayment(userId: Long, teamType: TeamType) {
paymentDao.getSuccessPaymentFromUserIdAndTeamType(userId, teamType)?.let {
throw UserAlreadyHavePaymentException()
}
}

Expand All @@ -121,11 +128,10 @@ class PortOneService(
?: throw PaymentNotFoundException()

try {
val accessToken = portOneAPIService.getAccessToken(impKey, impSecret)
val portOneStatus =
portOneAPIService.checkPayment(accessToken, paymentCheckRequest.impUid)
val portOneStatus = portOneAPIService.checkPayment(paymentCheckRequest.impUid)

if (portOneStatus.isPaid()) {
logger.info("[결제 성공 확인] marchantUid : ${payment.merchantUid}")
payment.updatePayment(paymentCheckRequest.impUid, PaymentStatus.SUCCESS)
return PaymentResponseDto.PaymentCheckResponse(true, "")
}
Expand All @@ -149,8 +155,7 @@ class PortOneService(
if (!validator.isAlreadyPaid(payment)) throw PaymentInValidException()

try {
val accessToken = portOneAPIService.getAccessToken(impKey, impSecret)
portOneAPIService.refundPayment(accessToken, payment.impUid, payment.price)
portOneAPIService.refundPayment(payment.impUid, payment.price)

payment.status = PaymentStatus.REFUND
logger.info(
Expand Down Expand Up @@ -192,8 +197,7 @@ class PortOneService(
"[환불 성공] payment_id: ${payment.id}, impUid: ${payment.impUid}, marchantUid: ${payment.merchantUid}"
)
try {
val accessToken = portOneAPIService.getAccessToken(impKey, impSecret)
portOneAPIService.refundPayment(accessToken, payment.impUid, payment.price)
portOneAPIService.refundPayment(payment.impUid, payment.price)
payment.status = PaymentStatus.REFUND
} catch (e: ExternalApiFailedException) {
logger.info(
Expand All @@ -216,7 +220,7 @@ class PortOneService(
val phoneNumber = user.phoneNumber ?: throw PhoneNumberNotFoundException()

val payments =
paymentDao.getPaymentWithUserAndTeamType(user, teamType)
paymentDao.getAllPaymentWithUserAndTeamType(user, teamType)
?: throw PaymentNotFoundException()

var pendingPayment: Payment? = null
Expand All @@ -239,9 +243,7 @@ class PortOneService(
if (pendingPayment == null) throw PaymentNotFoundException()

try {
val accessToken = portOneAPIService.getAccessToken(impKey, impSecret)
val portOneStatus =
portOneAPIService.findPaymentByMID(accessToken, pendingPayment!!.merchantUid)
val portOneStatus = portOneAPIService.findPaymentByMID(pendingPayment!!.merchantUid)

if (portOneStatus.isPaid()) {
pendingPayment!!.updatePayment(
Expand Down Expand Up @@ -288,6 +290,13 @@ class PortOneService(

payment.updatePayment(paymentWebhookResponse.imp_uid!!, PaymentStatus.SUCCESS)
logger.info("[웹훅 - 결제 성공] marchantUid : ${payment.merchantUid}")
} else if (paymentWebhookResponse.isCancelled()) {
val payment =
paymentRepository.findByMerchantUid(paymentWebhookResponse.merchant_uid!!)
?: throw PaymentNotFoundException()

payment.updatePayment(paymentWebhookResponse.imp_uid!!, PaymentStatus.FAILED)
logger.info("[웹훅 - 결제 실패] marchantUid : ${payment.merchantUid}")
}
return
}
Expand Down

0 comments on commit 67264bd

Please sign in to comment.