diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/methodselection/ChangeDueCalculatorFragment.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/methodselection/ChangeDueCalculatorFragment.kt index 832ccaa33e6..67766074d00 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/methodselection/ChangeDueCalculatorFragment.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/methodselection/ChangeDueCalculatorFragment.kt @@ -4,67 +4,24 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.fragment.app.DialogFragment import androidx.fragment.app.viewModels -import com.woocommerce.android.R +import androidx.navigation.fragment.findNavController +import com.woocommerce.android.ui.base.BaseFragment import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class ChangeDueCalculatorFragment : DialogFragment() { +class ChangeDueCalculatorFragment : BaseFragment() { private val viewModel: ChangeDueCalculatorViewModel by viewModels() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { return ComposeView(requireContext()).apply { setContent { - ChangeDueCalculatorScreen() - } - } - } - - @Composable - fun ChangeDueCalculatorScreen() { - val uiState by viewModel.uiState.collectAsState() - - Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - // Display dynamic content based on UI state - when (uiState) { - is ChangeDueCalculatorViewModel.UiState.Loading -> { - Text(text = stringResource(R.string.loading), style = MaterialTheme.typography.h5) - } - - is ChangeDueCalculatorViewModel.UiState.Success -> { - val state = uiState as ChangeDueCalculatorViewModel.UiState.Success - Text( - text = stringResource(R.string.cash_payments_take_payment_title, state.amountDue), - style = MaterialTheme.typography.h5, - modifier = Modifier.padding(bottom = 16.dp) - ) - } - - is ChangeDueCalculatorViewModel.UiState.Error -> { - Text(text = stringResource(R.string.error_generic), style = MaterialTheme.typography.h5) - } + val uiState by viewModel.uiState.collectAsState() + ChangeDueCalculatorScreen(uiState = uiState, onNavigateUp = { findNavController().navigateUp() }) } } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/methodselection/ChangeDueCalculatorScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/methodselection/ChangeDueCalculatorScreen.kt new file mode 100644 index 00000000000..e57f5a17280 --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/methodselection/ChangeDueCalculatorScreen.kt @@ -0,0 +1,102 @@ +package com.woocommerce.android.ui.payments.methodselection + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import com.woocommerce.android.R + +@Composable +fun ChangeDueCalculatorScreen( + uiState: ChangeDueCalculatorViewModel.UiState, + onNavigateUp: () -> Unit +) { + Scaffold( + topBar = { + TopAppBar( + title = { Text(text = getTitleText(uiState)) }, + navigationIcon = { + IconButton(onClick = onNavigateUp) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.back) + ) + } + }, + backgroundColor = colorResource(id = R.color.color_toolbar), + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + .padding(16.dp), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally + ) { + when (uiState) { + is ChangeDueCalculatorViewModel.UiState.Loading -> Text( + stringResource( + R.string.loading, + ), + style = MaterialTheme.typography.h6 + ) + + is ChangeDueCalculatorViewModel.UiState.Success -> Text( + stringResource( + R.string.cash_payments_take_payment_title, + uiState.amountDue + ), + style = MaterialTheme.typography.body1 + ) + + is ChangeDueCalculatorViewModel.UiState.Error -> Text( + stringResource( + R.string.error_generic, + ), + style = MaterialTheme.typography.h6 + ) + } + } + } +} + +@Composable +private fun getTitleText(uiState: ChangeDueCalculatorViewModel.UiState): String { + return when (uiState) { + is ChangeDueCalculatorViewModel.UiState.Success -> stringResource( + R.string.cash_payments_take_payment_title, + uiState.amountDue + ) + + else -> stringResource(id = R.string.cash_payments_take_payment_title) + } +} + +@Composable +@PreviewLightDark() +fun ChangeDueCalculatorScreenSuccessPreview() { + ChangeDueCalculatorScreen( + uiState = ChangeDueCalculatorViewModel.UiState.Success( + amountDue = "$100.00", + change = 0.00.toBigDecimal() + ), + onNavigateUp = {} + ) +} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/methodselection/ChangeDueCalculatorViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/methodselection/ChangeDueCalculatorViewModel.kt index bf441e9620d..ac2da99205d 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/methodselection/ChangeDueCalculatorViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/methodselection/ChangeDueCalculatorViewModel.kt @@ -1,18 +1,24 @@ package com.woocommerce.android.ui.payments.methodselection import androidx.lifecycle.SavedStateHandle +import com.woocommerce.android.tools.SelectedSite import com.woocommerce.android.ui.orders.details.OrderDetailRepository +import com.woocommerce.android.util.CurrencyFormatter import com.woocommerce.android.viewmodel.ScopedViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import org.wordpress.android.fluxc.store.WooCommerceStore import java.math.BigDecimal import javax.inject.Inject @HiltViewModel class ChangeDueCalculatorViewModel @Inject constructor( + private val selectedSite: SelectedSite, + private val currencyFormatter: CurrencyFormatter, savedStateHandle: SavedStateHandle, + private val wooCommerceStore: WooCommerceStore, private val orderDetailRepository: OrderDetailRepository ) : ScopedViewModel(savedStateHandle) { @@ -21,7 +27,7 @@ class ChangeDueCalculatorViewModel @Inject constructor( sealed class UiState { data object Loading : UiState() - data class Success(val amountDue: BigDecimal, val change: BigDecimal) : UiState() + data class Success(val amountDue: String, val change: BigDecimal) : UiState() data object Error : UiState() } @@ -35,7 +41,12 @@ class ChangeDueCalculatorViewModel @Inject constructor( private fun loadOrderDetails() { launch { val order = orderDetailRepository.getOrderById(orderId)!! - _uiState.value = UiState.Success(amountDue = order.total, 0.00.toBigDecimal()) + _uiState.value = UiState.Success(amountDue = formatOrderTotal(order.total), 0.00.toBigDecimal()) } } + + private fun formatOrderTotal(total: BigDecimal): String { + val currencyCode = wooCommerceStore.getSiteSettings(selectedSite.get())?.currencyCode ?: "" + return currencyFormatter.formatCurrency(total, currencyCode) + } } diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/payments/methodselection/ChangeDueCalculatorViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/payments/methodselection/ChangeDueCalculatorViewModelTest.kt index 9f0fca75621..9a9903acc18 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/payments/methodselection/ChangeDueCalculatorViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/payments/methodselection/ChangeDueCalculatorViewModelTest.kt @@ -1,33 +1,98 @@ package com.woocommerce.android.ui.payments.methodselection +import androidx.lifecycle.SavedStateHandle +import com.woocommerce.android.model.Order +import com.woocommerce.android.tools.SelectedSite +import com.woocommerce.android.ui.orders.details.OrderDetailRepository +import com.woocommerce.android.util.CurrencyFormatter import com.woocommerce.android.viewmodel.BaseUnitTest import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.assertj.core.api.Assertions.assertThat import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.store.WooCommerceStore +import java.math.BigDecimal + +private const val ORDER_TOTAL = "100.00" @ExperimentalCoroutinesApi class ChangeDueCalculatorViewModelTest : BaseUnitTest() { + + private val site: SiteModel = mock() + private val order: Order = mock { + on { total }.thenReturn(BigDecimal(1L)) + } + + private val selectedSite: SelectedSite = mock { + on { get() }.thenReturn(site) + } + private val currencyFormatter: CurrencyFormatter = mock { + on { formatCurrency(any(), any(), any()) }.thenReturn(ORDER_TOTAL) + } + private val wooCommerceStore: WooCommerceStore = mock { + on { getSiteSettings(site) }.thenReturn(mock()) + } + + private val orderDetailRepository: OrderDetailRepository = mock() + + private val savedStateHandle: SavedStateHandle = mock() + + private lateinit var viewModel: ChangeDueCalculatorViewModel + @Test - fun `given valid order id, when order details are requested, then success state is emitted`() = testBlocking { + fun `when ViewModel is initialized, then initial state is loading`() { // GIVEN - // TODO val viewModel: ChangeDueCalculatorViewModel = mock() - // TODO + whenever(savedStateHandle.get("orderId")).thenReturn(1L) // WHEN - // TODO + viewModel = initViewModel() // THEN - // TODO + assertThat(viewModel.uiState.value).isInstanceOf(ChangeDueCalculatorViewModel.UiState.Loading::class.java) } @Test - fun `given order details retrieval failure, when order details are loaded, then error state is emitted`() = testBlocking { + fun `given valid order details, when order details are requested, then success state is emitted`() = runTest { // GIVEN - // TODO val viewModel: ChangeDueCalculatorViewModel = mock() + val expectedAmountDue = "100.00" + val expectedChange = BigDecimal("0.0") + whenever(orderDetailRepository.getOrderById(1L)).thenReturn(order) + whenever(savedStateHandle.get("orderId")).thenReturn(1L) + + // WHEN + viewModel = initViewModel() + + // THEN + val uiState = viewModel.uiState.value + assertThat(uiState).isInstanceOf(ChangeDueCalculatorViewModel.UiState.Success::class.java) + uiState as ChangeDueCalculatorViewModel.UiState.Success + assertThat(uiState.change).isEqualTo(expectedChange) + assertThat(uiState.amountDue).isEqualTo(expectedAmountDue) + } + + @Test + fun `given order details retrieval failure, when order details are loaded, then error state is emitted`() = runTest { + // GIVEN + // TODO // WHEN // TODO // THEN - // TODO assertThat(viewModel.uiState.value).isEqualTo(ChangeDueCalculatorViewModel.UiState.Error) + // TODO + } + + private fun initViewModel(): ChangeDueCalculatorViewModel { + return ChangeDueCalculatorViewModel( + selectedSite, + currencyFormatter, + savedStateHandle, + wooCommerceStore, + orderDetailRepository + ) } }