Skip to content

[ALFMOB-81] Add to Bag #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package au.com.alfie.ecomm.data.bag

import au.com.alfie.ecomm.data.toRepositoryResult
import au.com.alfie.ecomm.repository.bag.BagProduct
import au.com.alfie.ecomm.repository.bag.BagRepository
import au.com.alfie.ecomm.repository.result.RepositoryResult
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import javax.inject.Inject

class BagRepositoryImpl @Inject constructor() : BagRepository {

// TODO consider removing this property when the products in the bag are saved on database or api
private val _bag = MutableStateFlow<List<BagProduct>>(listOf())

// TODO change this implementation to a proper implementation using data base or api to get the products in the bag
override fun getBag(): Flow<RepositoryResult<List<BagProduct>>> {
return _bag.map { bag ->
Result.success(bag).toRepositoryResult()
}
}

// TODO change this implementation to a proper implementation using data base or api to save the product
override fun addToBag(bagProduct: BagProduct): RepositoryResult<Boolean> {
_bag.value = _bag.value.toMutableList().apply { add(bagProduct) }
return RepositoryResult.Success(true)
}

override fun removeFromBag(bagProduct: BagProduct): RepositoryResult<Boolean> {
_bag.value = _bag.value.toMutableList().apply { remove(bagProduct) }
return RepositoryResult.Success(true)
}
}
18 changes: 18 additions & 0 deletions data/src/main/java/au/com/alfie/ecomm/data/bag/di/BagModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package au.com.alfie.ecomm.data.bag.di

import au.com.alfie.ecomm.data.bag.BagRepositoryImpl
import au.com.alfie.ecomm.repository.bag.BagRepository
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
internal interface BagModule {

@Binds
@Singleton
fun bindBagRepository(bagRepositoryImpl: BagRepositoryImpl): BagRepository
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
package au.com.alfie.ecomm.designsystem.component.price

import androidx.compose.runtime.Stable

@Stable
sealed interface PriceType {

@Stable
data class Default(
val price: String
) : PriceType

@Stable
data class Sale(
val fullPrice: String,
val salePrice: String
) : PriceType

@Stable
data class Range(
val startPrice: String,
val endPrice: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ sealed interface ProductCardType {
val image: ImageUI
val brand: String
val name: String
val price: PriceType?
val price: PriceType
val cardTestTag: String
val imageTestTag: String
val brandTestTag: String
Expand All @@ -30,6 +30,7 @@ sealed interface ProductCardType {
override val price: PriceType,
val color: String,
val size: String,
val onRemoveClick: ClickEvent? = null,
override val cardTestTag: String = PRODUCT_CARD,
override val imageTestTag: String = PRODUCT_IMAGE,
override val brandTestTag: String = PRODUCT_DESIGNER,
Expand All @@ -43,7 +44,7 @@ sealed interface ProductCardType {
override val image: ImageUI,
override val brand: String,
override val name: String,
override val price: PriceType?,
override val price: PriceType,
override val cardTestTag: String = PRODUCT_CARD,
override val imageTestTag: String = PRODUCT_IMAGE,
override val brandTestTag: String = PRODUCT_DESIGNER,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
Expand Down Expand Up @@ -137,6 +140,18 @@ internal fun ProductCardXSmall(
.testTag(productCard.priceTestTag)
)
}
if (isLoading.not() && productCard.onRemoveClick != null) {
IconButton(
modifier = Modifier.size(Theme.iconSize.large),
onClick = productCard.onRemoveClick
) {
Icon(
painter = painterResource(id = R.drawable.ic_action_close_dark),
contentDescription = null,
modifier = Modifier.size(Theme.iconSize.small)
)
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package au.com.alfie.ecomm.repository.bag

data class BagProduct(
val productId: String,
val variantSku: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package au.com.alfie.ecomm.repository.bag

import au.com.alfie.ecomm.repository.result.RepositoryResult
import kotlinx.coroutines.flow.Flow

interface BagRepository {

fun getBag(): Flow<RepositoryResult<List<BagProduct>>>

fun addToBag(bagProduct: BagProduct): RepositoryResult<Boolean>

fun removeFromBag(bagProduct: BagProduct): RepositoryResult<Boolean>
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import au.com.alfie.ecomm.repository.shared.model.Size
data class Variant(
val attributes: List<Attribute>,
val color: Color?,
val media: Media,
val media: Media.Image,
val price: Price,
val size: Size?,
val sku: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package au.com.alfie.ecomm.domain.usecase.bag

import au.com.alfie.ecomm.domain.UseCaseInteractor
import au.com.alfie.ecomm.domain.UseCaseResult
import au.com.alfie.ecomm.domain.doOnResult
import au.com.alfie.ecomm.repository.bag.BagProduct
import au.com.alfie.ecomm.repository.bag.BagRepository
import javax.inject.Inject

class AddToBagUseCase @Inject constructor(
private val bagRepository: BagRepository
) : UseCaseInteractor {

suspend operator fun invoke(
productId: String,
variantSku: String
) = run(
bagRepository.addToBag(
BagProduct(
productId = productId,
variantSku = variantSku
)
)
).doOnResult(
onSuccess = { UseCaseResult.Success(it) },
onError = { UseCaseResult.Error(it) }
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package au.com.alfie.ecomm.domain.usecase.bag

import au.com.alfie.ecomm.domain.UseCaseInteractor
import au.com.alfie.ecomm.domain.UseCaseResult
import au.com.alfie.ecomm.repository.bag.BagProduct
import au.com.alfie.ecomm.repository.bag.BagRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject

class GetBagUseCase @Inject constructor(
private val bagRepository: BagRepository
) : UseCaseInteractor {

suspend operator fun invoke(): Flow<UseCaseResult<List<BagProduct>>> =
bagRepository.getBag().map { repositoryResult ->
run(repositoryResult)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package au.com.alfie.ecomm.domain.usecase.bag

import au.com.alfie.ecomm.domain.UseCaseInteractor
import au.com.alfie.ecomm.domain.UseCaseResult
import au.com.alfie.ecomm.repository.bag.BagProduct
import au.com.alfie.ecomm.repository.bag.BagRepository
import javax.inject.Inject

class RemoveFromBagUseCase @Inject constructor(
private val bagRepository: BagRepository
) : UseCaseInteractor {

suspend operator fun invoke(bagProduct: BagProduct): UseCaseResult<Boolean> =
run(bagRepository.removeFromBag(bagProduct))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package au.com.alfie.ecomm.domain.bag

import au.com.alfie.ecomm.domain.usecase.bag.AddToBagUseCase
import au.com.alfie.ecomm.repository.bag.BagProduct
import au.com.alfie.ecomm.repository.bag.BagRepository
import au.com.alfie.ecomm.repository.product.ProductRepository
import au.com.alfie.ecomm.repository.result.ErrorResult
import au.com.alfie.ecomm.repository.result.RepositoryResult
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.impl.annotations.InjectMockKs
import io.mockk.impl.annotations.RelaxedMockK
import io.mockk.junit5.MockKExtension
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(MockKExtension::class)
class AddToBagUseCaseTest {
@RelaxedMockK
private lateinit var bagRepository: BagRepository

@RelaxedMockK
private lateinit var productRepository: ProductRepository

@InjectMockKs
lateinit var subject: AddToBagUseCase

@Test
fun `add to bag, with success result`() = runTest {
val mockProduct = mockk<BagProduct> {
every { productId } returns "10"
every { variantSku } returns "34535"
}

coEvery { bagRepository.addToBag(mockProduct) } returns RepositoryResult.Success(true)
subject(productId = "10", variantSku = "34535")
coVerify { bagRepository.addToBag(mockProduct) }
}

@Test
fun `add to bag, with error result`() = runTest {
val mockProduct = mockk<BagProduct> {
every { productId } returns "10"
every { variantSku } returns "34535"
}
val errorResult = mockk<ErrorResult>()

coEvery { bagRepository.addToBag(mockProduct) } returns RepositoryResult.Error(errorResult)
subject("10", "34535")
coVerify { bagRepository.addToBag(mockProduct) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package au.com.alfie.ecomm.domain.bag

import au.com.alfie.ecomm.domain.UseCaseResult
import au.com.alfie.ecomm.domain.usecase.bag.GetBagUseCase
import au.com.alfie.ecomm.repository.bag.BagProduct
import au.com.alfie.ecomm.repository.bag.BagRepository
import au.com.alfie.ecomm.repository.result.ErrorResult
import au.com.alfie.ecomm.repository.result.RepositoryResult
import io.mockk.coEvery
import io.mockk.impl.annotations.InjectMockKs
import io.mockk.impl.annotations.RelaxedMockK
import io.mockk.junit5.MockKExtension
import io.mockk.mockk
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import kotlin.test.assertEquals

@ExtendWith(MockKExtension::class)
class GetBagUseCaseTest {
@RelaxedMockK
private lateinit var bagRepository: BagRepository

@InjectMockKs
lateinit var subject: GetBagUseCase

@Test
fun `get list of products in the bag`() = runTest {
val mockBag = mockk<List<BagProduct>>()
coEvery { bagRepository.getBag() } returns flowOf(RepositoryResult.Success(mockBag))

val expected = UseCaseResult.Success(mockBag)
val result = subject().first()

assertEquals(expected, result)
}

@Test
fun `get list of products in the bag, and returns an error`() = runTest {
val errorResult = mockk<ErrorResult>()
coEvery { bagRepository.getBag() } returns flowOf(RepositoryResult.Error(errorResult))

val expected = UseCaseResult.Error(errorResult)
val result = subject().first()

assertEquals(expected, result)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import io.mockk.impl.annotations.RelaxedMockK
import io.mockk.junit5.MockKExtension
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.extension.ExtendWith
import kotlin.test.assertEquals
Expand All @@ -26,11 +25,6 @@ class AccountViewModelTest {

private lateinit var subject: AccountViewModel

@BeforeEach
fun setup() {
subject = setupViewModel()
}

@Test
fun `WHEN viewmodel initializes THEN accountUI should be built via factory`() = runTest {
coEvery { accountUIFactory() } returns accountUILoaded
Expand All @@ -50,6 +44,8 @@ class AccountViewModelTest {
fun `WHEN nav event is triggered THEN event should be handled`() = runTest {
val uiEvent = mockk<UIEvent>()

val subject = setupViewModel()

subject.uiEvent.test {
subject.handleEvent(AccountEvent.OpenEntry(uiEvent))

Expand Down
Loading