feat(charge): 첫 충전 보너스 신규 생성을 중단한다

This commit is contained in:
2026-10-19 18:33:34 +09:00
parent 2aef413adf
commit 45b97088ca
6 changed files with 777 additions and 34 deletions
@@ -1,14 +1,32 @@
package kr.co.vividnext.sodalive.v2.admin.event.charge
import kr.co.vividnext.sodalive.can.charge.Charge
import kr.co.vividnext.sodalive.can.charge.ChargeRepository
import kr.co.vividnext.sodalive.can.charge.ChargeStatus
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
import kr.co.vividnext.sodalive.can.payment.PaymentStatus
import kr.co.vividnext.sodalive.fcm.FcmEvent
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.v2.can.charge.event.ChargeEventJob
import kr.co.vividnext.sodalive.v2.can.charge.event.ChargeEventJobRepository
import kr.co.vividnext.sodalive.v2.can.charge.event.ChargeEventJobService
import kr.co.vividnext.sodalive.v2.can.charge.event.ChargeEventJobStatus
import kr.co.vividnext.sodalive.v2.can.charge.event.ChargeEventJobType
import kr.co.vividnext.sodalive.v2.can.charge.event.ChargeEventJobWorker
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.Mockito
import org.springframework.context.ApplicationEventPublisher
import org.springframework.transaction.TransactionDefinition
import org.springframework.transaction.support.AbstractPlatformTransactionManager
import org.springframework.transaction.support.DefaultTransactionStatus
import java.time.LocalDateTime
import java.util.Optional
class AdminChargeEventJobServiceTest {
@Test
@@ -54,6 +72,111 @@ class AdminChargeEventJobServiceTest {
assertEquals(2, pending.retryCount)
}
@Test
@DisplayName("실패한 첫 충전 작업은 관리자 재시도 후 저장 스냅샷 그대로 워커에서 한 번 지급한다")
fun shouldRetryFailedFirstChargeJobWithoutChangingSnapshot() {
val repository = Mockito.mock(ChargeEventJobRepository::class.java)
val chargeRepository = Mockito.mock(ChargeRepository::class.java)
val memberRepository = Mockito.mock(MemberRepository::class.java)
val service = AdminChargeEventJobService(repository)
val notifications = mutableListOf<FcmEvent>()
val payoutService = ChargeEventJobService(
repository,
chargeRepository,
memberRepository,
applicationEventPublisher = ApplicationEventPublisher { notifications.add(it as FcmEvent) }
)
val transactionManager = object : AbstractPlatformTransactionManager() {
override fun doGetTransaction(): Any = Any()
override fun doBegin(transaction: Any, definition: TransactionDefinition) {}
override fun doCommit(status: DefaultTransactionStatus) {}
override fun doRollback(status: DefaultTransactionStatus) {}
}
val worker = ChargeEventJobWorker(repository, payoutService, transactionManager)
val member = Member(password = "pw", nickname = "tester").also {
it.id = 10L
it.appleChargeCan = 100
}
val failed = ChargeEventJob(
sourceChargeId = 100L,
memberId = 10L,
jobType = ChargeEventJobType.FIRST_CHARGE,
idempotencyKey = "charge-event:100:FIRST_CHARGE:none",
additionalCan = 15,
paymentGateway = PaymentGateway.APPLE_IAP,
container = "ios",
methodSnapshot = "첫 충전 이벤트",
status = ChargeEventJobStatus.FAILED,
retryCount = 3,
nextRetryAt = LocalDateTime.now().minusDays(1),
lastError = "bonus down"
).also { it.id = 1L }
val savedCharges = mutableListOf<Charge>()
Mockito.`when`(repository.findByIdForUpdate(1L)).thenReturn(failed)
Mockito.`when`(repository.findById(1L)).thenReturn(Optional.of(failed))
Mockito.`when`(
repository.findNextPendingJobIdsForUpdate(
Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.now(),
Mockito.eq(30)
)
).thenAnswer {
if (failed.status == ChargeEventJobStatus.PENDING) listOf(1L) else emptyList<Long>()
}
Mockito.`when`(memberRepository.findByIdForUpdate(10L)).thenReturn(member)
Mockito.`when`(chargeRepository.save(Mockito.any(Charge::class.java))).thenAnswer {
assertEquals(ChargeEventJobStatus.PROCESSING, failed.status)
(it.arguments[0] as Charge).also { charge ->
charge.id = 200L
savedCharges.add(charge)
}
}
val beforeRetry = LocalDateTime.now()
worker.runPendingJobs()
assertEquals(ChargeEventJobStatus.FAILED, failed.status)
assertTrue(savedCharges.isEmpty())
service.retry(1L)
assertEquals(ChargeEventJobStatus.PENDING, failed.status)
assertEquals(0, failed.retryCount)
assertNull(failed.lastError)
assertTrue(requireNotNull(failed.nextRetryAt) >= beforeRetry)
assertEquals(100L, failed.sourceChargeId)
assertEquals(10L, failed.memberId)
assertNull(failed.chargeEventId)
assertEquals(ChargeEventJobType.FIRST_CHARGE, failed.jobType)
assertEquals("charge-event:100:FIRST_CHARGE:none", failed.idempotencyKey)
assertEquals(15, failed.additionalCan)
assertEquals(PaymentGateway.APPLE_IAP, failed.paymentGateway)
assertEquals("ios", failed.container)
assertEquals("첫 충전 이벤트", failed.methodSnapshot)
worker.runPendingJobs()
worker.runPendingJobs()
payoutService.processJob(1L)
assertEquals(ChargeEventJobStatus.DONE, failed.status)
assertEquals(200L, failed.resultChargeId)
assertNotNull(failed.processingStartedAt)
assertNotNull(failed.processedAt)
assertNull(failed.nextRetryAt)
assertEquals(listOf(0, 15, 0), listOf(member.pgRewardCan, member.appleRewardCan, member.googleRewardCan))
assertEquals(listOf(0, 100, 0), listOf(member.pgChargeCan, member.appleChargeCan, member.googleChargeCan))
val paidCharge = savedCharges.single()
assertEquals(0, paidCharge.chargeCan)
assertEquals(15, paidCharge.rewardCan)
assertEquals(ChargeStatus.EVENT, paidCharge.status)
assertEquals(member, paidCharge.member)
assertEquals(PaymentGateway.APPLE_IAP, paidCharge.payment?.paymentGateway)
assertEquals(PaymentStatus.COMPLETE, paidCharge.payment?.status)
assertEquals("첫 충전 이벤트", paidCharge.payment?.method)
val notification = notifications.single()
assertEquals("can.charge.event.first_title", notification.titleKey)
assertEquals("can.charge.event.additional_can_paid", notification.messageKey)
assertEquals(listOf(15), notification.args)
assertEquals(listOf(10L), notification.recipients)
}
private fun job(id: Long, status: ChargeEventJobStatus): ChargeEventJob {
return ChargeEventJob(
sourceChargeId = 100L,
@@ -1,15 +1,31 @@
package kr.co.vividnext.sodalive.v2.can.charge.event
import kr.co.vividnext.sodalive.admin.event.charge.ChargeEvent
import kr.co.vividnext.sodalive.can.charge.Charge
import kr.co.vividnext.sodalive.can.charge.ChargeRepository
import kr.co.vividnext.sodalive.can.charge.ChargeStatus
import kr.co.vividnext.sodalive.can.charge.event.ChargeEventRepository
import kr.co.vividnext.sodalive.can.payment.Payment
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
import kr.co.vividnext.sodalive.can.payment.PaymentStatus
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.fcm.FcmEvent
import kr.co.vividnext.sodalive.fcm.FcmEventType
import kr.co.vividnext.sodalive.fcm.notification.PushNotificationCategory
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.member.auth.Auth
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
import org.junit.jupiter.params.provider.EnumSource
import org.mockito.Mockito
import org.springframework.context.ApplicationEventPublisher
import org.springframework.transaction.TransactionDefinition
import org.springframework.transaction.support.AbstractPlatformTransactionManager
import org.springframework.transaction.support.DefaultTransactionStatus
@@ -78,6 +94,264 @@ class ChargeEventJobServiceTest {
assertEquals(TransactionDefinition.PROPAGATION_REQUIRES_NEW, transactionTemplate.propagationBehavior)
}
@ParameterizedTest
@EnumSource(PaymentGateway::class, names = ["PG", "PAYVERSE", "APPLE_IAP", "GOOGLE_IAP"])
@DisplayName("인증 회원의 첫 충전도 활성 이벤트가 없으면 보너스 작업을 만들지 않는다")
fun shouldNotCreateFirstChargeJobForNewAuthenticatedCharge(gateway: PaymentGateway) {
val fixture = SnapshotFixture(gateway)
val result = fixture.service.createAndProcessImmediate(100L, 10L)
assertNull(result)
assertEquals(emptyList<ChargeEventJob>(), fixture.savedJobs)
assertEquals(emptyList<Charge>(), fixture.savedCharges)
assertEquals(emptyList<FcmEvent>(), fixture.notifications)
fixture.assertWallet(1, 2, 3)
fixture.assertNoEligibilityLookup()
}
@ParameterizedTest
@CsvSource(
"PG, pg, false, 11, 2, 3",
"PAYVERSE, pg, false, 11, 2, 3",
"APPLE_IAP, ios, false, 1, 12, 3",
"GOOGLE_IAP, aos, false, 1, 2, 13",
"PG, pg, true, 11, 2, 3",
"PAYVERSE, pg, true, 11, 2, 3",
"APPLE_IAP, ios, true, 1, 12, 3",
"GOOGLE_IAP, aos, true, 1, 2, 13"
)
@DisplayName("모든 결제 수단과 인증 여부에서 첫 충전 대신 일반 이벤트의 10퍼센트를 지급한다")
fun shouldApplyActiveEventToNewChargeAcrossGateways(
gateway: PaymentGateway,
container: String,
authenticated: Boolean,
pgReward: Int,
appleReward: Int,
googleReward: Int
) {
val fixture = SnapshotFixture(gateway, authenticated)
fixture.activeEvent(paymentCount = 1)
val result = requireNotNull(fixture.service.createAndProcessImmediate(100L, 10L))
assertEquals(ChargeEventJobType.ACTIVE_CHARGE_EVENT, result.jobType)
assertEquals(20L, result.chargeEventId)
assertEquals(100L, result.sourceChargeId)
assertEquals(10L, result.memberId)
assertEquals("charge-event:100:ACTIVE_CHARGE_EVENT:20", result.idempotencyKey)
assertEquals(10, result.additionalCan)
assertEquals(gateway, result.paymentGateway)
assertEquals(container, result.container)
assertEquals("봄 이벤트", result.methodSnapshot)
assertEquals(listOf(result), fixture.savedJobs)
assertEquals(ChargeEventJobStatus.DONE, result.status)
fixture.assertPayout(10, gateway, "봄 이벤트")
fixture.assertWallet(pgReward, appleReward, googleReward)
fixture.assertNotification(10, title = "봄 이벤트")
fixture.assertNoEligibilityLookup()
Mockito.verify(fixture.eventRepository).getPaymentCount(
fixture.member,
"봄 이벤트",
fixture.event.startDate,
fixture.event.endDate
)
}
@Test
@DisplayName("일반 이벤트 한도를 소진한 인증 회원은 첫 충전 보너스로 우회하지 않는다")
fun shouldNotCreateJobWhenActiveEventLimitIsExhausted() {
val fixture = SnapshotFixture(PaymentGateway.PG)
fixture.activeEvent(paymentCount = 2)
val result = fixture.service.createAndProcessImmediate(100L, 10L)
assertNull(result)
assertEquals(emptyList<ChargeEventJob>(), fixture.savedJobs)
assertEquals(emptyList<Charge>(), fixture.savedCharges)
assertEquals(emptyList<FcmEvent>(), fixture.notifications)
fixture.assertWallet(1, 2, 3)
fixture.assertNoEligibilityLookup()
}
@Test
@DisplayName("결제 정보가 없으면 활성 이벤트 조회보다 먼저 기존 예외를 유지한다")
fun shouldRejectMissingGatewayBeforeLookingUpActiveEvent() {
val fixture = SnapshotFixture(PaymentGateway.PG)
fixture.sourceCharge.payment = null
assertThrows(SodaException::class.java) { fixture.service.createProcessingJob(100L, 10L) }
Mockito.verifyNoInteractions(fixture.eventRepository, fixture.jobRepository)
}
@ParameterizedTest
@CsvSource(
"PG, pg, PENDING, 16, 2, 3",
"PAYVERSE, pg, PENDING, 16, 2, 3",
"APPLE_IAP, ios, PENDING, 1, 17, 3",
"GOOGLE_IAP, aos, PENDING, 1, 2, 18",
"PG, pg, PROCESSING, 16, 2, 3",
"PAYVERSE, pg, PROCESSING, 16, 2, 3",
"APPLE_IAP, ios, PROCESSING, 1, 17, 3",
"GOOGLE_IAP, aos, PROCESSING, 1, 2, 18"
)
@DisplayName("기존 첫 충전 작업은 현재 이벤트나 인증 재계산 없이 저장된 15캔을 한 번 지급한다")
fun shouldPayExistingFirstChargeSnapshotWithoutActiveEvent(
gateway: PaymentGateway,
container: String,
status: ChargeEventJobStatus,
pgReward: Int,
appleReward: Int,
googleReward: Int
) {
val fixture = SnapshotFixture(gateway)
val storedJob = ChargeEventJob(
sourceChargeId = 100L,
memberId = 10L,
jobType = ChargeEventJobType.FIRST_CHARGE,
idempotencyKey = "charge-event:100:FIRST_CHARGE:none",
additionalCan = 15,
paymentGateway = gateway,
container = container,
methodSnapshot = "첫 충전 이벤트",
status = status,
nextRetryAt = LocalDateTime.now(),
lastError = "previous failure"
).also { it.id = 1L }
Mockito.`when`(fixture.jobRepository.findByIdForUpdate(1L)).thenReturn(storedJob)
fixture.service.processJob(1L)
fixture.service.processJob(1L)
assertEquals(ChargeEventJobStatus.DONE, storedJob.status)
assertEquals(15, storedJob.additionalCan)
assertEquals(gateway, storedJob.paymentGateway)
assertEquals(container, storedJob.container)
assertEquals("첫 충전 이벤트", storedJob.methodSnapshot)
assertEquals(200L, storedJob.resultChargeId)
assertNotNull(storedJob.processedAt)
assertNull(storedJob.nextRetryAt)
assertNull(storedJob.lastError)
assertEquals(emptyList<ChargeEventJob>(), fixture.savedJobs)
fixture.assertPayout(15, gateway, "첫 충전 이벤트")
fixture.assertWallet(pgReward, appleReward, googleReward)
fixture.assertNotification(15, titleKey = "can.charge.event.first_title")
fixture.assertNoEligibilityLookup()
Mockito.verifyNoInteractions(fixture.eventRepository)
Mockito.verify(fixture.chargeRepository, Mockito.never()).findByIdForUpdate(Mockito.anyLong())
}
private class SnapshotFixture(gateway: PaymentGateway, authenticated: Boolean = true) {
val jobRepository = Mockito.mock(ChargeEventJobRepository::class.java)
val chargeRepository = Mockito.mock(ChargeRepository::class.java)
private val memberRepository = Mockito.mock(MemberRepository::class.java)
val eventRepository = Mockito.mock(ChargeEventRepository::class.java)
val savedJobs = mutableListOf<ChargeEventJob>()
val savedCharges = mutableListOf<Charge>()
val notifications = mutableListOf<FcmEvent>()
val member = Member(password = "pw", nickname = "tester").also {
it.id = 10L
it.pgChargeCan = 100
it.appleChargeCan = 200
it.googleChargeCan = 300
it.pgRewardCan = 1
it.appleRewardCan = 2
it.googleRewardCan = 3
if (authenticated) {
Auth("tester", "19900101", "test-ci", "test-di", 1).member = it
}
}
val sourceCharge = Charge(100, 5).also {
it.id = 100L
it.member = member
it.payment = Payment(PaymentStatus.COMPLETE, gateway).also { payment -> payment.method = "카드" }
}
val event = ChargeEvent(
"봄 이벤트",
LocalDateTime.of(2026, 1, 1, 0, 0),
LocalDateTime.of(2026, 12, 31, 0, 0),
2,
0.10f
).also { it.id = 20L }
val service = ChargeEventJobService(
jobRepository,
chargeRepository,
memberRepository,
chargeEventRepository = eventRepository,
applicationEventPublisher = ApplicationEventPublisher { notifications.add(it as FcmEvent) }
)
init {
Mockito.`when`(chargeRepository.findByIdForUpdate(100L)).thenReturn(sourceCharge)
Mockito.`when`(memberRepository.findByIdForUpdate(10L)).thenReturn(member)
Mockito.`when`(jobRepository.saveAndFlush(Mockito.any(ChargeEventJob::class.java))).thenAnswer {
(it.arguments[0] as ChargeEventJob).also { job ->
job.id = 1L
savedJobs.add(job)
}
}
Mockito.`when`(jobRepository.findByIdForUpdate(1L)).thenAnswer { savedJobs.singleOrNull() }
Mockito.`when`(chargeRepository.save(Mockito.any(Charge::class.java))).thenAnswer {
(it.arguments[0] as Charge).also { charge ->
charge.id = 200L
savedCharges.add(charge)
}
}
}
fun activeEvent(paymentCount: Int) {
Mockito.`when`(eventRepository.getChargeEvent()).thenReturn(event)
Mockito.`when`(
eventRepository.getPaymentCount(member, "봄 이벤트", event.startDate, event.endDate)
).thenReturn(paymentCount)
}
fun assertNoEligibilityLookup() {
Mockito.verify(chargeRepository, Mockito.never()).getChargeCountAfterDate(
Mockito.anyLong(),
Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.now()
)
}
fun assertWallet(pgReward: Int, appleReward: Int, googleReward: Int) {
assertEquals(
listOf(pgReward, appleReward, googleReward),
listOf(member.pgRewardCan, member.appleRewardCan, member.googleRewardCan)
)
assertEquals(listOf(100, 200, 300), listOf(member.pgChargeCan, member.appleChargeCan, member.googleChargeCan))
assertEquals(100, sourceCharge.chargeCan)
assertEquals(5, sourceCharge.rewardCan)
assertEquals(ChargeStatus.CHARGE, sourceCharge.status)
assertEquals(PaymentStatus.COMPLETE, sourceCharge.payment?.status)
assertEquals("카드", sourceCharge.payment?.method)
}
fun assertPayout(amount: Int, gateway: PaymentGateway, method: String) {
val paidCharge = savedCharges.single()
assertEquals(0, paidCharge.chargeCan)
assertEquals(amount, paidCharge.rewardCan)
assertEquals(ChargeStatus.EVENT, paidCharge.status)
assertEquals("$amount 캔", paidCharge.title)
assertEquals(member, paidCharge.member)
assertEquals(gateway, paidCharge.payment?.paymentGateway)
assertEquals(PaymentStatus.COMPLETE, paidCharge.payment?.status)
assertEquals(method, paidCharge.payment?.method)
}
fun assertNotification(amount: Int, title: String = "", titleKey: String? = null) {
val notification = notifications.single()
assertEquals(FcmEventType.INDIVIDUAL, notification.type)
assertEquals(PushNotificationCategory.SYSTEM, notification.category)
assertEquals(title, notification.title)
assertEquals(titleKey, notification.titleKey)
assertEquals("can.charge.event.additional_can_paid", notification.messageKey)
assertEquals(listOf(amount), notification.args)
assertEquals(listOf(10L), notification.recipients)
assertNull(notification.isAuth)
}
}
private fun job(
id: Long,
memberId: Long = 10L,
@@ -1,10 +1,20 @@
package kr.co.vividnext.sodalive.v2.can.charge.event
import kr.co.vividnext.sodalive.can.charge.Charge
import kr.co.vividnext.sodalive.can.charge.ChargeRepository
import kr.co.vividnext.sodalive.can.charge.ChargeStatus
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
import kr.co.vividnext.sodalive.can.payment.PaymentStatus
import kr.co.vividnext.sodalive.fcm.FcmEvent
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRepository
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.Mockito
import org.springframework.context.ApplicationEventPublisher
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.transaction.support.AbstractPlatformTransactionManager
import org.springframework.transaction.support.DefaultTransactionStatus
@@ -92,6 +102,75 @@ class ChargeEventJobWorkerTest {
assertTrue(retryJob.nextRetryAt!!.isBefore(LocalDateTime.now().plusMinutes(6)))
}
@Test
@DisplayName("워커는 저장된 첫 충전 대기 작업을 지급하고 완료 작업 재호출에는 중복 지급하지 않는다")
fun shouldProcessPendingFirstChargeJobAndSkipDoneJob() {
val repository = Mockito.mock(ChargeEventJobRepository::class.java)
val chargeRepository = Mockito.mock(ChargeRepository::class.java)
val memberRepository = Mockito.mock(MemberRepository::class.java)
val notifications = mutableListOf<FcmEvent>()
val service = ChargeEventJobService(
repository,
chargeRepository,
memberRepository,
applicationEventPublisher = ApplicationEventPublisher { notifications.add(it as FcmEvent) }
)
val worker = ChargeEventJobWorker(repository, service, ChargeEventJobTestTransactionManager())
val member = Member(password = "pw", nickname = "tester").also {
it.id = 10L
it.pgChargeCan = 100
}
val pending = ChargeEventJob(
sourceChargeId = 100L,
memberId = 10L,
jobType = ChargeEventJobType.FIRST_CHARGE,
idempotencyKey = "charge-event:100:FIRST_CHARGE:none",
additionalCan = 15,
paymentGateway = PaymentGateway.PAYVERSE,
container = "pg",
methodSnapshot = "첫 충전 이벤트",
nextRetryAt = LocalDateTime.now().minusMinutes(1)
).also { it.id = 1L }
val savedCharges = mutableListOf<Charge>()
Mockito.`when`(repository.findNextPendingJobIdsForUpdate(anyLocalDateTime(), Mockito.eq(30))).thenAnswer {
if (pending.status == ChargeEventJobStatus.PENDING) listOf(1L) else emptyList<Long>()
}
Mockito.`when`(repository.findById(1L)).thenReturn(Optional.of(pending))
Mockito.`when`(repository.findByIdForUpdate(1L)).thenReturn(pending)
Mockito.`when`(memberRepository.findByIdForUpdate(10L)).thenReturn(member)
Mockito.`when`(chargeRepository.save(Mockito.any(Charge::class.java))).thenAnswer {
assertEquals(ChargeEventJobStatus.PROCESSING, pending.status)
(it.arguments[0] as Charge).also { charge ->
charge.id = 200L
savedCharges.add(charge)
}
}
worker.runPendingJobs()
worker.runPendingJobs()
service.processJob(1L)
assertEquals(ChargeEventJobStatus.DONE, pending.status)
assertNotNull(pending.processingStartedAt)
assertNotNull(pending.processedAt)
assertEquals(200L, pending.resultChargeId)
assertEquals(listOf(15, 0, 0), listOf(member.pgRewardCan, member.appleRewardCan, member.googleRewardCan))
assertEquals(listOf(100, 0, 0), listOf(member.pgChargeCan, member.appleChargeCan, member.googleChargeCan))
val paidCharge = savedCharges.single()
assertEquals(0, paidCharge.chargeCan)
assertEquals(15, paidCharge.rewardCan)
assertEquals(ChargeStatus.EVENT, paidCharge.status)
assertEquals(member, paidCharge.member)
assertEquals(PaymentGateway.PAYVERSE, paidCharge.payment?.paymentGateway)
assertEquals(PaymentStatus.COMPLETE, paidCharge.payment?.status)
assertEquals("첫 충전 이벤트", paidCharge.payment?.method)
val notification = notifications.single()
assertEquals("can.charge.event.first_title", notification.titleKey)
assertEquals("can.charge.event.additional_can_paid", notification.messageKey)
assertEquals(listOf(15), notification.args)
assertEquals(listOf(10L), notification.recipients)
}
private fun job(id: Long, retryCount: Int = 0): ChargeEventJob {
return ChargeEventJob(
sourceChargeId = 100L,