fix(charge): 쿠폰 충전 회원 락을 적용한다

This commit is contained in:
2026-05-18 15:39:11 +09:00
parent fefb5c24eb
commit ddac78a666
4 changed files with 289 additions and 2 deletions

View File

@@ -0,0 +1,166 @@
package kr.co.vividnext.sodalive.can.charge
import com.fasterxml.jackson.databind.ObjectMapper
import kr.co.vividnext.sodalive.can.CanRepository
import kr.co.vividnext.sodalive.can.coupon.CanCoupon
import kr.co.vividnext.sodalive.can.coupon.CanCouponNumber
import kr.co.vividnext.sodalive.can.coupon.CanCouponNumberRepository
import kr.co.vividnext.sodalive.can.coupon.CouponType
import kr.co.vividnext.sodalive.google.GooglePlayService
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.point.MemberPoint
import kr.co.vividnext.sodalive.point.MemberPointRepository
import kr.co.vividnext.sodalive.point.PointGrantLog
import kr.co.vividnext.sodalive.point.PointGrantLogRepository
import kr.co.vividnext.sodalive.v2.can.charge.event.ChargeEventJobService
import okhttp3.OkHttpClient
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.ArgumentCaptor
import org.mockito.Mockito
import java.time.LocalDateTime
class ChargeServiceTest {
private lateinit var chargeRepository: ChargeRepository
private lateinit var canRepository: CanRepository
private lateinit var memberRepository: MemberRepository
private lateinit var couponNumberRepository: CanCouponNumberRepository
private lateinit var grantLogRepository: PointGrantLogRepository
private lateinit var memberPointRepository: MemberPointRepository
private lateinit var messageSource: SodaMessageSource
private lateinit var service: ChargeService
@BeforeEach
fun setUp() {
chargeRepository = Mockito.mock(ChargeRepository::class.java)
canRepository = Mockito.mock(CanRepository::class.java)
memberRepository = Mockito.mock(MemberRepository::class.java)
couponNumberRepository = Mockito.mock(CanCouponNumberRepository::class.java)
grantLogRepository = Mockito.mock(PointGrantLogRepository::class.java)
memberPointRepository = Mockito.mock(MemberPointRepository::class.java)
messageSource = Mockito.mock(SodaMessageSource::class.java)
Mockito.`when`(chargeRepository.save(Mockito.any(Charge::class.java))).thenAnswer { it.arguments[0] }
Mockito.`when`(messageSource.getMessage("can.charge.title", LangContext().lang)).thenReturn("%d 캔")
Mockito.`when`(messageSource.getMessage("can.coupon.use_complete", LangContext().lang)).thenReturn("%d 캔 충전 완료")
Mockito.`when`(messageSource.getMessage("can.coupon.use_complete_point", LangContext().lang))
.thenReturn("%d 포인트 충전 완료")
service = ChargeService(
chargeRepository = chargeRepository,
canRepository = canRepository,
memberRepository = memberRepository,
couponNumberRepository = couponNumberRepository,
grantLogRepository = grantLogRepository,
memberPointRepository = memberPointRepository,
objectMapper = Mockito.mock(ObjectMapper::class.java),
okHttpClient = Mockito.mock(OkHttpClient::class.java),
chargeEventJobService = Mockito.mock(ChargeEventJobService::class.java),
googlePlayService = Mockito.mock(GooglePlayService::class.java),
messageSource = messageSource,
langContext = LangContext(),
bootpayApplicationId = "bootpayApplicationId",
bootpayPrivateKey = "bootpayPrivateKey",
bootpayHectoApplicationId = "bootpayHectoApplicationId",
bootpayHectoPrivateKey = "bootpayHectoPrivateKey",
appleInAppVerifySandBoxUrl = "https://sandbox.example.com",
appleInAppVerifyUrl = "https://apple.example.com",
payverseMid = "payverseMid",
payverseClientKey = "payverseClientKey",
payverseSecretKey = "payverseSecretKey",
payverseUsdMid = "payverseUsdMid",
payverseUsdClientKey = "payverseUsdClientKey",
payverseUsdSecretKey = "payverseUsdSecretKey",
payverseJpyMid = "payverseJpyMid",
payverseJpyClientKey = "payverseJpyClientKey",
payverseJpySecretKey = "payverseJpySecretKey",
payverseHost = "https://payverse.example.com",
serverEnv = "test"
)
}
@Test
fun shouldUseLockedMemberWhenChargingCanCoupon() {
val originalMember = member(id = 10L)
val lockedMember = member(id = 10L)
val couponNumber = canCouponNumber(couponType = CouponType.CAN, can = 300)
Mockito.`when`(couponNumberRepository.findByCouponNumber("COUPON1234")).thenReturn(couponNumber)
Mockito.`when`(memberRepository.findByIdForUpdate(10L)).thenReturn(lockedMember)
service.chargeByCoupon("COUPON1234", originalMember)
Mockito.verify(memberRepository).findByIdForUpdate(10L)
assertEquals(300, lockedMember.pgRewardCan)
assertEquals(0, originalMember.pgRewardCan)
}
@Test
fun shouldSaveCouponChargeWithLockedMember() {
val originalMember = member(id = 11L)
val lockedMember = member(id = 11L)
val couponNumber = canCouponNumber(couponType = CouponType.CAN, can = 150)
val captor = ArgumentCaptor.forClass(Charge::class.java)
Mockito.`when`(couponNumberRepository.findByCouponNumber("COUPON5678")).thenReturn(couponNumber)
Mockito.`when`(memberRepository.findByIdForUpdate(11L)).thenReturn(lockedMember)
service.chargeByCoupon("COUPON5678", originalMember)
Mockito.verify(chargeRepository).save(captor.capture())
assertEquals(lockedMember, captor.value.member)
}
@Test
fun shouldNotLockMemberWhenCanCouponAlreadyUsed() {
val member = member(id = 12L)
val couponNumber = canCouponNumber(couponType = CouponType.CAN, can = 100).also {
it.member = member(id = 99L)
}
Mockito.`when`(couponNumberRepository.findByCouponNumber("USED1234")).thenReturn(couponNumber)
org.junit.jupiter.api.assertThrows<kr.co.vividnext.sodalive.common.SodaException> {
service.chargeByCoupon("USED1234", member)
}
Mockito.verify(memberRepository, Mockito.never()).findByIdForUpdate(Mockito.anyLong())
}
@Test
fun shouldKeepPointCouponBehaviorWithoutMemberLock() {
val member = member(id = 13L)
val couponNumber = canCouponNumber(couponType = CouponType.POINT, can = 500)
Mockito.`when`(couponNumberRepository.findByCouponNumber("POINT1234")).thenReturn(couponNumber)
val result = service.chargeByCoupon("POINT1234", member)
assertEquals("500 포인트 충전 완료", result)
Mockito.verify(memberRepository, Mockito.never()).findByIdForUpdate(Mockito.anyLong())
Mockito.verify(grantLogRepository).save(Mockito.any(PointGrantLog::class.java))
Mockito.verify(memberPointRepository).save(Mockito.any(MemberPoint::class.java))
Mockito.verify(chargeRepository, Mockito.never()).save(Mockito.any(Charge::class.java))
}
private fun canCouponNumber(couponType: CouponType, can: Int): CanCouponNumber {
val coupon = CanCoupon(
couponName = "테스트 쿠폰",
couponType = couponType,
can = can,
couponCount = 1,
validity = LocalDateTime.now().plusDays(1),
isActive = true,
isMultipleUse = false
)
return CanCouponNumber("COUPON").also { it.canCoupon = coupon }
}
private fun member(id: Long): Member {
return Member(password = "password", nickname = "member$id").also { it.id = id }
}
}