From 72d10f9443478caa22106a3380f5f51de48202a1 Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 4 Mar 2025 17:05:41 +0900 Subject: [PATCH] =?UTF-8?q?=EC=BA=94=20=EC=B6=A9=EC=A0=84=20-=20=ED=8A=B8?= =?UTF-8?q?=EB=9E=98=ED=82=B9=20=EB=A1=9C=EC=A7=81=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../can/charge/ChargeCompleteResponse.kt | 7 ++ .../sodalive/can/charge/ChargeController.kt | 84 +++++++++++++++---- .../sodalive/can/charge/ChargeRepository.kt | 16 ++++ .../sodalive/can/charge/ChargeService.kt | 41 +++++++-- 4 files changed, 124 insertions(+), 24 deletions(-) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeCompleteResponse.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeCompleteResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeCompleteResponse.kt new file mode 100644 index 0000000..066cd8f --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeCompleteResponse.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.can.charge + +data class ChargeCompleteResponse( + val price: Double, + val currencyCode: String, + val isFirstCharged: Boolean +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeController.kt index cd6b8da..3748c96 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeController.kt @@ -3,17 +3,22 @@ package kr.co.vividnext.sodalive.can.charge import kr.co.vividnext.sodalive.can.payment.PaymentGateway import kr.co.vividnext.sodalive.common.ApiResponse 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 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.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController +import java.time.LocalDateTime @RestController @RequestMapping("/charge") -class ChargeController(private val service: ChargeService) { +class ChargeController( + private val service: ChargeService, + private val trackingService: AdTrackingService +) { @PostMapping fun charge( @@ -30,14 +35,30 @@ class ChargeController(private val service: ChargeService) { @PostMapping("/verify") fun verify( @RequestBody verifyRequest: VerifyRequest, - @AuthenticationPrincipal user: User - ) = ApiResponse.ok(service.verify(user, verifyRequest)) + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) { + throw SodaException("로그인 정보를 확인해주세요.") + } + + val response = service.verify(memberId = member.id!!, verifyRequest) + trackingCharge(member, response) + ApiResponse.ok(Unit) + } @PostMapping("/verify/hecto") fun verifyHecto( @RequestBody verifyRequest: VerifyRequest, - @AuthenticationPrincipal user: User - ) = ApiResponse.ok(service.verifyHecto(user, verifyRequest)) + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) { + throw SodaException("로그인 정보를 확인해주세요.") + } + + val response = service.verifyHecto(memberId = member.id!!, verifyRequest) + trackingCharge(member, response) + ApiResponse.ok(Unit) + } @PostMapping("/apple") fun appleCharge( @@ -54,8 +75,16 @@ class ChargeController(private val service: ChargeService) { @PostMapping("/apple/verify") fun appleVerify( @RequestBody verifyRequest: AppleVerifyRequest, - @AuthenticationPrincipal user: User - ) = ApiResponse.ok(service.appleVerify(user, verifyRequest)) + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) { + throw SodaException("로그인 정보를 확인해주세요.") + } + + val response = service.appleVerify(memberId = member.id!!, verifyRequest) + trackingCharge(member, response) + ApiResponse.ok(Unit) + } @PostMapping("/google") fun googleCharge( @@ -78,17 +107,40 @@ class ChargeController(private val service: ChargeService) { paymentGateway = request.paymentGateway ) - ApiResponse.ok( - service.processGoogleIap( - memberId = member.id!!, - chargeId = chargeId, - productId = request.productId, - purchaseToken = request.purchaseToken, - paymentGateway = request.paymentGateway - ) + val response = service.processGoogleIap( + memberId = member.id!!, + chargeId = chargeId, + productId = request.productId, + purchaseToken = request.purchaseToken, + paymentGateway = request.paymentGateway ) + + trackingCharge(member, response) + ApiResponse.ok(Unit) } else { 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 + ) + } + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeRepository.kt index 1b65e1a..dbb2128 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeRepository.kt @@ -18,6 +18,7 @@ interface ChargeQueryRepository { fun getOldestChargeWhereRewardCanGreaterThan0(chargeId: Long, memberId: Long, container: String): Charge? fun getOldestChargeWhereChargeCanGreaterThan0(chargeId: Long, memberId: Long, container: String): Charge? fun getChargeCountAfterDate(memberId: Long, date: LocalDateTime): Int + fun isFirstCharged(memberId: Long): Boolean } class ChargeQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : ChargeQueryRepository { @@ -76,6 +77,21 @@ class ChargeQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : Cha .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? { val paymentGatewayCondition = when (container) { "aos" -> { diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt index 64529b0..4b8486e 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt @@ -23,9 +23,10 @@ import org.springframework.data.repository.findByIdOrNull import org.springframework.http.HttpHeaders import org.springframework.retry.annotation.Backoff import org.springframework.retry.annotation.Retryable -import org.springframework.security.core.userdetails.User import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional +import java.math.BigDecimal +import java.math.RoundingMode @Service @Transactional(readOnly = true) @@ -102,10 +103,10 @@ class ChargeService( } @Transactional - fun verify(user: User, verifyRequest: VerifyRequest) { + fun verify(memberId: Long, verifyRequest: VerifyRequest): ChargeCompleteResponse { val charge = chargeRepository.findByIdOrNull(verifyRequest.orderId.toLong()) ?: throw SodaException("결제정보에 오류가 있습니다.") - val member = memberRepository.findByEmail(user.username) + val member = memberRepository.findByIdOrNull(memberId) ?: throw SodaException("로그인 정보를 확인해주세요.") if (charge.payment!!.paymentGateway == PaymentGateway.PG) { @@ -130,6 +131,12 @@ class ChargeService( 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 { throw SodaException("결제정보에 오류가 있습니다.") } @@ -142,10 +149,10 @@ class ChargeService( } @Transactional - fun verifyHecto(user: User, verifyRequest: VerifyRequest) { + fun verifyHecto(memberId: Long, verifyRequest: VerifyRequest): ChargeCompleteResponse { val charge = chargeRepository.findByIdOrNull(verifyRequest.orderId.toLong()) ?: throw SodaException("결제정보에 오류가 있습니다.") - val member = memberRepository.findByEmail(user.username) + val member = memberRepository.findByIdOrNull(memberId) ?: throw SodaException("로그인 정보를 확인해주세요.") if (charge.payment!!.paymentGateway == PaymentGateway.PG) { @@ -174,6 +181,12 @@ class ChargeService( 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 { throw SodaException("결제정보에 오류가 있습니다.") } @@ -208,10 +221,10 @@ class ChargeService( } @Transactional - fun appleVerify(user: User, verifyRequest: AppleVerifyRequest) { + fun appleVerify(memberId: Long, verifyRequest: AppleVerifyRequest): ChargeCompleteResponse { val charge = chargeRepository.findByIdOrNull(verifyRequest.chargeId) ?: throw SodaException("결제정보에 오류가 있습니다.") - val member = memberRepository.findByEmail(user.username) + val member = memberRepository.findByIdOrNull(memberId) ?: throw SodaException("로그인 정보를 확인해주세요.") if (charge.payment!!.paymentGateway == PaymentGateway.APPLE_IAP) { @@ -228,6 +241,12 @@ class ChargeService( 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 { throw SodaException("결제정보에 오류가 있습니다.") } @@ -271,7 +290,7 @@ class ChargeService( productId: String, purchaseToken: String, paymentGateway: PaymentGateway - ) { + ): ChargeCompleteResponse { val charge = chargeRepository.findByIdOrNull(id = chargeId) ?: throw SodaException("결제정보에 오류가 있습니다.") val member = memberRepository.findByIdOrNull(id = memberId) @@ -290,6 +309,12 @@ class ChargeService( 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 { throw SodaException("구매를 하지 못했습니다.\n고객센터로 문의해 주세요") }