캔 충전

- 트래킹 로직 적용
This commit is contained in:
Klaus 2025-03-04 17:05:41 +09:00
parent 81b11976a7
commit 72d10f9443
4 changed files with 124 additions and 24 deletions

View File

@ -0,0 +1,7 @@
package kr.co.vividnext.sodalive.can.charge
data class ChargeCompleteResponse(
val price: Double,
val currencyCode: String,
val isFirstCharged: Boolean
)

View File

@ -3,17 +3,22 @@ package kr.co.vividnext.sodalive.can.charge
import kr.co.vividnext.sodalive.can.payment.PaymentGateway import kr.co.vividnext.sodalive.can.payment.PaymentGateway
import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.marketing.AdTrackingHistoryType
import kr.co.vividnext.sodalive.marketing.AdTrackingService
import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.Member
import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.security.core.userdetails.User
import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import java.time.LocalDateTime
@RestController @RestController
@RequestMapping("/charge") @RequestMapping("/charge")
class ChargeController(private val service: ChargeService) { class ChargeController(
private val service: ChargeService,
private val trackingService: AdTrackingService
) {
@PostMapping @PostMapping
fun charge( fun charge(
@ -30,14 +35,30 @@ class ChargeController(private val service: ChargeService) {
@PostMapping("/verify") @PostMapping("/verify")
fun verify( fun verify(
@RequestBody verifyRequest: VerifyRequest, @RequestBody verifyRequest: VerifyRequest,
@AuthenticationPrincipal user: User @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = ApiResponse.ok(service.verify(user, verifyRequest)) ) = run {
if (member == null) {
throw SodaException("로그인 정보를 확인해주세요.")
}
val response = service.verify(memberId = member.id!!, verifyRequest)
trackingCharge(member, response)
ApiResponse.ok(Unit)
}
@PostMapping("/verify/hecto") @PostMapping("/verify/hecto")
fun verifyHecto( fun verifyHecto(
@RequestBody verifyRequest: VerifyRequest, @RequestBody verifyRequest: VerifyRequest,
@AuthenticationPrincipal user: User @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = ApiResponse.ok(service.verifyHecto(user, verifyRequest)) ) = run {
if (member == null) {
throw SodaException("로그인 정보를 확인해주세요.")
}
val response = service.verifyHecto(memberId = member.id!!, verifyRequest)
trackingCharge(member, response)
ApiResponse.ok(Unit)
}
@PostMapping("/apple") @PostMapping("/apple")
fun appleCharge( fun appleCharge(
@ -54,8 +75,16 @@ class ChargeController(private val service: ChargeService) {
@PostMapping("/apple/verify") @PostMapping("/apple/verify")
fun appleVerify( fun appleVerify(
@RequestBody verifyRequest: AppleVerifyRequest, @RequestBody verifyRequest: AppleVerifyRequest,
@AuthenticationPrincipal user: User @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = ApiResponse.ok(service.appleVerify(user, verifyRequest)) ) = run {
if (member == null) {
throw SodaException("로그인 정보를 확인해주세요.")
}
val response = service.appleVerify(memberId = member.id!!, verifyRequest)
trackingCharge(member, response)
ApiResponse.ok(Unit)
}
@PostMapping("/google") @PostMapping("/google")
fun googleCharge( fun googleCharge(
@ -78,17 +107,40 @@ class ChargeController(private val service: ChargeService) {
paymentGateway = request.paymentGateway paymentGateway = request.paymentGateway
) )
ApiResponse.ok( val response = service.processGoogleIap(
service.processGoogleIap( memberId = member.id!!,
memberId = member.id!!, chargeId = chargeId,
chargeId = chargeId, productId = request.productId,
productId = request.productId, purchaseToken = request.purchaseToken,
purchaseToken = request.purchaseToken, paymentGateway = request.paymentGateway
paymentGateway = request.paymentGateway
)
) )
trackingCharge(member, response)
ApiResponse.ok(Unit)
} else { } else {
throw SodaException("결제정보에 오류가 있습니다.") throw SodaException("결제정보에 오류가 있습니다.")
} }
} }
private fun trackingCharge(
member: Member,
response: ChargeCompleteResponse
) {
if (
!member.activePid.isNullOrBlank() &&
member.partnerExpirationDatetime?.isAfter(LocalDateTime.now()) == true
) {
trackingService.saveTrackingHistory(
pid = member.activePid!!,
type = if (response.isFirstCharged) {
AdTrackingHistoryType.FIRST_PAYMENT
} else {
AdTrackingHistoryType.REPEAT_PAYMENT
},
memberId = member.id!!,
price = response.price,
locale = response.currencyCode
)
}
}
} }

View File

@ -18,6 +18,7 @@ interface ChargeQueryRepository {
fun getOldestChargeWhereRewardCanGreaterThan0(chargeId: Long, memberId: Long, container: String): Charge? fun getOldestChargeWhereRewardCanGreaterThan0(chargeId: Long, memberId: Long, container: String): Charge?
fun getOldestChargeWhereChargeCanGreaterThan0(chargeId: Long, memberId: Long, container: String): Charge? fun getOldestChargeWhereChargeCanGreaterThan0(chargeId: Long, memberId: Long, container: String): Charge?
fun getChargeCountAfterDate(memberId: Long, date: LocalDateTime): Int fun getChargeCountAfterDate(memberId: Long, date: LocalDateTime): Int
fun isFirstCharged(memberId: Long): Boolean
} }
class ChargeQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : ChargeQueryRepository { class ChargeQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : ChargeQueryRepository {
@ -76,6 +77,21 @@ class ChargeQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : Cha
.size .size
} }
override fun isFirstCharged(memberId: Long): Boolean {
return queryFactory
.select(charge.id)
.from(charge)
.innerJoin(charge.member, member)
.innerJoin(charge.payment, payment)
.where(
member.id.eq(memberId),
charge.status.eq(ChargeStatus.CHARGE),
payment.status.eq(PaymentStatus.COMPLETE)
)
.fetch()
.size <= 1
}
private fun getPaymentGatewayCondition(container: String): BooleanExpression? { private fun getPaymentGatewayCondition(container: String): BooleanExpression? {
val paymentGatewayCondition = when (container) { val paymentGatewayCondition = when (container) {
"aos" -> { "aos" -> {

View File

@ -23,9 +23,10 @@ import org.springframework.data.repository.findByIdOrNull
import org.springframework.http.HttpHeaders import org.springframework.http.HttpHeaders
import org.springframework.retry.annotation.Backoff import org.springframework.retry.annotation.Backoff
import org.springframework.retry.annotation.Retryable import org.springframework.retry.annotation.Retryable
import org.springframework.security.core.userdetails.User
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import java.math.BigDecimal
import java.math.RoundingMode
@Service @Service
@Transactional(readOnly = true) @Transactional(readOnly = true)
@ -102,10 +103,10 @@ class ChargeService(
} }
@Transactional @Transactional
fun verify(user: User, verifyRequest: VerifyRequest) { fun verify(memberId: Long, verifyRequest: VerifyRequest): ChargeCompleteResponse {
val charge = chargeRepository.findByIdOrNull(verifyRequest.orderId.toLong()) val charge = chargeRepository.findByIdOrNull(verifyRequest.orderId.toLong())
?: throw SodaException("결제정보에 오류가 있습니다.") ?: throw SodaException("결제정보에 오류가 있습니다.")
val member = memberRepository.findByEmail(user.username) val member = memberRepository.findByIdOrNull(memberId)
?: throw SodaException("로그인 정보를 확인해주세요.") ?: throw SodaException("로그인 정보를 확인해주세요.")
if (charge.payment!!.paymentGateway == PaymentGateway.PG) { if (charge.payment!!.paymentGateway == PaymentGateway.PG) {
@ -130,6 +131,12 @@ class ChargeService(
memberId = member.id!! memberId = member.id!!
) )
) )
return ChargeCompleteResponse(
price = BigDecimal(charge.payment!!.price).setScale(2, RoundingMode.HALF_UP).toDouble(),
currencyCode = charge.payment!!.locale?.takeLast(3) ?: "KRW",
isFirstCharged = chargeRepository.isFirstCharged(memberId)
)
} else { } else {
throw SodaException("결제정보에 오류가 있습니다.") throw SodaException("결제정보에 오류가 있습니다.")
} }
@ -142,10 +149,10 @@ class ChargeService(
} }
@Transactional @Transactional
fun verifyHecto(user: User, verifyRequest: VerifyRequest) { fun verifyHecto(memberId: Long, verifyRequest: VerifyRequest): ChargeCompleteResponse {
val charge = chargeRepository.findByIdOrNull(verifyRequest.orderId.toLong()) val charge = chargeRepository.findByIdOrNull(verifyRequest.orderId.toLong())
?: throw SodaException("결제정보에 오류가 있습니다.") ?: throw SodaException("결제정보에 오류가 있습니다.")
val member = memberRepository.findByEmail(user.username) val member = memberRepository.findByIdOrNull(memberId)
?: throw SodaException("로그인 정보를 확인해주세요.") ?: throw SodaException("로그인 정보를 확인해주세요.")
if (charge.payment!!.paymentGateway == PaymentGateway.PG) { if (charge.payment!!.paymentGateway == PaymentGateway.PG) {
@ -174,6 +181,12 @@ class ChargeService(
memberId = member.id!! memberId = member.id!!
) )
) )
return ChargeCompleteResponse(
price = BigDecimal(charge.payment!!.price).setScale(2, RoundingMode.HALF_UP).toDouble(),
currencyCode = charge.payment!!.locale?.takeLast(3) ?: "KRW",
isFirstCharged = chargeRepository.isFirstCharged(memberId)
)
} else { } else {
throw SodaException("결제정보에 오류가 있습니다.") throw SodaException("결제정보에 오류가 있습니다.")
} }
@ -208,10 +221,10 @@ class ChargeService(
} }
@Transactional @Transactional
fun appleVerify(user: User, verifyRequest: AppleVerifyRequest) { fun appleVerify(memberId: Long, verifyRequest: AppleVerifyRequest): ChargeCompleteResponse {
val charge = chargeRepository.findByIdOrNull(verifyRequest.chargeId) val charge = chargeRepository.findByIdOrNull(verifyRequest.chargeId)
?: throw SodaException("결제정보에 오류가 있습니다.") ?: throw SodaException("결제정보에 오류가 있습니다.")
val member = memberRepository.findByEmail(user.username) val member = memberRepository.findByIdOrNull(memberId)
?: throw SodaException("로그인 정보를 확인해주세요.") ?: throw SodaException("로그인 정보를 확인해주세요.")
if (charge.payment!!.paymentGateway == PaymentGateway.APPLE_IAP) { if (charge.payment!!.paymentGateway == PaymentGateway.APPLE_IAP) {
@ -228,6 +241,12 @@ class ChargeService(
memberId = member.id!! memberId = member.id!!
) )
) )
return ChargeCompleteResponse(
price = BigDecimal(charge.payment!!.price).setScale(2, RoundingMode.HALF_UP).toDouble(),
currencyCode = charge.payment!!.locale?.takeLast(3) ?: "KRW",
isFirstCharged = chargeRepository.isFirstCharged(memberId)
)
} else { } else {
throw SodaException("결제정보에 오류가 있습니다.") throw SodaException("결제정보에 오류가 있습니다.")
} }
@ -271,7 +290,7 @@ class ChargeService(
productId: String, productId: String,
purchaseToken: String, purchaseToken: String,
paymentGateway: PaymentGateway paymentGateway: PaymentGateway
) { ): ChargeCompleteResponse {
val charge = chargeRepository.findByIdOrNull(id = chargeId) val charge = chargeRepository.findByIdOrNull(id = chargeId)
?: throw SodaException("결제정보에 오류가 있습니다.") ?: throw SodaException("결제정보에 오류가 있습니다.")
val member = memberRepository.findByIdOrNull(id = memberId) val member = memberRepository.findByIdOrNull(id = memberId)
@ -290,6 +309,12 @@ class ChargeService(
memberId = member.id!! memberId = member.id!!
) )
) )
return ChargeCompleteResponse(
price = BigDecimal(charge.payment!!.price).setScale(2, RoundingMode.HALF_UP).toDouble(),
currencyCode = charge.payment!!.locale?.takeLast(3) ?: "KRW",
isFirstCharged = chargeRepository.isFirstCharged(memberId)
)
} else { } else {
throw SodaException("구매를 하지 못했습니다.\n고객센터로 문의해 주세요") throw SodaException("구매를 하지 못했습니다.\n고객센터로 문의해 주세요")
} }