Updated on 2026-08-14
This commit is contained in:
parent
7472e7b1dc
commit
2debaafcbb
33 changed files with 1386 additions and 7 deletions
34
domain/promo/build.gradle.kts
Normal file
34
domain/promo/build.gradle.kts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.domain.promo"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
// region Kotlin
|
||||
api(deps.kotlin.coroutines)
|
||||
api(deps.arrow.core)
|
||||
// endregion
|
||||
|
||||
// region Core modules
|
||||
api(projects.core.utils)
|
||||
// endregion
|
||||
|
||||
// region Domain models
|
||||
api(projects.domain.models)
|
||||
api(projects.domain.promo.models)
|
||||
// endregion
|
||||
|
||||
// region Tests
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.junit5)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(projects.test.core)
|
||||
// endregion
|
||||
}
|
||||
20
domain/promo/models/build.gradle.kts
Normal file
20
domain/promo/models/build.gradle.kts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
plugins {
|
||||
alias(deps.plugins.kotlin.jvm)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
// region Kotlin
|
||||
api(deps.kotlin.datetime)
|
||||
// endregion
|
||||
|
||||
// region Domain models
|
||||
api(projects.domain.models)
|
||||
// endregion
|
||||
|
||||
// region Tests
|
||||
testImplementation(deps.test.junit5)
|
||||
testImplementation(deps.test.truth)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.domain.promo.models
|
||||
|
||||
enum class PromoCampaignId(val deeplinkId: Int, val slug: String) {
|
||||
WhaleSwapCashback(deeplinkId = 1, slug = "whale-swap-cashback"),
|
||||
ReactivationCashback(deeplinkId = 2, slug = "reactivation-cashback"),
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun fromDeeplinkId(id: Int): PromoCampaignId? = entries.firstOrNull { it.deeplinkId == id }
|
||||
fun fromSlug(slug: String): PromoCampaignId? = entries.firstOrNull { it.slug == slug }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.domain.promo.models
|
||||
|
||||
sealed interface PromoCampaignState {
|
||||
|
||||
val campaign: PromoCampaignId
|
||||
|
||||
data class Available(
|
||||
override val campaign: PromoCampaignId,
|
||||
val payoutTokens: List<PromoPayoutToken>,
|
||||
val timeline: PromoTimeline,
|
||||
) : PromoCampaignState
|
||||
|
||||
data class Enrolled(
|
||||
override val campaign: PromoCampaignId,
|
||||
val tokenReward: TokenReward,
|
||||
) : PromoCampaignState
|
||||
|
||||
data class NotActive(
|
||||
override val campaign: PromoCampaignId,
|
||||
) : PromoCampaignState
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.domain.promo.models
|
||||
|
||||
import kotlinx.datetime.Instant
|
||||
|
||||
data class PromoPayoutToken(
|
||||
val tokenAddress: String,
|
||||
val tokenSymbol: String,
|
||||
val tokenName: String,
|
||||
val networkId: String,
|
||||
)
|
||||
|
||||
data class PromoTimeline(
|
||||
val start: Instant,
|
||||
val end: Instant,
|
||||
)
|
||||
|
||||
data class TokenReward(
|
||||
val tokenAddress: String,
|
||||
val networkId: String,
|
||||
)
|
||||
|
||||
sealed interface EnrollResult {
|
||||
val tokenReward: TokenReward
|
||||
|
||||
data class Success(override val tokenReward: TokenReward) : EnrollResult
|
||||
data class AlreadyEnrolled(override val tokenReward: TokenReward) : EnrollResult
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.domain.promo.models
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class PromoCampaignIdTest {
|
||||
|
||||
@Test
|
||||
fun `GIVEN known deeplink id WHEN fromDeeplinkId THEN returns campaign`() {
|
||||
assertThat(PromoCampaignId.fromDeeplinkId(1)).isEqualTo(PromoCampaignId.WhaleSwapCashback)
|
||||
assertThat(PromoCampaignId.fromDeeplinkId(2)).isEqualTo(PromoCampaignId.ReactivationCashback)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN unknown deeplink id WHEN fromDeeplinkId THEN returns null`() {
|
||||
assertThat(PromoCampaignId.fromDeeplinkId(99)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN known slug WHEN fromSlug THEN returns campaign`() {
|
||||
assertThat(PromoCampaignId.fromSlug("whale-swap-cashback")).isEqualTo(PromoCampaignId.WhaleSwapCashback)
|
||||
assertThat(PromoCampaignId.fromSlug("reactivation-cashback")).isEqualTo(PromoCampaignId.ReactivationCashback)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN unknown slug WHEN fromSlug THEN returns null`() {
|
||||
assertThat(PromoCampaignId.fromSlug("nope")).isNull()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.domain.promo
|
||||
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.promo.models.EnrollResult
|
||||
import com.tangem.domain.promo.models.PromoCampaignId
|
||||
import com.tangem.domain.promo.models.PromoCampaignState
|
||||
import com.tangem.domain.promo.models.TokenReward
|
||||
|
||||
/**
|
||||
* Backend promo-campaign plumbing (enrollment state and registration).
|
||||
*
|
||||
* Both methods propagate the underlying error (network, parsing, etc.) by throwing rather than
|
||||
* returning an error type — callers (use cases) are expected to wrap the call, e.g. with `Either.catch`.
|
||||
*/
|
||||
interface PromoRepository {
|
||||
|
||||
/**
|
||||
* Resolves the state of [campaign] for [userWalletId]: locally enrolled, available, or not active.
|
||||
* Throws if the campaign list can't be fetched and no cached/local data is available.
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
suspend fun getCampaignState(
|
||||
campaign: PromoCampaignId,
|
||||
userWalletId: UserWalletId,
|
||||
forceRefresh: Boolean = false,
|
||||
): PromoCampaignState
|
||||
|
||||
/**
|
||||
* Registers [walletIds] for [campaign] with the given [tokenReward]. Throws on any non-conflict
|
||||
* API error; a 409 conflict resolves to [EnrollResult.AlreadyEnrolled] instead of throwing.
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
suspend fun enroll(
|
||||
campaign: PromoCampaignId,
|
||||
tokenReward: TokenReward,
|
||||
walletIds: List<UserWalletId>,
|
||||
): EnrollResult
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.domain.promo.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.promo.PromoRepository
|
||||
import com.tangem.domain.promo.models.EnrollResult
|
||||
import com.tangem.domain.promo.models.PromoCampaignId
|
||||
import com.tangem.domain.promo.models.TokenReward
|
||||
|
||||
class EnrollPromoCampaignUseCase(
|
||||
private val repository: PromoRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(
|
||||
campaign: PromoCampaignId,
|
||||
tokenReward: TokenReward,
|
||||
walletIds: List<UserWalletId>,
|
||||
): Either<Throwable, EnrollResult> = Either.catch {
|
||||
repository.enroll(campaign, tokenReward, walletIds)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.domain.promo.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.promo.PromoRepository
|
||||
import com.tangem.domain.promo.models.PromoCampaignId
|
||||
import com.tangem.domain.promo.models.PromoCampaignState
|
||||
|
||||
class GetPromoCampaignStateUseCase(
|
||||
private val repository: PromoRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(
|
||||
campaign: PromoCampaignId,
|
||||
userWalletId: UserWalletId,
|
||||
forceRefresh: Boolean = false,
|
||||
): Either<Throwable, PromoCampaignState> = Either.catch {
|
||||
repository.getCampaignState(campaign, userWalletId, forceRefresh)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.tangem.domain.promo.usecase
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.promo.PromoRepository
|
||||
import com.tangem.domain.promo.models.EnrollResult
|
||||
import com.tangem.domain.promo.models.PromoCampaignId
|
||||
import com.tangem.domain.promo.models.TokenReward
|
||||
import com.tangem.test.core.assertEitherLeft
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import java.io.IOException
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class EnrollPromoCampaignUseCaseTest {
|
||||
|
||||
private val repository: PromoRepository = mockk()
|
||||
private val useCase = EnrollPromoCampaignUseCase(repository)
|
||||
|
||||
private val campaign = PromoCampaignId.WhaleSwapCashback
|
||||
private val walletIds = listOf(UserWalletId("abcdef012345"))
|
||||
private val tokenReward = TokenReward("0xToken", "ethereum")
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() = clearMocks(repository)
|
||||
|
||||
@Test
|
||||
fun `GIVEN repo returns Success WHEN invoke THEN Right Success`() = runTest {
|
||||
// Arrange
|
||||
val expected = EnrollResult.Success(tokenReward)
|
||||
coEvery { repository.enroll(campaign, tokenReward, walletIds) } returns expected
|
||||
|
||||
// Act
|
||||
val result = useCase(campaign, tokenReward, walletIds)
|
||||
|
||||
// Assert
|
||||
assertThat(result.getOrNull()).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN repo throws WHEN invoke THEN Left`() = runTest {
|
||||
// Arrange
|
||||
val error = IOException("x")
|
||||
coEvery { repository.enroll(campaign, tokenReward, walletIds) } throws error
|
||||
|
||||
// Act
|
||||
val result = useCase(campaign, tokenReward, walletIds)
|
||||
|
||||
// Assert
|
||||
assertEitherLeft(result, error)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.tangem.domain.promo.usecase
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.promo.PromoRepository
|
||||
import com.tangem.domain.promo.models.PromoCampaignId
|
||||
import com.tangem.domain.promo.models.PromoCampaignState
|
||||
import com.tangem.test.core.assertEitherLeft
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import java.io.IOException
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class GetPromoCampaignStateUseCaseTest {
|
||||
|
||||
private val repository: PromoRepository = mockk()
|
||||
private val useCase = GetPromoCampaignStateUseCase(repository)
|
||||
|
||||
private val campaign = PromoCampaignId.WhaleSwapCashback
|
||||
private val userWalletId = UserWalletId("abcdef012345")
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() = clearMocks(repository)
|
||||
|
||||
@Test
|
||||
fun `GIVEN repo returns state WHEN invoke THEN Right of state`() = runTest {
|
||||
// Arrange
|
||||
val expected = PromoCampaignState.NotActive(campaign)
|
||||
coEvery { repository.getCampaignState(campaign, userWalletId, false) } returns expected
|
||||
|
||||
// Act
|
||||
val result = useCase(campaign, userWalletId)
|
||||
|
||||
// Assert
|
||||
assertThat(result.getOrNull()).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN repo throws WHEN invoke THEN Left`() = runTest {
|
||||
// Arrange
|
||||
val error = IOException("x")
|
||||
coEvery { repository.getCampaignState(campaign, userWalletId, false) } throws error
|
||||
|
||||
// Act
|
||||
val result = useCase(campaign, userWalletId)
|
||||
|
||||
// Assert
|
||||
assertEitherLeft(result, error)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue